目录
1.进程等待的概念
通过系统调用wait/waitpid,来对子进程进行状态检测与回收的功能
2.进程等待必要性
- 子进程退出,父进程如果不管不顾,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成结果对还是不对,或者是否正常退出。(可选)
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.进程退出方法
3.1wait
|----------|--------------------------------------------|
| 函数原型 | pid_t wait(int *wstatus); |
| 头文件 | <sys/types.h> <sys/wait.h> |
| 功能 | 等待任意子进程终止或停止,并回收其资源 |
| 参数 | wstatus - 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL |
| 返回值 | 成功 :返回终止的子进程PID 失败:返回-1,设置errno |
| 阻塞行为 | 如果没有子进程已终止,则阻塞调用进程 |
| 作用 | 防止产生僵尸进程(Zombie Process) |
单子进程回收
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6 #include<errno.h>
7 #include<string.h>
8
9 int main()
10 {
11 //验证wait函数回收僵尸进程
12 //创建进程
13 pid_t id = fork();
14 if(id == 0)
15 {
16 //子进程运行两秒后先退出
17 int cnt = 2;
18 while(cnt--)
19 {
20 printf("I am a child, my PID: %d, my PPID: %d, cnt: %d\n", getpid(), getppid() , cnt);
21 sleep(1);
22 }
23 exit(0);//子进程不运行后续代码
24 }
25 else if(id > 0)
26 {
27 //父进程运行5秒后退出
28 int cnt = 5;
29 while(cnt--)
30 {
31 printf("I am a parent, my PID: %d, my PPID: %d, cnt:%d\n", getpid(), getppid( ), cnt);
32 sleep(1);
33 }
34 pid_t ret = wait(NULL);//暂时不关心子进程的退出状态,将输出型参数置空
35 if(ret < 0)
36 {
37 printf("等待子进程失败,错误码: %d, 错误描述:%s\n", errno, strerror(errno));
38 sleep(2);//不立即退出
39 exit(errno);
40 }
41 else
42 {
43 printf("等待子进程成功,子进程PID:%d\n", ret);
44 sleep(2);//不立即退出
45 exit(0);
46 }
47 }
48 else
49 {
50 //出错,暂时不管
51 }
63 return 0;
64 }

多子进程回收
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6 #include<errno.h>
7 #include<string.h>
8
9 #define N 5
10
11 void childRun()
12 {
13 int cnt = 3;
14 while(cnt--)
15 {
16 printf("I am a child, my PID: %d, my PPID: %d, cnt: %d\n", getpid (), getppid(), cnt);
17 sleep(1);
18 }
19 }
20
21 int main()
22 {
23 //验证wait函数回收僵尸进程
24 //多子进程回收
25 //瞬间创建 N 个子进程
26 for(int i = 0; i < N; ++i)
27 {
28 pid_t id = fork();
29 if(id == 0)//子进程运行三秒就退出
30 {
31 childRun();
32 exit(i);//退出码为0~N
33 }
34 }
35 //父进程运行5秒再回收子进程
36 int cnt = 5;
37 while(cnt--)
38 {
39 printf("I am a parent, my PID: %d, my PPID: %d, cnt: %d\n", getpi d(), getppid(), cnt);
40 sleep(1);
41 }
42 //批量回收子进程
43 for(int i = 0; i < N; ++i)
44 {
45 pid_t ret = wait(NULL);//暂时不关心退出状态
46 if(ret < 0)
47 {
48 printf("等待子进程失败,错误码:%d,错误描述:%s\n", errno, strer ror(errno));
49 exit(errno);
50 }
51 else
52 {
53 printf("等待子进程成功,子进程PID:%d\n", ret);
54 }
55 }
56 sleep(3);//回收完子进程之后等待3秒再退出
121 return 0;
122 }
父进程瞬间创建N个子进程,3秒后所有子进程都退出,此时父进程还没对他们进行回收。
所有子进程成为僵尸进程,2秒后父进程回收所有子进程,3秒后父进程也退出了
- 值得注意的是,wait等待的是任意一个终止的子进程
- 当子进程未退出而父进程调用wait函数等待子进程时,父进程会处于阻塞等待状态,直到子进程退出,wait函数回收子进程资源完毕;
- 阻塞等待:内核将父进程加入自身的等待队列中,当子进程退出时,内核唤醒等待队列中的父进程,父进程随后回收子进程资源并继续执行

