fork 詳解

 

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 之後纔將數據寫回標準輸出文件中

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章