🦄 个人主页: 小米里的大麦-CSDN博客
🎏 所属专栏: Linux_小米里的大麦的博客-CSDN博客
🎁 GitHub主页: 小米里的大麦的 GitHub
⚙️ 操作环境: Visual Studio 2022
文章目录
- 进程控制 —— 进程等待
- 1. 进程等待必要性
- 2. 常用等待方法(重点掌握)
- 1. `wait()` 示例(阻塞等待子进程)
- 2. `waitpid()` 示例(等待指定子进程,更灵活)
- 3. status 退出状态详解
- 1. 什么是 `status`?
- 2. `status` 的位布局(Linux 下)
- 3. WIFEXITED 和 WEXITSTATUS 的底层原理
- 1. `WIFEXITED(status)`
- 2. `WEXITSTATUS(status)`
- 4. 实验测试
- 4. 非阻塞轮询
- 1. 什么是非阻塞轮询(Non-blocking Polling)?
- 场景一:阻塞等待(wait)
- 场景二:非阻塞轮询(WNOHANG)
- 场景三:阻塞轮询(极端示例)
- 术语与现实对应表
- 2. 联系总结(术语图谱)
- 3. 我该怎么选?怎么使用?
- 4. 小结一句话
- 5. 总结记忆点
- 共勉
进程控制 —— 进程等待
1. 进程等待必要性
- 当父进程通过
fork()
创建了子进程后,子进程终止时,其退出信息必须由父进程读取,父进程如果不管不顾,就可能造成 僵尸进程 的问题,进而造成内存泄漏。 - 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的
kill -9
也无能为力,因为谁也没有办法杀死一个已经死去的进程。 - 最后,父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
如果不等待会怎样?
- 子进程退出了,但父进程没有调用
wait()
系列函数。- 子进程的“退出状态”会保留在内核中,直到父进程读取它。
- 此时子进程的 PCB 没有完全释放,占用系统资源。
- 如果产生大量僵尸进程,系统资源将耗尽,导致无法创建新进程。、
所以:父进程需要“等待”子进程终止并获取其退出状态,以释放系统资源。
面试点拨: 如果不调用
wait()
会怎样?回答:子进程的退出信息留在内核,
PCB
未释放,形成僵尸进程,长期不回收会占满系统资源。
2. 常用等待方法(重点掌握)
函数名 | 作用 |
---|---|
wait(int *status) | 阻塞等待任意一个子进程结束,并获取其退出状态 |
waitpid(pid, &status, options) | 更灵活:等待指定子进程,或非阻塞等 |
1. wait()
示例(阻塞等待子进程)
wait()
:
- 原型:
pid_t wait(int *status);
- 功能:阻塞等待任意一个子进程退出,并回收其资源。
- 参数:
status
(输出型参数):保存/获取子进程退出状态(需用宏解析,如WIFEXITED
)。不关心可设置为 NULL。- 返回值:成功返回子进程 PID,失败返回
-1
。
实验目的:
- 学会使用
wait()
函数阻塞等待子进程结束。 - 理解如何通过
status
获取子进程的退出状态。 - 掌握如何判断子进程是否正常退出以及获取其退出码。
[!CAUTION]
下面代码会涉及部分知识盲区,在文章后面会讲到!
实验:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>int main()
{pid_t id = fork();if (id == 0) // 子进程{int count = 10;while (count--){printf("我是子进程...PID:%d, PPID:%d\n", getpid(), getppid()); // 子进程逻辑:打印 PID 和 PPIDsleep(1);}exit(0); // 子进程退出}int status = 0; // 存储子进程退出状态pid_t ret = wait(&status); // 阻塞等待子进程结束if (ret > 0) // 父进程{// 父进程等待子进程结束printf("等待子进程结束...\n");if (WIFEXITED(status)) // 判断子进程是否正常退出{// 子进程正常结束printf("子进程正常结束,退出状态码:%d\n", WEXITSTATUS(status));}}sleep(3);return 0;
}
实验示例结果:
我是子进程...PID:1234, PPID:1233
我是子进程...PID:1234, PPID:1233
...
等待子进程结束...
子进程正常结束,退出状态码:0
2. waitpid()
示例(等待指定子进程,更灵活)
waitpid()
- 原型:
pid_t waitpid(pid_t pid, int *status, int options);
- 功能:更灵活地等待指定子进程,支持非阻塞模式。
- 参数:
pid
:指定子进程 PID,或 -1 表示任意子进程。options
:常用的有 WNOHANG 表示非阻塞等待(立即返回,无子进程退出时返回0
)。- 返回值:成功返回子进程 PID,WNOHANG 模式下无退出子进程时返回
0
,失败返回-1
。
实验目的:
- 学会使用
waitpid()
函数等待指定子进程。 - 理解非阻塞等待(WNOHANG)的使用场景和优势。
- 掌握如何在等待子进程的同时处理其他任务。
实验 1:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>int main()
{pid_t pid = fork();if (pid == 0){exit(10);}else{int status;pid_t wpid;while ((wpid = waitpid(pid, &status, WNOHANG)) == 0){printf("父进程忙别的事...\n");sleep(1);}if (WIFEXITED(status)){printf("子进程退出码 = %d\n", WEXITSTATUS(status));}}return 0;
}
实验示例结果:
父进程忙别的事...
父进程忙别的事...
...
子进程退出码 = 10
WNOHANG 的用途(后面详讲):它用于非阻塞轮询场景,让父进程可以边处理任务边检查子进程状态。
实验 2:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork(); // 创建子进程if (id == 0){int time = 5;int n = 0;while (n < time){printf("我是子进程,我已经运行了:%d秒 PID:%d PPID:%d\n", n + 1, getpid(), getppid());sleep(1);n++;}exit(244); // 子进程退出}int status = 0; // 状态pid_t ret = waitpid(id, &status, 0); // 参数3 为0,为默认选项if (ret == -1){printf("进程等待失败!进程不存在!\n");}else if (ret == 0){printf("子进程还在运行中!\n");}else{printf("进程等待成功,子进程已被回收\n");}printf("我是父进程, PID:%d PPID:%d\n", getpid(), getppid());//通过 status 判断子进程运行情况if ((status & 0x7F)){printf("子进程异常退出,core dump:%d 退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));}else{printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);}return 0;
}
实验示例结果:
我是子进程,我已经运行了:1秒 PID:1234 PPID:1233
...
进程等待成功,子进程已被回收
我是父进程, PID:1233 PPID:1232
子进程正常退出,退出码:244
3. status 退出状态详解
1. 什么是 status
?
当你用 wait()
或 waitpid()
等函数回收子进程时,会通过一个整型变量 status
返回子进程的 终止状态/状态码 status 信息。这个 status
是一个 32 位整数,它的 各个位(bit)存储了子进程退出的不同信息,主要包括:
- 子进程是否正常退出
- 退出的返回码
- 是否是被信号中断
- 是否是
core dump
等
当子进程结束时,它就会返回一个 状态码 status,通过宏函数解读它:
宏函数 | 判断或提取内容 | 实现底层逻辑 | 本质 |
---|---|---|---|
WIFEXITED() | 是否正常退出 | (status & 0x7F) == 0 | 判断是否未被信号终止(是否正常退出) |
WEXITSTATUS() | 获取退出码 | (status >> 8) & 0xFF | 提取退出码所在的 8 位(获取 exit 返回码) |
这些宏的 设计目的 就是为了 屏蔽底层实现细节,让你写代码时更易读。但其实就是对 status
进行的位运算封装。
2. status
的位布局(Linux 下)
通常(glibc 实现下),status 的位布局如下:
31...........16 | 15.....8 | 7......0保留位 | 退出码 | 信号位
31 16 15 8 7 0
+-----------------------------+-------------+--------+
| 保留 | 退出码(exit) | 信号码 |
+-----------------------------+-------------+--------+↑ ↑| |(status >> 8) status & 0x7F
3. WIFEXITED 和 WEXITSTATUS 的底层原理
1. WIFEXITED(status)
判断子进程是否 正常退出(调用了 exit()
或 return
)
#define WIFEXITED(status) (((status) & 0x7F) == 0)
🔸 它检测的是 低 7 位(status & 0x7F)是否为 0,即 没有被信号终止。
2. WEXITSTATUS(status)
获取子进程的 退出码(exit() 或 return 的值)
#define WEXITSTATUS(status) (((status) >> 8) & 0xFF)
🔸 它提取的是 第 8~15 位,因为退出码就被编码在这里。
4. 实验测试
实验目的:
- 学会解析
status
的各个位,了解子进程的退出状态。 - 掌握如何通过宏函数判断子进程是否正常退出以及获取其退出码。
- 理解如何手动解析
status
的位信息。
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t pid = fork();if (pid == 0){exit(66); // 子进程退出码设为 66}else{int status = 0;waitpid(pid, &status, 0);printf("原始 status:%d (0x%x)\n", status, status);if (WIFEXITED(status)){printf("正常退出,返回值 = %d\n", WEXITSTATUS(status));printf("手动解析返回值 = %d\n", (status >> 8) & 0xFF);}else{printf("非正常退出\n");}}return 0;
}
输出示例:
原始 status:16896 (0x4200)
正常退出,返回值 = 66
手动解析返回值 = 66
示例:手动解析 status
:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t pid = fork();if (pid == 0){exit(66); // 子进程退出码设为 66}else{int status = 0;waitpid(pid, &status, 0);printf("原始 status: 0x%x\n", status);// 手动解析 statusif ((status & 0x7F) == 0) // 判断是否正常退出{ int exit_code = (status >> 8) & 0xFF; // 提取退出码printf("手动解析:子进程正常退出,退出码: %d\n", exit_code);}else{printf("手动解析:子进程异常退出,信号码: %d\n", (status & 0x7F));}}return 0;
}
扩展:
写法模板:
#include <stdio.h> #include <stdlib.h> #include <sys/wait.h> #include <unistd.h>int main() {pid_t pid = fork();if (pid == 0) // 子进程逻辑{exit(0);}else if (pid > 0) // 父进程逻辑{int status;pid_t ret = waitpid(pid, &status, 0);if (ret == -1){perror("waitpid error");}else if (WIFEXITED(status)){printf("子进程正常退出,退出码: %d\n", WEXITSTATUS(status));}else{printf("子进程异常退出\n");}}else{perror("fork error");}return 0; }
若子进程是被信号杀死的,还可用:
WIFSIGNALED(status)
:是否被信号终止。WTERMSIG(status)
:哪个信号导致的。这些也都是对
status
特定位的封装。
面试点拨:
Q:只创建一个子进程也要
wait()
吗?A:要,不然会产生僵尸进程。
Q:
wait(NULL)
和wait(&status)
有何不同?A:前者不关心子进程退出码,后者可以判断退出状态。
Q:
wait()
和waitpid()
的区别是什么?(详见下文)A:
wait()
阻塞等待任意一个子进程,而waitpid()
可以指定子进程,并支持非阻塞模式。Q:怎么判断子进程是否异常退出?
A:
WIFEXITED(status)
为假时即为异常,可结合WIFSIGNALED
查看是否被信号终止。
4. 非阻塞轮询
1. 什么是非阻塞轮询(Non-blocking Polling)?
非阻塞轮询 是一种在程序中检查某项资源状态(比如文件描述符、输入输出、子进程状态等)时,不会阻塞(挂起)当前线程或进程的技术。非阻塞轮询其实是 进程等待的一种特殊形式,本质上就是使用 waitpid()
函数时,配合选项 WNOHANG
,来实现 非阻塞地检查子进程是否退出。
非阻塞轮询底层依赖:
waitpid(..., WNOHANG)
:设置为非阻塞检查子进程。read()
/write()
配合O_NONBLOCK
标志。select()
/poll()
/epoll()
这些高级接口也支持非阻塞 I/O 检测。
联系:
- 非阻塞轮询 ≈ 进程等待 +
WNOHANG
参数。 - 是进程等待的一种实现方式,可以避免父进程“卡死”在等待中。
- 适合场景:父进程还有其他任务要处理、需要同时监控多个子进程、构造后台守护程序等。
这样说难以理解,我们用一个示例来帮助理解:假如你是快递员,你今天安排了送货任务,但你同时还在等一个客户签收你的包裹。现在有两种做法:
场景一:阻塞等待(wait)
你在客户门口等着他开门签字,你哪儿也不去,什么都不干,就在那儿等。就是 wait()
或 waitpid(pid, NULL, 0)
。
- 优点:等到了就能马上处理。
- 缺点:你被“卡住”了,浪费了等的这段时间。
场景二:非阻塞轮询(WNOHANG)
你不一直站在门口,而是 每隔 10 分钟回来敲一次门,空闲的时候你还可以去送别的快递。就是 waitpid(pid, &status, WNOHANG)
+ sleep(1)
。
- 优点:你不会被“卡住”,还能干其他事。
- 缺点:客户签收可能不能第一时间知道(需要“轮询”)。
场景三:阻塞轮询(极端示例)
你不停敲门、再敲门,一直不走,一直问:“你签了没?你签了没?” 程序中表现为没有 sleep
的非阻塞 waitpid(pid, WNOHANG)
死循环。
- 缺点:会让 CPU 疯狂运转(忙等待)。
术语与现实对应表
系统术语 | 现实中的你 |
---|---|
阻塞等待 | 在门口站着等,不做别的事 |
非阻塞轮询 | 每隔一段时间回来问一次,期间干别的事 |
阻塞轮询 | 疯狂按门铃,问个不停,CPU 很累 |
进程等待 | 等子进程结束,获取退出状态 |
2. 联系总结(术语图谱)
wait/waitpid┌────────────┐│ 进程等待机制│└────┬───────┘│┌─────────────┴────────────┐│ │┌─────▼─────┐ ┌─────▼─────────┐│ 阻塞等待 │ │ 非阻塞轮询 ││ wait() │ │ waitpid(pid, WNOHANG) │└────────────┘ └──────────────────────┘
3. 我该怎么选?怎么使用?
场景 | 推荐方法 | 原因 |
---|---|---|
父进程只等子进程结束,没别的事干 | 阻塞等待 (wait ) | 简单、直接、不会浪费资源 |
父进程还有其他重要任务 | 非阻塞轮询 (waitpid + WNOHANG ) | 不中断其他逻辑,更灵活 |
你同时要监控多个子进程 | 非阻塞轮询 | 可以处理多个子进程,适合服务端/守护程序 |
写简单的练习题/实验代码 | 阻塞等待即可 | 写起来方便,看得懂 |
实验目的:
- 学会使用非阻塞轮询等待子进程结束。
- 理解如何在等待子进程的同时处理其他任务。
- 掌握如何通过
WNOHANG
选项实现非阻塞等待。
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t pid = fork();if (pid == 0) // 子进程{printf("子进程开始运行...\n");sleep(5);printf("子进程即将退出\n");exit(0);}else // 父进程:非阻塞方式轮询子进程状态{int status;while (1){pid_t result = waitpid(pid, &status, WNOHANG); // 非阻塞调用if (result == 0){// 子进程还未退出printf("父进程:子进程还在运行...\n");}else if (result == pid){// 子进程已经退出if (WIFEXITED(status)){printf("父进程:子进程正常退出,退出码为 %d\n", WEXITSTATUS(status));}break;}else{perror("waitpid error");break;}sleep(1); // 轮询间隔}}return 0;
}
实验示例结果:
父进程:子进程还在运行...
父进程:子进程还在运行...
...
子进程开始运行...
子进程即将退出
父进程:子进程正常退出,退出码为 0
4. 小结一句话
非阻塞轮询是一种“智能等待”方式,让父进程在等待子进程的同时,还能处理其他任务,是并发编程的常见技巧。
5. 总结记忆点
内容 | 说明 |
---|---|
为什么等待 | 防止僵尸进程,释放系统资源,获取子进程退出信息,确保系统稳定性和资源高效利用。 |
常用函数 | wait() :阻塞等待任意子进程结束;waitpid() :灵活等待指定子进程,支持非阻塞模式。 |
状态解析 | 使用宏函数 WIFEXITED() 判断子进程是否正常退出,WEXITSTATUS() 获取退出码。 |
非阻塞轮询 | 适用于父进程需要同时处理其他任务或监控多个子进程的场景,通过 waitpid() 配合 WNOHANG 实现。 |
推荐写法 | 常用 waitpid(pid, &status, 0) ,安全灵活,适合大多数场景。 |
注意事项 | 父进程必须回收子进程资源,否则会导致僵尸进程,长期不回收会耗尽系统资源。 |
适用场景 | 简单程序使用阻塞等待,复杂程序或需要并发处理时使用非阻塞轮询。 |
实战技巧
- 调试技巧:在调试时,若发现僵尸进程,检查父进程是否正确调用了
wait()
或waitpid()
。 - 性能优化:在高并发场景下,使用非阻塞轮询避免父进程被长时间阻塞,提高系统响应速度。
- 代码健壮性:始终检查
wait()
和waitpid()
的返回值,处理可能的错误情况。
共勉