進程控制之fork函數

一個現有進程能夠調用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)。shell

子進程和父進程繼續執行fork調用以後的指令子進程是父進程的副本。例如,子進程得到父進程的數據空間、堆和棧的副本注意,這是子進程所擁有的副本。父、子進程並不共享這些存儲空間部分父、子進程共享正文段(text,代碼段)。編程

因爲在fork以後常常跟隨着exec,因此如今的不少實現並不執行一個父進程數據段、棧和堆的徹底複製。做爲替代,使用了寫時複製(Copy-On-Write,COW)技術。這些區域由父、子進程共享,並且內核將它們的訪問權限改變爲只讀的。若是父、子進程中的任一個試圖修改這些區域,則內核只爲修改區域的那塊內存製做一個副本,一般是虛擬存儲器系統中的一「頁」。網絡

Linux 2.4.22提供了另外一種新進程建立函數——clone(2)系統調用。這是一種fork的泛型,它容許調用者控制哪些部分由父、子進程共享。函數

程序清單8-1中的程序演示了fork函數,從中能夠看到子進程對變量所做的改變並不影響父進程中該變量的值。學習

程序清單8-1 fork函數示例spa

[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";

int
main(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-1
a write to stdout
before fork
pid = 13367, glob = 7, var = 89    子進程的變量值改變了
pid = 13366, glob = 6, var = 88    父進程的變量值沒有改變
[root@localhost apue]# ./prog8-1 > tmp.out
[root@localhost apue]# cat tmp.out
a write to stdout
before fork
pid = 13369, glob = 7, var = 89
before fork
pid = 13368, glob = 6, var = 88

通常來講,在fork以後是父進程先執行仍是子進程先執行是不肯定的。這取決於內核所使用的調度算法。若是要求父、子進程之間相互同步,則要求某種形式的進程間通訊。.net

當寫到標準輸出時,咱們將buf長度減去1做爲輸出字節數,這是爲了不將終止null字節寫出。strlen計算不包括終止null字節的字符串長度,而sizeof則計算包括終止null字節的緩衝區長度。二者之間的另外一個差異是,使用strlen需進行一次函數調用,而對於sizeof而言,由於緩衝區已用已知字符串進行了初始化,其長度是固定的,因此sizeof在編譯時計算緩衝區長度3d

注意程序清單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所示的結構。

8-share-file

這種共享文件的方式使父、子進程對同一文件使用了一個文件偏移量。若是父、子進程寫到同一描述符文件,但又沒有任何形式的同步(例如使父進程等待子進程),那麼它們的輸出就會相互混合(假定全部的描述符是在fork以前打開的)。

在fork以後處理文件描述符有兩種常見的狀況

(1)父進程等待子進程完成。在這種狀況下,父進程無需對其描述符作任何處理。當子進程終止後,它曾進行過讀、寫操做的任一共享描述符的文件偏移量已執行了相應的更新。

(2)父、子進程各自執行不一樣的程序段。在這種狀況下,在fork以後,父、子進程各自關閉它們不須要使用的文件描述符,這樣就不會干擾對方使用的文件描述符。這種方法是網絡服務進程中常用的。

除了打開文件以外,父進程的不少其餘屬性也由子進程繼承(能夠理解爲共享),包括:

  • 實際用戶ID、實際組ID、有效用戶ID、有效組ID。
  • 附加組ID。
  • 進程組ID。
  • 會話ID。
  • 控制終端。
  • 設置用戶ID標誌和設置組ID標誌。
  • 當前工做目錄。
  • 根目錄。
  • 文件模式建立屏蔽字。
  • 信號屏蔽和安排。
  • 針對任一打開文件描述符的在執行時關閉(close-on-exec)標誌。
  • 環境。
  • 鏈接的共享存儲段。
  • 存儲映射。
  • 資源限制。

父、子進程之間的區別是:

  • fork的返回值。
  • 進程ID不一樣。
  • 兩個進程具備不一樣的父進程ID:子進程的父進程ID是建立它的進程的ID,而父進程的父進程ID則不變。
  • 子進程的tms_utime、tms_stime、tms_cutime已經tms_ustime均被設置爲0.
  • 父進程設置的文件鎖不會被子進程繼承。
  • 子進程的未處理的鬧鐘(alarm)被清除。
  • 子進程的未處理信號集設置爲空集。

使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/

相關文章
相關標籤/搜索