第二十三章:等待之道
筑基中期涉及内核源码:
一
林小源开始研究 系统调用。
是父进程回收子进程的唯一方式。它有几种变体: 等待任意子进程, 等待指定的子进程, 提供更精细的控制, 可以获取资源使用信息。
它们的内核入口都是 或 。
/*
* wait 系统调用家族:
* wait(&status) — 等待任意子进程
* waitpid(pid, &status, 0) — 等待指定子进程
* waitid(idtype, id, &infop, options) — 更精细的控制
* wait4(pid, &status, options, &rusage) — 获取资源使用
*/
printf("=== wait 系统调用家族 ===\n\n");
printf("--- wait(&status) ---\n");
printf(" 阻塞等待任意子进程退出\n");
printf(" 返回退出子进程的 PID\n\n");
printf("--- waitpid(pid, &status, options) ---\n");
printf(" pid > 0: 等待指定 PID\n");
printf(" pid == -1: 等待任意子进程\n");
printf(" pid == 0: 等待同组任意子进程\n");
printf(" pid < -1: 等待进程组 |pid| 中的任意子进程\n\n");
printf("--- options ---\n");
printf(" WNOHANG: 非阻塞,没有子进程退出立即返回\n");
printf(" WUNTRACED: 报告已停止的子进程\n");
printf(" WCONTINUED: 报告已继续的子进程\n\n");
printf("--- status 解析 ---\n");
printf(" WIFEXITED(s): 正常退出?\n");
printf(" WEXITSTATUS(s): 退出码\n");
printf(" WIFSIGNALED(s): 被信号杀死?\n");
printf(" WTERMSIG(s): 杀死它的信号号\n");
printf(" WIFSTOPPED(s): 被停止?\n");
printf(" WSTOPSIG(s): 停止它的信号号\n");#include <stdio.h>
#include <string.h>
/*
* wait 系统调用家族:
* wait(&status) — 等待任意子进程
* waitpid(pid, &status, 0) — 等待指定子进程
* waitid(idtype, id, &infop, options) — 更精细的控制
* wait4(pid, &status, options, &rusage) — 获取资源使用
*/
int main() {
printf("=== wait 系统调用家族 ===\n\n");
printf("--- wait(&status) ---\n");
printf(" 阻塞等待任意子进程退出\n");
printf(" 返回退出子进程的 PID\n\n");
printf("--- waitpid(pid, &status, options) ---\n");
printf(" pid > 0: 等待指定 PID\n");
printf(" pid == -1: 等待任意子进程\n");
printf(" pid == 0: 等待同组任意子进程\n");
printf(" pid < -1: 等待进程组 |pid| 中的任意子进程\n\n");
printf("--- options ---\n");
printf(" WNOHANG: 非阻塞,没有子进程退出立即返回\n");
printf(" WUNTRACED: 报告已停止的子进程\n");
printf(" WCONTINUED: 报告已继续的子进程\n\n");
printf("--- status 解析 ---\n");
printf(" WIFEXITED(s): 正常退出?\n");
printf(" WEXITSTATUS(s): 退出码\n");
printf(" WIFSIGNALED(s): 被信号杀死?\n");
printf(" WTERMSIG(s): 杀死它的信号号\n");
printf(" WIFSTOPPED(s): 被停止?\n");
printf(" WSTOPSIG(s): 停止它的信号号\n");
return 0;
}林小源望着 wait 家族的接口,感叹道:"一个简单的'等待子进程',居然有这么多变体。"
"你不觉得这很合理吗?"一个温润的声音从等待队列的方向传来。林小源抬头,看到一个身披白色长袍的女子正坐在一棵红黑树下——那是等待队列的守护者,名为"候者"。她的周围飘浮着无数 节点,每一个都代表一个正在等待的进程。
" 是最简单的——等待任意子进程,阻塞。"候者伸出一根手指," 更精确——你可以指定等哪个子进程。 最精细——你可以选择等退出的、等停止的、等继续的。 额外给你资源使用信息。"
"为什么要分这么多?"
"因为不同的场景需要不同的等待方式。"候者说,"shell 用 waitpid(pid, &status, 0)——它知道自己在等哪个子进程。init 用 waitpid(-1, &status, WNOHANG)——它不知道哪个子进程会退出,也不想阻塞。调试器用 ——它需要知道子进程是退出了还是被停止了。"
二
的内核实现比林小源想象的要复杂。
它不是简单地检查子进程是否退出——它需要遍历所有子进程、检查各种状态(退出、停止、继续)、处理线程组、处理 ptrace 关系。如果没有任何子进程满足条件,父进程会被放入等待队列,进入睡眠状态。
候者解释道:"当父进程调用 时,它先遍历所有子进程,检查有没有已经退出的。如果有,直接返回。如果没有——"
"它就睡觉?"林小源问。
"对。"候者点头,"它把自己的状态设置为 ,然后调用 让出 CPU。它会一直睡,直到有子进程退出。"
"那子进程退出时怎么唤醒它?"
"子进程在 中向父进程发送 信号。信号会唤醒正在等待的父进程—— 是一个普通的信号,它会把父进程从 唤醒到 。父进程被唤醒后,重新检查子进程状态,发现有子进程退出了,就返回。"
林小源在 的代码中看到了一种"耐心"的设计。父进程不是忙等——它把自己的状态设置为 ,然后调用 让出 CPU。当子进程退出时,内核会唤醒父进程。
这是一种"事件驱动"的等待——不是轮询,而是通知。
候者补充道:" 标志改变了整个行为。有了它, 变成非阻塞——如果没有子进程退出,立即返回 0,不睡觉。init 进程必须用 ,因为它不能为了等一个子进程而阻塞——它需要不停地处理其他事务。"
三
林小源在研究 wait 的过程中,观察到了 shell 小妹的行为。
shell 小妹——bash 进程——每次执行一条命令时,都会 fork 一个子进程,然后调用 等待子进程结束。这是一个无限循环:
while (1) {
read_command(&cmd);
pid = fork();
if (pid == 0) {
execve(cmd.path, cmd.argv, cmd.envp);
perror("execve failed");
exit(1);
}
waitpid(pid, &status, 0);
print_exit_status(status);
}林小源望着 shell 小妹忙碌的身影,忽然对候者说:"她看起来很简单。"
候者笑了:"简单?你觉得简单?她的每一次命令执行都是一次完整的进程生命周期:创建(fork)、蜕变(execve)、执行、退出、回收(waitpid)。五步,一步都不能少。她不懂内核的深处,但她把这五步重复了无数次——每一次都正确,每一次都不出错。"
"她是一个熟练的'进程管理者'。"林小源说。
"何止熟练,"候者说,"她是 Unix 世界中最经典的进程管理模式的化身。fork + execve + waitpid——简单、优雅、高效。Linus 设计 Linux 的时候,大概也没想到这个模式能用几十年不衰。"
林小源望着 shell 小妹,心中升起一股敬意。她不懂内核的深处,但她的每一次命令执行都是对 Unix 哲学的完美诠释。
道藏笔记
内核启示
wait() 是进程生命周期的最后一步。
的核心逻辑:
- 遍历所有子进程
- 检查是否有满足条件的子进程(已退出、已停止、已继续)
- 如果有,返回子进程的 PID 和状态
- 如果没有,把父进程放入等待队列,调用 睡眠
- 当子进程退出时, 唤醒父进程
标志让 变成非阻塞——如果没有子进程退出,立即返回 0。这对于事件循环(如 init 进程的服务管理)非常重要。
shell 的核心循环是 fork + execve + waitpid。这是 Unix 世界中最经典的进程管理模式——简单、优雅、高效。
等待不是被动,是主动的耐心。
等待之试
父进程要等待并回收子进程,正文中对应到用户态的哪个等待接口?