老师:假设我有两个正在运行的子进程,我让 fork 等待第二个,当第一个结束时,而第二个正在执行 sleep 100。所以它将运行得比第一个长得多。
那么当第一个结束时,它的退出码被设置为零。然而,操作系统不能丢弃它的PCB,因为父进程正在对第二个进程进行显式的 waitpid 调用,并且阻塞在第二个进程上。所以,直到第二个进程完成,并且父进程过来等待第一个进程,你必须将第一个子进程的PCB保留在进程表(process table)中。
这涉及到了僵尸进程(zombie process) 和 waitpid 系统调用的行为。我们来详细分析一下:
场景还原
- 父进程 fork 出两个子进程:子进程1(P1) 和 子进程2(P2)。
- 父进程调用
waitpid(P2, ...)等待 P2 结束(阻塞在 P2 上)。 - P1 很快结束(exit code = 0)。
- P2 执行
sleep 100,将运行很长时间。 - 问题 :P1 结束时,父进程并没有调用
waitpid(P1, ...),父进程在等待 P2,那么 P1 的 PCB 会怎样?
正确答案:P1 会变成僵尸进程(Zombie)
1. 为什么不能立即丢弃 PCB?
因为 PCB 中包含父进程需要获取的信息:
- 退出状态(exit status):exit code(0)、终止信号等。
- 资源使用统计:CPU 时间、内存使用等。
- 进程ID(PID):需要保持,直到父进程获取。
父进程需要调用 wait() 或 waitpid() 来获取这些信息。如果操作系统在父进程读取前就丢弃 PCB,那么退出信息将永久丢失。
2. 操作系统的处理方式
当一个进程结束(通过 exit() 或信号终止)时:
- 释放大部分资源:内存、打开的文件、信号量等。
- 但保留最小的 PCB 结构:只保存退出状态和资源统计信息。
- 进程状态变为
ZOMBIE(或EXIT_ZOMBIE)。 - 进程仍占用一个 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) {
// 处理结束的子进程
}
}
总结
你的理解完全正确:
- 第一个结束的子进程(P1)会变成僵尸,保留最小 PCB。
- 操作系统必须保留其 PCB ,因为父进程还没有调用
wait()获取退出信息。 - 即使父进程在等待另一个子进程(P2),也不会自动清理 P1。
- 清理僵尸的唯一方式 :父进程调用
waitpid(P1)或父进程终止。
这是 UNIX 进程管理的一个基本特性,也是为什么正确处理子进程的 wait() 如此重要,否则系统会积累僵尸进程,占用有限的 PID 资源。