天天看點

31-wait 大戰僵屍

按照正常的邏輯,應該講講 vfork 的(專為 exec 而定制)。不過鑒于 vfork 現在已經很少使用了,而且現在的 fork 也完全可以替代 vfork,是以講 vfork 有點重複的意思。當然了不排除面試或者考試會有人問到 vfork,這裡稍微提兩筆。vfork 采用了類似讀時共享的機制,但是其不保證寫時複制,它産生的子程序和父程序共享程序空間,是以,如果在 vfork 後沒有使用 exec 或者 _exit 函數,其行為将是未定義的。

好了,點到為止,而且實際開發中,也不希望使用 vfork(如果你對vfork感興趣,請自行 man 或者查閱 《apue》)。下面來造僵屍吧 ^_^。

1. 僵屍程序

如果你對父子程序還不了解,趕緊先跳到前面的章節。

“如果子程序運作結束了而父程序還沒有,就會産生僵屍程序。”

這很容易了解,比如你 fork 了一個子程序後,父程序由于耗時任務還沒結束,子程序卻未老先衰,父程序還對它視而不見,它就死不瞑目。

1.1 造 5 個僵小魚

下面的代碼示範了僵屍程序的制造方法。其原理很簡單,就是保證“白發人送黑發人”這一條件就OK。

  • 代碼
// mywait.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main() {
  printf("before fork\n");

  pid_t pid, n = 5;

  // 父程序生出 5 個子程序
  while(n--) {
    pid = fork();
    if (pid == 0) break;
    else if (pid < 0) {
      perror("fork");
      return 1;
    }   
  }

  // 子程序列印一句話就死了。
  if (pid == 0) {
    printf("hello, I'm child %d; my father is %d\n", getpid(), getppid());
    return 0;
  }


  // 父程序永遠在這列印。
  while(1) {
    sleep(3);
    printf("hello, I'm father %d\n", getpid());
  }
  return 0;
}      
  • 編譯
$ gcc mywait.c -o mywait      
  • 運作
$ ./mywait      
  • 結果
before fork
hello, I'm child 7923; my father is 7918
hello, I'm child 7922; my father is 7918
hello, I'm child 7921; my father is 7918
hello, I'm child 7920; my father is 7918
hello, I'm child 7919; my father is 7918
hello, I'm father 7918
hello, I'm father 7918
hello, I'm father 7918
hello, I'm father 7918      

1.2 檢視僵屍

再開啟一個終端,使用 ​

​ps -af​

​指令,結果如下:

UID        PID  PPID  C STIME TTY          TIME CMD
……
allen     7918  4565  0 14:19 pts/1    00:00:00 ./mywait
allen     7919  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>
allen     7920  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>
allen     7921  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>
allen     7922  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>
allen     7923  7918  0 14:19 pts/1    00:00:00 [mywait] <defunct>      

可以看到,這裡有 5 個程序,名字被加了方括号,後面還跟着 ​

​<defunct>​

​ 字樣。可是子程序明明已經結束了呢?為什麼還死不瞑目?

實際上,子程序在死的時候,通知了它父親:父親我要死了,快來給我收屍吧!(發送 SIGCHILD信号給父程序)

可是前面的代碼父親除了一直在喊:我是父親 7918, 我是父親 7918,……好像沒有幹其它任何事情。子程序未等到父親的回複,是以在那死不瞑目。除非子程序的父親也死了,這時候會有 init 程序來替代原來的父親替這些子程序收屍。

2. wait 函數——讓逝者安息

弄明白了僵屍程序的由來,我們就能想出對策來清理這些僵屍程序。有人說,不清理不行嗎?這樣說吧,如果僵屍程序的數量非常少,其實對系統造不成什麼威脅,如果僵屍程序越來越多,最後就會造成資源耗盡。是以,代碼的健壯性很重要!!!特别是伺服器開發領域中,通常一運作都上數月甚至好幾年,如果代碼不健壯,就會造成系統中的僵屍程序越來越多,最後癱瘓(雖然重新開機也可以解決,但是這治标不治本)。

前面講到,子程序在死的時候會通知父親(給父程序發送 SIGCHILD 信号,信号的概念後面才會講到,這裡隻要知道就行了),是以父程序隻要妥尚處理好子程序的通知就行了,需要用到的函數就是 wait.

wait 函數原型如下:

// 參數儲存子程序退出通知碼,傳回 -1 表示沒有子程序或者錯誤。否則傳回子程序的程序 id 号。
pid_t wait(int *status);      

前面的代碼改成如下的樣子:

  • 代碼
// wipeoutzombie.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>

int main() {
  printf("before fork\n");

  pid_t pid, n = 5;
  while(n--) {
    pid = fork();
    if (pid == 0) break;
    else if (pid < 0) {
      perror("fork");
      return 1;
    }   
  }

  if (pid == 0) {
    printf("hello, I'm child %d; my father is %d\n", getpid(), getppid());
    return 0;
  }


  while(1) {
    sleep(3);
    pid = wait(NULL); // 忽略子程序通知碼
    if (pid == -1) {
      perror("wait");
      sleep(10);
      printf("I'm father %d; I have wiped out all zombies\n", getpid());
      return 1;
    }   
    printf("Hello, I'm father %d; child %d, getpid(), pid);
  }
  return 0;
}      
before fork
hello, I'm child 8092; my father is 8087
hello, I'm child 8091; my father is 8087
hello, I'm child 8090; my father is 8087
hello, I'm child 8089; my father is 8087
hello, I'm child 8088; my father is 8087
Hello, I'm father 8087; child 8088 exit
Hello, I'm father 8087; child 8089 exit
Hello, I'm father 8087; child 8090 exit
Hello, I'm father 8087; child 8091 exit
Hello, I'm father 8087; child 8092 exit
wait: No child processes
I'm father 8087; I have wiped out all      

3. 總結

  • 了解什麼是僵屍程序
  • 知道子程序退出時會給父程序發送 SIGCHILD 信号
  • 學會使用 wait 函數來清理僵屍程序

繼續閱讀