1. 程式人生 > >Linux程序分析(三) fork迴圈建立Linux子程序

Linux程序分析(三) fork迴圈建立Linux子程序

fork

fork的意思是複製程序,就是把當前的程式再載入一次,載入後,所有的狀態和當前程序是一樣的(包括變數)。fork不象執行緒需提供一個函式做為入口, fork後,新程序的入口就在 fork的下一條語句。
返回值為pid_t,實際是unsigned int:子程序中為0,父程序中為子程序ID,出錯則為-1。

開始寫了幾個都不成功,最後終於成功了:

pid_t p = getpid();     // 需要 #include <sys/types.h>
printf("before fork(), pid = %d\n\n", p);
int n=0;
pid_t pid=-1
; while(n<5 && pid!=0) { pid = fork(); if(getpid()!=p) //是建立的子程序,不是原程序 { printf("child pid:%d ",getpid()); printf("parent pid:%d\n",getppid()); printf("\n\n"); //sleep(2); } waitpid(pid,NULL,0); //父程序等待子程序執行後,才能繼續fork其他子程序 n++; }

結果如下:

網上抄來抄去的一個程式是這樣的,邏輯大致相同:

pid_t p1;
for(i=0;i<4;i++)
{
   if((p1=fork())==0)
   {
        printf("child pid  %d  ",getpid());
        return 0;   //非常關鍵
   }
   waitpid(p1,NULL,0);
   printf("parent pid  %d \n\n\n",getpid());
}

孤兒程序與殭屍程序

兩種程序都屬於子程序回收的範疇。
程式碼裡不加waitpid這句,導致出現了這樣的結果:

孤兒程序:pid為1305的程序是systemd,因為如果父程序早於子程序終結,子程序就會成為一個孤兒程序。孤兒程序會被過繼給systemd程序,systemd程序也就成了該程序的父程序。systemd程序負責該子程序終結時呼叫wait函式。

如果程式碼修改如下:

while(n<5 && pid!=0)
{
    pid = fork();
    if(getpid()!=p) //子程序
    {
        printf("child pid:%d   ",getpid());
        sleep(5);
        printf("parent pid:%d\n",getppid());
        printf("\n\n");
    }
    else{
        while(1)
            sleep(2);
    }
    n++;
}

執行之後產生的子程序ID為2514,用ps命令檢視的結果:
其中的Z+表示程序為殭屍程序。

殭屍程序:正常情況下,子程序死後,系統會發送SIGCHLD訊號給父程序,父程序對其預設處理是忽略。如果想響應這個訊息,父程序通常使用wait函式來響應子程序的終止。
如果子程序在其父程序還沒有呼叫wait()或waitpid()的情況下退出,導致父程序無法對其進行回收,子程序就會成為殭屍程序,其殘留資源(PCB)存放於核心,不能用kill命令終結。殭屍程序會佔用系統資源,如果很多,則會嚴重影響伺服器的效能。如果父程序到死也沒有執行wait,那麼子程序最終過繼給init程序,init程序週期執行wait系統呼叫收割其收養的所有殭屍程序。

wait和waitpid

兩個函式的原型:

pid_t wait(int *status);

pid_t waitpid(pid_t pid, int *status, int options);

當子程序終結時,它會通知父程序,並清空自己所佔據的記憶體,並在核心裡留下自己的退出資訊(exit code,正常則為0;如果有錯誤或異常狀況,為大於0的整數)。在這個資訊裡,會解釋該程序為什麼退出。父程序在得知子程序終結時,應當對該子程序使用wait函式呼叫,然後就會阻塞自己,由wait分析子程序是否已經退出,如果子程序成為殭屍程序,wait收集其資訊並將其銷燬。如果沒有找到這樣一個子程序,wait就會一直阻塞在這裡,直到有一個出現為止。

如果wait執行成功則返回子程序PID,如果有錯誤發生則返回-1,失敗原因存於errno 中。wait的引數為int指標status,用來獲取子程序的退出資訊,如果我們只要銷燬殭屍程序而不管退出資訊,就將指標設定為NULL。獲取退出狀態不能用常規方法,需要用到幾種巨集,常用的一種方法如下:

int status;
ret = wait(&status);
if(WIFEXITED(status))   /* 如果WIFEXITED 返回非零值 */
{
   printf("the child process %d exit normally.\n", ret);
   printf("the return code is %d.\n",WEXITSTATUS(status));
}
else /* 如果WIFEXITED返回零 */
   printf("the child process %d exit abnormally.\n", ret);

如果引數status的值不是NULL,wait就會把子程序退出時的狀態exit code取出並存入其中。
WIFEXITED(status)的意思是Wait if exited,用來指出子程序是否為正常退出的,如果是,它會返回一個非零值。
WIFEXITED返回非零值時,可以用巨集WEXITSTATUS(status)來提取子程序的返回值,意思是wait exit status,如果子程序呼叫exit(5)退出,WEXITSTATUS(status) 就會返回5。

所以說,wait和waitpid有三個作用:
1. 阻塞自身等待子程序退出
2. 回收子程序殘餘資源
3. 獲取子程序退出資訊

其他避免殭屍程序的方法

除了wait函式外,還有多種方法避免殭屍程序,但常用的是兩種。

兩次fork

原理:當子程序成為殭屍程序後,如果父程序結束,那麼殭屍程序會過繼給init程序,最終被init程序回收。所以可以人為地讓父程序結束,這樣子程序就不會變為殭屍,說白了就是讓殭屍程序先成為孤兒程序,然後交給init

APUE的程式碼:

    pid_t pid;
    printf("before fork, pid is %d\n",getpid());
    sleep(2);
    if ( (pid = fork()) < 0)
        perror("fork error");
    else if (pid == 0)       /* first child */
    {
        if ( (pid = fork()) < 0)
            perror("fork error");
        else if (pid > 0)   /* parent from second fork == first child */
        {
            printf("first child pid is %d\n",getpid());
            exit(0);
        }
        /* We're the second child; our parent becomes init as soon
           as our real parent calls exit() in the statement above.
           Here's where we'd continue executing, knowing that when
           we're done, init will reap our status. */
        sleep(2);
        printf("second child, pid = %d, parent pid = %d\n", getpid(),getppid());
        exit(0);
    }
    if (waitpid(pid, NULL, 0) != pid)        /* wait for first child */
            perror("waitpid error");
    /* We're the parent (the original process); we continue executing,
       knowing that we're not the parent of the second child. */
    exit(0);

人為地建立一個子程序,再建立一個孫子程序,子程序退出,實際處理事務的是孫子程序,這個時候孫子程序成為孤兒程序,被系統程序init回收,而init程序的子程序不會成為殭屍程序。