018 进程控制 —— 进程等待

进程控制 —— 进程等待

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]

下面代码会涉及部分知识盲区,在文章后面会讲到!

实验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#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 和 PPID
sleep(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;
}

实验示例结果:

1
2
3
4
5
我是子进程...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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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;
}

实验示例结果:

1
2
3
4
父进程忙别的事...
父进程忙别的事...
...
子进程退出码 = 10

WNOHANG 的用途(后面详讲):它用于非阻塞轮询场景,让父进程可以边处理任务边检查子进程状态。

实验 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#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
2
3
4
5
我是子进程,我已经运行了: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 的位布局如下:

image-20250414221815934

1
2
31...........16 | 15.....8 | 7......0
保留位 | 退出码 | 信号位
1
2
3
4
5
6
7
  31                            16 15         8 7      0
+-----------------------------+-------------+--------+
| 保留 | 退出码(exit) | 信号码 |
+-----------------------------+-------------+--------+
↑ ↑
| |
(status >> 8) status & 0x7F

3. WIFEXITED 和 WEXITSTATUS 的底层原理

1. WIFEXITED(status)

判断子进程是否 正常退出(调用了 exit()return

1
#define WIFEXITED(status)  (((status) & 0x7F) == 0)

🔸 它检测的是 低 7 位(status & 0x7F)是否为 0,即 没有被信号终止

2. WEXITSTATUS(status)

获取子进程的 退出码(exit() 或 return 的值)

1
#define WEXITSTATUS(status)  (((status) >> 8) & 0xFF)

🔸 它提取的是 第 8~15 位,因为退出码就被编码在这里。

4. 实验测试

实验目的:

  • 学会解析 status 的各个位,了解子进程的退出状态。
  • 掌握如何通过宏函数判断子进程是否正常退出以及获取其退出码。
  • 理解如何手动解析 status 的位信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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;
}

输出示例:

image-20250414223219766

1
2
3
原始 status:16896 (0x4200)
正常退出,返回值 = 66
手动解析返回值 = 66

示例:手动解析 status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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);

// 手动解析 status
if ((status & 0x7F) == 0) // 判断是否正常退出
{
int exit_code = (status >> 8) & 0xFF; // 提取退出码
printf("手动解析:子进程正常退出,退出码: %d\n", exit_code);
}
else
{
printf("手动解析:子进程异常退出,信号码: %d\n", (status & 0x7F));
}
}
return 0;
}

扩展:

写法模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
>#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. 联系总结(术语图谱)

image-20250414230218594

1
2
3
4
5
6
7
8
9
10
11
               wait/waitpid
┌────────────┐
│ 进程等待机制│
└────┬───────┘

┌─────────────┴────────────┐
│ │
┌─────▼─────┐ ┌─────▼─────────┐
│ 阻塞等待 │ │ 非阻塞轮询 │
│ wait() │ │ waitpid(pid, WNOHANG) │
└────────────┘ └──────────────────────┘

3. 我该怎么选?怎么使用?

场景 推荐方法 原因
父进程只等子进程结束,没别的事干 阻塞等待 (wait) 简单、直接、不会浪费资源
父进程还有其他重要任务 非阻塞轮询 (waitpid + WNOHANG) 不中断其他逻辑,更灵活
你同时要监控多个子进程 非阻塞轮询 可以处理多个子进程,适合服务端/守护程序
写简单的练习题/实验代码 阻塞等待即可 写起来方便,看得懂

实验目的:

  • 学会使用非阻塞轮询等待子进程结束。
  • 理解如何在等待子进程的同时处理其他任务。
  • 掌握如何通过 WNOHANG 选项实现非阻塞等待。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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;
}

实验示例结果:

1
2
3
4
5
6
父进程:子进程还在运行...
父进程:子进程还在运行...
...
子进程开始运行...
子进程即将退出
父进程:子进程正常退出,退出码为 0

4. 小结一句话

非阻塞轮询是一种“智能等待”方式,让父进程在等待子进程的同时,还能处理其他任务,是并发编程的常见技巧。


5. 总结记忆点

内容 说明
为什么等待 防止僵尸进程,释放系统资源,获取子进程退出信息,确保系统稳定性和资源高效利用。
常用函数 wait():阻塞等待任意子进程结束;waitpid():灵活等待指定子进程,支持非阻塞模式。
状态解析 使用宏函数 WIFEXITED() 判断子进程是否正常退出,WEXITSTATUS() 获取退出码。
非阻塞轮询 适用于父进程需要同时处理其他任务或监控多个子进程的场景,通过 waitpid() 配合 WNOHANG 实现。
推荐写法 常用 waitpid(pid, &status, 0),安全灵活,适合大多数场景。
注意事项 父进程必须回收子进程资源,否则会导致僵尸进程,长期不回收会耗尽系统资源。
适用场景 简单程序使用阻塞等待,复杂程序或需要并发处理时使用非阻塞轮询。

实战技巧

  1. 调试技巧:在调试时,若发现僵尸进程,检查父进程是否正确调用了 wait()waitpid()
  2. 性能优化:在高并发场景下,使用非阻塞轮询避免父进程被长时间阻塞,提高系统响应速度。
  3. 代码健壮性:始终检查 wait()waitpid() 的返回值,处理可能的错误情况。