Linux之子進程父進程及waitpid()函數使用


轉自:https://www.cnblogs.com/diegodu/p/3966332.html

wait() 和 waitpid()

wait() 系統調用掛起調用進程的執行直到有一個孩子終止。調用 wait(&status) 等價於:
    waitpid(-1, &status, 0);

waitpid() 系統調用掛起調用進程的執行直到由參數 pid 指定的孩子的狀態發生改變。默認情況下,waitpid() 只等待孩子的終止,但是這個行為可能通過 options 參數來改變,具體描述如下。

pid 值可以是:

< -1
表示等待 進程組ID 為  pid 絕對值的進程組內的所有孩子進程。
-1
表示等待任何一個孩子進程。
0
表示等待其 進程組ID 與調用進程相等的任何孩子進程。
> 0
表示等待 進程ID 與  pid 相等的孩子。

options 的值是下面常量的中的零個或多個進行位或運算的結果:

WNOHANG
如果沒有孩子存在立即返回。
WUNTRACED
如果孩子停止(但沒有通過  ptrace(2) 追蹤)也返回。如果這個選項沒有指定,已經停止的將通過 traced 返回其狀態。
WCONTINUED  (從 Linux 2.6.10 開始)
如果已經停止的孩子因為  SIGCONT 的遞送而繼續執行也返回。

(只對 Linux 有效的選項,見下面。)

如果 status 不是 NULL,wait() 和 waitpid() 保存狀態信息在那個 int 指針指向的內存里。這個整數可以通過下面的宏(它們接受整數自身,而不指向它的指針,wait() 和 waitpid() 需要指針!)進行審視:

WIFEXITED( status )
如果孩子是正常終止則返回真,這說明孩子是調用  exit(3) 或 _exit(2),或者由 main() 函數返回而終止。
WEXITSTATUS( status )
返回孩子的退出狀態。這是  status 參數的最低 8 位值,這個值由孩子調用 exit(3) 或 _exit(2) 或者作為 main() return 語句的參數來指定。這個宏只應該在 WIFEXITED 返回真時調用。
WIFSIGNALED( status )
如果孩子進程因為一個信號而終止則返回真。
WTERMSIG( status )
返回導致孩子終止的信號個數。這個宏只應該在  WIFSIGNALED 返回真時調用。
WCOREDUMP( status )
如果孩子進程產生核心轉儲文件則返回真。這個宏只應該在  WIFSIGNALED 返回真時調用。這個沒有在 POSIX.1-2001 里指定並且在一些 UNIX 實現(如 AIX、SunOS)里也沒有提供。只在 #ifdef WCOREDUMP ... #endif 內部使用。
WIFSTOPPED( status )
如果孩子進程因為信號而停止則返真;這只有在使用了  WUNTRACED 調用或當孩子被追蹤(見 ptrace(2)) 時才可能。
WSTOPSIG( status )
返回導致孩子停止的信號個數。這個宏只應該在  WIFSTOPPED 返回真時調用。
WIFCONTINUED( status )
(從 Linux 2.6.10 開始) 如果孩子進程因為  SIGCONT 信號繼續執行則返回真
 

 

我們運行如下命令,可看到Linux支持的信號列表:

~$ kill -l
1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL
5) SIGTRAP 6) SIGABRT 7) SIGBUS 8) SIGFPE
9) SIGKILL 10) SIGUSR1 11) SIGSEGV 12) SIGUSR2
13) SIGPIPE 14) SIGALRM 15) SIGTERM 17) SIGCHLD
18) SIGCONT 19) SIGSTOP 20) SIGTSTP 21) SIGTTIN
22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO
30) SIGPWR 31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1
36) SIGRTMIN+2 37) SIGRTMIN+3 38) SIGRTMIN+4 39) SIGRTMIN+5
40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8 43) SIGRTMIN+9
44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13
52) SIGRTMAX-12 53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9
56) SIGRTMAX-8 57) SIGRTMAX-7 58) SIGRTMAX-6 59) SIGRTMAX-5
60) SIGRTMAX-4 61) SIGRTMAX-3 62) SIGRTMAX-2 63) SIGRTMAX-1
64) SIGRTMAX

