本文关于处理子进程退出状态码的内容主体来自于《Pro Perl》的第21章。

子进程退出状态码

每个子进程在退出时,操作系统都会保留它们的退出状态码,并在内核维护的进程表中保留子进程项。对于进程的退出状态码,只有在父进程读走之后或者收走(reap)之后才会被清除。注意这里的一个词语“收走(reap)”,这是一个Unix操作系统的进程术语,可以理解为对死了的进程进行收尸,收走之后称为reaped。如果父进程没有去读走或者收走子进程的退出状态码,这个子进程就会成为一个僵尸进程(zombie process)。如果在Unix系统中使用ps类的命令,将可以发现标记为zombie或defunct的进程,它们就是僵尸进程。

不难理解,所谓的僵尸进程,就是子进程执行完毕后父进程没有对子进程进行收尸后导致的,在内核维护的进程表中还留有子进程信息的尸体,但子进程毕竟已经执行完毕了,这个尸体不会再被调度到,它放在内核进程表中纯属徒占空间,时间久了就会导致资源泄露问题。只是需要注意的是,每个子进程退出的那一瞬间(很短时间的意思),都属于僵尸进程,只不过正常情况下父进程会瞬间收尸,所以这样短暂的僵尸进程无法被ps等工具捕捉到。

不要将僵尸进程和孤儿进程搞混淆。僵尸进程是子进程死了,父进程没有收尸。孤儿进程是父进程死了,但子进程依然在运行,前面的一篇文章解释过,子进程可以脱离父进程所在的进程组,这样当父进程退出时,子进程成为孤儿进程,然后挂靠在pid=1的init或systemd进程下由它们进行管理(比如收尸)。

Perl的内置函数(除了fork)都会自动处理收尸问题,因此多数时候我们无需太过关心这方面的问题,对于fork(还有IPC::Open2IPC::Open3),我们必须手动去收尸。

wait等待单个子进程

要收走子进程的退出状态码,我们可以在父进程中使用简单的wait函数或者更复杂一点的waitpid函数,它们会阻塞父进程让父进程去等待子进程终止,然后收走它们的状态码。

对于等待单个子进程来说,使用wait即可。wait的返回值是等待到的子进程的PID(只等一个子进程,等到哪个就是哪个),而不是子进程的退出状态码。如果没有子进程可等待了,则wait返回-1。当然,可以将wait放在空上下文中丢弃wait的返回值。

例如,下面的示例程序中,在父进程中使用了wait函数等待子进程睡眠的完成。

  1. #!/usr/bin/perl
  2. use strict;
  3. use warnings;
  4. unless(fork){
  5. print "(Child)->my PID: $$\n";
  6. sleep 3;
  7. exit 0;
  8. }
  9. my $child_pid = wait;
  10. print "reaped Child: $child_pid\n";

执行结果:

  1. (Child)->my PID: 220
  2. reaped Child: 220

获取退出状态码

当wait返回的时候,它会将子进程的退出状态码设置到特殊变量$?。这个特殊变量是一个16比特位的值,高8位是退出状态码,低8位中的低7位是导致进程退出的信号(如果是信号导致子进程退出的话),高位是coredump的flag(即表示这个退出的进程是否进行了coredump)。这个16比特位的返回值和Unix的wait系统调用的返回值完全一致。

所以,要获取这个16位返回值中的3部分,可以使用下面的位操作方式:

  1. my $exitsig = $? & 127 # 127 = 0000 0000 0111 1111
  2. my $cored = $? & 128 # 128 = 0000 0000 1000 0000
  3. my $exitcode = $? >> 8

POSIX模块中,有一些很方便的函数(它们都和C中的宏名称相同),比如这里用来提取状态码的函数:

  1. use POSIX qw(:sys_wait_h);
  2. $exitsig = WSTOPSIG($?);
  3. $exitcode = WEXITSTATUS($?);

在本文的后面还会继续提到一些POSIX模块中的函数。

wait只会设置一个退出状态码,对于成功执行后退出的子进程,意味着执行完毕,且没有信号中断,也没有coredump(只有失败的进程才可能会由coredump),所以其状态码为0,也就是说$?将等于0。于是,我们可以通过这个值去做布尔判断,如果$?为false,则子进程成功。

  1. wait;
  2. $exitcode = $? >> 8;
  3. if ($exitcode) {
  4. print "Child Process failed: $exitcode";
  5. }

有些调用外部命令的情况下,退出状态码可能会是一个errno值,我们可以将其赋值给$!来完善错误描述。例如:

  1. wait;
  2. $exitcode = $? >> 8;
  3. if ($exitcode) {
  4. $! = $exitcode; # 赋值给 $! 来重新创建error
  5. die "Child aborted with error: $!";
  6. }

