本文關於處理子進程退出狀態碼的內容主體來自於《Pro Perl》的第21章。html
每一個子進程在退出時,操做系統都會保留它們的退出狀態碼,並在內核維護的進程表中保留子進程項。對於進程的退出狀態碼,只有在父進程讀走以後或者收走(reap)以後纔會被清除。注意這裏的一個詞語「收走(reap)」,這是一個Unix操做系統的進程術語,能夠理解爲對死了的進程進行收屍,收走以後稱爲reaped。若是父進程沒有去讀走或者收走子進程的退出狀態碼,這個子進程就會成爲一個殭屍進程(zombie process)。若是在Unix系統中使用ps類的命令,將能夠發現標記爲zombie或defunct的進程,它們就是殭屍進程。數組
不難理解,所謂的殭屍進程,就是子進程執行完畢後父進程沒有對子進程進行收屍後致使的,在內核維護的進程表中還留有子進程信息的屍體,但子進程畢竟已經執行完畢了,這個屍體不會再被調度到,它放在內核進程表中純屬徒佔空間,時間久了就會致使資源泄露問題。只是須要注意的是,每一個子進程退出的那一瞬間(很短期的意思),都屬於殭屍進程,只不過正常狀況下父進程會瞬間收屍,因此這樣短暫的殭屍進程沒法被ps等工具捕捉到。less
不要將殭屍進程和孤兒進程搞混淆。殭屍進程是子進程死了,父進程沒有收屍。孤兒進程是父進程死了,但子進程依然在運行,前面的一篇文章解釋過,子進程能夠脫離父進程所在的進程組,這樣當父進程退出時,子進程成爲孤兒進程,而後掛靠在pid=1的init或systemd進程下由它們進行管理(好比收屍)。函數
Perl的內置函數(除了fork)都會自動處理收屍問題,所以多數時候咱們無需太過關心這方面的問題,對於fork(還有IPC::Open2
和IPC::Open3
),咱們必須手動去收屍。工具
要收走子進程的退出狀態碼,咱們能夠在父進程中使用簡單的wait
函數或者更復雜一點的waitpid
函數,它們會阻塞父進程讓父進程去等待子進程終止,而後收走它們的狀態碼。操作系統
對於等待單個子進程來講,使用wait
便可。wait
的返回值是等待到的子進程的PID(只等一個子進程,等到哪一個就是哪一個),而不是子進程的退出狀態碼。若是沒有子進程可等待了,則wait返回-1。固然,能夠將wait放在空上下文中丟棄wait的返回值。scala
例如,下面的示例程序中,在父進程中使用了wait函數等待子進程睡眠的完成。code
#!/usr/bin/perl use strict; use warnings; unless(fork){ print "(Child)->my PID: $$\n"; sleep 3; exit 0; } my $child_pid = wait; print "reaped Child: $child_pid\n";
執行結果:htm
(Child)->my PID: 220 reaped Child: 220
當wait返回的時候,它會將子進程的退出狀態碼設置到特殊變量$?
中。這個特殊變量是一個16比特位的值,高8位是退出狀態碼,低8位中的低7位是致使進程退出的信號(若是是信號致使子進程退出的話),高位是coredump的flag(即表示這個退出的進程是否進行了coredump)。這個16比特位的返回值和Unix的wait系統調用的返回值徹底一致。blog
因此,要獲取這個16位返回值中的3部分,可使用下面的位操做方式:
my $exitsig = $? & 127 # 127 = 0000 0000 0111 1111 my $cored = $? & 128 # 128 = 0000 0000 1000 0000 my $exitcode = $? >> 8
在POSIX
模塊中,有一些很方便的函數(它們都和C中的宏名稱相同),好比這裏用來提取狀態碼的函數:
use POSIX qw(:sys_wait_h); $exitsig = WSTOPSIG($?); $exitcode = WEXITSTATUS($?);
在本文的後面還會繼續提到一些POSIX模塊中的函數。
wait只會設置一個退出狀態碼,對於成功執行後退出的子進程,意味着執行完畢,且沒有信號中斷,也沒有coredump(只有失敗的進程纔可能會由coredump),因此其狀態碼爲0,也就是說$?
將等於0。因而,咱們能夠經過這個值去作布爾判斷,若是$?
爲false,則子進程成功。
wait; $exitcode = $? >> 8; if ($exitcode) { print "Child Process failed: $exitcode"; }
有些調用外部命令的狀況下,退出狀態碼可能會是一個errno值,咱們能夠將其賦值給$!
來完善錯誤描述。例如:
wait; $exitcode = $? >> 8; if ($exitcode) { $! = $exitcode; # 賦值給 $! 來從新建立error die "Child aborted with error: $!"; }
若是wait時沒有子進程能夠等待,那麼wait將當即返回-1。固然,大多數時候這沒什麼用,由於咱們的wait不會在沒有fork的狀況下使用。
若是想要等待多個子進程或者某個指定的子進程,wait函數就不夠用了,由於wait是隻要等到任意一個子進程退出就能夠。
waitpid函數能夠指定等待的pid,也能夠一次性等待多個子進程(稍後解釋)。
waitpid $pid, 0;
第一個參數是要等待的pid,第二個參數是flag,用於指示waitpid的等待模式。flag=0表示waitpid以阻塞的方式等待pid。waitpid的返回值是等待到的子進程PID(也就是已死的子進程),若是指定的等待進程不存在則返回-1。
例如,要等待某個指定的子進程:
$pid = fork; unless($pid){ #子進程中 ... do something ... } # 父進程中 waitpid $pid, 0;
另外一個經常使用的flag是POSIX模塊中的"WNOHANG",它指示waitpid不要阻塞等待子進程,而是當即返回0。這時,只要有能匹配指定的PID出現終止的子進程,waitpid就返回大於0的對應的PID值。若是沒有子進程可等(或等待的子進程不存在),則返回-1。(參見man waitpid)
use POSIX qw(:sys_wait_h); # 或 use POSIX qw(WNOHANG);
在非阻塞的"WNOHAGN"指示符下,能夠按期去檢查子進程是否退出,而無需強制阻塞在那裏等待子進程。例如,每3秒去檢查一次子進程。
use POSIX qw(WNOHANG); my $pid = fork; unless($pid){ ...child... } # 等待單子進程,能夠檢測返回值是否等於0 while((waitpid $pid, WNOHANG) == 0){ say "waiting"; sleep 3; } # 多個子進程(見下文),能夠檢測返回值是否等於-1, # 不等於-1就表示還有要等待的子進程 while((waitpid -1, WNOHANG) != -1) { print "Waiting for PID: $pid...\n"; sleep 3; }
因爲waitpid只有兩個參數,第一個參數是要等待的PID。要想waitpid等待多個子進程,只能將子進程的PID收集到一個列表中,而後將這個列表做爲waitpid的參數。
可喜的是,waitpid的第一個PID參數能夠指定爲3種特殊的值(https://perldoc.perl.org/functions/waitpid.html):
0
:表示等待當前進程所在進程組中的任意子進程-1
:表示等待任意該父進程的子進程這時的waitpid就像wait函數同樣,只要有任意子進程退出能夠。
例如:
# wait until any child exits waitpid -1, 0; # nonblocking version waitpid -1, WNOHANG;
若是fork了多個子進程,且父進程還想要等待它們所有都退出,這是很是常見的需求。
這裏先複習下wait()和waitpid()的返回值,它們很重要:
因此,要等待全部子進程退出,有3種最基本的方法。
若是使用阻塞的wait()函數,當沒有子進程能夠等待後,它將返回-1。因而能夠判斷,若是返回值爲-1,就表示子進程全退出了,不然就一直阻塞地等待:
# 父進程 until(wait() == -1){}
使用阻塞的waitpid()時,只要指定第一個參數爲-1表示等待任意子進程,那麼方法也同樣:
until(waitpid(-1, 0) == -1){}
若是使用非阻塞的waitpid(-1, WNOHANG)
,由於在沒有子進程存在時將返回-1,因此不等於-1的返回值表示還有子進程存在,還需繼續等:
while(waitpid(-1, WNOHANG) != -1){} until(waitpid(-1, WNOHANG) == -1){}
比較上面三種狀況的代碼,不難發現其實都同樣:
until((wait/waitpid) == -1){}
除了上面三種方法以外,還能夠在fork以後在父進程中將每次fork的子進程pid收集到hash結構(或數組)中,並定義SIGCHLD信號處理器,並在這個信號處理器中將等待到的pid從容器中移除。只要容器的元素數量大於0,就表示還有子進程存在。大體代碼的邏輯以下:
# 父進程,註冊SIGCHLD handler $SIG{CHLD} = \&reap_childs; # fork 3個子進程 for (1..3){ my $pid = fork; # 父進程跟蹤子進程,將其放進hash結構 if($pid){ $kids{$pid} = 1; } else { # 子進程 ...... } } # 父進程:容器中還有元素,繼續等待 while( scalar(keys %kids) > 0){ sleep 1; } # SIGCHLD handler sub reap_childs { local $!; # 好習慣,省得被waitpid()更改errno while(1){ my $kid = waitpid(-1, WNOHANG); # $kid>0表示等待到了子進程,將其移除 last unless ($kid > 0); delete $kids{$kid}; } }
若是父進程要等待子進程結束,須要使用wait或waitpid函數。但有時候,也可能子進程等待父進程結束。若是父進程先結束,那麼子進程將變成孤兒進程,從而被pid=1的init/systemd進程收養。
因而,能夠在子進程中經過getppid()來獲取父進程的pid,而後不斷地比較它是否等於1,這個不斷比較的過程稱爲輪詢(polling)。
例如:
while(getppid() != 1){ # 父進程尚未退出 sleep 1; }
不少時候,咱們使用waitpid並不是想要檢查子進程是否退出了,特別是子進程的退出狀態碼對咱們來講是可有可無時,咱們想要作的僅僅是在子進程退出時將它們從進程表中移除。
子進程退出時會發送SIGCHLD信號,因而咱們能夠在父進程中定義一個該信號的處理子程序,在該子程序中經過waitpid對全部可能的子進程收屍。並且,子進程可能會有多個,因此在一個循環中去無限收屍直到沒有子進程。代碼以下:
use POSIX qw(WNOHANG); sub waitforchildren { my $pid; until ($pid == -1){ $pid = waitpid -1, WNOHANG; } } $SIG{CHLD} = \&waitforchildren;
也能夠在程序中設置忽略CHLD信號,讓操做系統來爲咱們對子進程收屍:
$SIG{CHLD} = 'IGNORE';
或者,咱們還能夠更改進程的進程組,讓pid=1的init/systemd進程來負責收屍,可是這並不是好主意,除非這是一個daemon類程序。
因此,正常狀況下,前面的設置CHLD信號處理是最通用的收屍方式。
POSIX模塊定義了一些方便的功能,好比前面用過的WNOHANG修飾符。能夠導入:sys_wait_h
標籤:
use POSIX qw(:sys_wait_h);
其實有兩個flag可用於waitpid,一個是WNOHAGN,另外一個是WUNTRACED,也用於返回當前已中止(確切地說是經過SIGSTOP中止)且還未恢復(經過SIGCONT信號來恢復)的子進程的PID。例如:
$possibly_stopped_pid = waitpid -1, WNOHANG | WUNTRACED;
此外,還有如下一些比較好用的函數。在看這些函數以前,先明確幾點:
exit
或執行完畢的方式退出WEXITSTATUS 提取已推出進程的退出狀態碼,它等價於"$? >> 8"。例如"$exitcode = WEXITSTATUS($?);"。若是進程是被信號終止的,則退出狀態碼爲0。 WTERMSIG 提取終止進程的信號,前提是這個進程是被信號所終止的。例如"$exitsig = WTERMSIG($?);"。若進程正常退出(即便是因錯退出)而非信號退出,則提取值爲0。 WIFEXITED 檢查進程是否已經退出,檢測的是信號中斷方式的對立面,也就是和WIFSIGNALED的對立面。 WIFSIGNALED 檢查進程是不是被信號終止的,是exit退出方式的對立面,也就是WIFEXITED的對立面。例如: if(WIFEXITED $?){ print "exited with error"; return WEXITSTATUS($?); } elseif(WIFSIGNALED $?) { print "aborted by signal"; return WTREMSIG($?); } else { # exit code was 0 print "Success!"; } WSTOPSIG 在指定了WUNTRACED的狀況下,會返回已stopped的進程PID,該函數提取致使進程進入stopped狀態的信號(數值格式),通常來講致使進程進入stopped狀態的信號都是SIGSTOP信號,但並不是絕對。例如"$stopsig = WSTOPSIG($?);"。 WIFSTOPPED 若是指定了WUNTRACED的狀況下,若是返回的進程是stopped狀態的,則返回true。例如: if(WIFSTOPPED $?){ print "process stopped by signal", WSTOPSIG($?), "\n"; } else{ ... }