4. 进程生命周期:创建、执行、等待与终止
进程在 Linux 内核中并非静态实体,而是经历一套严格的状态转换:从 创建 (fork) 开始,经过 执行 (exec),进入 就绪/运行/等待 状态,最终通过 终止 (exit) 并由父进程通过 等待 (wait4) 回收资源。本章深入剖析这一完整生命周期,重点解读 exit() 与 wait4() 系统调用的内核实现。
4.1 进程生命周期的四个阶段
一个进程从诞生到消亡,在内核中经历以下核心阶段:
| 阶段 | 系统调用/事件 | 内核主要行为 |
|---|---|---|
| 创建 | fork() / clone() |
复制父进程 task_struct、mm_struct、文件描述符表;子进程进入就绪队列 |
| 执行 | execve() |
加载新程序映像,替换进程地址空间,重置信号处理函数 |
| 等待 | 调度器 / wait4() |
进程因 I/O、锁、信号等进入 TASK_INTERRUPTIBLE 或 TASK_UNINTERRUPTIBLE;父进程调用 wait4 阻塞等待子进程退出 |
| 终止 | exit() / exit_group() |
释放大部分资源,将 task_struct 保留为僵尸状态,向父进程发送 SIGCHLD |
关键概念: 进程终止后并不会立即销毁 task_struct,而是进入 僵尸状态 (EXIT_ZOMBIE),等待父进程调用
wait4() 读取退出码后,内核才最终释放进程描述符。这是 Linux 进程生命周期中最重要的设计之一。
4.2 进程终止:exit() 系统调用
用户态程序通过 exit(int status) 或从 main() 返回触发终止流程。内核入口为 sys_exit(),实际工作由 do_exit() 完成。
4.2.1 do_exit() 执行步骤
- 设置退出标志: 将当前进程状态设为
EXIT_ZOMBIE,记录退出码current->exit_code。 - 释放资源: 依次释放 mm_struct(地址空间)、文件描述符表、信号结构、命名空间等。
- 通知父进程: 调用
exit_notify(),向父进程发送SIGCHLD信号。 - 进程再父化: 如果父进程已先退出,将当前进程托付给
init进程(PID 1)。 - 调度退出: 调用
schedule()选择新进程运行,当前进程不再被调度。
// kernel/exit.c (简化逻辑)
void do_exit(long code) {
struct task_struct *tsk = current;
set_current_state(TASK_DEAD); // 设置死亡状态
tsk->exit_code = code; // 保存退出码
exit_mm(tsk); // 释放地址空间
exit_files(tsk); // 释放文件描述符
exit_signal(tsk); // 释放信号结构
exit_notify(tsk); // 通知父进程 + 再父化
schedule(); // 切换出去,不再返回
}
注意:
do_exit() 调用 schedule() 后,当前进程的 task_struct 并未释放。内核保留该结构体,直到父进程调用 wait4() 读取退出状态。此时进程处于 僵尸状态,仅占用一个 task_struct 槽位,不消耗其他资源。
4.3 父进程等待:wait4() 系统调用
wait4() 是父进程回收子进程退出状态的核心系统调用。用户态常见的 wait()、waitpid() 最终都调用 sys_wait4()。
4.3.1 wait4() 内核流程
| 步骤 | 函数 | 行为 |
|---|---|---|
| 1 | sys_wait4() |
解析用户态参数(pid、options、rusage) |
| 2 | kernel_wait4() |
遍历子进程链表,查找符合 pid 条件的子进程 |
| 3 | wait_consider_task() |
检查子进程状态:若为 EXIT_ZOMBIE,则收集退出码;否则根据 options 决定是否阻塞 |
| 4 | wait_task_zombie() |
读取退出码、统计信息(rusage),释放 task_struct |
| 5 | 返回 | 将退出码和资源统计拷贝回用户空间 |
4.3.2 关键选项解析
- WNOHANG: 如果没有子进程退出,立即返回 0,不阻塞。
- WUNTRACED: 也返回因信号停止的子进程状态。
- WCONTINUED: 也返回因 SIGCONT 恢复运行的子进程状态。
// 用户态典型用法
pid_t pid = fork();
if (pid == 0) {
// 子进程
exit(42);
} else {
int status;
waitpid(pid, &status, 0); // 阻塞等待子进程退出
if (WIFEXITED(status)) {
printf("exit code: %d\n", WEXITSTATUS(status));
}
}
4.4 僵尸进程与孤儿进程
| 类型 | 产生条件 | 内核处理 | 危害 |
|---|---|---|---|
| 僵尸进程 | 子进程 exit,父进程未 wait | task_struct 保留,等待父进程回收 | 占用 PID 槽位,大量僵尸会耗尽 PID 上限 |
| 孤儿进程 | 父进程先于子进程 exit | 子进程被托付给 init 进程 (PID 1) | 无直接危害,init 会定期 wait 回收 |
最佳实践: 父进程应始终调用
waitpid() 或 wait4() 回收子进程,或使用 signal(SIGCHLD, SIG_IGN) 让内核自动回收,避免僵尸进程堆积。
4.5 进程生命周期完整流程图
4.6 进程生命周期中的关键数据结构变化
| 阶段 | task_struct 状态字段 | mm_struct | 文件描述符表 |
|---|---|---|---|
| 创建后 | TASK_RUNNING | 共享或写时复制 (COW) | 共享或复制 |
| 执行 execve | TASK_RUNNING | 完全替换为新映像 | 保留(除 FD_CLOEXEC 标记的) |
| 等待 I/O | TASK_INTERRUPTIBLE / TASK_UNINTERRUPTIBLE | 不变 | 不变 |
| 终止 (do_exit) | EXIT_ZOMBIE | 释放 (put_mm) | 释放 (put_files_struct) |
| 回收 (wait4) | EXIT_DEAD → 释放 | 已释放 | 已释放 |
4.7 父子进程同步机制
exit() 与 wait4() 之间通过内核的 等待队列 (wait queue) 和 SIGCHLD 信号 实现同步:
- 子进程 exit 时: 调用
exit_notify(),遍历父进程的wait_chldexit等待队列,唤醒所有因wait4()阻塞的父进程。 - 父进程 wait4 时: 若子进程尚未退出,父进程将自己加入
wait_chldexit队列,调用schedule()进入睡眠。 - 信号辅助: 子进程 exit 时发送 SIGCHLD,父进程若设置了信号处理函数,可在信号上下文中调用
waitpid()实现异步回收。
内核设计哲学: 将资源释放与进程描述符回收分离。子进程 exit 时释放大部分资源(内存、文件、信号),但保留最小的 task_struct 用于传递退出状态;父进程 wait4 时读取状态并释放最后的 task_struct。这种设计避免了父进程在子进程 exit 时必须立即响应的同步压力。