fork() 函數詳解
轉自 《unix 環境高級編程》+ 個人補充
一個現存進程調用f o r k函數是U N I X內核創建一個新進程的唯一方法(這並不適用於前節提 及的交換進程、i n i t進程和頁精靈進程。這些進程是由內核作爲自舉過程的一部分以特殊方式 創建的)。
#i nclude <sys/types.h> #i nclude <unistd.h> pid_t fork(void); 返回:子進程中爲0,父進程中爲子進程I D,出錯爲-1
由f o r k創建的新進程被稱爲子進程(child process)。該函數被調用一次,但返回兩次。兩次返 回的區別是子進程的返回值是0,而父進程的返回值則是新子進程的進程 I D。將子進程I D返回 給父進程的理由是:因爲一個進程的子進程可以多於一個,所以沒有一個函數使一個進程可以 獲得其所有子進程的進程I D。f o r k使子進程得到返回值0的理由是:一個進程只會有一個父進 程,所以子進程總是可以調用g e t p p i d以獲得其父進程的進程I D (進程ID 0總是由交換進程使用, 所以一個子進程的進程I D不可能爲0 )。 子進程和父進程繼續執行f o r k之後的指令。子進程是父進程的複製品。例如,子進程獲得 父進程數據空間、堆和棧的複製品。注意,這是子進程所擁有的拷貝。父、子進程並不共享這 些存儲空間部分。如果正文段是隻讀的,則父、子進程共享正文段(見7 . 6節)。 現在很多的實現並不做一個父進程數據段和堆的完全拷貝,因爲在 f o r k之後經常跟隨着 e x e c。作爲替代,使用了在寫時複製( C o p y - O n - Write, COW)的技術。這些區域由父、子進程共 享,而且內核將它們的存取許可權改變爲只讀的。如果有進程試圖修改這些區域,則內核爲有 關部分,典型的是虛存系統中的“頁”,做一個拷貝。B a c h〔1 9 8 6〕的9 . 2節和L e ff l e r等〔1 9 8 9〕 的5 . 7節對這種特徵做了更詳細的說明。
實例 程序8 - 1例示了f o r k函數。如果執行此程序則得到: $ a . o u t a write to stdout before fork pid = 430, glob = 7, var = 89 子進程的變量值改變了 pid = 429, glob = 6, var = 88 父進程的變量值沒有改變 $ a.out > temp.out $ cat temp.out a write to stdout before fork pid = 432, glob = 7, var = 89 before fork pid = 431, glob = 6, var = 88 一般來說,在f o r k之後是父進程先執行還是子進程先執行是不確定的。這取決於內核所使用的 調度算法。如果要求父、子進程之間相互同步,則要求某種形式的進程間通信。在程序 8 - 1中,父進程使自己睡眠2秒鐘,以此使子進程先執行。但並不保證 2秒鐘已經足夠,在8 . 8節說明竟 爭條件時,還將談及這一問題及其他類型的同步方法。在 1 0 . 6節中,在f o r k之後將用信號使父、子進程同步。 注意,程序8 - 1中f o r k與I / O函數之間的關係。回憶第3章中所述,w r i t e函數是不帶緩存的。 因爲在f o r k之前調用w r i t e,所以其數據寫到標準輸出一次。但是,標準 I / O庫是帶緩存的。回 憶一下5 . 1 2節,如果標準輸出連到終端設備,則它是行緩存的,否則它是全緩存的。當以交互 方式運行該程序時,只得到p r i n t f輸出的行一次,其原因是標準輸出緩存由新行符刷新。但是 當將標準輸出重新定向到一個文件時,卻得到p r i n t f輸出行兩次。其原因是,在f o r k之前調用了 p r i n t f一次,但當調用f o r k時,該行數據仍在緩存中,然後在父進程數據空間複製到子進程中時, 該緩存數據也被複制到子進程中。於是那時父、子進程各自有了帶該行內容的緩存。在 e x i t之 前的第二個p r i n t f將其數據添加到現存的緩存中。當每個進程終止時,其緩存中的內容被寫到 相應文件中。
程序8-1 fork函數實例
文件共享 對程序8 - 1需注意的另一點是:在重新定向父進程的標準輸出時,子進程的標準輸出也被 重新定向。實際上,f o r k的一個特性是所有由父進程打開的描述符都被複制到子進程中。父、 子進程每個相同的打開描述符共享一個文件表項(見圖3 - 3 )。 考慮下述情況,一個進程打開了三個不同文件,它們是:標準輸入、標準輸出和標準出錯。 在從f o r k返回時,我們有了如圖8 - 1中所示的安排。 這種共享文件的方式使父、子進程對同一文件使用了一個文件位移量。考慮下述情況:一 個進程f o r k了一個子進程,然後等待子進程終止。假定,作爲普通處理的一部分,父、子進程 都向標準輸出執行寫操作。如果父進程使其標準輸出重新定向 (很可能是由s h e l l實現的),那麼 子進程寫到該標準輸出時,它將更新與父進程共享的該文件的位移量。在我們所考慮的例子中, 當父進程等待子進程時,子進程寫到標準輸出;而在子進程終止後,父進程也寫到標準輸出上,
並且知道其輸出會添加在子進程所寫數據之後。如果父、子進程不共享同一文件位移量,這種 形式的交互就很難實現。
如果父、子進程寫到同一描述符文件,但又沒有任何形式的同步(例如使父進程等待子進 程),那麼它們的輸出就會相互混合(假定所用的描述符是在 f o r k之前打開的)。雖然這種情況 是可能發生的(見程序8 - 1),但這並不是常用的操作方式。 在f o r k之後處理文件描述符有兩種常見的情況: (1) 父進程等待子進程完成。在這種情況下,父進程無需對其描述符做任何處理。當子進 程終止後,它曾進行過讀、寫操作的任一共享描述符的文件位移量已做了相應更新。 (2) 父、子進程各自執行不同的程序段。在這種情況下,在f o r k之後,父、子進程各自關閉 它們不需使用的文件描述符,並且不干擾對方使用的文件描述符。這種方法是網絡服務進程中 經常使用的。 除了打開文件之外,很多父進程的其他性質也由子進程繼承: * 實際用戶I D、實際組I D、有效用戶I D、有效組I D。 * 添加組I D。 * 進程組I D。 * 對話期I D。 * 控制終端。 * 設置-用戶- I D標誌和設置-組- I D標誌。 * 當前工作目錄。
* 根目錄。 * 文件方式創建屏蔽字。 * 信號屏蔽和排列。 * 對任一打開文件描述符的在執行時關閉標誌。 * 環境。 * 連接的共享存儲段。 * 資源限制。 父、子進程之間的區別是: * fork的返回值。 * 進程I D。 * 不同的父進程I D。 * 子進程的t m s _ u t i m e , t m s _ s t i m e , t m s _ c u t i m e以及t m s _ u s t i m e設置爲0。 * 父進程設置的鎖,子進程不繼承。 * 子進程的未決告警被清除。 * 子進程的未決信號集設置爲空集。 其中很多特性至今尚末討論過,我們將在以後幾章中對它們進行說明。 使f o r k失敗的兩個主要原因是:( a )系統中已經有了太多的進程(通常意味着某個方面出了問 題),或者( b )該實際用戶I D的進程總數超過了系統限制。回憶表2 - 7,其中C H I L D _ M A X規定了 每個實際用戶I D在任一時刻可具有的最大進程數。 f o r k有兩種用法: (1) 一個父進程希望複製自己,使父、子進程同時執行不同的代碼段。這在網絡服務進程 中是常見的——父進程等待委託者的服務請求。當這種請求到達時,父進程調用 f o r k,使子進 程處理此請求。父進程則繼續等待下一個服務請求。 (2) 一個進程要執行一個不同的程序。這對 s h e l l是常見的情況。在這種情況下,子進程在 從f o r k返回後立即調用e x e c (我們將在8 . 9節說明e x e c )。 某些操作系統將( 2 )中的兩個操作( f o r k之後執行e x e c )組合成一個,並稱其爲s p a w n。U N I X 將這兩個操作分開,因爲在很多場合需要單獨使用 f o r k,其後並不跟隨e x e c。另外,將這兩個 操作分開,使得子進程在f o r k和e x e c之間可以更改自己的屬性。例如I / O重新定向、用戶I D、信 號排列等。在第1 4章中有很多這方面的例子。
自己試驗得結論:
#i nclude <unistd.h> #i nclude <stdio.h> int main() { int pid; int pid2; //printf("%d ",getppid()); printf("=============/n"); printf("LLLLLLLLLLLLL"); if((pid=fork())==0){ printf("This is the child process:%d/n",pid); } else { if((pid2=fork())==0) printf("This is another child process:%d/n ",pid2); else printf("This is the parent process:%d %d/n",pid,pid2); } printf("--------/n"); return 1; } 輸出結果爲
============= LLLLLLLLLLLLLThis is the child process:0 -------- LLLLLLLLLLLLLThis is another child process:0 -------- LLLLLLLLLLLLLThis is the parent process:22180 22181 --------
#i nclude <unistd.h> #i nclude <stdio.h> int main() { int pid; int pid2; //printf("%d ",getppid()); //printf("=============/n"); printf("LLLLLLLLLLLLL"); if((pid=fork())==0){ printf("This is the child process:%d/n",pid); } else { if((pid2=fork())==0) printf("This is another child process:%d/n ",pid2); else printf("This is the parent process:%d %d/n",pid,pid2); } printf("--------/n"); return 1; }
輸出結果爲:
LLLLLLLLLLLLLThis is the child process:0 -------- LLLLLLLLLLLLLThis is another child process:0 -------- LLLLLLLLLLLLLThis is the parent process:22212 22213 --------
原因是
子進程同時複製父進程的緩存內容 printf()進行行緩存 在exit 之後纔將數據寫回標準輸出文件中
|