32-wait大变身之waitpid

上一篇,介绍了僵尸进程的概念,也学会了用 wait 清除它。实际上,wait 函数的本质并不是为清除僵尸进程而存在,而是为了获取子进程状态。

只要子进程的状态发生了改变,它就会给父进程发信号(SIGCHLD)。比如子进程暂停执行,恢复执行。这些信号,父进程都可以忽略,没什么关系。当然父进程可以选择处理它,也不可不处理,没什么大问题。

不过当子进程在正常退出时给父进程发送信号,如果父进程还忽视,子进程自然就变僵尸了。

到了这里,提及信号的概念次数越来越多,马上就要正式讲它了,不过现在还不是时候。本篇需要重点讨论的是 wait 函数的加强版——waitpid 函数。

waitpid 函数和 wait 函数功能一样,都是用来获取子进程的状态(的改变)。

1. waitpid 函数

  • waitpid 函数的原型
pid_t waitpid(pid_t pid, int *status, int options);

初看上去感觉这个函数很复杂,只要掌握了规律,其实就没什么了。这里将从参数和返回值展开说明。

1.1 参数 pid

  • pid > 0,表示 waitpid 只等待子进程 pid。
  • pid = 0,表示 waitpid 等待和当前调用 waitpid 一个组的所有子进程。
  • pid = -1,表示等待所有子进程。
  • pid < -1,表示等待组 i d = ∣ p i d ∣ id = |pid| id=pid (绝对值 pid)的所有子进程。

调用 wait(&status) 函数实际上就相当于调用 waitpid(-1, &status, 0)

1.2 参数 status

这里的 status 参数实际上和 wait 的参数是一样的,之前 wait 没有说明,所以放到这里一起解释了。

status 描述的是子进程的退出状态,它包括了不止一种信息。可以通过宏来判断 status 到底属于哪种信息。

首先,status 分成下面四类:

  • 1 进程正常退出
  • 2 进程被信号终止
  • 3 进程被暂停执行
  • 4 进程被恢复执行

这四种情况,由 4 个宏函数判断到底是哪一种。分别是 WIFEXITED(status)WIFSIGNALED(status)WIFSTOPPED(status)WIFCONTINUED(status)。这些都能从宏函数的名字里看到(exited, signaled, stopped, continued)。这种实现技巧实际上归因于 status 不同比特位代表了不同含义。

所以当你拿到了 status 后的第一件事情就是先判断 status 到底属于哪个类别。

前方预警:waitpid 能否接收到状态 3(stopped) 和 4(continued),是取决于 options 参数的,马上就讲!

1.2.1 正常退出

如果 WIFEXITED(status) 返回 true,这说明进程是正常退出。在这种情况下,你可以使用宏WEXITSTATUS(status)来获取进程的退出码(main 函数的 return 值或者 exit 函数的参数值)。

1.2.3 进程被信号终止

如果WIFSIGNALED(status) 返回 true,这说明进程是被信号终止的。这时候,可以通过宏 WTERMSIG(status) 来获取子进程是被哪种信号终止的。

1.2.3 进程被暂停执行

如果 WIFSTOPPED(status) 返回 true,说明进程收到信号暂停执行。这时候,可以通过宏 WSTOPSIG(status)来获取子进程是被哪个信号暂停的。

这时候你可以通过宏 WCOREDUMP(status) 返回 true 还是 false 来判断是否生生了 core 文件。在某些系统中,WCOREDUMP 宏可能没有定义,你需要使用 #ifdef WCOREDUMP ... #endif 来判断。

1.2.4 进程被恢复执行

如果 WIFCONTINUED(status) 返回 true,说明进程收到信号恢复执行(SIGCONT)。已经不需要额外的宏函数来获取是哪个信息让它恢复执行了,默认就是 SIGCONT。

1.3 参数 options

参数 options 是可个组合选项,这也是参数写 options 而不是 option 原因。

options 的三个可组合选项如下(可以理解成开关组合):

  • WNOHANG (设置非阻塞,即使子进程全部正常运行,waitpid 也会立即返回 0)
  • WUNTRACED (可获取子进程暂停状态,也就是可获取 stopped 状态)
  • WCONTINUED (可获取子进程恢复执行的状态,也就是可获取 continued 状态)

你可以任意使用位或 ∣ | 运算符自由组合他们。如果 options 置空(这三个开关都不打开),也就是 0,这意味着 waitpid 函数是阻塞的(在有子进程正常运行的情况下)。

waitpid 能否接收 stopped 和 continued 状态,取决于 WUNTRACEDWCONTINUED 开关是否打开。

如果你的 WUNTRACED 开关未打开,waitpid 函数是不会理会子进程停止状态的。同理,如果 WCONTINUED 开关未打开,waitpid 也不会理会子进程恢复执行的状态。后面的实验,读者可以自行验证,代码也会提供。

1.4 返回值

waitpid 的返回值通常也会有 > 0, = 0, 以及 = -1 这几种情况。

  • = − 1 = -1 =1,意味着没有子进程或者其它错误。
  • = 0 = 0 =0,这只有在打开了 WNOHANG 的情况下才会可能出现。如果子进程都是正常运行没有发生状态改变,它就会返回 0.
  • > 0 > 0 >0,只要有任意一个子进程状态发生改变,比如停止,终止,恢复执行,waitpid 就会返回该子进程的 pid。

