26 监控子进程 wait()
子进程的状态
1. 状态的分类
- 调用
_exit()正常退出(无论是手动调用还是运行时在main()函数退出后自动调用)。 - 收到信号而终止。
- 收到信号而暂停执行。
- 收到信号而恢复执行。
2. 如何在父进程中区分子进程状态
为了区分这 4 种状态,等待函数中 int 类型的输出参数实际上会有低 2 个字节被使用(虽然实际上返回状态只需要用 1 个字节表示):

可以分别通过 WIFEXITED (status)、WIFSIGNALED (status)、WIFSTOPPED (status)、WIFCONTINUED (status) 来判断是否属于这几种情况。这几种情况是互斥的。在进程被信号所杀时,内核转储标志可能会被设置,这时如果宏 WCOREDUMP 被定义,可以用 WCOREDUMP 来检查内核转换标志是否被设置。
3. 在信号处理函数中终止子进程
如果需要对某个信号做出一定的处理再终止子进程,则不能调用 _exit() 函数,否则子进程会被视为正常终止。如果想要让子进程终止时可以反应真实的终止原因,需要在信号处理器函数中做出善后处理之后,再将该信号的处置方式设置成默认,然后用 raise() 系统调用发送相同的信号。
用来等待子进程的 API
pid_t wait(int *_Nullable wstatus);
pid_t waitpid(pid_t pid, int *_Nullable wstatus, int options); -------+
int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options); |
/* This is the glibc and POSIX interface; see |
NOTES for information on the raw system call. */ |
|
+------------------------------------------+
|
V
pid_t wait3(int *_Nullable wstatus, int options,
struct rusage *_Nullable rusage);
pid_t wait4(pid_t pid, int *_Nullable wstatus, int options,
struct rusage *_Nullable rusage);
wait 是等待任一子进程停止。
waitpid 比 wait 多了两个参数,其中 pid 参数是等待给定子进程停止,但其含义很多,详见 man 手册;options 参数则可以指定其他标志:WUNTRACED 表示还会返回因信号暂停的进程的信息,WCONTINUED 表示还会返回因信号恢复执行的进程的信息,WNOHANG 表示本次操作转换为轮询,在有子进程、但子进程状态未改变时立即返回 0。
在 Linux 中,
waitpid还有一些别的 options 可以使用,这些选项和 Linux 特有的clone()系统调用相关:__WCLONE、__WALL、__WNOTHREAD。
wait3、wait4 和 waitpid 很像,但是可以额外获取子进程的资源信息。wait3 还额外固定了 pid 这个参数为 -1(表示等待任意一个子进程。
waitid 源于 System V,已经被 SUSv3 采纳,并从 2.6.9 版本开始加入了 Linux 内核。其参数含义如下:
idtype == P_ALL时,表示等待任何子进程;idtype == P_PID时,表示等待特定子进程;idtype == P_PGID时,表示等待特定进程组内的任意子进程。- 在
idtype不同时,id的含义不同。 infop参数提供了waitpid等系统调用无法获取的、更加详细的子进程信息。(看了一下这个类型,好像里面的信息也没有特别有用?)options参数提供的控制也比waitpid更加精细。尤其是可以控制只等待某些类型的子进程事件。WEXITED表示等待已经终止的子进程,无论其是否正常返回。在waitpid中无法控制,相当于永远启用。(因信号而终止的子进程应该也在此列。)WSTOPPED表示等待已通过信号而停止的子进程。相当于是waitpid中的WUNTRACED(命名是历史遗留问题)。WCONTINUED表示等待通过SIGCONT信号恢复的子进程。WNOHANG表示本次操作转换为轮询,同waitpid。WNOWAIT表示虽然waitid()本次返回了子进程的信息,但是不将其回收,下次还能再获取相同的信息。就像输入流的 peek 操作一样。
孤儿进程和僵尸进程
之所以有僵尸进程,是因为系统需要提供一种方式让父进程来检查子进程的信息。在父进程调用 wait() 系列的系统调用获取已结束的子进程的信息之前,子进程的绝大部分资源可以释放,但是关于子进程的信息条目需要被保留下来。这些条目仍然会需要占用内核的资源(子进程的进程号、终止状态、资源使用数据等),所以如果有大量僵尸进程会影响系统的正常运行。
如果自己还在运行,但是父进程没了,那么就成为了孤儿进程。孤儿进程会被 init 进程收养,从而保证其退出时资源可以正常被回收。如果有进程成为僵尸,可以将其父进程杀死(2025/4/21 前提是 init 能正确回收它,某些容器环境的 init 可能不具备这个能力)。
Tip
僵尸进程概念的引入是操作系统为了“父进程能检查子进程退出状态及资源”这盘醋包的饺子。
Warning
僵尸进程是杀不死的,只能由父进程来回收。所以如果父进程不去回收它,应该杀掉其父进程,使僵尸被 init 收养并回收。
SIGCHLD 信号
1. 正确回收子进程的资源
先设置好 SIGCHLD 的信号处理器,在信号处理器函数中用循环的方式轮询(WNOHANG)回收子进程。这种方式也是可移植性最强的。
Caution
由于 waitpid 可能会修改 errno,虽然它是异步信号安全的,但是是不可重入的。最好在调用前保存 errno,返回信号处理器函数前恢复。
2. 显式忽略 SIGCHLD 信号(也是一种回收子进程资源的方式,但不可移植)
虽然 SIGCHLD 信号的默认处理方式就是忽略,但是还是可以调用信号处理器函数来显式忽略 SIGCHLD,这样做的语义是有子进程退出时自动回收资源,无需先将其转换成僵尸进程。其实这也是和默认处理方式有区别的,因为默认处理方式是 SIG_DFL,改成 SIG_IGN 也算是有变化了。
不可移植性:在包括 Linux 在内的实现中,这不会影响已经成为僵尸的进程,而 Solaris 8 则会将僵尸进程一并删除。而且在较老的 UNIX 实现中,忽略 SIGCHLD 信号并没有阻止僵尸进程创建的语义。因而这样的操作是不可移植的。
其他影响:SUSv3 规定,将 SIGCHLD 处理方式设置成 SIG_IGN,还会丢弃子进程的资源使用信息。这会影响带有 RUSAGE_CHILDREN 标志的 getrusage(),以及 times() 函数等。
3. 什么时候父进程会收到 SIGCHLD?
在子进程状态发生变化的时候父进程就会收到 SIGCHLD,而并不一定是子进程退出的时候。调用 sigaction() 来设置 SIGCHLD 的处理方式时可以指定 SA_NOCLDWAIT 标志禁止子进程在暂停或者恢复执行时向父进程传递 SIGCHLD 信号。
之所以 wait 默认不等待暂停 / 恢复的进程,而 SIGCHLD 在子进程暂停 / 恢复时也默认传递,可能是因为暂停 / 恢复的子进程并不是僵尸,不需要回收。
4. sigaction() 的 SA_NOCLDWAIT 标志(不可移植)
类似显式设置 SIGCHLD 的处理方式为 SIG_IGN 的效果,子进程资源会被自动回收。但是 SUSv3 没有规定系统在子进程终止时是否还需要发送 SIGCHLD 信号。在 Linux 在内的一些实现中,系统还会产生 SIGCHLD 信号,信号处理器函数会执行,但是无法用 wait() 获得子进程信息(因为被丢弃了)。而在有的实现中,则由于不产生信号而不会进入信号处理器函数,就和 SIG_IGN 一样。
5. System V 的 SIGCLD 信号
历史原因。Linux 中现在同时有 SIGCLD 和 SIGCHLD,两者意义相同。