第 26 章 监控子进程 (Monitoring Child Processes)
核心结论
-
wait/waitpid 是父回收子状态的系统调用:返回子 PID + 终止状态;wait 等任意子,waitpid 可指定 PID + flags(WUNTRACED/WCONTINUED/WNOHANG)。
-
wait status 4 种情形:正常 exit、被信号杀、被信号 stop(WUNTRACED)、被 SIGCONT 恢复(WCONTINUED,2.6.10+);用 WIFEXITED/WIFSIGNALED/WIFSTOPPED/WIFCONTINUED + WEXITSTATUS/WTERMSIG/WSTOPSIG 宏提取。
-
僵尸进程:子终止但父未 wait——内核保留进程表项记录 PID + status;不可被信号杀(包括 SIGKILL);父 wait 后或父终止(init 收养)后清除。
-
SIGCHLD 是子状态变化的默认通知:默认 ignore;handler 中必须用
while(waitpid(-1, NULL, WNOHANG) > 0)循环回收——因为标准信号不排队,多次 SIGCHLD 只投递一次。 -
SIGCHLD = SIG_IGN 自动回收:Linux 上让子终止时立即清除,不变僵尸(不需 handler);SA_NOCLDWAIT 标志等效。
-
孤儿进程:父先终止 → 子被 init(PID 1)收养;
getppid()返回 1。
-
|
本章主旨
本章是「进程生命周期」四章的第三章——父进程如何知道子进程的状态变化。读者应掌握:wait/waitpid/waitid 系统调用族、wait status 的位布局与 W* 宏提取、僵尸进程的成因与清理、SIGCHLD handler 的标准循环模式、SIGCHLD = SIG_IGN 与 SA_NOCLDWAIT 自动清理、孤儿进程被 init 收养。理解这些是写「长生命周期父进程」(daemon、shell、服务器)的关键——避免僵尸耗尽进程表、SIGCHLD handler 正确回收所有子进程。 |
一、核心概念
本章围绕 7 个核心概念展开:从 wait 基础入手,到 waitpid 选项、wait status 解析、waitid/wait3/wait4 变体、僵尸与孤儿、SIGCHLD handler 设计、SIGCHLD 处置的特殊性。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
wait/waitpid 系统调用 |
|
§26.1.1-§26.1.2;WNOHANG 非阻塞;WUNTRACED 包括 stop;WCONTINUED 包括 SIGCONT 恢复;wait 等价 waitpid(-1, &status, 0)。 |
wait status 4 情形 + W 宏* |
正常 exit、被信号杀、被信号 stop、被 SIGCONT 恢复;WIFEXITED + WEXITSTATUS;WIFSIGNALED + WTERMSIG + WCOREDUMP;WIFSTOPPED + WSTOPSIG;WIFCONTINUED。 |
§26.1.3;W* 宏是跨 UNIX 可移植方式;不要直接位操作 status;WCOREDUMP 非 SUSv3 但 Linux 有;WIFCONTINUED 自 Linux 2.6.10。 |
waitid/wait3/wait4 变体 |
|
§26.1.5/§26.1.6;waitid 自 2.6.9 完整支持;wait3/wait4 非 SUSv3,不推荐用于可移植代码。 |
僵尸进程 (Zombie) |
子终止但父未 wait;内核保留进程表项记录 PID + status + rusage;不可被信号杀;父 wait 后或父终止(init 收养 wait)后清除。 |
§26.2;僵尸耗尽进程表 → fork 失败 EAGAIN; |
孤儿进程 (Orphan) |
父先终止 → 子被 init(PID 1)收养;子 getppid 返回 1;init 自动 wait 子。 |
§26.2;孤儿进程组还会收到 SIGHUP + SIGCONT(详见第 34 章); |
SIGCHLD handler 标准模式 |
子终止时内核发 SIGCHLD(默认 ignore);handler 中循环 |
§26.3.1;handler 进入时保存 errno 防止破坏主程序 errno;用 SA_NOCLDSTOP 屏蔽 stop 通知;现代代码常用 signalfd 替代。 |
SIGCHLD = SIG_IGN 自动回收 |
设置 SIGCHLD 为 SIG_IGN(不是默认 ignore!默认 ignore 不变僵尸)——子终止时立即清除,不变僵尸;父 wait 返回 ECHILD。 |
§26.3.3;SA_NOCLDWAIT 等效;Linux 2.6+;最简单防止僵尸的方法——但不保留子状态信息。 |
二、详细笔记
26.1 wait/waitpid 基础
What:父调用 wait/waitpid 阻塞等待子进程终止,并获取子进程状态。
Why:防止僵尸积累;让父知道子是否成功、为什么终止。
How:核心 API:
#include <sys/wait.h>
pid_t wait(int *status); /* 等任意子 */
pid_t waitpid(pid_t pid, int *status, int options); /* 等指定子 */
pid_t waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
wait 语义(§26.1.1):
-
若所有子都已 wait 过且没有 unwaited 子 → 返回 -1,errno = ECHILD。
-
若无子终止 → 阻塞。
-
status 非 NULL 时填入子进程状态;返回子 PID。
-
waitpid 的 pid 取值(§26.1.2):
-
> 0:等指定 PID。
-
== 0:等同进程组的任意子。
-
< -1:等进程组 ID = |pid| 的任意子。
-
== -1:等任意子(等价 wait)。
options 标志(§26.1.2):
-
WNOHANG——无子终止立即返回 0(不阻塞)。 -
WUNTRACED——包括被 stop 的子。 -
WCONTINUED——包括被 SIGCONT 恢复的子(Linux 2.6.10+)。
waitid 比 waitpid 更灵活(§26.1.5):
-
idtype = P_ALL / P_PID / P_PGID。
-
options 独立标志 WEXITED / WSTOPPED / WCONTINUED + WNOHANG + WNOWAIT。
-
填 siginfo_t,含 si_pid、si_code(CLD_EXITED/KILLED/STOPPED/CONTINUED)、si_status、si_signo = SIGCHLD、si_uid。
-
wait3/wait4 比 wait/waitpid 多返回 rusage(§26.1.6);非 SUSv3——避免。
When:
-
wait(&status)——简单 shell 模式,阻塞等所有子。 -
waitpid(pid, &status, WNOHANG)——非阻塞查询特定子。-
waitpid(-1, &status, WNOHANG)——SIGCHLD handler 中循环回收所有僵尸。 -
waitid——需要 siginfo_t 详细信息的场景。
-
Example:第 26 章 Listing 26-1 multi_wait.c——创建多子并 wait:
for (j = 1; j < argc; j++) {
switch (fork()) {
case 0:
sleep(getInt(argv[j], GN_NONNEG, "sleep-time"));
_exit(EXIT_SUCCESS);
default: break;
}
}
while ((childPid = wait(NULL)) != -1) continue; /* 回收所有子 */
/* wait 返回 -1 + errno = ECHILD 表示没有未回收的子 */
26.1.3 wait status 解析
What:status 是 int,只有低 16 位有意义;按 4 种情形组织(§26.1.3 + Figure 26-1)。
Why:用 W* 宏提取比直接位操作可移植——SUSv3 不规定位布局。
How:status 布局(Linux/x86-32,布局因实现而异):
| 情形 | status 高 8 位 | status 低 8 位 |
|---|---|---|
正常 exit |
0 |
exit status (0-255) |
被信号杀 |
信号号 |
0;若 core 则 0x80 |
被 SIGSTOP 等 |
0x7F |
stop 信号号 |
被 SIGCONT 恢复 |
0xFFFF |
0xFF |
提取宏:
| 宏 | 含义 | 配套宏 |
|---|---|---|
WIFEXITED(status) |
true if 正常 exit |
WEXITSTATUS → exit status |
WIFSIGNALED(status) |
true if 被信号杀 |
WTERMSIG → 信号号;WCOREDUMP → 是否 core |
WIFSTOPPED(status) |
true if 被 stop |
WSTOPSIG → 信号号 |
WIFCONTINUED(status) |
true if 被 SIGCONT 恢复 |
(无) |
When:
-
W* 宏是必须——不要直接位操作。
-
WCOREDUMP 不是 SUSv3 但 Linux/BSD/Solaris 都有——用
#ifdef WCOREDUMP保护。
Example:第 26 章 Listing 26-2 printWaitStatus():
if (WIFEXITED(status)) {
printf("child exited, status=%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("child killed by signal %d (%s)", WTERMSIG(status), strsignal(WTERMSIG(status)));
#ifdef WCOREDUMP
if (WCOREDUMP(status)) printf(" (core dumped)");
#endif
printf("\n");
}
信号 handler 中终止子(§26.1.4):
-
在 SIGTERM 等信号的 handler 中清理后调 exit——父 wait 看到正常退出。
-
想让父知道是因信号终止——handler 中
signal(sig, SIG_DFL); raise(sig);——让默认动作再杀一次。
26.2 孤儿与僵尸
What:僵尸 = 子终止但父未 wait;孤儿 = 父先终止子还在跑。
Why:理解这两种「特殊状态」才能设计不会内存泄漏的父进程。
How:僵尸(§26.2):
-
子终止时内核保留进程表项(PID + status + rusage)——其他资源释放。
-
父调用 wait 后清除该表项。
-
不可被信号杀(包括 SIGKILL)——确保父总能 wait。
-
父未 wait 就终止 → 子被 init 收养 → init 自动 wait → 僵尸清除。
-
大量僵尸会塞满进程表 → fork 失败 EAGAIN。
-
孤儿(§26.2):
-
父终止后子仍在跑 → 子被 init(PID 1)收养。
-
子 getppid() 返回 1。
-
若进程组变孤儿且有 stopped 进程 → 内核给该组所有进程发 SIGHUP + SIGCONT(第 34 章)。
-
When:
-
长生命周期父(daemon、shell、服务器)——必须回收所有子,避免僵尸。
-
prctl(PR_SET_PDEATHSIG, sig) 让子进程在父死时收到信号——docker/k8s 用此检测父死。
Example:第 26 章 Listing 26-4 make_zombie.c——演示僵尸:
$ ./make_zombie
Child (PID=1014) exiting
1014 pts/4 00:00:00 make_zombie <defunct> # <defunct> = 僵尸
$ kill -9 1014 # 杀不掉
1014 pts/4 00:00:00 make_zombie <defunct>
# 父退出后 init 收养 + wait → 僵尸清除
26.3 SIGCHLD handler 设计
What:子终止/停止/恢复时内核发 SIGCHLD 给父(默认 ignore);handler 中回收僵尸。
Why:避免「wait 阻塞主程序」与「waitpid 轮询浪费 CPU」——SIGCHLD 是「事件驱动」的理想模式。
How:标准 SIGCHLD handler(§26.3.1):
static void sigchldHandler(int sig) {
int savedErrno = errno; /* handler 可能改 errno */
while (waitpid(-1, NULL, WNOHANG) > 0) /* 循环回收所有僵尸 */
continue;
errno = savedErrno; /* 恢复 errno */
}
int main(void) {
struct sigaction sa = {0};
sa.sa_handler = sigchldHandler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP; /* 不发 stop 通知 */
sigaction(SIGCHLD, &sa, NULL);
/* ... */
}
```
关键设计点:
* **`while` 循环**——标准信号不排队,多个子并发终止只发一次 SIGCHLD;handler 调用一次只能 wait 一次会漏回收。
* **`WNOHANG`**——非阻塞,没子可回收立即返回 0。
* **保存 errno**——waitpid 可能改 errno,主程序可能正在检查 errno。
* **`SA_NOCLDSTOP`**——不接收子 stop 通知(除非需要 job control)。
* **建立 handler 在 fork 之前**——Linux 不为已终止的子回送 SIGCHLD;建立 handler 后再 fork 才完整。
*When*:
* 长生命周期父——SIGCHLD handler 是「事件驱动 + 不阻塞」的标准模式。
* shell job control——SIGCHLD + SA_NOCLDSTOP 不设置 + WUNTRACED/WCONTINUED 检测子状态变化。
*Example*:第 26 章 Listing 26-5 `multi_SIGCHLD.c`——3 个子不同时间退出,handler 循环回收:
[source,bash]
$ ./multi_SIGCHLD 1 2 4 Child 1 exiting # 1 秒后 handler: Caught SIGCHLD # 第 1 次 handler: Reaped child 17767 Child 2 exiting # 2 秒后(在 handler sleep 中) Child 3 exiting # 4 秒后(在 handler sleep 中) handler: returning # 5 秒后 handler 返回 handler: Caught SIGCHLD # 第 2 次——之前 SIGCHLD 不排队 handler: Reaped child 17768 handler: Reaped child 17769 # 一次 handler 回收 2 个僵尸
*SIGCHLD 与 stopped 子*(§26.3.2) * `SA_NOCLDSTOP` 不设置 → 子 stop 时父也收 SIGCHLD(job control 用)。 * `SA_NOCLDSTOP` 设置 → 只在子 terminate 时收 SIGCHLD。 * Linux 2.6.10+ 还发 SIGCHLD 给子 resume(SIGCONT)。 *SIGCHLD = SIG_IGN 自动回收*(§26.3.3): * 设置 `signal(SIGCHLD, SIG_IGN)` 或 `sigaction(... SIG_IGN)` → 子终止时立即清除,**不变僵尸**。 * 父 wait 返回 ECHILD。 * 这是「防止僵尸」的最简单方法——不需 handler。 * 但**默认 ignore**(SIGCHLD 默认动作)**不变僵尸**——必须显式 `SIG_IGN` 才生效。 * `SA_NOCLDWAIT` 标志等效(Linux 2.6+)。 *When*: * 不关心子退出状态——用 SIG_IGN 或 SA_NOCLDWAIT 简化代码。 * daemon fork worker 后不想 wait——SIG_IGN 自动清理。 == 三、关键图表 (本章无独立编号图表) [NOTE] .wait status 4 情形与提取宏 ==== [cols="1,2,3,2", options="header"] |=== | 情形 | 判定宏 | 提取宏 | 状态字段含义 | 正常 exit | WIFEXITED | WEXITSTATUS | 0-255 的 exit status | 被信号杀 | WIFSIGNALED | WTERMSIG / WCOREDUMP | 信号号 / 是否 core | 被 stop | WIFSTOPPED | WSTOPSIG | 信号号(SIGSTOP/SIGTSTP 等) | 被 SIGCONT 恢复 | WIFCONTINUED | - | - |=== ==== [NOTE] .防止僵尸的三种方法 ==== [cols="1,2,3", options="header"] |=== | 方法 | 代码 | 优点 | 缺点 | wait/waitpid | 显式 wait 或 SIGCHLD handler 循环 | 保留子状态;可移植 | 需写 handler 或 wait | SIGCHLD = SIG_IGN | signal(SIGCHLD, SIG_IGN) | 简单;不需 handler | 丢失子状态 | SA_NOCLDWAIT | sigaction flag | 等效 SIG_IGN 但可保留 handler | Linux 2.6+ 才完整 |=== ==== == 四、思维导图 [source,mermaid]
mindmap root第 26 章 监控子进程 wait waitpid wait 等任意子 waitpid 指定 pid options flags waitid 更精细 wait3 wait4 rusage wait status 4 种情形 WIF EXITED SIGNALED WIF STOPPED CONTINUED WEXIT WTERM WSTOP 宏 16 位布局 僵尸进程 子终止未 wait 内核保留表项 不可被信号杀 ps defunct 耗尽进程表 孤儿进程 父先死 init PID 1 收养 getppid 返回 1 SIGHUP SIGCONT SIGCHLD handler while waitpid NOHANG 循环回收 标准信号不排队 保存恢复 errno SA NOCLDSTOP SIGCHLD SIG IGN 自动清理 不变僵尸 默认 ignore 无效 SA NOCLDWAIT 等效 设计要点 handler 在 fork 前 WNOHANG 非阻塞 SA RESTART 防止中断 短任务 SA NOCLDWAIT 长任务 handler
五、重点与易错点
-
wait 返回 -1 + ECHILD = 没有未回收的子——可用此判断「所有子已 wait」;不能用返回值判断「等待超时」(wait 默认阻塞)。
-
waitpid 的 WNOHANG 让 wait 非阻塞——用于 SIGCHLD handler 中循环回收;无子可回收返回 0。
-
W 宏是必须*——不要直接位操作 status;SUSv3 不规定位布局;WCOREDUMP 不是 SUSv3 但 Linux 有。
-
WIFCONTINUED 自 Linux 2.6.10——之前内核不支持;WAITPID 选项 WCONTINUED 也是 2.6.10+。
-
僵尸进程不可被信号杀——包括 SIGKILL;只能父 wait 或父终止(init 收养 wait)清除。
-
SIGCHLD 默认 ignore 不变僵尸——必须显式
signal(SIGCHLD, SIG_IGN)才让子终止时立即清除;这是「SIGCHLD 默认动作」与「显式 SIG_IGN」的关键区别。 -
SIGCHLD handler 必须用 while 循环 waitpid——标准信号不排队,handler 调用一次可能漏回收多个僵尸。
-
建立 SIGCHLD handler 必须在 fork 之前——Linux 不为已终止的子回送 SIGCHLD。
-
SIGCHLD handler 中保存/恢复 errno——waitpid 可能改 errno;主程序可能正在检查 errno。
-
SA_NOCLDSTOP 控制 stop 通知——不设 → 子 stop 时父收 SIGCHLD;设 → 只 terminate 通知;job control 通常不设。
-
孤儿进程被 init (PID 1) 收养——
getppid()返回 1;孤儿进程组中 stopped 子收到 SIGHUP+SIGCONT。 -
SA_NOCLDWAIT 等效 SIG_IGN——Linux 2.6+;handler 仍可能被调用但 wait 返回 ECHILD。
-
wait/waitpid 不会回收 init 的子——只回收调用进程直接 fork 的子。
-
kill -9 杀僵尸无效——必须杀父或重启。
-
跨章衔接:第 20-22 章信号基础与 SIGCHLD 投递;第 24 章 fork 创建子;第 25 章 _exit 终止;第 34 章 shell job control 用 WUNTRACED/WCONTINUED;第 36 章 rusage 与 getrusage。
-