linux系統程式設計之程式(三):程式複製fork,孤兒程式,殭屍程式

mickole發表於2013-07-12

本節目標:

  • 複製程式映像
  • fork系統呼叫
  • 孤兒程式、殭屍程式
  • 寫時複製

一,程式複製(或產生)

     使用fork函式得到的子程式從父程式的繼承了整個程式的地址空間,包括:程式上下文、程式堆疊、記憶體資訊、開啟的檔案描述符、訊號控制設定、程式優先順序、程式組號、當前工作目錄、根目錄、資源限制、控制終端等。

子程式與父程式的區別在於:

1、父程式設定的鎖,子程式不繼承(因為如果是排它鎖,被繼承的話,矛盾了)

2、各自的程式ID和父程式ID不同

3、子程式的未決告警被清除;

4、子程式的未決訊號集設定為空集。

二,fork系統呼叫

包含標頭檔案 <sys/types.h> 和 <unistd.h>

函式功能:建立一個子程式

函式原型

pid_t fork(void);  //一次呼叫兩次返回值,是在各自的地址空間返回,意味著現在有兩個基本一樣的程式在執行

引數:無引數。

返回值:

  • 如果成功建立一個子程式,對於父程式來說返回子程式ID
  • 如果成功建立一個子程式,對於子程式來說返回值為0
  • 如果為-1表示建立失敗

流程圖:

QQ截圖20130712140302

父程式呼叫fork()系統呼叫,然後陷入核心,進行程式複製,如果成功:

1,則對呼叫程式即父程式來說返回值為剛產生的子程式pid,因為程式PCB沒有子程式資訊,父程式只能通過這樣獲得。

2,對子程式(剛產生的新程式),則返回0,

這時就有兩個程式在接著向下執行

如果失敗,則返回0,呼叫程式繼續向下執行

注:fork英文意思:分支,fork系統呼叫複製產生的子程式與父程式(呼叫程式)基本一樣:程式碼段+資料段+堆疊段+PCB,當前的執行環境基本一樣,所以子程式在fork之後開始向下執行,而不會從頭開始執行。

示例程式:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#define ERR_EXIT(m) \
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }\
    while (0)\

int main(void)
{
    pid_t pid;
    printf("before calling fork,calling process pid = %d\n",getpid());
    pid = fork();
    if(pid == -1)
        ERR_EXIT("fork error");
    if(pid == 0){
        printf("this is child process and child's pid = %d,parent's pid = %d\n",getpid(),getppid());
    }
    if(pid > 0){
        //sleep(1);
        printf("this is parent process and pid =%d ,child's pid = %d\n",getpid(),pid);
    }

    return 0;
}

執行結果:

QQ截圖20130712142934

當沒給父程式沒加sleep時,由於父程式先執行完,子程式成了孤兒程式,系統將其託孤給了1(init)程式,

所以ppid =1。

當加上sleep後,子程式先執行完:

QQ截圖20130712143106

這次可以正確看到想要的結果。

三,孤兒程式、殭屍程式

fork系統呼叫之後,父子程式將交替執行,執行順序不定。

如果父程式先退出,子程式還沒退出那麼子程式的父程式將變為init程式(託孤給了init程式)。(注:任何一個程式都必須有父程式)

如果子程式先退出,父程式還沒退出,那麼子程式必須等到父程式捕獲到了子程式的退出狀態才真正結束,否則這個時候子程式就成為僵程式(殭屍程式:只保留一些退出資訊供父程式查詢)

示例:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#define ERR_EXIT(m) \
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }\
    while (0)\

int main(void)
{
    pid_t pid;
    printf("before calling fork,calling process pid = %d\n",getpid());
    pid = fork();
    if(pid == -1)
        ERR_EXIT("fork error");
    if(pid == 0){
        printf("this is child process and child's pid = %d,parent's pid = %d\n",getpid(),getppid());
    }
    if(pid > 0){
        sleep(100);
        printf("this is parent process and pid =%d ,child's pid = %d\n",getpid(),pid);
    }

    return 0;
}