如果wait时没有子进程可以等待,那么wait将立即返回-1。当然,大多数时候这没什么用,因为我们的wait不会在没有fork的情况下使用。

waitpid等待指定子进程

如果想要等待多个子进程或者某个指定的子进程,wait函数就不够用了,因为wait是只要等到任意一个子进程退出就可以。

waitpid函数可以指定等待的pid,也可以一次性等待多个子进程(稍后解释)。

  1. waitpid $pid, 0;

第一个参数是要等待的pid,第二个参数是flag,用于指示waitpid的等待模式。flag=0表示waitpid以阻塞的方式等待pid。waitpid的返回值是等待到的子进程PID(也就是已死的子进程),如果指定的等待进程不存在则返回-1

例如,要等待某个指定的子进程:

  1. $pid = fork;
  2. unless($pid){
  3. #子进程中
  4. ... do something ...
  5. }
  6. # 父进程中
  7. waitpid $pid, 0;

另一个常用的flag是POSIX模块中的"WNOHANG",它指示waitpid不要阻塞等待子进程,而是立即返回0。这时,只要有能匹配指定的PID出现终止的子进程,waitpid就返回大于0的对应的PID值。如果没有子进程可等(或等待的子进程不存在),则返回-1。(参见man waitpid)

  1. use POSIX qw(:sys_wait_h);
  2. # 或
  3. use POSIX qw(WNOHANG);

在非阻塞的"WNOHAGN"指示符下,可以定期去检查子进程是否退出,而无需强制阻塞在那里等待子进程。例如,每3秒去检查一次子进程。

  1. use POSIX qw(WNOHANG);
  2. my $pid = fork;
  3. unless($pid){
  4. ...child...
  5. }
  6. # 等待单子进程,可以检测返回值是否等于0
  7. while((waitpid $pid, WNOHANG) == 0){
  8. say "waiting";
  9. sleep 3;
  10. }
  11. # 多个子进程(见下文),可以检测返回值是否等于-1,
  12. # 不等于-1就表示还有要等待的子进程
  13. while((waitpid -1, WNOHANG) != -1) {
  14. print "Waiting for PID: $pid...\n";
  15. sleep 3;
  16. }

waitpid等待多个子进程

由于waitpid只有两个参数,第一个参数是要等待的PID。要想waitpid等待多个子进程,只能将子进程的PID收集到一个列表中,然后将这个列表作为waitpid的参数。