3.2waitpid
|----------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| 函数原型 | pid_t waitpid(pid_t pid, int *status, int options); |
| 头文件 | #include <sys/types.h> #include <sys/wait.h> |
| 功能 | 等待指定子进程的状态变化,并获取其状态信息 |
| 参数 pid | - >0:等待进程ID为pid的特定子进程 - -1:等待任意子进程(等效于wait) - 0:等待与调用进程同一进程组的任意子进程 - <-1:等待进程组ID等于pid绝对值的任意子进程 |
| 参数 status | 指向整数的指针,用于存储子进程退出状态。可用宏分析: - WIFEXITED(status):正常退出返回真 - WEXITSTATUS(status):获取退出码 - WIFSIGNALED(status):被信号终止返回真 - WTERMSIG(status):获取终止信号 - WIFSTOPPED(status):暂停状态返回真 - WSTOPSIG(status):获取暂停信号 |
| 参数 options | 选项组合(可用|连接): - 0:默认行为,阻塞等待 - WNOHANG:非阻塞,立即返回 - WUNTRACED:报告暂停的子进程 - WCONTINUED:报告继续运行的子进程 |
| 返回值 | - 成功:返回状态变化的子进程PID - 使用WNOHANG且无状态变化:返回0 - 失败:返回-1,errno设置错误码 |
| 常见错误 | - ECHILD:指定子进程不存在 - EINTR:被信号中断 - EINVAL:无效选项 |
| 应用场景 | 1. 等待特定子进程结束 2. 非阻塞轮询子进程状态 3. 处理子进程暂停/继续信号 4. 获取子进程退出状态 |
| 备注 | - waitpid提供了比wait更精细的控制 - 结合信号处理时需注意竞态条件 - 多次调用waitpid可能回收同一子进程,但第二次会失败(ECHILD) |
- 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
- 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。

解读status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由用户传入,操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- 否则,操作系统会根据该参数,将子进程的终止状态信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

下面是Linux下的所有信号

