目录
[3. 等待多个进程](#3. 等待多个进程)
前言
前面我们花了大量的时间去学习进程的退出,退出并不难,但更深入的学习能为本章进程等待打好基础,因此没看过的小伙伴可以先学习进程退出。
一、进程等待
之前讲过,子进程退出,父进程一直在运行,不对子进程进行回收,就可能造成'僵尸进程'的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,"杀人不眨眼"的kill -9 也无能为力,因为谁也没有办法 杀死一个已经死去的进程。
父进程派给子进程的任务完成的如何,我们需要知道。如子进程运行完成,结果对还是不对, 或者是否正常退出。
父进程通过进程等待的方式,可以获取子进程退出的信息。(虽然不是一定要获取,但是得有这个功能)
二、如何进行进程等待
1.wait
我们看看2号手册中的wait函数,他可以等待任意一个子进程的退出,参数是int类型的指针,等待成功返回子进程的pid,失败返回-1。
我们使用如下代码进行进程等待,这里wait的参数先给NULL,代表不关心子进程退出的状态(后续会再提到)。子进程运行5秒后变成僵尸状态,父进程先休眠10秒再去调用wait函数。
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
void Work()
{
int cnt = 5;
while(cnt)
{
printf("我是子进程, pid: %d, ppid: %d, cnt: %d\n",getpid(),getppid(),cnt--);
sleep(1);
}
}
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
Work();
exit(0);
}
else
{
sleep(10);
//father
pid_t rid = wait(NULL);
if(rid == id)
{
printf("等待成功,pid: %d\n",getpid());
}
}
return 0;
}
我们写了一个脚本来监控进程的运行情况,代码如下(注意myprocess是我设置的进程名)
bash
while :; do ps ajx | head -1 && ps ajx | grep myprocess | grep -v "grep"; sleep 1; echo "###################"; done
结果发现0-5秒中,父子进程正常运行,5-10秒中,子进程变成了僵尸状态,父进程此时在sleep,并没有回收子进程,10秒后,父进程sleep结束,wait函数等到了子进程,于是将子进程回收了,同时父进程也运行完毕。
由此我们可以得知:
进程等待能回收子进程僵尸状态。
还有一个结论,在父进程进行等待的时候,如果子进程还没有处理完,那么父进程必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收,再继续执行后续代码。这可以通过打印的方式查看。就类似于scanf需要等待用户输入一样,用户不输入就一直在这里阻塞着,直到输入后才继续往后执行。
一般而言,父子进程谁先运行我们不知道,但能知道一般都是父进程最后退出,多进程由父进程发起,也由父进程统一回收
2.waitpid
wait是等待任意一个子进程,而waitpid可以等待指定的那一个,第一个参数传等待子进程的pid代表等待这个进程,传-1代表等待任意进程。 第二个参数和wait的参数一样,第三个参数也先不管,设置为0代表默认阻塞等待。
将上面的代码从wait修改为waitpid,因为我们只fork了一次,只创建了一个子进程,因此如下修改即可。
2.1第二个参数
重点我们得讲解一下第二个参数 status ,他是输出型参数,我们可以随便定义一个int变量,将变量的值传递给第二个参数,waitpid会将子进程退出码和信号写到这个变量里。
这里我们将子进程的退出码设置为10,看看打印出来的status值为多少。
发现status为2560,这似乎不像退出码。他是通过下面这个图片的方式得来的,int整形32位,只用低16位,其中高8位代表退出码,低8位代表终止信号,
正常终止看高八位即可,因为未收到信号,因此低8位为0。
被信号所杀,看低八位,其中第7位不看,他代表core dump标志(暂时不考虑),只看0-6位。
那么2560的二进制为 0000 1010 0000 0000 如果右移8位,也就是只看高8位,即0000 1010,即为10,我们退出码也就是10。
公式为 :*status = (exit_code<<8)| exit_signal;
status不能直接使用,如下经过右移操作和与操作,就可以得到准确的退出码和信号了。
执行一下,没有问题
小总结一下
- 当一个进程异常了(收到信号) ,那么退出码就无意义了
- 通过信号码是否非零,来判断是否收到信号
- 手动杀死子进程,也能得到相应信号
虽然我们会通过位运算来得到退出码与信号,但这样也比较麻烦,linux系统提供了如下两个接口帮我们处理status。
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
如下,查看是否正常退出,退出码为多少。
结果也符合预期
2.2第三个参数
0:即阻塞等待
WNOHANG::若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。(非阻塞式等待)
讲个小故事:
张三约翠花去电竞酒店打麻将, 他已经到翠花楼下了,现在在等待翠花先来一起出发。
此时张三有两个方法,一个是打电话,询问翠花在干嘛,什么时候下楼,打完翠花说等一下,她化个妆,于是张三就在楼下开一把金铲铲之战,过了半个小时又打,翠花说在穿鞋了等一小下,于是张三挂断电话去刷抖音,过一会再打电话,翠花说已经下楼了,刚刚准备出门又上了个厕所,张三没有什么脾气,谁叫我想约人家呢,于是挂断电话,又去看看淘宝,要买点什么,最后再打电话,翠花此时终于到达了,于是两个人开开心心的去打麻将了。
另一个方法也是打电话,询问翠花在干嘛,什么时候下楼,翠花也说等一下,还在化妆,但是张三今天电话不挂,就一直等翠花,时刻知道翠花在干嘛,直到翠花下楼一起去打麻将。
在这个故事中**张三:**父进程
**翠花:**子进程
**打电话:**调用系统接口
第一个方法: 等待的条件不满足,wait/waitpid不阻塞,而是立即返回!可以做自己占据时间并不多的事情。这是非阻塞式调用,即非阻塞+轮询方案进行进程等待,该方案往往要进行重复调用。返回值>0等待成功,子进程已退出;返回值==0;等待成功,子进程未退出,返回值<0等待失败
**第二个方法:**翠花不结束,电话不挂机,即阻塞式调用。子进程不退出,wait/waitpid不返回。
代码如下,waitpid第三个参数为 WNOHANG 借此观看非阻塞轮询等待。
cpp
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
void Work(int number)
{
printf("我是子进程, pid: %d, ppid: %d, number: %d\n",getpid(),getppid(),number);
}
int main()
{
pid_t id = fork();
if(id == 0)
{
//child
int number = 5;
while(number)
{
Work(number);
number--;
sleep(1);
}
exit(10);
}
//father
int status = 0;
while(1)
{
pid_t rid = waitpid(id,&status,WNOHANG);
if(rid>0)
{
//等待成功,子进程退出了
printf("等待子进程成功,子进程退出码: %d,退出信号: %d\n",WEXITSTATUS(status),status&0x7F);
break;
}
else if(rid == 0)
{
//等待成功,但子进程没有退出
printf("等待成功,子进程还没推出,父进程做其他事情去了\n");
sleep(2);
}
else
{
printf("等待失败\n");
break;
}
}
return 0;
}
运行结果如下,父进程间歇性询问子进程是否完成,没完成就做自己的事情,待会再来询问。
3. 等待多个进程
我们使用for循坏来fork多个进程,同时给每个进程编号,创建顺序从0-9。waitpid第一个参数为-1,代表等待任意的子进程。
虽然我们也可以用数组的方式,将子进程的pid放到数组里,但是这样就只能一个一个进程的等待,比如最先会等待退出码为0进程,如果该进程不结束,父进程会一直等待,那么后续的进程永远不会被回收,就会造成内存泄漏的问题。
cpp
1: myprocess.c ? ? ?? buffers
#include<sys/types.h>
#include<sys/wait.h>
void Work(int number)
{
int cnt = 2;
while(cnt)
{
printf("我是子进程, pid: %d, ppid: %d, cnt: %d, number: %d\n",getpid(),getppid(),cnt--,number);
sleep(1);
}
}
const int n = 10;
int main()
{
int i = 0;
for(;i<n;i++)
{
pid_t id = fork();
if(id == 0)
{
//child
Work(i);
exit(i);
}
}
//fork的子进程已近全部退出了,下面是父进程执行的代码
for(i=0;i<n;i++)
{
int status;
pid_t rid = waitpid(-1,&status,0); //-1:任意一个子进程退出
if(rid>0)
{
printf("等待子进程 %d 成功, 退出码: %d\n",rid, WEXITSTATUS(status));
}
}
return 0;
}
看看运行的情况,发现调度运行与终止都是没有规律的,谁先谁后我们不确定,我们只知道肯定是父进程先创建并最后退出。
三、为什么不用全局变量获取子进程的退出信息
刚刚我们提到, 可以用数组获取子进程的pid,然后传值进行等待,虽然效果不一定很好,但这也算是一个解决办法,为什么不用全局变量获取子进程的退出信息,而是采用写入的方式进行传参获取呢?
这是因为进程之间具有独立性,父进程无法直接获取子进程的退出信息,比如status我们设置为0,父子进程看到的status值就都为0,此时我们获取到了子进程的退出码,将子进程的退出码写入status变量,此时会发生写时拷贝,子进程看到的是我自己写的新值,而父进程看到的还是0。父子进程代码共享,但数据不一定相同。
而父进程通过fork,返回的id是子进程的pid,子进程返回的id为0,已经写时拷贝过了,因此可以获取。