两个子进程都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 资源。

相关推荐
开开心心就好21 小时前
发票合并打印工具,多页布局设置实时预览
linux·运维·服务器·windows·pdf·harmonyos·1024程序员节
css趣多多1 天前
add组件增删改的表单处理
java·服务器·前端
予枫的编程笔记1 天前
【Linux进阶篇】从基础到实战:grep高亮、sed流编辑、awk分析,全场景覆盖
linux·sed·grep·awk·shell编程·文本处理三剑客·管道命令
Sheep Shaun1 天前
揭开Linux的隐藏约定:你的第一个文件描述符为什么是3?
linux·服务器·ubuntu·文件系统·缓冲区
devmoon1 天前
在 Polkadot Runtime 中添加多个 Pallet 实例实战指南
java·开发语言·数据库·web3·区块链·波卡
Tfly__1 天前
在PX4 gazebo仿真中加入Mid360(最新)
linux·人工智能·自动驾驶·ros·无人机·px4·mid360
野犬寒鸦1 天前
从零起步学习并发编程 || 第七章:ThreadLocal深层解析及常见问题解决方案
java·服务器·开发语言·jvm·后端·学习
陈桴浮海1 天前
【Linux&Ansible】学习笔记合集二
linux·学习·ansible
认真的薛薛1 天前
数据库-sql语句
数据库·sql·oracle
生活很暖很治愈1 天前
Linux——环境变量PATH
linux·ubuntu