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