跳到主要内容Linux 进程等待机制:wait/waitpid 与僵尸进程治理 | 极客日志C
Linux 进程等待机制:wait/waitpid 与僵尸进程治理
进程等待通过 wait 或 waitpid 系统调用实现,用于回收子进程资源并获取退出状态,防止僵尸进程产生。支持阻塞与非阻塞模式,可通过宏解析退出信息。父进程需使用这些接口从内核获取子进程状态,无法通过全局变量共享。非阻塞轮询允许父进程在等待期间执行其他任务,但应避免重负载操作以免影响回收效率。
小熊软糖20 浏览 一、进程等待是什么?
进程等待是通过 wait/waitpid 这两个系统调用,实现对子进程进行状态检测与资源回收的功能。
它的核心作用有两点:
- 回收资源:避免子进程退出后成为'僵尸进程',占用 PID 等系统资源;
- 状态检测:获取子进程的退出状态(如正常结束、被信号终止等)。
二、为什么需要进程等待?
进程等待的必要性可以分为'必须解决'和'可选关注'两类场景:
1. 必须解决:清理僵尸进程,避免资源泄漏
僵尸进程的状态为 Z——它的数据、代码会被内核自动清理,但 PCB(进程控制块)会残留 PID、退出状态、调度信息等属性,持续占用系统资源,且无法通过 kill -9 直接杀死。
只有父进程通过进程等待(当然,如果父进程退出,子进程会成为孤儿进程被系统进程回收),才能彻底回收 PCB 资源,否则僵尸进程会长期占用 PID、内存,最终导致系统无法创建新进程。
2. 可选关注:获取子进程的退出状态
通过进程等待,父进程可以拿到子进程的退出信息(比如是正常完成任务,还是被信号中断),以此判断子进程的任务执行结果。这一步可根据业务需求选择是否关注,但对需要确保任务可靠性的场景(如后台服务)至关重要。
三、wait
wait 函数介绍
wait 函数是 Linux 系统中一类进程控制函数,其作用是让父进程以阻塞方式等待任意一个子进程终止(不支持指定等待子进程),同时完成该子进程的资源回收与退出状态获取。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
- 返回值:成功返回被等待进程 pid,失败返回 -1。
- 参数:输出型参数,获取子进程退出状态,不关心则可以设置为 NULL。
场景 1:wait 等待单个子进程(仅回收资源,不关心退出状态)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/types.h>
int main {
id = fork();
(id < ) {
perror();
;
}
(id == ) {
cnt = ;
(cnt) {
(, getpid(), getppid(), cnt);
cnt--;
sleep();
}
();
}
{
cnt = ;
(cnt) {
(, getpid(), getppid(), cnt);
cnt--;
sleep();
}
ret = wait();
(ret == id) {
(, ret);
}
sleep();
}
;
}
()
pid_t
if
0
"fork"
return
1
else
if
0
int
5
while
printf
"I am child, pid:%d, ppid:%d, cnt: %d\n"
1
exit
2
else
int
10
while
printf
"I am father, pid:%d, ppid:%d, cnt: %d\n"
1
pid_t
NULL
if
printf
"wait success, ret: %d\n"
5
return
0
这段代码中,父进程创建子进程后,子进程仅循环 5 次就退出并进入僵尸状态;父进程继续执行自身 10 次循环任务,待自身任务完成后,才调用 wait 阻塞回收已处于僵尸状态的子进程,最终 'wait success' 标识僵尸进程被成功清理,无资源残留。
wait 参数传 NULL,是因为当前场景暂不关注子进程的退出状态,仅需回收子进程资源,传 NULL 可忽略退出状态信息。
场景 2:wait 等待多个子进程(仅回收资源,不关心退出状态)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/types.h>
#define N 5
void RunChild() {
int cnt = 5;
while (cnt) {
printf("I am Child Process, pid: %d, ppid:%d\n", getpid(), getppid());
sleep(1);
cnt--;
}
}
int main() {
for (int i = 0; i < N; i++) {
pid_t id = fork();
if (id == 0) {
RunChild();
exit(0);
}
printf("create child process: %d success\n", id);
}
sleep(10);
for (int i = 0; i < N; i++) {
pid_t id = wait(NULL);
if (id > 0) {
printf("wait %d success\n", id);
}
}
sleep(5);
return 0;
}
这段代码中,父进程创建 5 个子进程后,每个子进程仅循环 5 次就退出并进入僵尸状态;父进程继续执行自身休眠 10 秒的任务,待自身任务完成后,才通过循环调用 wait 阻塞回收已处于僵尸状态的多个子进程,最终 'wait 进程 ID success' 依次标识所有僵尸进程被成功清理,无资源残留。
提示:wait 系统调用的阻塞等待特性
若子进程处于运行状态、未主动退出,父进程调用 wait 时会进入阻塞状态——暂停自身代码执行,持续等待子进程结束;直到有子进程退出,wait 才会完成该子进程的资源回收并返回结果,父进程随之恢复执行。
该特性确保父进程能可靠回收子进程资源,避免子进程成为僵尸进程,但也会让父进程暂时'停滞',适用于需要严格等待子进程完成的场景。
由于 waitpid 的功能比 wait 更丰富,且完全涵盖了 wait 的所有能力,因此获取退出状态的测试我就不用 wait 来演示了。学会通过 waitpid 获取退出状态后,对应的 wait 用法也就自然掌握了。
四、waitpid
waitpid 函数介绍
waitpid 函数是 Linux 系统中让父进程灵活等待指定子进程终止,可控制阻塞/非阻塞模式、回收资源并获取退出状态的进程控制函数。
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
- 返回值:
- 正常返回:收集到的子进程的进程 ID;
- 若设置
WNOHANG 且无已退出子进程可收集:返回 0;
- 调用出错:返回 -1,同时
errno 会被设置以指示错误原因。
- 参数:
- pid:
pid=-1:等待任意一个子进程,与 wait 等效;
pid>0:等待进程 ID 与 pid 相等的子进程。
- status:输出型参数,操作系统会自动往里面填子进程的退出信息。
- options:
WNOHANG:非阻塞模式,若 pid 指定的子进程未结束,函数直接返回 0;若子进程已正常结束,则返回其进程 ID。
0:阻塞模式,调用 waitpid 后父进程会暂停执行,直到指定的子进程状态改变(退出/被信号终止),再返回子进程 ID。
场景 1:waitpid 等待单个子进程(仅资源回收)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/types.h>
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
}
else if (id == 0) {
int cnt = 5;
while (cnt) {
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(11);
}
else {
int cnt = 5;
while (cnt) {
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
pid_t ret = waitpid(-1, NULL, 0);
if (ret == id) {
printf("wait success, ret: %d\n", ret);
}
sleep(5);
}
return 0;
}
这段代码中,父进程创建子进程后,父子进程会并发执行各自的 5 次循环任务;子进程完成循环后退出并短暂进入僵尸状态,父进程完成自身任务后,调用 waitpid(-1, NULL, 0) 阻塞回收该僵尸子进程,最终 'wait success' 标识子进程资源被成功清理,无残留。
场景 2:waitpid 等待单个子进程(获取退出状态)
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <sys/types.h>
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
}
else if (id == 0) {
int cnt = 5;
while (cnt) {
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else {
int cnt = 5;
while (cnt) {
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
int status;
pid_t ret = waitpid(id, &status, 0);
if (ret == id) {
printf("wait success, ret: %d, status: %d\n", ret, status);
}
sleep(5);
}
return 0;
}
执行后可以看到,打印的 status 值是 256 而非预期的 1——这是因为 waitpid 接收的 status 并非直接存储退出码,而是一个复合格式的整数(包含退出码、终止信号等信息),需要通过 WEXITSTATUS(status) 等宏才能正确解析出子进程实际的退出码。
status 参数解析
wait 和 waitpid 都有一个 status 参数,该参数是输出型参数,由操作系统自动填充:
- 若传递
NULL,表示不关心子进程的退出状态信息;
- 若传递有效的变量地址,操作系统会将子进程的退出信息(退出码、终止信号等)填充到该变量中。
但 status 不能简单当作整数看待,而要以'位图'的形式解析(通常只需关注低 16 比特位):
- 低 8 比特位(0~7 位):存储子进程的终止信号(若子进程是被信号终止的);
- 高 8 比特位(8~15 位):存储子进程的退出码(若子进程是正常退出的)。
以之前输出的 status=256 为例:256 对应的二进制是 100000000,其高 8 比特位(第 8 位)为 1,低 8 比特位为 0——这意味着子进程是正常退出的,退出码为 1(对应高 8 位的值)。
手动解析 status
通过手动位运算解析 waitpid 获取的 status 值:
if (WIFEXITED(status)) {
int code = WEXITSTATUS(status);
}
验证逻辑:低 7 位为 0,说明子进程未被信号终止、属于正常退出;将 status 右移 8 位后取低 8 位得到 1,与子进程 exit(1) 设置的退出码一致。
用宏解析 status
if (WIFEXITED(status)) {
printf("正常退出,退出码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("异常退出,终止信号:%d\n", WTERMSIG(status));
}
从运行结果看,子进程执行后'正常退出,退出码:1',说明代码中宏的逻辑生效。
问题:父进程为何必须用 wait 获取子进程状态?不能用全局变量吗?
因为进程具有独立性,父子进程地址空间独立,全局变量是各自副本,子进程修改后父进程拿不到。
而且父进程不能直接访问子进程的内核数据(操作系统不允许直接碰内核数据),wait 是操作系统提供的'合法接口'——帮父进程从内核里取子进程状态,这是唯一可靠的方式。
提示:wait/waitpid 的核心作用
- 从内核中读取子进程的 exitcode(退出码)和 sigcode(终止信号)等退出信息;
- 将子进程的'僵尸状态(Z)'修改为'消亡状态(X)',释放其占用的内核资源。
特殊场景:父进程等待非目标子进程导致 waitpid 失败
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork");
return 1;
}
else if (id == 0) {
int cnt = 5;
while (cnt) {
printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else {
int cnt = 5;
while (cnt) {
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt);
cnt--;
sleep(1);
}
int status;
pid_t ret = waitpid(id + 4, &status, 0);
if (ret == id) {
if (WIFEXITED(status)) {
printf("正常退出,退出码:%d\n", WEXITSTATUS(status));
} else if (WIFSIGNALED(status)) {
printf("异常退出,终止信号:%d\n", WTERMSIG(status));
}
} else {
printf("该子进程不是我要等待的进程,wait fail\n");
}
}
sleep(5);
return 0;
}
父进程创建子进程后,故意传入错误的子进程 PID 调用 waitpid,因实际退出的子进程 PID 与目标 PID 不符,导致等待失败,触发'该子进程不是我要等待的进程,wait fail'提示,证明 waitpid 仅能正确获取指定 PID 子进程的状态。
五、waitpid 非阻塞轮询
阻塞与非阻塞等待对比
- 阻塞等待:父进程调用
wait/waitpid 后,暂停自己的所有工作,直到子进程退出才恢复执行。适用于需要严格等待子进程完成的场景。
- 非阻塞轮询:父进程调用
waitpid(..., WNOHANG) 后,不会暂停工作,而是'问一句就走',一边轮询子进程状态,一边处理自己的任务。适用于需要兼顾其他任务的场景。
非阻塞轮询等待演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main() {
pid_t id = fork();
if (id < 0) {
perror("fork error");
return 1;
}
else if (id == 0) {
int cnt = 5;
while (cnt) {
printf("子进程 (PID:%d) 正在执行,剩余%d秒...\n", getpid(), cnt);
cnt--;
sleep(1);
}
exit(10);
}
else {
int status = 0;
while (1) {
pid_t ret = waitpid(id, &status, WNOHANG);
if (ret > 0) {
printf("父进程成功等待子进程 (PID:%d)\n", ret);
if (WIFEXITED(status)) {
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
} else {
printf("子进程异常退出,终止信号:%d\n", WTERMSIG(status));
}
break;
}
else if (ret < 0) {
printf("等待子进程失败!\n");
break;
}
else {
printf("子进程尚未退出,父进程执行其他任务...\n");
sleep(1);
}
}
}
return 0;
}
父进程 fork 出子进程(子进程睡眠 5 秒后退出),父进程用 waitpid+WNOHANG 开启非阻塞模式,轮询子进程状态——子进程未退出时父进程可执行自身任务,退出后用 WIFEXITED 解析退出码;WNOHANG 是实现'边轮询边工作'的核心。
非阻塞轮询回收子进程的核心定位
非阻塞等待的核心目的是'回收子进程(避免僵尸进程)',父进程同时执行的任务只是'附带轻量级工作'——这类任务需是耗时短、资源占用少的操作(比如打印日志、检测状态);若在轮询中执行'拷贝 100G 数据'这类重任务,会大幅延迟子进程的回收时机,甚至导致僵尸进程长期占用资源,违背了非阻塞等待'及时回收'的设计初衷。
相关免费在线工具
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
- JSON 压缩
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
- JSON美化和格式化
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online