这篇博客我们将学习进程等待的相关知识,会比较难,不过没关系,我会带你一步一步梳理理解的
1.进程等待的必要性(为什么)
• 之前讲过,⼦进程退出,⽗进程如果不管不顾,就可能造成'僵⼫进程'的问题,进⽽造成内存
泄漏
• 另外,进程⼀旦变成僵⼫状态,那就⼑枪不⼊,"杀⼈不眨眼"的kill -9 也⽆能为⼒,因为谁也
没有办法杀死⼀个已经死去的进程
• 最后,⽗进程派给⼦进程的任务完成的如何,我们需要知道。如,⼦进程运⾏完成,结果对还是
不对,或者是否正常退出
• ⽗进程通过进程等待的⽅式,回收⼦进程资源(最重要),获取⼦进程退出信息(可选择,不算最重要
2.进程等待的⽅法
1.wait⽅法
我们先来写一个僵尸进程:
下面是code.c代码
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
//子进程
if (id == 0)
{
for (int i = 0; i < 5; i++)
{
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//父进程
sleep(100);
return 0;
}
运行结果如下图

我们通过man 2 wait来找到关于wait的信息

发现wait函数基本使用就是pid_t wait(int* status)
那么这里的rid是什么呢,如果等待成功就返回等待的子进程pid,不成功就返回-1
所以我们可以将上面code.c代码写为如下形式:
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t id = fork();
//子进程
if (id == 0)
{
for (int i = 0; i < 5; i++)
{
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//父进程
sleep(10);
pid_t rid = wait(NULL);
if (rid > 0)
{
printf("等待成功,等待的子进程pid : %d\n", rid);
}
return 0;
}
运行结果:

等待成功,僵尸进程消失!!
至于status,我们在waitpid的时候讲,上面的代码实现了一个婴儿版的wait,如果需要知道更多信息,我们会需要使用waitpid了
2.waitpid⽅法

根据表得出,waitpid基本使用是pid_t waitpid(pid_t pid, int *status, int options);
这里面的options我们暂且不谈,到博客最后才谈论
这里面pid是什么呢?
我们通过man可以查看

我们不关心的:
pid < -1就是取绝对值,我们不用关心这个
pid = 0就是等待与当前进程同属一个进程组的所有子进程(即进程组 ID 相同的子进程)
我们关心的:
Pid = -1,等待任⼀个⼦进程,与wait等效
Pid > 0,等待其进程ID与pid相等的⼦进程
基于上面的描述(只需要传入id + 1),我们可以来看看如果等待失败,会发生什么
我们先将code.c代码改为如下
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int main()
{
pid_t id = fork();
//子进程
if (id == 0)
{
for (int i = 0; i < 5; i++)
{
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//父进程
sleep(10);
pid_t rid = waitpid(id + 1, NULL, 0);
if (rid > 0)
{
printf("等待成功,等待的子进程pid : %d\n", rid);
}
else
{
printf("等待失败,%d - > %s\n", errno, strerror(errno));
}
return 0;
}
运行结果

发现打印出了错误码和对应的错误信息
这里面status是什么呢?

其实就是我们学习进程退出时的退出码,我们通过指针方式得到退出码
我们可以实验一下,下面的代码子进程结束以后会返回1,即退出码为1
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int main()
{
pid_t id = fork();
//子进程
if (id == 0)
{
for (int i = 0; i < 3; i++)
{
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
exit(1);
}
//父进程
sleep(6);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("等待成功,等待的子进程pid : %d, status : %d\n", rid, status);
}
else
{
printf("等待失败,%d - > %s\n", errno, strerror(errno));
}
return 0;
}
运行结果

很奇怪,为什么是256呢,不应该是1吗???
下面为你解答
• wait和waitpid,都有⼀个status参数,该参数是⼀个输出型参数,由操作系统填充。
• 如果传递NULL,表⽰不关⼼⼦进程的退出状态信息。
• 否则,操作系统会根据该参数,将⼦进程的退出信息反馈给⽗进程。
• status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16
⽐特位)

其中前8个存放我们的的退出码,后8个存放我们的退出信息(是否异常退出(也就是kill 的对应数字)),所以我们刚刚exit(1),正常退出,即0000 0000 0000 0000 0000 0001 0000 0000
所以是256
所以要想获取真正的退出码,我们只需要位运算一下:(status >> 8) & 0xFF
同理获取退出状态:status & 0x7F
下面是kill的全部信息

我们为了获得这些东西,我们修改一下code.c并且运行
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int main()
{
pid_t id = fork();
//子进程
if (id == 0)
{
for (int i = 0; i < 3; i++)
{
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
exit(1);
}
//父进程
sleep(6);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("等待成功,等待的子进程pid : %d, exit code : %d, exit signal : %d\n", rid, (status >> 8) & 0xFF, status & 0x7F);
}
else
{
printf("等待失败,%d - > %s\n", errno, strerror(errno));
}
return 0;
}

得到退出码为1,退出状态为0,即正常退出
如果我们想得到非正常退出的怎么办呢,我们只需要改变一下code.c就行
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int main()
{
pid_t id = fork();
//子进程
if (id == 0)
{
while (1)
{
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
exit(1);
}
//父进程
sleep(6);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
printf("等待成功,等待的子进程pid : %d, exit code : %d, exit signal : %d\n", rid, (status >> 8) & 0xFF, status & 0x7F);
}
else
{
printf("等待失败,%d - > %s\n", errno, strerror(errno));
}
return 0;
}
运行发现exit signal为9,对应kill -9 pid命令,此时退出码为0,无意义(他都异常了,不需要退出码了)

3.status怎么获取,存放哪里
怎么获取
我们上面一直在谈论status的用法,但是我们需要思考一下他从哪里来的,我们不可以直接从系统拿到status,所以只能通过系统调用(也就是传入一个status指针)来获得
存放哪里
僵尸进程会保留pcb,等待父进程回收,所以我们可以大胆推测,是不是pcb里面有对应的东西存放exid_code与exid_signal??对的!!
所以我们的exid_code与exid_signal就存放在pcb里面,等待父进程拿到子进程的exid_code与exid_signal
对应下图
linux的pcb源代码也是这样的

!!确实是这样的,所以为什么要有僵尸状态,我们也就理解了
4.关于status的一些扩展操作
操作系统不希望我们直接使用位操作来通过status来获取exid_code与exid_signal,所以提供了宏定义,我们主要了解下面两个即可
WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程
的退出码)
WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程
是否是正常退出
下面我们使用这两个宏做一下实验,code.c代码如下
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int main()
{
pid_t id = fork();
//子进程
if (id == 0)
{
while (1)
{
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
exit(1);
}
//父进程
sleep(6);
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if (rid > 0)
{
//printf("等待成功,等待的子进程pid : %d, exit code : %d, exit signal : %d\n", rid, (status >> 8) & 0xFF, status & 0x7F);
if (WIFEXITED(status))
printf("等待成功,等待的子进程pid : %d, exit code : %d\n", rid, WEXITSTATUS(status));
else
printf("子进程异常退出\n"); //拿不到exit signal,需要拿的时候使用最开的方法
}
else
{
printf("等待失败,%d - > %s\n", errno, strerror(errno));
}
return 0;
}

5.options的使用
先了解阻塞与非阻塞:
对于阻塞我们已经学过了,如果我们让父进程一直等待子进程退出,那么就相当于让父进程一直阻塞,父进程干不了别的事情,只能等待子进程退出
于是我们有了非阻塞(非阻塞轮询),就可以让父进程每隔一段时间来看一下子进程是否结束,就可以节约等待的时间来干别的事情(比如运行别的进程)
简单来说:阻塞是 "专注等待,啥也不干",非阻塞是 "等待的同时,能抽空干别的"

之前我们使用阻塞调用,只有rid < 0 和 > 0的情况,至于=0,是非阻塞专有的,指的是父进程调用结束,但是子进程没有结束,下面我们用实验来验证
下面是code.c代码
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <errno.h>
#include <string.h>
int main()
{
pid_t id = fork();
//子进程
if (id == 0)
{
int cur = 10;
while (cur--)
{
printf("我是一个子进程,pid : %d, ppid : %d\n", getpid(), getppid());
sleep(1);
}
exit(1);
}
//父进程
//sleep(6);
int status = 0;
while (1)
{
pid_t rid = waitpid(id, &status, WNOHANG);
if (rid == 0)
{
printf("子进程未结束!\n");
sleep(2);
}
else if (rid > 0)
{
printf("子进程结束!\n");
break;
}
else
{
printf("等待失败");
break;
}
}
//pid_t rid = waitpid(id, &status, 0);
//if (rid > 0)
//{
// //printf("等待成功,等待的子进程pid : %d, exit code : %d, exit signal : %d\n", rid, (status >> 8) & 0xFF, status & 0x7F);
// if (WIFEXITED(status))
// printf("等待成功,等待的子进程pid : %d, exit code : %d\n", rid, WEXITSTATUS(status));
// else
// printf("子进程异常退出\n"); //拿不到exit signal,需要拿的时候使用最开的方法
//}
//else
//{
// printf("等待失败,%d - > %s\n", errno, strerror(errno));
//}
return 0;
}
每两秒轮询一次