可喜的是,waitpid的第一个PID参数可以指定为3种特殊的值(https://perldoc.perl.org/functions/waitpid.html):

  • 0:表示等待当前进程所在进程组中的任意子进程
  • -1:表示等待任意该父进程的子进程
  • 小于-1的值:表示等待进程组为-PID的子进程(也就是PID所在组内的子进程)

这时的waitpid就像wait函数一样,只要有任意子进程退出可以。

例如:

  1. # wait until any child exits
  2. waitpid -1, 0;
  3. # nonblocking version
  4. 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,就表示子进程全退出了,否则就一直阻塞地等待:

  1. # 父进程
  2. until(wait() == -1){}

使用阻塞的waitpid()时,只要指定第一个参数为-1表示等待任意子进程,那么方法也一样:

  1. until(waitpid(-1, 0) == -1){}

如果使用非阻塞的waitpid(-1, WNOHANG),因为在没有子进程存在时将返回-1,所以不等于-1的返回值表示还有子进程存在,还需继续等:

  1. while(waitpid(-1, WNOHANG) != -1){}
  2. until(waitpid(-1, WNOHANG) == -1){}

比较上面三种情况的代码,不难发现其实都一样:

  1. until((wait/waitpid) == -1){}

除了上面三种方法之外,还可以在fork之后在父进程中将每次fork的子进程pid收集到hash结构(或数组)中,并定义SIGCHLD信号处理器,并在这个信号处理器中将等待到的pid从容器中移除。只要容器的元素数量大于0,就表示还有子进程存在。大致代码的逻辑如下:

  1. # 父进程,注册SIGCHLD handler
  2. $SIG{CHLD} = \&reap_childs;
  3. # fork 3个子进程
  4. for (1..3){
  5. my $pid = fork;
  6. # 父进程跟踪子进程,将其放进hash结构
  7. if($pid){
  8. $kids{$pid} = 1;
  9. } else {
  10. # 子进程
  11. ......
  12. }
  13. }
  14. # 父进程:容器中还有元素,继续等待
  15. while( scalar(keys %kids) > 0){
  16. sleep 1;
  17. }
  18. # SIGCHLD handler
  19. sub reap_childs {
  20. local $!; # 好习惯,免得被waitpid()更改errno
  21. while(1){
  22. my $kid = waitpid(-1, WNOHANG);
  23. # $kid>0表示等待到了子进程,将其移除
  24. last unless ($kid > 0);
  25. delete $kids{$kid};
  26. }
  27. }

子进程等待父进程

如果父进程要等待子进程结束,需要使用wait或waitpid函数。但有时候,也可能子进程等待父进程结束。如果父进程先结束,那么子进程将变成孤儿进程,从而被pid=1的init/systemd进程收养。

于是,可以在子进程中通过getppid()来获取父进程的pid,然后不断地比较它是否等于1,这个不断比较的过程称为轮询(polling)。

例如:

  1. while(getppid() != 1){
  2. # 父进程还没有退出
  3. sleep 1;
  4. }

使用waitpid收尸:CHLD信号的处理器

很多时候,我们使用waitpid并非想要检查子进程是否退出了,特别是子进程的退出状态码对我们来说是无关紧要时,我们想要做的仅仅是在子进程退出时将它们从进程表中移除。

子进程退出时会发送SIGCHLD信号,于是我们可以在父进程中定义一个该信号的处理子程序,在该子程序中通过waitpid对所有可能的子进程收尸。而且,子进程可能会有多个,所以在一个循环中去无限收尸直到没有子进程。代码如下:

  1. use POSIX qw(WNOHANG);
  2. sub waitforchildren {
  3. my $pid;
  4. until ($pid == -1){
  5. $pid = waitpid -1, WNOHANG;
  6. }
  7. }
  8. $SIG{CHLD} = \&waitforchildren;

也可以在程序中设置忽略CHLD信号,让操作系统来为我们对子进程收尸:

  1. $SIG{CHLD} = 'IGNORE';

或者,我们还可以更改进程的进程组,让pid=1的init/systemd进程来负责收尸,但是这并非好主意,除非这是一个daemon类程序。

所以,正常情况下,前面的设置CHLD信号处理是最通用的收尸方式。

POSIX的flags和函数

POSIX模块定义了一些方便的功能,比如前面用过的WNOHANG修饰符。可以导入:sys_wait_h标签:

  1. use POSIX qw(:sys_wait_h);

其实有两个flag可用于waitpid,一个是WNOHAGN,另一个是WUNTRACED,也用于返回当前已停止(确切地说是通过SIGSTOP停止)且还未恢复(通过SIGCONT信号来恢复)的子进程的PID。例如:

  1. $possibly_stopped_pid = waitpid -1, WNOHANG | WUNTRACED;

此外,还有以下一些比较好用的函数。在看这些函数之前,先明确几点:

  • 进程有两种退出方式:

    • 正常或因错误退出,也就是exit或执行完毕的方式退出
    • 被信号终止退出,这是和exit退出方式的对立面
  • 进程有退出状态和stopped状态
  • 下面介绍了3对函数,分别是if判断类的函数和提取类的函数,判断进程是exit方式退出的还是信号终止方式退出的,还是进入到了stopped状态,并在各种退出方式中提取对应的状态码或信号
  1. WEXITSTATUS
  2. 提取已推出进程的退出状态码,它等价于"$? >> 8"。例如"$exitcode = WEXITSTATUS($?);"。如果进程是被信号终止的,则退出状态码为0
  3. WTERMSIG
  4. 提取终止进程的信号,前提是这个进程是被信号所终止的。例如"$exitsig = WTERMSIG($?);"。若进程正常退出(即使是因错退出)而非信号退出,则提取值为0
  5. WIFEXITED
  6. 检查进程是否已经退出,检测的是信号中断方式的对立面,也就是和WIFSIGNALED的对立面。
  7. WIFSIGNALED
  8. 检查进程是否是被信号终止的,是exit退出方式的对立面,也就是WIFEXITED的对立面。例如:
  9. if(WIFEXITED $?){
  10. print "exited with error";
  11. return WEXITSTATUS($?);
  12. } elseif(WIFSIGNALED $?) {
  13. print "aborted by signal";
  14. return WTREMSIG($?);
  15. } else {
  16. # exit code was 0
  17. print "Success!";
  18. }
  19. WSTOPSIG
  20. 在指定了WUNTRACED的情况下,会返回已stopped的进程PID,该函数提取导致进程进入stopped状态的信号(数值格式),一般来说导致进程进入stopped状态的信号都是SIGSTOP信号,但并非绝对。例如"$stopsig = WSTOPSIG($?);"
  21. WIFSTOPPED
  22. 如果指定了WUNTRACED的情况下,如果返回的进程是stopped状态的,则返回true。例如:
  23. if(WIFSTOPPED $?){
  24. print "process stopped by signal", WSTOPSIG($?), "\n";
  25. } else{
  26. ...
  27. }

Perl处理和收走子进程(退出状态码和wait)的更多相关文章

  1. Shell揭秘——程序退出状态码

    程序退出状态码 前言 在本篇文章当中主要给大家介绍一个shell的小知识--状态码.这是当我们的程序退出的时候,子进程会将自己程序的退出码传递给父进程,有时候我们可以利用这一操作做一些进程退出之后的事 ...

  2. shell中的退出状态码

    shell中的退出状态码最大只有255,如果超过这个值,就会进行取余运算,即如果执行如下命令: exit exitCode 如果exitCode大于255,那么实际的状态码为exitCode % 25 ...

  3. linux退出状态码及exit命令

    Linux提供了一个专门的变量$?来保存上个已执行命令的退出状态码.对于需要进行检查的命令,必须在其运行完毕后立刻查看或使用$?变量.它的值会变成由shell所执行的最后一条命令的退出状态码: [ro ...

  4. Linux退出状态码

    命令成功结束 一般性未知错误 不适合的shell命令 命令不可执行 没找到命令 无效的退出参数 +x 与Linux信号x相关的严重错误 通过Ctrl+C终止的命令 正常范围之外的退出状态码

  5. linux 退出状态码

    状态码 描述 0 命令成功结束 1 一般性未知错误 2 不适合的shell 命令 123 命令不可执行 127 没找到命令 128 无效退出参数 128+x 与linux信号x相关的严重错误 130 ...

  6. Shell中退出状态码exit

    shell中运行的每个命令都使用退出状态码(exit status)来告诉shell它完成了处理.退出状态码是一个0~255之间的整数值,在命令结束时由命令传回shell. 1 .查看退出状态码 Li ...

  7. Linux 进程--父进程查询子进程的退出状态

    僵尸进程 当一个子进程先于父进程结束运行时,它与其父进程之间的关联还会保持到父进程也正常地结束运行,或者父进程调用了wait才告终止. 子进程退出时,内核将子进程置为僵尸状态,这个进程称为僵尸进程,它 ...

  8. Linux Shell编程(11)——退出和退出状态

    exit命令一般用于结束一个脚本,就像C语言的exit一样.它也能返回一个值给父进程.每一个命令都能返回一个退出状态(有时也看做返回状态).一个命令执行成功返回0,一个执行不成功的命令则返回一个非零值 ...

  9. Linux shell编程-退出的状态码

    linux 提供了一个专门的变量$?来保存上个已执行命令的状态码 linux 的错误状态退出状态码没有什么标准可遵循,但有一些参考 状态码 描述 0 命令成功结束 1 一般性未知错误 2 不适合的sh ...

随机推荐

  1. vue Error: No PostCSS Config found in

    最近在做一个vue的移动端的项目,遇到一个问题,我本地的项目运行正常,可是上传到github上的一启动就报错,就是标题上的错误,找了很久,刚开始以为是某个css没有配置,就把本地的复制过去还是报错,无 ...

  2. java方法的调用

    各种方法的调用实例 package cn.edu.fhj.day004; public class FunctionDemo { // 定义全局的变量 public int a = 3; public ...

  3. Java 将容器List里面的内容保存到数组

    import java.util.List; import java.util.ArrayList; public class listToArr { public static void main( ...

  4. 第一次冲刺意见汇总&团队第一阶段总结

    大家对我们小组的意见基本是: 1.设计界面简单 2.功能较少 3.没有实现切换歌曲的功能 谢谢HT小组的走心评价 接下来我们组内准备:1.先调节用户界面,插入一些图片,美化界面,给用户直观的体验上升. ...

  5. 封装ajax原理

    封装ajax原理 首先处理 用户如果不传某些参数,设置默认值 type默认get 默认url为当前页 默认async方式请求 data数据默认为{} 处理用户传进来的参数对象 遍历,拼接成key=va ...

  6. VS Code 常用插件

    1.Chinese (Simplified) Language Pack for Visual Studio Code              VS Code软件汉化 2.Auto Close Ta ...

  7. 搭建 RTMP 服务器

    主要步骤 具体步骤 FAQ docker 搭建版 参考 主要步骤 下载 nginx 的 rtmp 模块 编译nginx,带 hls,rtmp 配置 nginx.conf,设置 rtmp 的推流文件路径 ...

  8. javaEE学习路线与目标

    1.Java基础知识(15-30天) 2.了解html+css+js+jq+bootstrap(7天) 3.mysql+jdbc(重点)(3天) 4.xml(1天) 5.http协议+tomcat(1 ...

  9. python从入门到实践-8章函数

    #!/user/bin/env python# -*- coding:utf-8 -*- # 给形参指定默认值时,等号两边不要有空格 def function_name("parameter ...

  10. IPython绘图和可视化---matplotlib

    1. 启动 IPython 2. >> fig = plt.figure() >> ax1 = fig.add_subplot(346)          # 将画布分割成3行 ...