一個現有進程可以調用fork函數創建一個新進程。
#include <unistd.h>pid_t fork( void );返回值:子進程中返回0,父進程中返回子進程ID,出錯返回-1
由fork創建的新進程被稱為子進程(child PRocess)。fork函數被調用一次,但返回兩次。兩次返回的唯一區別是子進程的返回值是0,而父進程的返回值則是新子進程的進程ID。將子進程ID返回給父進程的理由是:因為一個進程的子進程可以有多個,并且沒有一個函數使一個進程可以獲得其所有子進程的進程ID。fork使子進程得到返回值0的理由是:一個進程只會有一個父進程,所以子進程總是可以調用getppid以獲得其父進程的進程ID(進程ID 0總是由內核交換進程使用,所以一個子進程的進程ID不可能為0)。
子進程和父進程繼續執行fork調用之后的指令。子進程是父進程的副本。例如,子進程獲得父進程的數據空間、堆和棧的副本。注意,這是子進程所擁有的副本。父、子進程并不共享這些存儲空間部分。父、子進程共享正文段(text,代碼段)。
由于在fork之后經常跟隨著exec,所以現在的很多實現并不執行一個父進程數據段、棧和堆的完全復制。作為替代,使用了寫時復制(Copy-On-Write,COW)技術。這些區域由父、子進程共享,而且內核將它們的訪問權限改變為只讀的。如果父、子進程中的任一個試圖修改這些區域,則內核只為修改區域的那塊內存制作一個副本,通常是虛擬存儲器系統中的一“頁”。
linux 2.4.22提供了另一種新進程創建函數——clone(2)系統調用。這是一種fork的泛型,它允許調用者控制哪些部分由父、子進程共享。
程序清單8-1中的程序演示了fork函數,從中可以看到子進程對變量所作的改變并不影響父進程中該變量的值。
程序清單8-1 fork函數示例
[root@localhost apue]# cat prog8-1.c#include "apue.h"int glob = 6; /* external variable in initialized data */char buf[] = "a write to stdout/n";intmain(void){ int var; /* automatic variable on the stack */ pid_t pid; var = 88; if(write(STDOUT_FILENO, buf, sizeof(buf) - 1) != sizeof(buf) -1) err_sys("write error"); printf("before fork/n"); /* we don't flush stdout */ if((pid = fork()) < 0) { err_sys("fork error"); } else if(pid == 0) /* child */ { glob++; /* modify variables */ var++; } else { sleep(2); /* parent */ } printf("pid = %d, glob = %d, var = %d/n", getpid(), glob, var); exit(0);}
如果執行此程序則得到:
[root@localhost apue]# ./prog8-1a write to stdoutbefore forkpid = 13367, glob = 7, var = 89 子進程的變量值改變了pid = 13366, glob = 6, var = 88 父進程的變量值沒有改變[root@localhost apue]# ./prog8-1 > tmp.out[root@localhost apue]# cat tmp.outa write to stdoutbefore forkpid = 13369, glob = 7, var = 89before forkpid = 13368, glob = 6, var = 88
一般來說,在fork之后是父進程先執行還是子進程先執行是不確定的。這取決于內核所使用的調度算法。如果要求父、子進程之間相互同步,則要求某種形式的進程間通信。
當寫到標準輸出時,我們將buf長度減去1作為輸出字節數,這是為了避免將終止null字節寫出。strlen計算不包括終止null字節的字符串長度,而sizeof則計算包括終止null字節的緩沖區長度。兩者之間的另一個差別是,使用strlen需進行一次函數調用,而對于sizeof而言,因為緩沖區已用已知字符串進行了初始化,其長度是固定的,所以sizeof在編譯時計算緩沖區長度。
注意程序清單8-1中fork與I/O函數之間的交互關系。write函數是不帶緩沖的。因為在fork之前調用write,所以其數據寫到標準輸出一次。但是標準I/O庫是帶緩沖的(這里用到了標準I/O庫的printf函數)。如果標準輸出連到終端設備,則它是行緩沖的,否則它是全緩沖的。當以交互方式運行該程序時(此時是行緩沖的),只得到該printf輸出的行一次,其原因是標準輸出緩沖區在fork之前已由換行符沖洗。但是當將標準輸出重定向到一個文件時(此時是全緩沖的),卻得到printf輸出行兩次。其原因是,在fork之前調用了printf一次,但當調用fork時,該行數據仍在緩沖區中(我們沒有用fflush沖洗緩沖區),然后在將父進程數據空間復制到子進程中時,該緩沖區也被復制到子進程中。于是那時父、子進程各自有了帶該行內容的標準I/O緩沖區。(子進程復制父進程緩沖區對程序的影響實例解析可參考:http://blog.csdn.net/lollipop_jin/article/details/8774057)在exit之前的第二個printf將其數據添加到現有的緩沖區中。當每個進程終止時,最終會沖洗其緩沖區中的副本。
文件共享對程序清單8-1需注意的另一點是:在重定向父進程的標準輸出時,子進程的標準輸出也被重定向。實際上,fork的一個特性是父進程的所有打開文件描述符都被復制到子進程中。父、子進程的每個相同的打開描述符共享一個文件表項。
考慮下述情況,一個進程具有三個不同的打開文件,它們是標準輸入、標準輸出和標準出錯。在從fork返回時,我們有了如圖8-1所示的結構。
這種共享文件的方式使父、子進程對同一文件使用了一個文件偏移量。如果父、子進程寫到同一描述符文件,但又沒有任何形式的同步(例如使父進程等待子進程),那么它們的輸出就會相互混合(假定所有的描述符是在fork之前打開的)。
在fork之后處理文件描述符有兩種常見的情況:
(1)父進程等待子進程完成。在這種情況下,父進程無需對其描述符做任何處理。當子進程終止后,它曾進行過讀、寫操作的任一共享描述符的文件偏移量已執行了相應的更新。
(2)父、子進程各自執行不同的程序段。在這種情況下,在fork之后,父、子進程各自關閉它們不需要使用的文件描述符,這樣就不會干擾對方使用的文件描述符。這種方法是網絡服務進程中經常使用的。
除了打開文件之外,父進程的很多其他屬性也由子進程繼承(可以理解為共享),包括:
父、子進程之間的區別是:
使fork失敗的兩個主要原因是:系統中已經有了太多的進程(通常意味著某個方面出了問題),或者該實際用戶ID的進程總數超過了系統限制(CHILD_MAX)。
fork有下面兩種用法:
(1)一個父進程希望復制自己,使父、子進程同時執行不同的代碼段。這在網絡服務進程中是常見的——父進程等待客戶端的服務請求。當這種請求到達時,父進程調用fork,使子進程處理此請求。父進程則繼續等待下一個服務請求到達。
(2)一個進程要執行一個不同的程序。這對shell是常見的情況。在這種情況下,子進程從fork返回后立即調用exec。
某些操作系統將(2)中的兩個操作(fork之后執行exec)組合成一個,并稱其為spawn。UNIX將這兩個操作分開,因為在很多場合需要單獨使用fork,其后并不跟隨exec。另外,將這兩個操作分開,使得子進程在fork和exec之間可以更改自己的屬性。例如I/O重定向、用戶ID、信號安排等。
本篇博文內容摘自《UNIX環境高級編程》(第二版),僅作個人學習記錄所用。關于本書可參考:http://www.apuebook.com/。
新聞熱點
疑難解答