因为信号是从1开始的,所以,当status低七位为0时,表示子进程未收到信号且正常退出,否则就是收到了信号导致子进程终止或暂停
- 之所以要通过输出型参数status拿到子进程终止状态信息是因为,进程是具有独立性的,父进程无法访问子进程的代码和数据,只能通过参数交由操作系统帮忙获取
- 操作系统通过访问子进程的task_struct拿到子进程的终止状态信息,然后通过位运算将他们整合到变量status中
操作系统通过位运算得到status,那我们就可以通过位运算提取相关信息
但库里面提供了宏,简化了提取操作
// 进程状态判断宏(简化注释版)
// 判断是否正常退出:低7位为0表示正常退出
#define WIFEXITED(status) (((status) & 0x7F) == 0)
// 获取退出码:取8-15位(正常退出时有效)
#define WEXITSTATUS(status) (((status) >> 8) & 0xFF)
// 判断是否被信号终止:低7位非0且不是127
#define WIFSIGNALED(status) ((((status) & 0x7F) != 0) && \
(((status) & 0x7F) != 0x7F))
// 获取终止信号:取低7位(信号终止时有效)
#define WTERMSIG(status) ((status) & 0x7F)
// 检查是否生成core文件:第7位为1表示生成
#define WCOREDUMP(status) ((status) & 0x80)
// 判断是否被暂停:低8位为0x7F表示暂停
#define WIFSTOPPED(status) (((status) & 0xFF) == 0x7F)
// 获取暂停信号:取8-15位(暂停时有效)
#define WSTOPSIG(status) (((status) >> 8) & 0xFF)
-
演示
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>int main()
{
pid_t id = fork();if(id == 0) { // 子进程:运行两秒后退出 printf("子进程启动 PID=%d\n", getpid()); for(int cnt = 2; cnt > 0; cnt--) { printf("子进程: PID=%d, PPID=%d, 剩余时间: %d秒\n", getpid(), getppid(), cnt); sleep(1); } printf("子进程正常退出\n"); exit(9); // 退出码9 } else if(id > 0) { // 父进程:运行5秒后等待子进程 printf("父进程启动 PID=%d,子进程PID=%d\n", getpid(), id); for(int cnt = 5; cnt > 0; cnt--) { printf("父进程: PID=%d, 剩余时间: %d秒\n", getpid(), cnt); // 可以在这里检查子进程状态(非阻塞) int status; pid_t check = waitpid(id, &status, WNOHANG); if(check == id) { printf("子进程已提前退出,立即回收\n"); break; // 提前结束循环 } sleep(1); } // 等待子进程 int status = 0; printf("\n父进程开始等待子进程...\n"); pid_t ret = waitpid(id, &status, 0); // 指定等待特定子进程 if(ret < 0) { printf("等待失败: %s (errno=%d)\n", strerror(errno), errno); // 检查是否是ECHILD(子进程不存在) if(errno == ECHILD) { printf("可能的原因:子进程已被其他wait回收\n"); } return errno; } else { printf("等待成功,回收子进程PID=%d\n", ret); if(WIFEXITED(status)) { printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status)); } else if(WIFSIGNALED(status)) { printf("子进程被信号终止,信号:%d", WTERMSIG(status)); if(WCOREDUMP(status)) { printf(" (生成core dump)"); } printf("\n"); } else if(WIFSTOPPED(status)) { printf("子进程被暂停,信号:%d\n", WSTOPSIG(status)); } return 0; } } else { // fork失败的处理 perror("fork失败"); return 1; } return 0;}
-
进程结构是多叉树,父进程只对直系的子进程直接负责回收,爷孙关系隔代就不管了
进程等待失败原因
| 错误码 | 原因 | 常见场景 | 解决方法 |
|---|---|---|---|
| ECHILD | 没有子进程可等待 | 最常见错误 | 检查fork是否成功,子进程是否已被回收 |
| EINTR | 被信号中断 | 信号处理场景 | 重新调用wait/waitpid,或使用SA_RESTART |
| EINVAL | 无效参数 | 参数错误 | 检查pid值、options标志 |
| EFAULT | 无效地址 | status或rusage指针无效 | 检查指针是否有效 |
| EPERM | 权限不足 | 跨权限等待 | 确保有足够权限 |
解读option
| 选项常量 | 值(十六进制) | 作用 | 使用场景 |
|---|---|---|---|
| 0 | 0x00 | 默认阻塞模式 | 传统wait行为,阻塞等待 |
| WNOHANG | 0x01 | 非阻塞轮询 | 不阻塞,立即返回 |
- 父进程一味的等待子进程其实并不划算,因为等待过程可能还可以做一些轻量化的任务
- 所以waitpid提供了option参数,当option等于0时,父进程阻塞等待子进程,当option等于WNOHANG时,waitpid立即返回
- 那么,我们就可以选择在多次调用waitpid的过程中,做一些父进程自己的事
- 其中多次调用waitpid的过程就叫做非阻塞轮询
- 非阻塞轮询搭配父进程做自己的事效果更佳
演示非阻塞轮询
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
#include<string.h>
#define N 5
void childRun()
{
int cnt = 3;
while(cnt--)
{
printf("I am a child, my PID: %d, my PPID: %d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
}
}
#define TASK_NUM 10
typedef void(*task_t)();
task_t tasks[TASK_NUM];
void task1()
{
printf("这是一个打印 日志的任务\n");
}
void task2()
{
printf("这是一个监测网络状态的任务\n");
}
void task3()
{
printf("这是一个绘制图形界面的任务\n");
}
int addTask(task_t t);
//管理任务
void initTasks()
{
for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
addTask(task1);
addTask(task2);
addTask(task3);
}
int addTask(task_t t)
{
//遍历数组,在空的位置添加任务
int pos = 0;
for(; pos < TASK_NUM; ++pos)
{
if(tasks[pos] == NULL) break;
}
if(pos == TASK_NUM) return 0;//添加失败返沪0
tasks[pos] = t;
return 1;//添加成功返回1
}
void delTask() {}
void checkTask() {}
void updateTask() {}
void executeTasks()
{
//按照数组中存放顺序执行任务
for(int i = 0; i < TASK_NUM; ++i)
{
if(tasks[i] != NULL) tasks[i]();
else break;
}
}
int main()
{
//单子进程回收
////创建进程
pid_t id = fork();
if(id == 0)
{
//子进程运行5秒后退出
int cnt = 5;
while(cnt--)
{
printf("I am a child, my PID: %d, my PPID: %d, cnt: %d\n", getpid(), getppid(), cnt);
sleep(1);
}
exit(9);//子进程不运行后续代码,任给一个退出码
}
else if(id > 0)
{
//父进程运行3秒后阻塞等待子进程
int cnt = 3;
while(cnt--)
{
printf("I am a parent, my PID: %d, my PPID: %d, cnt:%d\n", getpid(), getppid(), cnt);
sleep(1);
}
initTasks();
while(1)//轮询
{
int status = 0;
pid_t ret = waitpid(id, &status, WNOHANG);//非阻塞
if(ret < 0)//等待失败
{
printf("等待子进程失败,错误码: %d, 错误描述:%s\n", errno, strerror(errno));
sleep(2);//不立即退出
exit(errno);
}
else if(ret > 0)//等待成功
{
printf("等待子进程成功,子进程PID:%d\n", ret);
if(WIFEXITED(status))
{
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
}
else
{
printf("终止信号:%d\n", WTERMSIG(status));
}
sleep(2);//不立即退出
exit(0);
}
else//子进程还没结束
{
printf("子进程还未结束,做点自己的事:\n");
executeTasks();
usleep(500000);
}
}
}
else
{
//出错,暂时不管
}
return 0;
}

需要注意的是,非阻塞轮询+做自己的任务的过程中,等待子进程是主要事件,周期性做自己的任务是次要事件,所以自己的任务一定要轻量化,可以晚一点点回收子进程,但一定不能太晚