列表中,編號為1 ~ 31的信號為傳統UNIX支持的信號,是不可靠信號(非實時的),編號為32 ~ 63的信號是后來擴充的,稱做可靠信號(實時信號)。不可靠信號和可靠信號的區別在於前者不支持排隊,可能會造成信號丟失,而后者不會。

下面我們對編號小於SIGRTMIN的信號進行討論。

1) SIGHUP
本信號在用戶終端連接(正常或非正常)結束時發出, 通常是在終端的控制進程結束時, 通知同一session內的各個作業, 這時它們與控制終端不再關聯。

登錄Linux時,系統會分配給登錄用戶一個終端(Session)。在這個終端運行的所有程序,包括前台進程組和后台進程組,一般都屬於這個 Session。當用戶退出Linux登錄時,前台進程組和后台有對終端輸出的進程將會收到SIGHUP信號。這個信號的默認操作為終止進程,因此前台進 程組和后台有終端輸出的進程就會中止。不過可以捕獲這個信號,比如wget能捕獲SIGHUP信號,並忽略它,這樣就算退出了Linux登錄,wget也 能繼續下載。

此外,對於與終端脫離關系的守護進程,這個信號用於通知它重新讀取配置文件。

2) SIGINT
程序終止(interrupt)信號, 在用戶鍵入INTR字符(通常是Ctrl-C)時發出,用於通知前台進程組終止進程。

3) SIGQUIT
和SIGINT類似, 但由QUIT字符(通常是Ctrl-\)來控制. 進程在因收到SIGQUIT退出時會產生core文件, 在這個意義上類似於一個程序錯誤信號。

4) SIGILL
執行了非法指令. 通常是因為可執行文件本身出現錯誤, 或者試圖執行數據段. 堆棧溢出時也有可能產生這個信號。

5) SIGTRAP
由斷點指令或其它trap指令產生. 由debugger使用。

6) SIGABRT
調用abort函數生成的信號。

7) SIGBUS
非法地址, 包括內存地址對齊(alignment)出錯。比如訪問一個四個字長的整數, 但其地址不是4的倍數。它與SIGSEGV的區別在於后者是由於對合法存儲地址的非法訪問觸發的(如訪問不屬於自己存儲空間或只讀存儲空間)。

8) SIGFPE
在發生致命的算術運算錯誤時發出. 不僅包括浮點運算錯誤, 還包括溢出及除數為0等其它所有的算術的錯誤。

9) SIGKILL
用來立即結束程序的運行. 本信號不能被阻塞、處理和忽略。如果管理員發現某個進程終止不了,可嘗試發送這個信號。

10) SIGUSR1
留給用戶使用

11) SIGSEGV
試圖訪問未分配給自己的內存, 或試圖往沒有寫權限的內存地址寫數據.

12) SIGUSR2
留給用戶使用

13) SIGPIPE
管道破裂。這個信號通常在進程間通信產生,比如采用FIFO(管道)通信的兩個進程,讀管道沒打開或者意外終止就往管道寫,寫進程會收到SIGPIPE信號。此外用Socket通信的兩個進程,寫進程在寫Socket的時候,讀進程已經終止。

14) SIGALRM
時鍾定時信號, 計算的是實際的時間或時鍾時間. alarm函數使用該信號.

15) SIGTERM
程序結束(terminate)信號, 與SIGKILL不同的是該信號可以被阻塞和處理。通常用來要求程序自己正常退出,shell命令kill缺省產生這個信號。如果進程終止不了,我們才會嘗試SIGKILL。

17) SIGCHLD
子進程結束時, 父進程會收到這個信號。

如果父進程沒有處理這個信號,也沒有等待(wait)子進程,子進程雖然終止,但是還會在內核進程表中占有表項,這時的子進程稱為僵屍進程。這種情 況我們應該避免(父進程或者忽略SIGCHILD信號,或者捕捉它,或者wait它派生的子進程,或者父進程先終止,這時子進程的終止自動由init進程 來接管)。

18) SIGCONT
讓一個停止(stopped)的進程繼續執行. 本信號不能被阻塞. 可以用一個handler來讓程序在由stopped狀態變為繼續執行時完成特定的工作. 例如, 重新顯示提示符

