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() 执行步骤

  1. 设置退出标志: 将当前进程状态设为 EXIT_ZOMBIE,记录退出码 current->exit_code。
  2. 释放资源: 依次释放 mm_struct(地址空间)、文件描述符表、信号结构、命名空间等。
  3. 通知父进程: 调用 exit_notify(),向父进程发送 SIGCHLD 信号。
  4. 进程再父化: 如果父进程已先退出,将当前进程托付给 init 进程(PID 1)。
  5. 调度退出: 调用 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 进程生命周期完整流程图

进程生命周期状态转换图 创建 (fork) 就绪 (TASK_RUNNING) 运行 (调度器选中) 等待 (TASK_INTERRUPTIBLE) 终止 (exit) 僵尸 (EXIT_ZOMBIE) 调度器加入就绪队列 schedule() 选中 I/O/锁/信号 条件满足,回到就绪 exit() 调用 do_exit() 完成 父进程 wait4() 回收 释放 task_struct

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 时必须立即响应的同步压力。