lienhua34 在“進(jìn)程控制三部曲”中,我們學(xué)習(xí)到了 fork 是三部曲的第一部,用于創(chuàng)建一個新進(jìn)程,。但是關(guān)于 fork 的更深入的一些的東西我們還沒有涉及到,例如,fork 創(chuàng)建的新進(jìn)程與調(diào)用進(jìn)程之間的關(guān)系,、父子進(jìn)程的數(shù)據(jù)共享問題等,。fork 是否可以無限制的調(diào)用?如果不行的話,最大限制是多少?另外,我們還將學(xué)習(xí)一個 fork 的變體 vfork,。 1 fork 創(chuàng)建的新進(jìn)程與調(diào)用進(jìn)程之間的關(guān)系UNIX 操作系統(tǒng)中的所有進(jìn)程之間的關(guān)系呈現(xiàn)一個樹形結(jié)構(gòu),。除了進(jìn)程 ID 為 0(swapper 進(jìn)程)和 1(init 進(jìn)程)的進(jìn)程之外的其他進(jìn)程,都會存在一個父進(jìn)程,。 fork 函數(shù)調(diào)用產(chǎn)生的新進(jìn)程的父進(jìn)程默認(rèn)即為調(diào)用進(jìn)程,。fork 函數(shù)調(diào)用產(chǎn)生的父子進(jìn)程各自的運行時間是不確定的。如果子進(jìn)程先于父進(jìn)程終止,這樣沒有什么問題,。但,如果父進(jìn)程先于子進(jìn)程終止,那么子進(jìn)程是不是就沒有了父進(jìn)程,進(jìn)程樹形結(jié)構(gòu)就被破壞了?對于這個問題,UNIX 系統(tǒng)這么處理的:如果某個進(jìn)程終止了,則將該進(jìn)程的所有尚未結(jié)束的子進(jìn)程的父進(jìn)程設(shè)置為 init 進(jìn)程(init 進(jìn)程是絕不會終止的),。其操作過程大致為:在一個進(jìn)程終止時,內(nèi)核逐個檢查所有活動進(jìn)程(因為 UNIX 沒有提供一個獲取某個進(jìn)程所有子進(jìn)程的接口),如果是正在終止的進(jìn)程的子進(jìn)程,則將其父進(jìn)程設(shè)置為 init 進(jìn)程。 2 父子進(jìn)程的數(shù)據(jù)共享問題fork 函數(shù)創(chuàng)建的子進(jìn)程會獲得父進(jìn)程的數(shù)據(jù)空間,、堆和棧的副本,。但是,大多數(shù)情況下,fork 之后都會緊接著調(diào)用 exec 執(zhí)行新程序,從而覆蓋了從父進(jìn)程拷貝的這些副本,這就造成了內(nèi)核做了很多無用功。 現(xiàn)在很多的實現(xiàn)都采用寫時復(fù)制(Copy-On-Write,COW)技術(shù),。fork函數(shù)調(diào)用之后,父子進(jìn)程共享這些區(qū)域,而且內(nèi)核將這些區(qū)域的權(quán)限改為只讀的,。如果父、子進(jìn)程中任何一個試圖修改這些區(qū)域,則內(nèi)核只為要修改的區(qū)域做一份拷貝給該進(jìn)程,。 下面我們來看一個共享數(shù)據(jù)的例子, #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> int glob = 0; int main(void) { int var; pid_t pid; var = 0; if ((pid = fork()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid == 0) { var++; glob++; printf("child: glob=%d, var=%d\n", glob, var); exit(0); } wait(NULL); printf("parent: glob=%d, var=%d\n", glob, var); exit(0); } 該程序在 fork 之后的父進(jìn)程等待子進(jìn)程結(jié)束,而子進(jìn)程將整型變量glob 和 var 都加了 1. 編譯該程序,生成并執(zhí)行 forkdemo. 從下面的運行結(jié)果,我們看到子進(jìn)程修改的 glob 和 var 變量對父進(jìn)程沒有任何影響,。 lienhua34:demo$ gcc -o forkdemo forkdemo.c lienhua34:demo$ ./forkdemo child: glob=1, var=1 parent: glob=0, var=0 雖說子進(jìn)程享用的是父進(jìn)程的數(shù)據(jù)副本,子進(jìn)程的修改對父進(jìn)程沒有任何影響。但有個比較特殊的情況:文件 I/O,。fork 會將父進(jìn)程的所有打開文件描述符都復(fù)制到子進(jìn)程,。父子進(jìn)程中相同的文件描述符則共享同一個文件表項(關(guān)于文件描述符和文件表項的關(guān)系請參考文檔“內(nèi)核 I/O 數(shù)據(jù)結(jié)構(gòu)”)。下面我們看一個例子, #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> int main(void) { pid_t pid; printf("before fork\n"); if ((pid = fork()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid == 0) { printf("in child process\n"); exit(0); } wait(NULL); printf("in parent process\n"); exit(0); } 編譯該程序,生成并執(zhí)行文件 forkdemo, lienhua34:demo$ gcc -o forkdemo forkdemo.c lienhua34:demo$ ./forkdemo before fork in child process in parent process lienhua34:demo$ ./forkdemo > foo lienhua34:demo$ cat foo before fork in child process before fork in parent process 在沒有對標(biāo)準(zhǔn)輸出重定向之前,運行 forkdemo 看不出啥問題,。當(dāng)重定向標(biāo)準(zhǔn)輸出到一個文件(./forkdemo > foo)時,我們可以看到父進(jìn)程打印的字符串在子進(jìn)程打印的字符串之后,。這是因為父子進(jìn)程標(biāo)準(zhǔn)輸出共享了同一個文件表項,也即共享了同一個文件偏移量。 另外,我們注意到在標(biāo)準(zhǔn)輸出沒有重定向時,字符串“before fork”只輸出一次,但是在標(biāo)準(zhǔn)輸出重定向到文件之后輸出了兩次,。這是因為標(biāo)準(zhǔn)I/O 庫函數(shù) printf 在標(biāo)準(zhǔn)輸出連接到終端設(shè)備時是行緩沖的,于是在 fork函數(shù)之后,緩沖區(qū)中的數(shù)據(jù)已經(jīng)被沖洗了,。而當(dāng)標(biāo)準(zhǔn)輸出重定向文件之后,printf 函數(shù)就變成了全緩沖了,在 fork 之前調(diào)用 printf 函數(shù)將字符串“before fork”寫到緩沖區(qū)中,fork 時該字符串還在緩沖區(qū)中,于是便拷貝一份給子進(jìn)程,。當(dāng)父子進(jìn)程都調(diào)用 exit 函數(shù)之后,緩沖區(qū)中的數(shù)據(jù)都被沖洗到文件中,于是被出現(xiàn)了兩份“before fork”。 3 fork 典型應(yīng)用場景fork 有兩種典型的應(yīng)用場景: · 創(chuàng)建一個新進(jìn)程執(zhí)行新的程序,。即調(diào)用 fork 之后子進(jìn)程立即調(diào)用 exec函數(shù)執(zhí)行一個新程序,例如文檔“進(jìn)程控制三部曲”中的示例 2. · 父進(jìn)程希望復(fù)制自己,使父,、子進(jìn)程同時執(zhí)行不同的代碼段。這在網(wǎng)絡(luò)服務(wù)進(jìn)程中比較常見:父進(jìn)程等待客戶端的服務(wù)請求,當(dāng)接收到一個請求之后,父進(jìn)程調(diào)用 fork,然后讓子進(jìn)程處理該請求,而父進(jìn)程繼續(xù)等待下一個服務(wù)請求,。其代碼框架如下所示: void serve(int sockfd) { int clfd; pid_t pid; for (;;) { clfd = accept(sockfd, NULL, NULL); if (clfd < 0) { /* print error message */ continue; } if ((pid = fork()) < 0) { /* fork error */ continue; } else if (pid == 0) { /* deal with clfd in child process */ close(clfd); exit(0); } else { /* in parent process, close the accepted socket "clfd", then continues to listen next socket connection. */ } } } 4 fork 函數(shù)調(diào)用次數(shù)的最大限制是多少每個實際用戶 ID 具有一個在任何時刻的最大進(jìn)程數(shù)。CHILD_MAX 規(guī)定了每個實際用戶 ID 在任一時刻可具有的最大進(jìn)程數(shù),。我們看下面一個例子, #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> int main(void) { pid_t pid; int count; printf("CHILD_MAX: %ld\n", sysconf(_SC_CHILD_MAX)); count = 1; for (;;) { if ((pid = fork()) < 0) { printf("fork error: %s\n", strerror(errno)); break; } else if (pid == 0) { sleep(3); exit(0); } count++; } printf("count: %d\n", count); exit(0); } 編譯該程序,生成并運行文件 forkdemo, lienhua34:demo$ gcc -o forkdemo forkdemo.c lienhua34:demo$ ./forkdemo CHILD_MAX: 15969 fork error: Resource temporarily unavailable count: 15737 從上面的運行結(jié)果可以看出我的系統(tǒng)規(guī)定了每個實際用戶 ID 在任一時刻可具有的最大進(jìn)程數(shù)為 15969,。而在 for 循環(huán)中 fork 創(chuàng)建了 15737 個進(jìn)程(包括調(diào)用進(jìn)程本身)之后,fork 就因為沒有可用資源而創(chuàng)建新進(jìn)程失敗。 5 fork 的變體vforkvfork 函數(shù)是 fork 函數(shù)的一個變體,其調(diào)用序列和返回值與 fork 函數(shù)一致,不過兩者的語義不同,。維基百科上關(guān)于 vfork 的說明如下(參考fork(system_call)),。
我們看到在 POSIX 2004 版本中已經(jīng)將 vfork 函數(shù)注為過時的,而且在之后的版本中已經(jīng)不再出現(xiàn) vfork 函數(shù)了。但是,既然《APUE》中講到了這個,那我們就來看一下 vfork 函數(shù)跟 fork 函數(shù)到底有什么區(qū)別吧,。 vfork 函數(shù)和 fork 函數(shù)的區(qū)別有兩點: 1. fork 會將父進(jìn)程的地址空間拷貝給子進(jìn)程;而 vfork 沒有,子進(jìn)程在父進(jìn)程的地址空間中運行,。 2. fork 無法確保父子進(jìn)程的執(zhí)行順序;而 vfork 保證子進(jìn)程先執(zhí)行,父進(jìn)程會一直阻塞直到子進(jìn)程調(diào)用 exit 或 exec。(注:vfork 的這個特征可能會導(dǎo)致死鎖,若子進(jìn)程在調(diào)用 exit 或 exec 之前依賴于父進(jìn)程的進(jìn)一步動作,而父進(jìn)程也正在等待子進(jìn)程,于是出現(xiàn)了循環(huán)等待的問題,。) 我們來對比一下 vfork 和 fork 在處理數(shù)據(jù)方面有什么不同, #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> int glob = 0; int main(void) { int var; pid_t pid; var = 0; if ((pid = vfork()) < 0) { printf("fork error: %s\n", strerror(errno)); exit(-1); } else if (pid == 0) { var++; glob++; printf("child: glob=%d, var=%d\n", glob, var); exit(0); } printf("parent: glob=%d, var=%d\n", glob, var); exit(0); } 上面程序拷貝了上面 fork 函數(shù)處理共享數(shù)據(jù)的示例程序,將 fork 改成vfork,并且去掉了 wait(NULL) 語句,。保存為 vforkdemo.c,編譯該程序,生成并執(zhí)行 vforkdemo 文件, lienhua34:demo$ gcc -o vforkdemo vforkdemo.c lienhua34:demo$ ./vforkdemo child: glob=1, var=1 parent: glob=1, var=1 從上面的運行結(jié)果,我們看到 vfork 創(chuàng)建的子進(jìn)程修改了 glob 和 var變量之后,父進(jìn)程也看到了這個修改。 vfork 函數(shù)的出現(xiàn)原因可能是早期系統(tǒng)的 fork 沒有實現(xiàn)寫時復(fù)制技術(shù),導(dǎo)致每次 fork 調(diào)用做了很多無用功(大多數(shù)情況下都是 fork 之后調(diào)用 exec執(zhí)行新程序)且效率不高,于是便創(chuàng)造了 vfork 函數(shù),。而現(xiàn)在的實現(xiàn)基本都是采用寫時復(fù)制技術(shù),而且 vfork 函數(shù)使用不當(dāng)還會出現(xiàn)死鎖,于是 vfork函數(shù)也便沒有了存在的必要性,。 (done) |
|