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,
| 宏 | 說明 |
| 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 文件,
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); }
我們在父進程最后 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 中各常量或運算的結果。
| 常量 | 說明 |
| 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); }
在上面的程序中,我們在第一個子進程中 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); }
在上面程序中,在第一個子進程中 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)
