UNIX環境編程學習筆記(21)——進程管理之獲取進程終止狀態的 wait 和 waitpid 函數


lienhua34
2014-10-12

當一個進程正常或者異常終止時,內核就向其父進程發送 SIGCHLD信號。父進程可以選擇忽略該信號,或者提供一個該信號發生時即被調用的函數(信號處理程序)。對於這種信號的系統默認動作是忽略它。

在文檔“進程控制三部曲”中,我們講的第三部曲是使用 wait 函數來獲取終止子進程的終止狀態。那么,有幾個問題我們這里需要詳細的學習一下。

1. 父進程一定能夠獲取到子進程的終止狀態嗎?如果子進程在父進程調用 wait 函數前就終止了,怎么辦?

2. 如果父進程沒有獲取子進程的終止狀態,那會發生什么?

3. 如果父進程有多個子進程,那么獲取的是哪個子進程的終止狀態呢?

對於第一個問題的回答是:內核為每個終止進程保存了一定量的信息,包括進程 ID、該進程的終止狀態、以及該進程使用的 CPU 時間總量。所以,當終止進程的父進程調用 wait 或者 waitpid 函數,即可獲取到這些信息。當父進程獲取終止進程的終止信息之后,內核就可以釋放終止進程所使用的所有存儲區、關閉其所有打開的文件。

在 UNIX 術語中,一個已經終止、但是其父進程尚未對其進行善后處理(獲取終止子進程的相關信息)的進程被稱為僵屍進程(zombie)。如果編寫一個長期運行的程序,調用 fork 產生子進程之后,需要調用 wait 來獲取這些子進程的終止狀態,否則這些子進程在終止之后將會變成僵屍進程。(后面會講到用一個技巧以避開父進程調用 wait 獲取所有子進程的終止狀態。)

那么如果那些被 init 進程領養的進程在終止之后會不會也變成僵屍進程?答案是:不會。因為 init 進程無論何時只要有一個子進程終止,init 就會調用 wait 函數獲取其終止狀態。

那么關於上面的第三個問題,我們得通過詳細學習 wait 和 waitpid 函數才能都做出回答了。

1 wait 函數

#include <sys/wait.h>

pid_t wait(int *statloc);

返回值:若成功則返回終止進程的ID,若出錯則返回-1

參數 statloc 是一個整形指針。如果 statloc 不是一個空指針,則終止進程的終止狀態將存儲在該指針所指向的內存單元中。如果不關心終止狀態,可以將 statloc 參數設置為空。

調用 wait 函數時,調用進程將會出現下面的情況:

• 如果其所有子進程都還在運行,則阻塞。

• 如果一個子進程已經終止,正等待父進程獲取其終止狀態,則獲取該子進程的終止狀態然后立即返回。

• 如果沒有任何子進程,則立即出錯返回。

wait 函數獲取的終止狀態是一個 int 型數值,那我們如何得到具體的終止信息呢?POSIX.1 規定終止狀態用定義在 <sys/wait.h> 中的各個宏來參看。有四個互斥的宏可以用來得到進程的終止原因。這四個宏見表 1,

表 1: 檢查終止狀態的宏
說明
WIFEXITED(status) 若正常終止子進程返回的狀態,則為真。此種情況,調用 WEXITSTATUS(status) 可以獲取子進程調用 exit 函數的參數的低 8位。
WIFSIGNALED(status) 若為異常終止子進程返回的狀態,則為真。此種情況,調用 WTERMSIG(status) 取得使子進程終止的信號編號。
WIFSTOPPED(status) 若為當前暫停子進程返回的狀態,則為真。
WIFCONTINUED(status) 若在作業控制暫停后已經繼續的子進程返回了狀態,則為真。

下面我們來看一下打印終止進程狀態說明的例子,

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>

extern void print_exit(int status);

int
main(void)
{
  pid_t pid;
  int status;

  if ((pid = fork()) < 0) {
    printf("fork error: %s\n", strerror(errno));
    exit(-1);
  } else if (pid == 0) {
    exit(8);
  }
  if (wait(&status) != pid) {
    printf("wait error: %s\n", strerror(errno));
    exit(-1);
  }
  print_exit(status);

  if ((pid = fork()) < 0) {
    printf("fork error: %s\n", strerror(errno));
    exit(-1);
  } else if (pid == 0) {
    abort();
  }
  if (wait(&status) != pid) {
    printf("wait error: %s\n", strerror(errno));
    exit(-1);
  }
  print_exit(status);
  
  exit(0);
}

