Perl處理和收走子進程(退出狀態碼和wait)

本文關於處理子進程退出狀態碼的內容主體來自於《Pro Perl》的第21章。html

子進程退出狀態碼

每一個子進程在退出時,操做系統都會保留它們的退出狀態碼,並在內核維護的進程表中保留子進程項。對於進程的退出狀態碼,只有在父進程讀走以後或者收走(reap)以後纔會被清除。注意這裏的一個詞語「收走(reap)」,這是一個Unix操做系統的進程術語,能夠理解爲對死了的進程進行收屍,收走以後稱爲reaped。若是父進程沒有去讀走或者收走子進程的退出狀態碼,這個子進程就會成爲一個殭屍進程(zombie process)。若是在Unix系統中使用ps類的命令,將能夠發現標記爲zombie或defunct的進程,它們就是殭屍進程。數組

不難理解,所謂的殭屍進程,就是子進程執行完畢後父進程沒有對子進程進行收屍後致使的,在內核維護的進程表中還留有子進程信息的屍體,但子進程畢竟已經執行完畢了,這個屍體不會再被調度到,它放在內核進程表中純屬徒佔空間,時間久了就會致使資源泄露問題。只是須要注意的是,每一個子進程退出的那一瞬間(很短期的意思),都屬於殭屍進程,只不過正常狀況下父進程會瞬間收屍,因此這樣短暫的殭屍進程沒法被ps等工具捕捉到。less

不要將殭屍進程和孤兒進程搞混淆。殭屍進程是子進程死了,父進程沒有收屍。孤兒進程是父進程死了,但子進程依然在運行,前面的一篇文章解釋過,子進程能夠脫離父進程所在的進程組,這樣當父進程退出時,子進程成爲孤兒進程,而後掛靠在pid=1的init或systemd進程下由它們進行管理(好比收屍)。函數

Perl的內置函數(除了fork)都會自動處理收屍問題,所以多數時候咱們無需太過關心這方面的問題,對於fork(還有IPC::Open2IPC::Open3),咱們必須手動去收屍。工具

wait等待單個子進程

要收走子進程的退出狀態碼,咱們能夠在父進程中使用簡單的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的狀況下使用。

waitpid等待指定子進程

若是想要等待多個子進程或者某個指定的子進程,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等待多個子進程

因爲waitpid只有兩個參數,第一個參數是要等待的PID。要想waitpid等待多個子進程,只能將子進程的PID收集到一個列表中,而後將這個列表做爲waitpid的參數。

可喜的是,waitpid的第一個PID參數能夠指定爲3種特殊的值(https://perldoc.perl.org/functions/waitpid.html):

  • 0:表示等待當前進程所在進程組中的任意子進程
  • -1:表示等待任意該父進程的子進程
  • 小於-1的值:表示等待進程組爲-PID的子進程(也就是PID所在組內的子進程)

這時的waitpid就像wait函數同樣,只要有任意子進程退出能夠。

例如:

# wait until any child exits
waitpid -1, 0;

# nonblocking version
waitpid -1, WNOHANG;

等待全部子進程退出

若是fork了多個子進程,且父進程還想要等待它們所有都退出,這是很是常見的需求。

這裏先複習下wait()和waitpid()的返回值,它們很重要:

  • wait()阻塞等待,等待到了則返回對應的pid,指定的子進程不存在或沒有了子進程都觸發error,將返回-1
  • waitpid($pid, 0)阻塞等待,等待到了則返回對應的pid,指定的子進程不存在或沒有了子進程都觸發error,將返回-1
  • waitpid($pid, WNOHANG)非阻塞等待,等待到了則返回對應的pid,一個子進程都沒等待到則返回0,等待的子進程不存在或沒有子進程可等則觸發error,並返回-1

因此,要等待全部子進程退出,有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收屍:CHLD信號的處理器

不少時候,咱們使用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的flags和函數

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或執行完畢的方式退出
    • 被信號終止退出,這是和exit退出方式的對立面
  • 進程有退出狀態和stopped狀態
  • 下面介紹了3對函數,分別是if判斷類的函數和提取類的函數,判斷進程是exit方式退出的仍是信號終止方式退出的,仍是進入到了stopped狀態,並在各類退出方式中提取對應的狀態碼或信號
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{
    ...
}
相關文章
相關標籤/搜索