19) SIGSTOP
停止(stopped)進程的執行. 注意它和terminate以及interrupt的區別:該進程還未結束, 只是暫停執行. 本信號不能被阻塞, 處理或忽略.

20) SIGTSTP
停止進程的運行, 但該信號可以被處理和忽略. 用戶鍵入SUSP字符時(通常是Ctrl-Z)發出這個信號

21) SIGTTIN
當后台作業要從用戶終端讀數據時, 該作業中的所有進程會收到SIGTTIN信號. 缺省時這些進程會停止執行.

22) SIGTTOU
類似於SIGTTIN, 但在寫終端(或修改終端模式)時收到.

23) SIGURG
有”緊急”數據或out-of-band數據到達socket時產生.

24) SIGXCPU
超過CPU時間資源限制. 這個限制可以由getrlimit/setrlimit來讀取/改變。

25) SIGXFSZ
當進程企圖擴大文件以至於超過文件大小資源限制。

26) SIGVTALRM
虛擬時鍾信號. 類似於SIGALRM, 但是計算的是該進程占用的CPU時間.

27) SIGPROF
類似於SIGALRM/SIGVTALRM, 但包括該進程用的CPU時間以及系統調用的時間.

28) SIGWINCH
窗口大小改變時發出.

29) SIGIO
文件描述符准備就緒, 可以開始進行輸入/輸出操作.

30) SIGPWR
Power failure

31) SIGSYS
非法的系統調用。

在以上列出的信號中,程序不可捕獲、阻塞或忽略的信號有:SIGKILL,SIGSTOP
不能恢復至默認動作的信號有:SIGILL,SIGTRAP
默認會導致進程流產的信號有:SIGABRT,SIGBUS,SIGFPE,SIGILL,SIGIOT,SIGQUIT,SIGSEGV,SIGTRAP,SIGXCPU,SIGXFSZ
默認會導致進程退出的信號有:SIGALRM,SIGHUP,SIGINT,SIGKILL,SIGPIPE,SIGPOLL,SIGPROF,SIGSYS,SIGTERM,SIGUSR1,SIGUSR2,SIGVTALRM
默認會導致進程停止的信號有:SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU
默認進程忽略的信號有:SIGCHLD,SIGPWR,SIGURG,SIGWINCH

此外,SIGIO在SVR4是退出,在4.3BSD中是忽略;SIGCONT在進程掛起時是繼續,否則是忽略,不能被阻塞

 

 

例子

 

復制代碼
 1 #include<stdio.h>
 2 #include<stdlib.h>
 3 #include<sys/types.h>
 4 #include<sys/wait.h>
 5 
 6 
 7 
 8 void pr_exit(int);
 9 
10 
11 
12 int main(void)
13 {
14     pid_t pid;
15     int status;
16 
17     if((pid=fork())<0){
18         printf("fork error");
19     }else if(pid==0){
20         exit(7);
21     }
22 
23     if(wait(&status)!=pid)
24         printf("wait error");
25     pr_exit(status);
26 
27     if((pid=fork())<0)
28         printf("fork error");
29     else if(pid==0)
30     {
31         int n = 0;
32         //scanf("%d", &n);
33         //      printf("child  pid=%d\n",getpid());
34         //printf("child pid=%d\n",getppid());
35         abort();
36     }
37 
38     
39         
40 
41     if(wait(&status)!=pid)
42         printf("wait error");
43 
44     pr_exit(status);
45 
46     if((pid=fork())<0)
47         printf("fork error");
48     else if(pid==0)
49         //printf(" parent pid=%d\n",getppid());
50 
51         //printf("child  pid=%d\n",getpid());
52 
53         status/=0;
54 
55     if(wait(&status)!=pid)
56         printf("wait error");
57     pr_exit(status); 
58     exit(0);
59 }
60 void pr_exit(int status) 
61 {
62     if(WIFEXITED(status)) 
63         printf("normal termination,exitstatus=%d\n",WEXITSTATUS(status));
64     else if(WIFSIGNALED(status)) 
65         printf("abnormal termination,signalstatus=%d\n",WTERMSIG(status),
66 #ifdef WCOREDUMP
67                 WCOREDUMP(status)?"(core file generated)":"");
68 #else
69     "");                                                                                                                                                         
70 #endif
71     else if(WIFSTOPPED(status))
72         printf("child stopped ,signal number=%d\n", WSTOPSIG(status));
73 }  
復制代碼

 