void print_exit(int status)
{
  if (WIFEXITED(status)) {
    printf("normal termination, exit status = %d\n",
           WEXITSTATUS(status));
  } else if (WIFSIGNALED(status)) {
    printf("abnormal termination, signal number =%d\n",
           WTERMSIG(status));
  }
}
waitdemo.c

編譯該程序,生成並運行 waitdemo 文件,

lienhua34:demo$ gcc -o waitdemo waitdemo.c
lienhua34:demo$ ./waitdemo
normal termination, exit status = 8
abnormal termination, signal number =6

下面我們再來看一個產生僵屍進程的示例,

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>

int
main(void)
{
  pid_t pid;

  if ((pid = fork()) < 0) {
    printf("fork error: %s\n", strerror(errno));
    exit(-1);
  } else if (pid == 0) {
    exit(0);
  }
  printf("fork child process:%d\n", pid);

  if ((pid = fork()) < 0) {
    printf("fork error: %s\n", strerror(errno));
    exit(-1);
  } else if (pid == 0) {
    exit(0);
  }
  printf("fork child process:%d\n", pid);
  
  if ((pid = wait(NULL)) < 0) {
    printf("wait error: %s\n", strerror(errno));
    exit(-1);
  }
  printf("get child process(%d) termination status\n", pid);
  sleep(5);
  printf("parent process exit\n");
  exit(0);
}
zombiedemo.c

我們在父進程最后 sleep(5) 讓父進程睡眠 5 秒鍾是避免父進程太早退出,我們觀察不到僵屍進程。我們編譯該程序文件,生成並執行文件

lienhua34:demo$ ps -A -ostat,pid | grep -e '[Zz]'
Z     1725
lienhua34:demo$ gcc -o zombiedemo zombiedemo.c 
lienhua34:demo$ ./zombiedemo &
[1] 2961
lienhua34:demo$ fork child process:2962
fork child process:2963
get child process(2963) termination status
ps -A -ostat,pid | grep -e '[Zz]'
Z     1725
Z     2962
lienhua34:demo$ parent process exit
ps -A -ostat,pid | grep -e '[Zz]'
Z     1725
[1]+  完成                  ./zombiedemo

ps 命令打印的進程中,Z 表示僵屍進程。從上面的運行結果,我們看到父進程(ID:2961)fork 了兩個子進程(ID:2962 和 2963),然后調用了 wait 函數獲取了子進程 2963 的終止狀態,於是子進程 2962 便成為了僵屍進程。但是,當父進程也退出時,生成僵屍進程的子進程 2962 也被內核釋放。

2 waitpid 函數

只要有一個子進程終止,wait 函數就會返回。那么如果父進程希望等待特定的子進程終止,該怎么辦?UNIX 提供了提供這樣功能的 waitpid 函數。

#include <sys/wait.h>

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

返回值:若成功則返回終止進程ID或0;若出錯則返回-1

其中 statloc 參數跟 wait 函數一樣,獲取終止子進程的狀態信息。waitpid 函數通過 pid 參數來控制父進程希望獲取特定進程的終止狀態信息,

• pid==-1:等待任一子進程,與 wait 函數等效。

• pid>0:等待其進程 ID 與 pid 相等的子進程。

• pid==0:等待其組 ID 等於調用進程組 ID 的任一子進程。(我們這里不學習進程組)

• pid<-1:等待其組 ID 等於 pid 絕對值的任一子進程。

waitpid 函數返回終止子進程的進程 ID。如果指定的進程或進程組不存在,或者參數 pid 指定的進程不是調用進程的子進程則都將出錯。waitpid 函數跟 wait 函數的另一個不同之處在於,wait 函數可能會使調用進程阻塞,而 waitpid 函數可以通過第三個參數 options 來控制調用進程是否要阻塞。options 參數可以是 0,也可以是表 2 中各常量或運算的結果。

表 2: waitpid 的 options 常量
常量 說明
WCONTINUED 若實現支持作業控制,那么由 pid 指定的任一子進程在暫停后已經繼續,但其狀態尚未報告,則返回其狀態。
WNOHANG 若由 pid 指定的子進程並不是立即可用的,則 waitpid 不阻塞,此時返回值為 0.
WUNTRACED
若某實現支持作業控制,而由 pid 指定的任一子進程已處於暫停狀態,並且其狀態自暫停以來還未報告過,則返回其狀態。

