分散式系統
| 華夏公益教科書使用者認為此頁面應該拆分為更小的頁面,每個頁面涵蓋更窄的主題。 您可以透過將此大頁面拆分為更小的頁面來提供幫助。請確保遵循 命名策略。將書籍劃分為更小的部分可以提供更多焦點,並使每個部分都能做好一件事,這將使每個人受益。 |
每個 Unix 程序都有一個地址空間,看起來像這樣
global vars—text segment—heap—stack
請記住,低記憶體位於頂部,高記憶體位於底部。因此,當專案新增到堆疊中時,它向上增長,每個元素都具有較低的記憶體地址。
您應該知道它做了什麼
main()
{
int i, cpid;
cpid = fork();
if (cpid < 0) exit(1);
else if (cpid == 0) {
/* this is the child process. */
for (i=0; i<1000; i++) printf("child: %d\n", i);
exit(0);
}
else {
/* this is parent process */
for (i=0; i<1000; i++) printf("parent: %d\n", i);
waitpid(cpid);
}
return 0;
}
呼叫 fork 建立一個新的程序,該程序具有一個全新的地址空間。這意味著對所有內容都進行了複製:新的全域性變數、新的文字程式碼、新的堆和新的堆疊。即使使用 malloc 動態分配的物件也會被複制。請記住,這些動態分配物件的指標是根據相對定址設定的,因此即使新複製的記憶體分配到其他地方,指向堆中的指標仍然有效。
四個最流行的 UNIX 程序狀態是就緒、等待、執行和殭屍。前三個很容易理解。殭屍程序是已完成的子程序,它們正在等待其父程序清理它們。通常,父程序透過呼叫 wait 或 waitpid 來清理已完成的(殭屍)子程序。
如果父程序在子程序完成之前死亡,最終根程序將出現並清理它們。
因此,在 fork() 之後,如果每個程序都有自己的記憶體空間,如何設定通訊?使用管道!每個程序可能有自己的檔案描述符表,但它們仍然引用相同的檔案。
示例管道程式碼,其中子程序讀取,父程序寫入
注意:隨意新增所有這些錯誤檢查,但儘量讓重點顯而易見
int p[2];
pipe(p); /* call by address. */
cpid = fork();
if (cpid < 0) exit(1);
/* child process */
if (cpid == 0) {
close(p[1]); /* child won't need to write, so close. */
read(p[0], buf, len);
close(p[0]);
_exit(0); /* read man 3 exit and man _exit to understand the difference. */
}
/* parent */
else {
close(p[0]); /* parents can be so close-minded! */
write(p[1], buf, len);
close(p[1]);
waitpid(cpid);
}
學習並理解上面的程式碼。
我們都在命令列中做過類似的事情
$ ls -l | sort | head
但它是如何工作的?在 MS-DOS 中,這個功能的實現有點像這樣
ls -l > /tmpfile1 sort /tmpfile1 > /tmpfile2 head /tmpfile2
這三個任務是順序執行的,而不是同時執行的。如果第一個程式的輸出很大,這很糟糕,而且這種設定肯定無法處理無限的資料流。
您可以在 C 中實現 ls -l | sort | head 之類的東西。為了便於管理,以下程式碼實現了 "ls | wc -l"
int pipes[2];
pipe(pipes);
int pid = fork();
if(pid < 0) /* Check for error */
{
fprintf(stderr, "fork unsuccessful.");
exit(1);
}
if(0 == pid) /* We are the child */
{
/*
* We don't need the writing end of the pipe. This closes it.
*/
close(pipes[1]);
/*
* Duplicate our output pipe to file descriptor 0.
* This means that anything that would normally be read from stdin
* is now read from the pipe.
*/
dup2(pipes[0], 0);
/*
* We've duplicated the pipe, so there is no need to leave this copy
* hanging around.
*/
close(pipes[0]);
/*
* Execute the "wc -l" command. This replaces our process.
*/
execlp("wc", "wc", "-l", (char*)0);
}
else /* We must be the parent */
{
/*
* We don't need the reading end of the pipe. This closes it.
*/
close(pipes[0]);
/*
* This duplicates the pipe into stdout. This means that standard
* output is redirected into the pipe.
*/
dup2(pipes[1], 1);
/*
* Since we've duplicated this end of the pipe, we won't need it
* anymore.
*/
close(pipes[1]);
/*
* We're done setting up. This replaces us with the "ls" part of the
* command.
*/
execlp("ls", "ls", (char*)0);
}
待辦事項:更多地討論 execv 的工作原理。
另一個經典
ls -l > out.txt
你怎麼能做到?
待辦事項:完成
錯誤?
執行緒共享記憶體,程序不共享。它們都允許程式同時(併發)執行多個操作。在 Linux 上,大多數時候,當人們談論執行緒時,他們實際上指的是 POSIX 執行緒,即 pthreads。
示例 pthread 程式碼
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int i; /* i is global, so it is visible to all functions. */
static void *foo()
{
int k;
for (k=0; k<10; k++) {
sleep(1);
printf("thread foo: %d.\n", i++);
}
return NULL;
}
static void *bar()
{
int k;
for (k=0; k<10; k++) {
sleep(1);
printf("thread bar: %d.\n", i++);
}
return NULL;
}
int main() {
int x;
pthread_t t1, t2;
x = pthread_create(&t1, NULL, foo, NULL);
if (x != 0) printf("pthread foo failed.\n");
x = pthread_create(&t2, NULL, bar, NULL);
if (x != 0) printf("pthread bar failed.\n");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("all pthreads finished.\n");
return 0;
}
這是輸出的一部分
$ ./pthreadfun thread foo: 0. thread bar: 1. thread foo: 2. ... thread foo: 18. thread bar: 19. all pthreads finished.
希望主要觀點能夠顯而易見。Foo 和 bar 交替遞增全域性整數 i。
重寫上面的程式碼以使用 fork() 建立兩個程序,而不是兩個執行緒。然後找出 i 的最大值並解釋與該執行緒版本的區別。
pthreads 是核心空間執行緒,這意味著作業系統排程器知道多個執行緒。每個執行緒在作業系統排程表中都有一個條目。
還存在使用者級執行緒,其中核心不知道執行緒。程序必須自己處理排程。使用者級執行緒需要兩級排程
OS scheduler handles all processes Process handles all threads.
如果程式設計師不小心,一個使用者級執行緒可以阻塞程序中的所有其他執行緒。如果一個使用者級執行緒進行了一些阻塞系統呼叫,那麼作業系統將暫停該程序,直到該系統呼叫返回。
使用者執行緒的優點是允許在程序中比核心級執行緒更快地線上程之間切換。程序本身執行上下文切換。
Sun Solaris 2.3 版本提供了混合執行緒,試圖將使用者級執行緒的速度與核心級執行緒的優勢相結合。混合執行緒最大限度地減少了在核心級別線上程之間切換的作業系統排程成本,但它們也允許執行緒進行阻塞系統呼叫而不會阻塞整個程序。
該系統允許執行緒和輕量級程序。作業系統排程程式只看到輕量級程序。每個執行緒都附加到一個輕量級程序。執行緒可以共享一個程序,因此具有多個執行緒的一個應用程式可能在作業系統排程程式中顯示為一個程序。
在兩個執行緒都繫結到一個程序的程式中,如果執行緒 1 進行阻塞系統呼叫,則作業系統將阻塞該程序。同時,為了允許執行緒 2 繼續執行,作業系統將建立一個新的程序,將執行緒 2 從第一個程序中分離,然後將其附加到新程序。
如果執行緒要共享變數,你需要小心,不要讓它們同時嘗試修改記憶體。
考慮這段經典的 C 程式碼
i++;
這看起來像是一個操作(將 i 加一),但它在彙編程式碼中轉換為三個操作
lda i; load the value stored in var i into the eax register. inc; increment up the eax register. sta i; store the value in the eax register into the var i.
有關如何使用 gcc 將 C 程式轉換為彙編程式的更多資訊,請參閱本書中的 C 程式設計附錄。
由於執行緒共享記憶體,因此一個執行緒可能開始執行某項操作,然後被作業系統排程程式掛起。
假設存在兩個執行緒,i 是一個全域性整數,最初設定為 99。每個執行緒都希望將 i 增加 1,因此每個執行緒都會執行上面三個彙編步驟。完成後,我們希望找到 i 設定為 101。
執行緒 1 首先開始
lda i; eax register holds 99. inc i; now, eax holds 100.
現在,假設在這一點上,作業系統排程程式掛起了執行緒 1 並啟動了執行緒 2
lda i; load 99 from i into eax. This is bad!!! inc i; increment eax to 100. sta i; store 100 into i.
然後,作業系統允許執行緒 1 完成
sta i; store eax into i. but eax only has 100!
所以現在你看到兩個執行緒訪問公共變數是如何有風險的。這個 bug 最糟糕的部分是它將是不一致的,難以重現。i++ 語句稱為**臨界區**,因為我們需要確保只有一個執行緒在該部分可以訪問變數。
一種解決方案是在每個執行緒中使用某種鎖,如下所示
lock(ilock);
i++;
unlock(ilock);
然後,如果執行緒 1 被掛起,執行緒 2 啟動,它將在 lock(ilock) 呼叫上阻塞。為了真正做到這一點,我們可以使用互斥鎖。互斥鎖就像一個只有 1 或 0 值的訊號量。此程式碼中的執行緒將在嘗試增加 i 之前鎖定和解鎖互斥鎖,因此可以避免上述問題。
#include <stdio.h>
#include <pthread.h>
pthread_mutex_t ilock = PTHREAD_MUTEX_INITIALIZER;
int i;
static void *f1()
{
pthread_mutex_lock(&ilock);
i++;
pthread_mutex_unlock(&ilock);
return NULL;
}
static void *f2()
{
pthread_mutex_lock(&ilock);
i++;
pthread_mutex_unlock(&ilock);
return NULL;
}
int main()
{
pthread_t t1, t2;
int x;
i = 99;
x = pthread_create(&t1, NULL, f1, NULL);
if (x != 0) printf("pthread foo failed.\n");
x = pthread_create(&t2, NULL, f2, NULL);
if (x != 0) printf("pthread bar failed.\n");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("all pthreads finished.\n");
printf("i set to %d.\n", i);
return 0;
}
就是這樣!
解決此問題的另一種方法是使用真正原子的“增量”指令。(“鎖定”和“解鎖”函式使用這種原子指令)。此類原子函式(以及如何使用它們來避免資料損壞和死鎖)在Wikipedia:無鎖和無等待演算法中進行了討論。
這個小妖精在現實世界中經常出現。廚師為餐廳製作了一碗碗湯。廚師必須等待餐廳要求供應湯,而餐廳必須等待廚師宣佈湯已準備就緒。如果你不小心,你可能會遇到**死鎖**,這就像它的名字一樣糟糕。
死鎖涉及兩個程序(p1 和 p2)和兩個資源(r1 和 r2)。每個程序都需要獨佔訪問這兩個資源才能完成。當 p1 持有 r1 並等待 r2 被釋放時,p2 持有 r2 並等待 r1 時,就會發生死鎖。如你所見,這兩個程序都無法繼續。
此虛擬碼說明了如何設定生產者/消費者系統並避免死鎖
/*
static soupbuffer buff;
semaphore moreplease, soupisready;
producerthread()
{
lock(moreplease); wait until the kids ask for more.
cooksoup(buff); refill the soup pot.
unlock(soupisready); announce that soup is ready.
}
consumerthread()
{
lock(soupisready); wait until the soup is ready.
eatsoup(buff); empty the soup pot.
unlock(moreplease); ask for more soup.
}
*/
System V 提供共享記憶體。一個程序可以將一些記憶體分配為共享記憶體,然後其他程序可以附加到該記憶體,這些程序可以進行通訊。
一個程序可以使用 shmget 分配記憶體。然後,該程序可以使用 shmat 將指標指向該記憶體。
另一個程序可以使用 shmat 將指標附加到同一記憶體。
shmdt 將從共享記憶體中分離指標。
shmctl 可用於釋放分配的記憶體。
你可以使用ipcs檢查任何分配的共享記憶體,這是一種檢查你的程式是否釋放了所有分配的記憶體的好方法。ipcrm將刪除分配的共享記憶體。
訊號很酷。當你按下 Ctrl+C 來殺死一個程序時,實際上是向該程序傳送 SIGINT。類似地,Ctrl+Z 傳送 SIGTSTP,而kill -9 pid傳送第 9 個訊號 SIGKILL。手冊頁(man -s 7 signal)的第七節中列出了所有可用的訊號。
當一個程序收到一個訊號時,會呼叫相應的函式。在 SIGKILL 的情況下,該函式會導致程式碼退出。許多這些訊號可以被捕獲並重新分配給自定義函式,如下面的程式碼所示
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
void sighandler(void)
{
printf("i am un-SIGINT-able!\n");
}
int main()
{
signal(SIGINT, sighandler);
perror("signal");
while (1) {
printf("sleeping...\n");
sleep(1);
}
return 0;
}
編譯並執行上面的程式,然後嘗試使用 Ctrl+c 殺死它。現在,嘗試使用kill -s SIGKILL pid殺死該程式。
當按下 Ctrl+c 時,程序會收到 SIGINT 訊號;但是,該程序不會終止,而是會呼叫自定義函式sighandler。由於 SIGKILL 訊號未被捕獲,因此程式碼正常退出。
signal() 函式不允許你為 SIGKILL 寫入處理程式。
在 HP-UX 中,訊號到達後,訊號處理程式會被忘記。因此,通常在訊號處理程式內部,它會重新註冊自身作為處理程式
void sighandler(void)
{
printf("i am un-SIGINT-able!\n");
signal(SIGINT, sighandler);
}
在 Linux 中,這沒有必要。
SIGALRM 訊號可以透過 alarm() 函式觸發,因此它是程式內部生成訊號的好方法。以下程式碼說明了這一點
signal(SIGALRM, onintr);
alarm(3);
while (1) ; /* run forever. */
void onintr()
{
printf("3 second violation.\n");
alarm(3);
}
該程式將一直執行,直到你殺死它。
此程式碼在程序收到 SIGALARM 訊號時呼叫 SIGALARM 處理程式,然後在處理程式內部,跳出處理程式而不返回。
jmp_buf env;
main()
{
setjmp(env);
signal(SIGALRM, onintr);
}
void onintr()
{
printf("SIGALRM handler...\n");
longjmp(env);
}
可能存在一個不明顯的問題。在程序收到訊號後,它會呼叫訊號處理程式。如果在第一個訊號仍在處理時又來了另一個訊號,則該程序會忽略第二個訊號。作業系統在處理程式執行時“阻塞”訊號。
當處理程式完成時,作為其返回的一部分,作業系統允許程序接收新訊號。
如果我們使用 longjmp 跳出函式,則訊號處理程式將永遠不會返回,並且程序將永遠不會聽到傳入的任何相同型別的訊號。但是,別擔心!你可以手動**解除阻塞**訊號,然後跳出。
此備用處理程式展示瞭如何
void handler()
{
sigset_t set;
sigempty(&set);
sigaddset(&set, SIGALRM);
sigprocmask(SIG_UNBLOCK, &set, NULL);
printf("3 second\n");
longjmp(env);
}
你可以使用 sigsetjmp 和 siglongjmp 函式跳出訊號處理程式並選擇性地重置訊號掩碼。下面的程式碼展示了一個例子。你必須使用 Ctrl-C 來結束程式
#include <stdio.h>
#include <signal.h>
#include <setjmp.h>
#include <unistd.h>
sigjmp_buf env;
void sh()
{
printf("handler received signal.\n");
siglongjmp(env, 99);
}
int main ()
{
int k;
k = sigsetjmp(env, 1);
if (k == 0) {
printf("setting alarm for 4 seconds in the future.\n");
signal(SIGALRM, sh);
alarm(4);
while (1) {
printf("sleeping...\n");
sleep(1);
}
}
else {
printf("k: %d.\n", k);
alarm(3);
printf("set alarm for another 3 seconds in the future.\n");
while (1) {
printf("sleeping...\n");
sleep(1);
}
}
return 0;
}
如果你想等待一個事件,但不想使用某種自旋鎖,你可以使用pause函式來掛起正在執行的程序,直到發生訊號。
示例程式碼
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
void sh()
{
printf("Caught SIGALRM\n");
}
int main()
{
signal(SIGALRM, sh);
printf("Registered sh as a SIGALRM signal handler.\n"
"BTW, sh is %d and &sh is %d.\n" , (int) sh, (int) &sh);
alarm(3);
printf("Alarm scheduled in three seconds. Calling pause()...\n");
printf("RAHUL IS IN WIKIPEDIA WEBSITE AT CHITKARA UNIVERSITY NEAR CHANDIGARH");
pause();
printf("pause() must have returned.\n");
return 0;
}
還要注意,第二個列印語句將函式 sh 轉換為整數,並將 sh 的地址也轉換為整數。以下是程式的輸出
$ ./pausefun Registered sh as a SIGALRM signal handler. BTW, sh is 134513700 and &sh is 134513700. Alarm scheduled in three seconds. Calling pause()... Caught SIGALRM pause() must have returned.
請注意,sh 轉換為整數後的值與 sh 的地址相同。134513700 是 sh 函式文字部分的記憶體地址。儘管乍一看可能有點奇怪,但從作業系統的角度來看,函式實際上只是另一種資料。
訊息傳遞不同於透過管道進行通訊,因為接收方可以從訊息佇列中選擇特定訊息。使用管道時,接收方只是從前面獲取一定數量的位元組。
type 引數允許優先順序排程。如果 msgrcv 的第 4 個引數 > 0,則系統將找到第一個型別等於指定第 4 個引數的型別。
如果型別 < 0,則系統將找到第一個型別低於型別的絕對值的型別。
如果我們有這些訊息
Name:Type A:400 B:200 C:300 D:200 E:100
那麼型別 -250 將返回 B。
以下兩個程式展示了訊息傳遞。
mpi1.c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define KEY1 9870
#define KEY2 7890
#define PERM 0666
typedef struct mymsgbuf
{
int type;
char msg[64];
} mtype;
int main()
{
int msg1, msg2, i;
mtype m1, m2;
msg1 = msgget(KEY1, PERM | IPC_CREAT);
msg2 = msgget(KEY2, PERM | IPC_CREAT);
msgrcv(msg2, &m2, 64, 0, 0);
printf("mpi1 received msg: \"%s\" from message queue %d.\n", m2.msg, msg2);
m1.type = 20;
strcpy(m1.msg, "I doubt");
printf("mpi1 about to send \"%s\" to message queue %d.\n", m1.msg, msg1);
i = msgsnd(msg1, &m1, 64, 0);
printf("mpi1 sent \"%s\" to message queue %d with return code %d.\n", m1.msg, msg1, i);
sleep(1);
msgctl(msg1, IPC_RMID, (struct msqid_ds *) 0);
msgctl(msg2, IPC_RMID, (struct msqid_ds *) 0);
return 0;
}
mpi2.c
#define KEY1 9870
#define KEY2 7890
#define PERM 0666
typedef struct mymsgbuf
{
int type;
char msg[64];
} mtype;
int main()
{
int msg1, msg2, i;
mtype m1, m2;
m1.type = 10;
strcpy(m1.msg, "Indians will win...");
/* attach to message queues. */
msg1 = msgget(KEY1, PERM);
msg2 = msgget(KEY2, PERM);
if (msg1 == -1 || msg2 == -1) perror("mpi2 msgget");
/* send message to mpi1. */
printf("mpi2 about to send message \"%s\" to message queue %d.\n", m1.msg, msg2);
msgsnd(msg2, &m1, 64, 0);
/* send message to mpi2. */
strcpy(m2.msg, "...");
/*
msgrcv(msg2, &m2, 64, 0, 0);
*/
i = msgrcv(msg1, &m2, 64, 0, 0);
if (i == -1) perror("mpi2 msgrcv");
printf("mpi2 received msg: \"%s\" from message queue %d with return code %d.\n", m2.msg, msg1, i);
/* cleanup. */
msgctl(msg1, IPC_RMID, (struct msqid_ds *) 0);
msgctl(msg2, IPC_RMID, (struct msqid_ds *) 0);
return 0;
}
現在將 mpi1.c 編譯為 mpi1,並將 mpi2.c 編譯為 mpi2
$ gcc -o mpi1 mpi1.c $ gcc -o mpi2 mpi2.c
現在在後臺執行 mpi1,然後執行 mpi2
$ ./mpi1 & [1] 29151 $ ./mpi2 mpi2 about to send message "Indians will win..." to message queue 2195457. mpi1 received msg: "Indians will win..." from message queue 2195457. mpi1 about to send "I doubt" to message queue 2162688. mpi1 sent "I doubt" to message queue 2162688 with return code 0. mpi2 received msg: "I doubt" from message queue 2162688 with return code 64. [1]+ Done ./mpi1
本節描述當一個函式呼叫另一個函式並傳遞幾個引數時,堆疊和暫存器中發生了什麼。
假設我們有以下 C 程式碼
void foo(x, y, z)
{
int i, j;
}
int a, b, c;
a = 4;
b = 5;
c = 6;
foo(a, b, c);
在幕後,首先將 c 推入堆疊,然後是 b,然後是 a,最後是呼叫函式的返回地址。這就是堆疊的樣子(最上面的內容是最新的推入物件)
-8 j -4 i 0 ebp +4 return address +8 a +12 b +16 c
我們都見過這個可愛的錯誤
$ ./try Segmentation fault
大多數情況下,你會發現類似於這樣的問題
char ss[3]; strcpy(ss, "this is a way too long string!\n");
在執行完以下命令後:char ss[3];堆疊可以將三個字元放入一個 4 位元組的字中。
-4 ; this is the space (4 bytes, 1 word) allocated for ss[3]. 0 ebp +4 r.a.
現在觀察當 strcpy() 命令填充堆疊時會發生什麼
-4 't', 'h', 'i' ,'s' ; we can fit the first 4 chars here, but the rest spill over.
0 ' ', 'i', 's', ' a' ; ACK! we just overwrote the old base pointer!
+4 ' ', 'w', 'a', 'y', ; now we're doomed; when this function calls it will try to hop to
; the return address stored here, but that has been overwritten.
因此,希望現在對段錯誤的含義更加清晰。你的程式試圖跳轉到程序記憶體之外的記憶體地址,而作業系統會阻止它。
7 層
- 應用層
- 表示層
- 會話層
- 傳輸層
- 網路層
- 資料鏈路層
- 物理層
在 Unix 中,前三層(應用層、表示層和會話層)通常捆綁在一起。
假設網路看起來像這樣
A---Router1---Router2---B.
如果 A 上的程序(應用程式)想要與 B 上的程序通訊,就會發生以下情況
傳輸層添加了 A 的埠和 B 的 RPC 伺服器埠。
網路層添加了 A 的 IP 地址和 B 的 IP 地址。
來自 A 的任何通訊都必須先透過兩個路由器。
資料鏈路層添加了 A 的 MAC 地址和路由器 1 的 MAC 地址。
物理層將幀轉換為物理訊號。
當訊息被路由器 1 接收時,幀被讀取併發送到 OSI 模型的底部。路由器 1 的 MAC 地址被刪除並替換為路由器 2 的 MAC 地址。
每個目錄實際上都是一個檔案。該檔案列出了該目錄中檔案和子目錄的名稱。它將名稱與索引節點關聯起來。
每個索引節點包含以下資料
- 使用者 ID
- 組 ID
- 模式
- 時間戳(分別跟蹤訪問時間、建立時間、修改時間)
- 連結計數
- 檔案大小
- 指向 10 個數據塊的直接指標,這些資料塊包含檔案內容
- 指向資料塊的單級間接指標,該資料塊指向包含檔案內容的資料塊
- 指向資料塊的雙級間接指標...
由於每個索引節點有 10 個直接地址指標,如果檔案足夠小,可以在 10 個數據塊或更少的塊中容納,那麼索引節點可以使用直接定址。
如果我們假設使用 4 個位元組來指定磁碟地址,那麼我們可以有 232 個不同的地址。如果我們將資料塊大小設定為 4 kb,那麼一個數據塊可以容納 1000 個地址。因此,使用單級間接定址,我們可以指向使用 1000 個數據塊的資料的檔案。
因此,單級間接定址允許的最大檔案大小(假設 4kb 資料塊和 4 位元組地址)為 4 兆位元組(1000 個數據塊 * 每個資料塊 4 kb)。
簡單公式:資料塊大小 / 磁碟地址 = 最大檔案大小。
當然,在雙級或三級(或四級、五級等)定址中,你只需要將分析擴充套件到更遠。
| 檔案大小 | 使用的塊 |
|---|---|
| 1 塊 | 透過直接定址使用 1 塊。 |
| 11 塊 | 使用 12 塊:透過直接定址使用 10 塊資料, 1 塊用於間接定址,1 塊用於資料。 |
| 150 塊 | 透過直接定址使用 10 塊, 透過單級定址使用 1 + 128 塊, 1 + 1 + 12 1 雙級間接 + 間接 + 12 個數據塊 |
資料塊大小(小 vs. 大)各有優缺點。許多小檔案和大資料塊會導致低利用率。例如,你的 .vimrc 檔案可能只有 300 位元組的資料,但它必須至少使用一個數據塊,所以如果你的資料塊大小為 4kb,那麼就會浪費很多空間。
但是,小塊會導致大檔案散佈在許多資料塊中,因此硬碟讀取器必須在所有地方跳躍來收集資料。大資料塊可以減少碎片。
隨著每個檔案塊數的增加,磁碟 IO 成本也會增加。
每個目錄都有一個表,該表將檔名對映到索引節點號。一個檔案可以有任意數量的與之關聯的名稱。每個索引節點都跟蹤其連結計數。命令 rm foo.txt 編輯目錄並刪除將 foo.txt 對映到特定索引節點的條目。如果該索引節點沒有連結,那麼它將被刪除。
此時,你可能想閱讀 ln 的手冊頁以瞭解軟連結和硬連結之間的區別。
與單體系統相比,分散式系統在今天發生故障的可能性要低得多,即使分散式系統在今天某一部分發生故障的可能性要高得多。高可用性分散式系統使用受 RAID(廉價磁碟冗餘陣列)啟發的技術,可以容忍任何一個部分的故障,而不會丟失資料或功能。
更多詳細資訊,請參閱
設定使用者 ID 位(有時簡稱為 setuid 或 suid)會改變作業系統處理許可權的方式。例如,chsh 命令允許使用者更改其登入 shell,例如從 bash 更改為 tcsh。每個使用者的登入 shell 都儲存在 /etc/passwd 中,但使用者沒有寫入許可權。那麼使用者如何執行 chsh 並更改該檔案呢?答案是使用 suid 位。檢視 chsh 的許可權
$ ls -l `which chsh` -rwsr-xr-x 1 root root 28088 2004-11-02 16:51 /usr/bin/chsh
那麼,其中的 s 是什麼意思呢?我的朋友,這就是設定使用者 ID 位,這意味著該程式執行時就像由 root 執行一樣。
設定了 setuid 位(或類似的 setgid 位)的程式將使用程式擁有者的使用者或組的許可權執行。
遠端過程呼叫 (RPC) 是一種隱藏在遠端機器上執行程序的所有細節的方法。
如果 A 想要對主機 B 發出遠端過程呼叫,那麼 A 需要 B 的 RPC 伺服器埠。
RPC 幀包括
- 函式編號
- 引數
命令 rpcinfo 將列出機器上執行的 RPC 服務。
通常,開發 RPC 程式涉及八個步驟
- 構建和測試傳統應用程式。
- 透過選擇要移到遠端機器的一組過程來劃分程式。將選定的過程放入單獨的檔案中。
- 為遠端程式編寫 rpcgen 規範檔案。選擇一個程式號和版本號。規範檔案應該以 .x 字尾命名。例如,遠端資料庫程式的規範檔案可以是
rdb.x. - 執行 rpcgen 命令以檢查規範檔案。如果檔案有效,rpcgen 將生成用於構建客戶端和伺服器的原始碼。例如
rpcgen -C rdb.x
將生成 4 個輸出檔案:rdb.h rdb_clnt.c rdb_svc.c rdb_xdr.c
- 為客戶端和伺服器端編寫存根介面例程。你可以使用步驟 1 中開發的程式碼。例如,你可以編寫一個
rdb.c檔案來處理客戶端介面,以及一個rdb_svc_proc.c來處理伺服器端的工作。 - 編譯和連結客戶端程式。
gcc -o rdb rdb.c rdb_clnt.c rdb_xdr.c
- 編譯和連結伺服器程式。
gcc -o rdb_svc rdb_svc_proc.c rdb_svc.c rdb_xdr.c
- 啟動伺服器並呼叫客戶端。
分散式演算法的特性
- 資訊可能散佈在多臺機器上。
- 每個程序根據本地資訊做出決策。
- 機器可以共享資訊。
- 單個故障點不應導致整個系統崩潰。
- 機器不能依賴單個公共時鐘或全域性時間。
一個關於非全域性時鐘導致問題的簡單例子
- 你在機器 A 上編譯 foo.c(透過 make)生成 foo.o。
- 你登入到機器 B,它的時鐘比 B 慢 15 分鐘。
- 你再次編輯 foo.c,然後執行 make。make 會比較 foo.o 和 foo.c 的時間戳,而 foo.o 仍然較新,因此 make 不會重新編譯 foo.o。
物理時鐘與“實際時間”一致,例如協調世界時。
邏輯時鐘不關心與某個全域性標準的準確性。相反,系統中的所有機器都試圖彼此達成一致。
保持本地物理時鐘同步很困難,因為即使機器可以聯絡權威時間伺服器,也存在一定的傳播延遲。
機器需要多久向時間伺服器請求一次更新?假設機器有一個時鐘 ,以 的速率遞增。
UTC 時鐘 以 的速率遞增。
如果 ,則本地物理時鐘 C 完美無缺,不需要更新。
如果 ,則本地物理時鐘 C 執行速度更快。
如果 ,則本地物理時鐘 C 的執行速度比時間伺服器慢。
代表製造商指定的最大漂移率。
代表最大容許誤差。每隔 週期,機器必須請求更新。
隨著容許誤差 增大,更新頻率降低。隨著漂移率 上升,更新頻率也隨之增加。
使用它們來做 C++、python、Java 等語言中丟擲異常的操作。首先呼叫 setjmp 設定要跳轉到的位置。然後其他函式可以呼叫 longjump 退出當前函式,並跳轉到 setjmp 指定的位置。
換句話說,printf(...) 是如何工作的?
一個例子
main()
{
#ifdef DEBUG
printf("this is a debug statement!\n");
#endif
}
$ gcc -DDEBUG prog.c; ./a.out this is a debug statement!
$gcc prog.c; ./a.out $
在第二次編譯中,printf 程式碼塊沒有被包含。
另一個類似的技巧用於防止重複包含同一個標頭檔案。一個 C 程式通常有多個 .c 檔案,這些檔案包含多個 .h 檔案。你可以在 .h 檔案的開頭新增這段程式碼,以防止同一個 .h 檔案被重複包含。
#ifndef _FOO_H #define _FOO_H 1 /* function foo is a worn-out cliche. */ int foo(); #endif
Makefile 中允許四種類型的語句:宏、依賴規則、命令和註釋。以下是一個簡單的 Makefile 例子。
CC = /usr/bin/gcc CFLAGS = -Wall -pedantic -g #saves me the trouble of typing all those flags. proxy: proxy.c proxy.h $(CC) $(CFLAGS) proxy proxy.c debug-proxy: proxy.c proxy.h $(CC) $(CFLAGS) -DDEBUG -o proxy proxy.c clean: /bin/rm -f *~ httpd a.out httpd.log proxy proxy.log CACHE.*
我可以透過鍵入 make 和名稱來執行任何語句。或者預設情況下,如果 make 沒有收到引數,它會執行遇到的第一個規則。
$ make # runs make proxy $ make clean #runs /bin/rm -f ....
宏在聲明後可以透過 $(CFLAGS) 或 ${CFLAGS} 訪問。$CFLAGS 無效。如果宏是一個字母,比如
A = a.out
那麼 $A 是有效的。但無論如何,你應該避免這樣做,因為許多單字元宏已經具有含義。
依賴規則下方的每個製表符都會啟動一個新的 shell,所以如果你想更改 shell 內容,你必須在一行中完成所有操作。
bogus:
echo $$PWD; cd ..; echo $$PWD;
echo $$PWD;
輸出
$ make bogus echo $PWD; cd ..; echo $PWD /home/student/wiwilson/cis620 /home/student/wiwilson echo $pwd
順便說一下,如果你想抑制對命令的預設回顯,在開頭加上一個 @。
bored:
echo "meh"
@echo "snah"
這裡是輸出
$ make bored echo "meh" meh snah
一個 make 命令如何遞迴地呼叫子目錄中的 make
SRCDIR = src1 src2
OBJ = src1/f1.o src2/f2.o
snake: ${SRCDIR} ${OBJ}
${CC} -o snake ${OBJ}
${SRCDIR}: /tmp
cd $@; make
這最初讓我感到困惑。$@ 符號指的是規則的目標,在本例中是 ${SRCDIR}。
你可以在 Makefile 中使用 makedepend 來自動解決所有那些複雜的標頭檔案問題。
CFILES = main.c foo.c bar.c
depend:
makedepend $(CFILES)
# don't edit below here.
待辦事項:顯示 Makefile 之前和之後的示例。
如果你有許多檔案,並且你想將它們都編譯成目標檔案,這個規則就可以工作。
.c.o:
$(CC) -c $<
將 C 轉換為彙編
[edit | edit source]gcc -S try.c
這會將你的程式轉換為彙編程式碼,使用 GNU 語法。gcc 會建立一個名為 try.s 的檔案。
gcc -S -masm=intel try.c
這會將你的程式碼轉換為使用 intel 語法的彙編程式碼。這可能只在 intel CPU 上有效。
你可以像這樣將 try.s 轉換為可執行檔案
gcc try.s
你甚至可以編譯可執行檔案以供 gdb 使用
gcc -g try.s
然後你可以在 gdb 中逐步執行底層的彙編程式碼。萬歲!
附錄 B:習題解答
[edit | edit source]4.1.1 POSIX 執行緒
[edit | edit source]此練習的任務是重寫示例程式碼以使用 fork() 而不是執行緒。你應該得到類似以下內容的東西
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
/*
* The name of our process. This lets us tell the difference when we
* do output.
*/
const char* name;
/*
* Our counter variable. Because this is all in one function, it need not
* be global.
*/
int i;
i = 0;
pid_t pid = fork();
if(pid == 0)
{
name = "Child";
}
else
{
name = "Parent";
}
/* Our output loop. */
int j;
for(j = 0; j < 10; j++)
{
sleep(1);
printf("%s process: %d\n", name, i);
i++;
}
return 0;
}
你還被問及輸出在哪些方面不同以及原因。在使用 POSIX 執行緒的版本中,計數器每秒遞增兩次:一次由每個執行緒遞增。當使用 fork() 時,它只遞增了一次。原因是,當呼叫 fork() 時,子程序會寫入它自己的計數器副本。然而,當使用執行緒時,兩個執行緒共享它們的記憶體空間。這意味著它們都在寫入計數器的同一個副本,並且它遞增了兩次。
貢獻者
[edit | edit source]如果你有任何貢獻,請給自己記功!
Matthew Wilson 在 2004 年秋季開始了這本教科書,當時他正在克利夫蘭州立大學修讀研究生比較作業系統介面課程。