輸出

normal termination,exitstatus=7
abnormal termination,signalstatus=6
abnormal termination,signalstatus=8

子進程結束的方式不同,子進程發送給父進程的信號也不同,6 是6) SIGABRT, 8是8) SIGFPE 在發生致命的算術運算錯誤時發出. 不僅包括浮點運算錯誤, 還包括溢出及除數為0等其它所有的算術的錯誤。

這都是abnormal 的結束

 

 

wait和waitpid函數
--------------------
當一個進程正常或異常終止時,內核就向其父進程發送SIGCHLD信號。因為子進程終止是個異步事件,所以這種信號也是內核向父進程發的異步通知。父進程可以選擇忽略該信號,或者提供一個該信號發生時即被調用執行的函數(信號處理程序)。對於這種信號的系統默認動作是忽略它。父進程調用wait或waitpid時可能會發生如下情況之一:
如果其所有子進程都還在運行,則阻塞。
如果一個子進程已終止,正等待父進程獲取其終止狀態,則取得該子進程的終止狀態立即返回。
如果它沒有任何子進程,則立即出錯返回。
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int *statloc, int options);
實際應用中,waitpid函數的使用比wait函數更為靈活,因為waitpid函數提供了wait函數沒有提供的三個功能:
waitpid可等待一個特定的進程,而wait則返回任一終止子進程的狀態。
waitpid提供了一個wait的非阻塞版本(將options設為WNOHANG)。
waitpid支持作業控制(將options設為WUNTRACED和WCONTINUED選項)。

 

復制代碼
 1 #include <stdio.h>
 2 #include <unistd.h>
 3 #include <errno.h>
 4 #include <stdlib.h>
 5 #include <sys/wait.h>
 6 
 7 #include <stdio.h>
 8 #include <unistd.h>
 9 #include <sys/types.h>
10 
11 void pr_exit(int status)
12 {
13     if (WIFEXITED(status))
14         printf("normal termination, exit status = %d\n",
15                 WEXITSTATUS(status));
16     else if (WIFSIGNALED(status))
17         printf("abnormal termination, signal number = %d%s\n",
18                 WTERMSIG(status),
19 #ifdef  WCOREDUMP
20                 WCOREDUMP(status) ? " (core file generated)" : "");
21 #else
22     "");
23 #endif
24     else if (WIFSTOPPED(status))
25         printf("child stopped, signal number = %d\n",
26                 WSTOPSIG(status));
27     else if (WIFCONTINUED(status))
28         printf("child continuing...\n");
29 }
30 
31 static void sig_hup(int signo)
32 {
33     printf("SIGHUP received, pid = %d\n", getpid());
34 }
35 
36 static void sig_tstp(int signo)
37 {
38     printf("SIGTSTP received, pid = %d\n", getpid());
39 }
40 
41 static void pr_ids(char *name)
42 {
43     printf("%s: pid = %d, ppid = %d, pgrp = %d, tpgrp = %d\n",
44             name , getpid(), getppid(), getpgrp(), tcgetpgrp(STDIN_FILENO));
45     fflush(stdout);
46 }
47 
48 int main(void)
49 {
50     char   c;
51     pid_t  pid;
52     int    status;
53 
54     pr_ids("parent");
55     if ((pid = fork()) < 0)
56     {
57         perror("fork error");
58         return 0;
59     }
60     else if (pid > 0)
61     {
62                 sleep(5);
63         printf("child process starts %d\n", pid);
64         printf("wait for child process to end\n" );
65         waitpid(pid, &status, WUNTRACED |WCONTINUED);
66         printf("child process ends\n" );
67         
68         pr_exit(status);
69         kill(pid, SIGCONT);
70         waitpid(pid, &status, WUNTRACED|WCONTINUED );
71         pr_exit(status);
72         exit(0);
73     }
74     else// child proc
75     {
76         sleep(10);
77         pr_ids("child 1");
78         //        signal(SIGHUP, sig_hup);
79         //        signal(SIGTSTP, sig_tstp);
80         kill(getpid(), SIGTSTP);
81     }
82 
83     return 0;
84 }
復制代碼

 

輸出結果:

