上一篇,介绍了僵尸进程的概念,也学会了用 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 状态,取决于 WUNTRACED
和 WCONTINUED
开关是否打开。
如果你的 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 文件