2. 实验

waitpid 函数有一点麻烦(恐怕是学习 linux 编程以来最长的一份代码了),同学们可以自己复制后面的代码,然后自己修改。

  • 代码
// wipeoutzombie2.c
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>

int main() {
  printf("before fork\n");

  // 生成 5 个子进程
  pid_t pid, n = 5;
  while(n--) {
    pid = fork();
    if (pid == 0) break;
    else if (pid < 0) {
      perror("fork");
      return 1;
    }   
  }

  // 每个子进程一生出来就睡眠 4*n + 1 秒,不要问为什么是 4*n + 1,这只是我随便写的,只想保证每个子进程睡眠时间不一样。
  if (pid == 0) {
    sleep(4*n + 1); 
    printf("hello, I'm child [ %d ]; my father is [ %d ]\n\n", getpid(), getppid());
    
    // 这一句,我故意让这个子进程 core 掉,主要用来看效果。
    if (4*n + 1 == 5) *((int*)0) = 0;
    
    // 子进程再退出的时候,返回它的睡眠时间
    return 4*n + 1;
  }


  int status = 0;
  while(1) {
    // 这里 waitpid 接收所有子进程状态,但是没有打开 WNOHANG 开关,同学们可以自行加上看看效果
    pid = waitpid(-1, &status, WUNTRACED | WCONTINUED);
    
    // 出错情况,这种一般是子进程全部结束了。
    if (pid == -1) {
      perror("wait");
      sleep(5);
      printf("I'm father [ %d ]; I have wiped out all zombies\n\n", getpid());
      return 1;
    }   
    // 这种只会在 WNOHANG 开关打开的情况下出现
    else if (pid == 0) {
      printf("Hello, I'm father; I'm waiting child\n\n");
    } 
    // 这种是只要有一个子进程状态发生改变就会进来。注意 4 类情况,比较多。  
    else {

      if (WIFEXITED(status)) {
        printf("child [ %d ] <exited> with code [ %d ]\n\n", pid, WEXITSTATUS(status));
      }   
      else if (WIFSIGNALED(status)) {
        printf("child [ %d ] <terminated> abnormally, signal [ %d ]\n\n", pid, WTERMSIG(status));
#ifdef WCOREDUMP
        if (WCOREDUMP(status)) {
          printf("<core file generated> in child [ %d ]\n\n", pid);
        }   
#endif
      }   
      else if (WIFSTOPPED(status)) {
        printf("child [ %d ] <stopped>, signal [ %d ]\n\n", pid, WSTOPSIG(status));
      }   
      else if (WIFCONTINUED(status)) {
        printf("child [ %d ] <continued>\n\n", pid);
      }   
    }   

  }
  return 0;
}
  • 编译
$ gcc wipeoutzombie2.c -o wipeoutzombie2
  • 运行
$ ./wipeoutzombie2
  • 结果

在我机器上的结果是这样的(你的可能与我有所不同):

before fork
hello, I'm child [ 3493 ]; my father is [ 3488 ]

child [ 3493 ] <exited> with code [ 1 ]

hello, I'm child [ 3492 ]; my father is [ 3488 ]

child [ 3492 ] <terminated> abnormally, signal [ 11 ]

<core file generated> in child [ 3492 ]

hello, I'm child [ 3491 ]; my father is [ 3488 ]

child [ 3491 ] <exited> with code [ 9 ]

hello, I'm child [ 3490 ]; my father is [ 3488 ]

child [ 3490 ] <exited> with code [ 13 ]

hello, I'm child [ 3489 ]; my father is [ 3488 ]

child [ 3489 ] <exited> with code [ 17 ]

wait: No child processes
I'm father [ 3488 ]; I have wiped out all zombies

2.1 kill 命令

特别特别注意的是,这个 kill 不是杀死的意思。在 linux 系统中,它表示发信号给指定进程。

比如 kill -9 2008,表示发送信号 9 给进程 2008。9 号信号是啥意思?你可通过 kill -l 命令来查看。

比如发送 19 号信号(SIGSTOP) kill -19 2008 给进程 2008,可以让 2008 号进程暂停执行。发送 18 号信号(SIGCONT) kill -18 2008 可以让 2008 号进程恢复执行。

那么当你执行 ./wipeoutzombie2 的时候,你可以通过 kill 发送信号给子进程,看看屏幕打印的情况的变化(这个任务是必须的,代码我都给你写好了,你就不要墨迹了)。

2.2 core 文件

由于各种异常或者bug导致程序运行过程中异常退出或者中止,并且在满足一定条件下(这里为什么说需要满足一定的条件呢?下面会分析)会产生一个叫做core的文件。

如果你的程序运行结果没有产生 core 文件,是不会打印 <core file generated> in child [ 3492 ] 这行的,你可以在你当前运行 wipeoutzombie2 的终端里执行:

$ ulimit -c unlimited

这时候再执行 ./wipeoutzombie2 看看效果吧。

有关 core 文件如何分析,不是我们的重点,同学如果感兴趣,可以自行查阅相关资料。

3. 总结

  • 熟练掌握 waitpid 函数
  • 知道 waitpid 获取的 status 有四种情况
  • 理解 waitpid 的 options 参数(我更喜欢称其为“开关”)
  • 学会使用 kill 命令发送信号
  • 了解如何产生 core 文件
  • 4
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值