parent: pid = 8116, ppid = 4151, pgrp = 8116, tpgrp = 8116
child process starts 8117
wait for child process to end
child 1: pid = 8117, ppid = 8116, pgrp = 8116, tpgrp = 8116
child process ends
child stopped, signal number = 20
child continuing...

 

可見抓到了stopped 和continue 信號

 

另外,對於處理SIG_CHLD信號的選擇,使用

復制代碼
1 void sig_chld(int signo)
2 {
3        pid_t   pid;
4        int     stat;
5        
6        while((pid = waitpid(-1, &stat, WNOHANG)) > 0){
7                printf("child %d terminated\n", pid);
8        }
9         return;
復制代碼

原因是,一是要有循環,可以避免SIG_CHILD信號丟失的情況,而是要使用WNOHANG選項,來避免waitpid一直等待。。

使用waitpid而不適用wait的原因是wait無法指定NOHANG,使用while循環的原因是處理信號不被緩存的情況

 

 

當有多個子進程的SIGCHLD信號到達父進程的時候,如果父進程用wait等待,那么父進程在處理第一個達到的SIGCHLD信號的時候,其他的SIGCHLD信號被堵塞,而且信號不被緩存,這樣就會導致信號丟失,這樣會產生很多的僵屍進程。。解決辦法是父進程用waitpid來等待子進程信號。。。

正好看到有人問這樣一個問題

看unix網絡編程第一卷的時候,碰到書上這樣一個例子:
一個並發服務器, 每一個客戶端連接服務器就fork一個子進程.書上講到當同時有n多個客戶端斷開連接時,
服務器端同時有n多個子進程終止, 這時候內核同時向父進程發送n多個sigchld信號.它的sigchld信號處理
函數如下:
void sig_chld(int signo)
{
       pid_t   pid;
       int     stat;
      
       while((pid = waitpid(-1, &stat, WNOHANG)) > 0){
               printf("child %d terminated\n", pid);
       }
        return;
}

我的問題是:既然sigchld是不可靠的信號,進程就不可能對sigchld進行排隊, 直接丟棄了sigchld信號(當進程注冊信號的時候,發現已有sigchld注冊進未決信號, 因為內核同時發送多個sigchld).請問大家上面的代碼是如何保證不產生僵屍進程的.謝謝!

超清晰的解答,來自與chinaunix論壇的flw大版主。。。:

復制代碼
根本就不需要找回來!
好比有五個進程,
不妨分別稱為 p1 p2 p3 p4 p5,
一開始 p1 結束了,發了一個 SIGCHLD(s1),
這時父進程可能空閑了,於是開始處理這個信號,假設處理的過程中 p2 又結束了,又發了一個 SIGCHLD(s2),
這時候已經有兩個信號了(一個正在處理,一個待處理),這時如果 p3 又結束了,那么它發的那個 SIGCHLD(s3) 勢必會丟失,
丟失了怎么辦?
沒關系,因為那個信號處理函數是個循環嘛,
所以 while(waitpid()) 的時候,會把 p1 p2 p3 都處理的。
即使是很不幸,因為十分湊巧的原因,p3 沒有被回收,導致變成僵屍進程了,也沒關系,
因為還有 p4 p5 嘛,等到 p4 或者 p5 結束的時候,
又會再一次調用 while(waitpid()),到時候雖說這個 while(waitpid()) 是由 p4/p5 引起的,但是它也會一並把 p3 也處理的,因為它是個循環嘛!


如果還搞不懂,你就再看看 waitpid 的 man。


記住一點:
waitpid 和 SIGCHLD 沒關系,即使是某個子進程對應的 SIGCHLD 丟失了,只要父進程在任何一個時刻調用了 waitpid,那么這個進程還是可以被回收的。


哎呀呀,簡直費勁死了,其實說白了,就是一個“生產者-消費者”問題。
子進程結束的時候,系統“生產”出一個僵屍進程,
同時用 SIGCHLD 通知父進程來“消費”這個僵屍進程,
即使是 SIGCHLD 丟失了,沒有來得及消費,
但是只要有一次消費,就會把所有的僵屍進程都處理光光!
.(我再說一遍:因為,while(waitpid()) 是個循環嘛!)


免責聲明!

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



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