本文关于处理子进程退出状态码的内容主体来自于《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{ ... }