目录
- 前言
- [1. 进程等待的必要性](#1. 进程等待的必要性)
-
- [1.1 进程等待的定义](#1.1 进程等待的定义)
- [2. 如何进行进程等待](#2. 如何进行进程等待)
-
- [2.1 wait 单进程](#2.1 wait 单进程)
- [2.2 wait 多进程](#2.2 wait 多进程)
- [2.3 status && 退出情况](#2.3 status && 退出情况)
-
- [2.3.1 status 参数构成](#2.3.1 status 参数构成)
- [2.3.2 简证 status 参数构成](#2.3.2 简证 status 参数构成)
- [2.3.3 进程等待失败](#2.3.3 进程等待失败)
- [2.3.4 宏调用查看退出信息](#2.3.4 宏调用查看退出信息)
- [3. 进程等待的原理](#3. 进程等待的原理)
前言
本篇文章继上一篇文章 进程的创建、终止 ,继续介绍关于进程控制中的进程等待,从理解进程等待的必要性,进而理解什么是进程等待,以及如何进行进程等待。
1. 进程等待的必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题(也就是 Z 状态进程),进而造成内存泄漏。 所以进行进程等待的其中一个原因就是为了读取子进程状态,解决内存泄漏问题。
- 另外,进程一旦变成僵尸状态,不管是 ctrl + c,还是 kill -9 命令都无法杀死这个进程,只能通过进程等待,将这个进程进行回收。所以进程等待的第二个原因是僵尸进程不可被 "杀死"。
- 子进程的出现就需要回归到创建子进程的本质:为了帮助用户完成某些任务。所以既然是完成任务,用户怎么知道子进程完成的如何了,当子进程退出了,用户又该如何得知任务办完没有?结果是什么?结果正不正确?或者中间异常中止了?所以进程等待的第三个原因是为了获取子进程任务执行的结果,也即退出情况。
僵尸进程造成的内存泄露问题是必须解决的!而至于要不要关心子进程的退出情况,则是可选项,不一定每个子进程的退出可能都要关心。
1.1 进程等待的定义
通过系统调用 wait/waitpid,来对子进程进行状态检测与回收的功能。
2. 如何进行进程等待
如何进程等待呢? ----- 调用系统调用 wait/waitpid(即等待一个进程,直到进程状态发生改变)
2.1 wait 单进程
wait 就代表只有父进程有子进程,并且子进程退出了,父进程就可以通过 wait 等待子进程的退出,其中的 status 参数代表子进程的退出情况,如果不关心其退出情况,设置为 NULL 即可。返回值 > 0,代表的是等待的子进程的 pid,如果返回值 < 0,等待失败。
接下来,我们先简单看看进程等待是什么样子的。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.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(11);
}
else
{
int cnt = 10;
while(cnt)
{
printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cn$
cnt--;
sleep(1);
}
pid_t ret = wait(NULL);
if(ret == id)
{
printf("wait success! ret: %d\n", ret);
}
sleep(5);
}
return 0;
}
当子进程退出后,父进程通过系统调用 wait 进行进程等待,回收了子进程,因此监控中的子进程也由原来的 Z 状态变为 X 状态(看不见了),再经过 3s 睡眠后,父进程也退出了(由 bash 进行等待回收)。
如果是多进程的进程等待呢?? 又该如何进程等待?
2.2 wait 多进程
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(i);
}
printf("create child process: %d success\n", id); // 只有父进程才会执行
}
// 等待
for(int i = 0; i < N; i++)
{
pid_t id = wait(NULL);
if(id > 0)
{
printf("wait %d success\n", id);
}
}
sleep(3);
}
借助循环结构,我们顺利的创建出多进程,并且对多个子进程进行等待回收。也即当任意一个进程退出时,wait 会回收子进程。
那如果任意一个子进程都不退出呢?----- 如果父进程在等待的子进程(一个或多个)不退出时,那么父进程也不退出,父进程会在 wait 处进行阻塞等待!换言之,wait 等待时,如果子进程不退出,父进程调用 wait 不返回,处于一直等待的状态,直到子进程退出时,父进程 wait 返回。
所以阻塞状态不一定就是等待硬件资源,这里的父进程阻塞在系统调用 wait 处,也即阻塞状态,只不过等待的不是硬件资源,而是子进程(即软件资源)。
2.3 status && 退出情况
pid_t waitpid(pid_t pid, int *status, int options); // 其中的 status 与 wait 一样可以置为 NULL(不关心)
返回值:
当正常返回的时候 waitpid 返回收集到的子进程的进程 ID;
如果设置了选项 WNOHANG,而调用中 waitpid 发现没有已退出的子进程可收集,则返回 0;
如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在;
参数:
pid:
Pid = -1 : 等待任一个子进程。与 wait 功能等效。
Pid > 0 : 等待指定进程
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
2.3.1 status 参数构成
-
status 是一个输出型参数,用于将子进程的退出结果带出给父进程
-
其 int 是被当作几个部分使用的(4字节)
// 修改部分代码:
else if(id == 0) { ...... exit(1); } else { ...... int status = 0; pid_t ret = waitpid(id, &status, 0); if(ret == id) { printf("wait success! ret: %d, status: %d\n", ret, status); } ...... }
运行结果的 status 为 256,我们之前说过 status 即子进程的退出结果,但是子进程中明明是 eixt(1),退出码是 1 啊,怎么 waitpid 返回的退出结果是 256 呢??
这就需要我们弄清楚几个问题。
- 子进程婆出,一共会有几种退出场景呢? ------ 代码运行完毕,结果正确或者不正确;代码异常终止
- 父进程等待,期望获得子进程退出的哪些信息呢? ----- 子进程代码是否异常?没有异常,结果是否正确? 即退出码 exitcode,如果结果不正确,又是因为什么呢?(不同的退出码即可表面不同的错误信息)换言之,父进程所关心的问题,就是子进程的退出情况。
正是因为父进程所关心的内容不只一点,因此 wait / waitpid 中的 status 才要被划分成多个部分,以此兼顾到父进程关心的全部信息。父进程希望通过 waitpid 等待子进程,获得子进程的退出结果(代码是否异常中止,如果不是,结果是否正确)。
我们只考虑 status 的低 16 位,其中的低7位用来表示进程的异常情况,第 8 位是 core dump 标志位(信号章节介绍),接下来的次低 8 位 用于表示进程的退出状态。
因此虽然子进程中 exit(1),但最终整体的 status 打印出来为 256,就是因为,代码没有异常中止,status 的低7位为0,而退出码为1,因此次低8位为 000000001,结合起来就是 256。
换言之,想要检查一个进程执行时是否发生异常,只需要检查 status 的低7位即可,如果为0,说明没有异常中止,如果异常中止了,不同的位结合起来也可以涵盖所有的异常情况(异常中止的本质就是收到了某种信号,这也是为什么 kill -l
查看信号编号时,没有所谓的 0 编号的信号请求,因为 0 代表没有异常中止);低7位为0之后,再检查次低8位的退出状态即可确定子进程的退出结果是否正确!
- 拓展问题:status 能不能直接定义成全局变量,而不使用系统调用 waitpid 获取?
不行,因为要保证进程独立性,status是用于存储子进程的执行结果的,无论子进程如何修改,进程独立性需要保证父进程的视角是无感的, 而如果是全局变量,那么无法做到这一点。换言之,只要是一个进程想要获取另一个进程的信息,因为进程独立性,所以这件事,进程自己无法做到,需要通过操作系统(即系统调用)来完成获取。
2.3.2 简证 status 参数构成
if(ret == id)
{
//status&0x7F: status的低7位,即终止信号
//(status>>8)&0xFF: status的次低8位,即退出状态
printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
}
因为子进程退出时 exit(1),代码执行完毕,因此退出信号为 0 表无异常中止,退出码为 1.
else if(id == 0)
{
......
int a = 10;
a /= 10;
......
}
除 0 错误,父进程 waitpid 等待子进程,返回的 status 中的终止信号 8,即 kill -l
信号中的 8) SIGFPE,因为代码中途异常终止了,所以就没有退出码,因此退出状态就为 0。再者,只要你愿意,当你访问野指针,运行结果的 exit sig 就会是 11,当你 kill -9 杀掉一个死循环的进程时,exit sig 就会是 9 号信息,这不仅印证了 status 参数的构成,也再一次印证了,进程异常终止,其本质就是收到了某种信号!
2.3.3 进程等待失败
关于 status 的返回值:如果调用中出错,则返回 -1,这时 errno 会被设置成相应的值以指示错误所在;
pid_t ret = waitpid(id + 4, &status, 0);
if(ret == id) { // 不变 }
else
{
printf("wait failed!\n");
}
如果父进程等待的并不是自己的子进程,那么就一定会等待失败。换言之,父进程在进行等待时,只能等待自己的子进程。
2.3.4 宏调用查看退出信息
status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
pid_t ret = waitpid(id, &status, 0);
if(ret == id)
{
if(WIFEXITED(status))
{
printf("process is normal, exit_code: %d\n", WEXITSTATUS(status));
}
else
{
printf("the process terminated abnormally! \n");
}
}
else
{
printf("wait failed!\n");
}
有了系统提供的宏,就不再需要我们自己通过位运算来获取进程的退出情况了。
3. 进程等待的原理
子进程执行完毕时,为了保证其退出结果被上层获取,它的代码和数据是允许被释放的,只不过需要将退出信息保存在子进程的 PCB 中而已。当进程收到信号时,会写入到 pcb 中的 exit_code,进程的退出码写入到 exit_signal 中,父进程再通过系统调用 wait / waitpid 检测子进程是否退出了,如果退出了,再读取子进程的退出信息,将退出信息合并成 status 传递给上层用户。
为什么不让上层用户直接访问子进程的退出信息呢?? ----- 与之前讲述的系统管理一样,因为操作系统不信任用户,子进程的退出信息就存储在子进程的 PCB 中,而用户是无法直接越过操作系统 访问 操作系统所管理的内核数据结构对象的,操作系统不允许任何用户访问它的底层数据。
关于进程等待,本篇文章就介绍到这里,后续还会介绍非阻塞轮询,并且非阻塞轮询的同时,是如何执行其它任务的。
如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!
感谢各位观看!