两个子进程都sleep, `waitpid` 系统调用

老师:假设我有两个正在运行的子进程,我让 fork 等待第二个,当第一个结束时,而第二个正在执行 sleep 100。所以它将运行得比第一个长得多。

那么当第一个结束时,它的退出码被设置为零。然而,操作系统不能丢弃它的PCB,因为父进程正在对第二个进程进行显式的 waitpid 调用,并且阻塞在第二个进程上。所以,直到第二个进程完成,并且父进程过来等待第一个进程,你必须将第一个子进程的PCB保留在进程表(process table)中。

这涉及到了僵尸进程(zombie process)waitpid 系统调用的行为。我们来详细分析一下:


场景还原

  1. 父进程 fork 出两个子进程:子进程1(P1)子进程2(P2)
  2. 父进程调用 waitpid(P2, ...) 等待 P2 结束(阻塞在 P2 上)。
  3. P1 很快结束(exit code = 0)。
  4. P2 执行 sleep 100,将运行很长时间。
  5. 问题 :P1 结束时,父进程并没有调用 waitpid(P1, ...),父进程在等待 P2,那么 P1 的 PCB 会怎样?

正确答案:P1 会变成僵尸进程(Zombie)

1. 为什么不能立即丢弃 PCB?

因为 PCB 中包含父进程需要获取的信息:

  • 退出状态(exit status):exit code(0)、终止信号等。
  • 资源使用统计:CPU 时间、内存使用等。
  • 进程ID(PID):需要保持,直到父进程获取。

父进程需要调用 wait()waitpid() 来获取这些信息。如果操作系统在父进程读取前就丢弃 PCB,那么退出信息将永久丢失。

2. 操作系统的处理方式

当一个进程结束(通过 exit() 或信号终止)时:

  1. 释放大部分资源:内存、打开的文件、信号量等。
  2. 但保留最小的 PCB 结构:只保存退出状态和资源统计信息。
  3. 进程状态变为 ZOMBIE (或 EXIT_ZOMBIE)。
  4. 进程仍占用一个 PID 和进程表项,但不再占用内存或 CPU。

关键点:即使父进程正在等待另一个子进程,操作系统也必须保留已结束子进程的僵尸状态,直到父进程显式地等待它。


3. 在你的例子中

复制代码
时间线:
t0: 父进程 fork P1, P2
t1: 父进程调用 waitpid(P2, ...) → 阻塞等待 P2
t2: P1 执行完毕 → 变为僵尸(ZOMBIE)
t3: P2 仍在 sleep(100)
t4: (100秒后) P2 结束
t5: 父进程的 waitpid(P2) 返回,获取 P2 的退出状态
t6: 父进程现在可以调用 waitpid(P1) 获取 P1 的退出状态
t7: 只有父进程调用 waitpid(P1) 后,P1 的 PCB 才被完全释放

4. 如果父进程从不等待 P1?

  • P1 将保持僵尸状态直到父进程终止
  • 当父进程终止时,所有子进程(包括僵尸进程)会被init 进程(PID 1)收养
  • init 进程会自动调用 wait() 清理这些僵尸进程。
  • 这就是为什么长时间运行的服务器程序必须正确处理所有子进程的 wait(),否则会积累僵尸进程。

5. waitpid 的行为澄清

你提到"父进程正在对第二个进程进行显式的 waitpid 调用,并且阻塞在第二个进程上",这意味着:

  • waitpid(P2, ...) 只等待 P2,不会处理 P1。
  • 即使 P1 先结束,waitpid(P2) 也不会返回,因为它只关心 P2。
  • 父进程需要另一个 waitpid(P1, ...) 来清理 P1 的僵尸。

6. 解决方案

如果父进程想同时等待多个子进程,有几种模式:

模式A:顺序等待

c 复制代码
// 先等 P2,再等 P1
waitpid(P2, &status2, 0);
waitpid(P1, &status1, 0);
// 问题:如果 P1 先结束,它会在中间保持僵尸状态

模式B:非阻塞轮询

c 复制代码
while (children_remaining > 0) {
    pid_t pid = waitpid(-1, &status, WNOHANG);  // 不阻塞
    if (pid > 0) {
        // 处理一个结束的子进程
        children_remaining--;
    } else if (pid == 0) {
        // 没有子进程结束,可以做其他事
        sleep(1);
    } else {
        // 错误处理
        break;
    }
}

模式C:信号处理

c 复制代码
// 设置 SIGCHLD 处理程序
signal(SIGCHLD, sigchld_handler);

void sigchld_handler(int sig) {
    int status;
    pid_t pid;
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        // 处理结束的子进程
    }
}

总结

你的理解完全正确

  1. 第一个结束的子进程(P1)会变成僵尸,保留最小 PCB。
  2. 操作系统必须保留其 PCB ,因为父进程还没有调用 wait() 获取退出信息。
  3. 即使父进程在等待另一个子进程(P2),也不会自动清理 P1。
  4. 清理僵尸的唯一方式 :父进程调用 waitpid(P1) 或父进程终止。

这是 UNIX 进程管理的一个基本特性,也是为什么正确处理子进程的 wait() 如此重要,否则系统会积累僵尸进程,占用有限的 PID 资源。

相关推荐
Ashley_Amanda16 小时前
人事事件配置梳理
数据库
シ風箏16 小时前
AutoGen【部署 02】Linux环境安装部署AutoGenStudio并调用本地 qwen2.5:0.5b 大模型举例
linux·autogen·本地大模型·autogenstudio
xingzhemengyou116 小时前
LINUX 通过/proc/interrupts了解系统硬件中断的使用情况
linux·arm开发
cici1587416 小时前
基于正交匹配追踪(OMP)算法的信号稀疏分解MATLAB实现
数据库·算法·matlab
Web极客码16 小时前
释放WordPress磁盘空间并减少Inode使用量
服务器·数据库·ubuntu
Knight_AL16 小时前
Redis ZSet 实现排行榜(支持分数相同按时间顺序排序)
数据库·redis·缓存
xingzhemengyou116 小时前
LINUX SSH长连接VS短连接
linux
w***954916 小时前
mysql之如何获知版本
数据库·mysql
火星数据-Tina16 小时前
如何构建一个支持多终端同步的体育比分网站?
大数据·前端·数据库·websocket