以上程式跟前面那個基本一致,就是讓父程式睡眠100秒,好讓子程式先退出

執行結果:

QQ截圖20130712144559

從上可以看到,子程式先退出,但程式列表中還可以檢視到子程式,[a.out] <defunct>,死的意思,即殭屍程式,如果系統中存在過多的殭屍程式,將會使得新的程式不能產生。

 

四,寫時複製

linux系統為了提高系統效能和資源利用率,在fork出一個新程式時,系統並沒有真正複製一個副本。

如果多個程式要讀取它們自己的那部分資源的副本,那麼複製是不必要的。

每個程式只要儲存一個指向這個資源的指標就可以了。

如果一個程式要修改自己的那份資源的“副本”,那麼就會複製那份資源。這就是寫時複製的含義

fork 和vfork:

在fork還沒實現copy on write之前。Unix設計者很關心fork之後立刻執行exec所造成的地址空間浪費,所以引入了vfork系統呼叫。

vfork有個限制,子程式必須立刻執行_exit或者exec函式。

即使fork實現了copy on write,效率也沒有vfork高,但是我們不推薦使用vfork,因為幾乎每一個vfork的實現,都或多或少存在一定的問題

vfork:

Linux Description
    vfork(), just like fork(2), creates a child process of the calling pro-
    cess.  For details and return value and errors, see fork(2).

    vfork()  is  a special case of clone(2).  It is used to create new pro-
    cesses without copying the page tables of the parent process.   It  may
    be  useful  in performance-sensitive applications where a child will be
    created which then immediately issues an execve(2).

    vfork() differs from fork(2) in that the parent is suspended until  the
    child  terminates (either normally, by calling _exit(2), or abnormally,
    after delivery of a fatal signal), or it makes  a  call  to  execve(2).
    Until  that point, the child shares all memory with its parent, includ-
    ing the stack. The child must not return from the current function  or
    call exit(3), but may call _exit(2).

    Signal  handlers  are inherited, but not shared.  Signals to the parent
    arrive after the child releases the parent’s memory  (i.e.,  after  the
    child terminates or calls execve(2)).

 

示例程式:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#define ERR_EXIT(m) \
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }\
    while (0)\

int main(void)
{
    pid_t pid;
    int val = 1;
    printf("before calling fork, val = %d\n",val);
    
    //pid = fork();
    pid = vfork();
    if(pid == -1)
        ERR_EXIT("fork error");
    if(pid == 0){
        printf("chile process,before change val, val = %d\n",val);
        val++;
        //sleep(1);
        printf("this is child process and val = %d\n",val);
        _exit(0);

    }
    if(pid > 0){
        sleep(1);
        //val++;
        printf("this is parent process and val = %d\n",val);
    }

    return 0;
}

當呼叫fork時:

執行結果:

QQ截圖20130712150818

可知寫時複製

當使用vfork但子程式沒使用exit退出時:

QQ截圖20130712151100

結果出錯了,

使用vfork且exit退出:

QQ截圖20130712151409

結果正常,父子程式共享

 

fork之後父子程式共享檔案:

QQ截圖20130712145713

 

fork產生的子程式與父程式相同的檔案檔案描述符指向相同的檔案表,引用計數增加,共享檔案檔案偏移指標

示例程式:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>

#define ERR_EXIT(m) \
    do\
    {\
        perror(m);\
        exit(EXIT_FAILURE);\
    }\
    while (0)\

int main(void)
{
    pid_t pid;
    int fd;
    fd = open("test.txt",O_WRONLY);
    if(fd == -1)
        ERR_EXIT("OPEN ERROR");
    pid = fork();
    if(pid == -1)
        ERR_EXIT("fork error");
    if(pid == 0){
        write(fd,"child",5);
    }
    if(pid > 0){
        //sleep(1);
        write(fd,"parent",6);
    }

    return 0;
}

執行結果:

QQ截圖20130712154102

可知父子程式共享檔案偏移指標,父程式寫完後檔案偏移到parent後子程式開始接著寫。

相關文章