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,
| 宏 | 說明 |
| 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]+ 完成 ./zombiedemops 命令打印的進程中,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); } 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)
轉載于:https://www.cnblogs.com/lienhua34/p/4021272.html
總結
以上是生活随笔為你收集整理的UNIX环境编程学习笔记(21)——进程管理之获取进程终止状态的 wait 和 waitpid 函数...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 软工网络15团队作业4——Alpha阶段
- 下一篇: java-接口与多态-