關於 options 用於作業控制 的兩個 常量 WCONTINUED 和 WUNTRACED,我們這里不學習,我們只關心常量 WNOHANG。我們來看一個例子。

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>

int
main(void)
{
  pid_t pid1, pid2;
  pid_t waitpidRes;

  if ((pid1 = fork()) < 0) {
    printf("fork error: %s\n", strerror(errno));
    exit(-1);
  } else if (pid1 == 0) {
    sleep(3);
    printf("child process %d exit\n", getpid());
    exit(0);
  }

  if ((pid2 = fork()) < 0) {
    printf("fork error: %s\n", strerror(errno));
    exit(-1);
  } else if (pid2 == 0) {
    printf("child process %d exit\n", getpid());
    exit(0);
  }

  if ((waitpidRes = waitpid(pid1, NULL, 0)) == pid1) {
    printf("get terminated child process %d.\n", waitpidRes);
  } else if (waitpidRes < 0) {
    printf("waitpid error: %s\n", strerror(errno));
    exit(-1);
  } else {
    printf("waitpid return 0\n");
  }
  printf("parent process exit\n");
  
  exit(0);
}
waitpiddemo.c

在上面的程序中,我們在第一個子進程中 sleep(3) 讓該子進程睡眠 3秒,以便在父進程調用 waitpid 函數時該子進程尚未結束。編譯該程序,生成並執行 waitpiddemo 文件,

lienhua34:demo$ gcc -o waitpiddemo waitpiddemo.c
lienhua34:demo$ ./waitpiddemo
child process 2972 exit
child process 2971 exit
get terminated child process 2971.
parent process exit

從上面的運行結果,我們可以看到父進程阻塞等待子進程 2971 終止。我們如果把上面程序的 waitpid 函數第三個參數 options 改為 WNOHANG,看一下其實際運行結果。

lienhua34:demo$ gcc -o waitpiddemo waitpiddemo.c
lienhua34:demo$ ./waitpiddemo
waitpid return 0
parent process exit
child process 2982 exit
lienhua34:demo$ child process 2981 exit

從上面的運行結果,我們可以看出父進程調用 waitpid 函數時,子進程2981 尚未終止,於是 waitpid 函數沒有阻塞父進程,直接返回 0.

3 避免調用大量WAIT函數來防止僵屍進程的技巧

前面講到僵屍進程時,我們提到要編寫一個長期運行的程序,要避免出現大量的僵屍情況,就需要每次 fork 出一個子進程時都需要調用 wait 函數來等待子進程的結束以便處理該子進程的終止狀態信息。但是,我們每次 fork 都要調用一個 wait 函數,實在是太麻煩了。

於是,我們就希望每次調用 fork 時不需要 wait 等待子進程終止,也不希望子進程處於僵死狀態直到程序結束。這里提供一個實現此要求的技巧:調用 fork 兩次。我們來看下面的例子:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/wait.h>

int
main(void)
{
  pid_t pid;

  if ((pid = fork()) < 0) {
    printf("fork error: %s\n", strerror(errno));
    exit(-1);
  } else if (pid == 0) {
    if ((pid = fork()) < 0) {
      printf("fork error: %s\n", strerror(errno));
      exit(-1);
    } else if (pid > 0) {
      printf("first child process: %d, parent process: %d\n", getpid(), getppid());
      exit(0);
    }

    sleep(2);
    printf("second child process: %d, parent process: %d\n", getpid(), getppid());
    exit(0);
  }
  if (wait(NULL) < 0) {
    printf("wait error: %s\n", strerror(errno));
    exit(-1);
  }
  printf("parent process %d exit\n", getpid());
  exit(0);
}
nozombiedemo.c

在上面程序中,在第一個子進程中 fork 處第二個子進程之后並終止第一個子進程。編譯該程序,生成並執行文件 nozombiedemo,

lienhua34:demo$ gcc -o nozombiedemo nozombiedemo.c
lienhua34:demo$ ./nozombiedemo
first child process: 2471, parent process: 2470
parent process 2470 exit
lienhua34:demo$ second child process: 2472, parent process: 1

從上面的運行結果,我們看到第一個子進程 2471 終止后,其子進程2472 的父進程 ID 變成了 1(即 init 進程)。前面我們提到過,父進程為 init進程的所有進程在終止時都會被 init 進程獲取其終止狀態,從而不會變成僵屍進程。於是,通過上面的 fork 兩次的技巧,我們就可以實現創建一個新進程,不需要等待該新進程終止,也不擔心該新進程會變成僵屍進程。

(done)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM