关于进程等待我们要回忆一下当时我们说到的进程状态中,子进程在终止的时候会出现一种状态叫僵尸状态,一旦僵尸了,那么子进程就需要等到我们的父进程或者系统对子进程进行回收,那么什么是进程等待呢?
其实是通过wait/waitpid的方式,让父进程(一般)对子进程进行资源回收的等待过程。
a.解决子进程僵尸问题带来的内存泄漏问题 ---这个工作在目前来看是必须要做的。
b.父进程为什么要创建子进程呢? 要让子进程来完成任务。子进程将任务完成的如何要不要 知道?要知道----需要通过进程等待的方式,获取子进程退出信息----两个数字,一个叫信号编号,一个叫进程退出码。而这个理由不是必须的,但是系统需要提供这样的基础功能!
进程等待的必要性:
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法
杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,
或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
wait方法手册
我们通过wait先验证两个问题:
下面我们用以下代码来进行演示该wait方法是如何实现进程等待的。
这段代码一开始是父进程子进程一起跑,而子进程只会跑前五秒,所以前五秒的时候子进程一定是S状态或者R状态,5秒过后子进程退出,退出之后,父进程后面sleep5秒之后子进程会变成僵尸状态,我们对应的父进程10s后会打印出上述代码那句话,打印出来之后,那么子进程的僵尸状态就消失了。
运行结果如下:
以上测试验证了如下结论:a.进程等待能回收子进程僵尸状态:Z->x。
关于wait需要验证的第二个问题:
在子进程运行期间,父进程有没有调用wait呢?在干什么呢?
下面我们用以下代码来进程验证:
运行该段代码:
监视窗口:
一开始我们的子进程和父进程都是一起运行的,而当子进程在运行这5秒期间,父进程是通过调用了wait方法进程阻塞等待的,因为这5秒期间过后才打印出来wait after,说明这段时间父进程是在等子进程变成僵尸状态,wait自动回收,最后父进程sleep10秒钟。
这也就说明了第二个问题的答案:
b.如果子进程根本没有退出,父进程必须在wait上进行阻塞等待,直到子进程僵尸,wait自动回收,返回。
一般而言,父子进程谁先运行我们是不知道的,但是一般都是父进程最后退出。因为一般来说子进程都是由父进程创建,然后最后由父进程统一回收的。
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
用了上述wait方法呢,我们大致了解了一下进程等待是如何等待的,那么下面呢我们通过man手册来看看waitpid方法的用法:
waitpid需要传3个参数,与wait不同的是,该方法可以等待指定的某个子进程。下面我们将代码改成如下图中代码:
运行结果:
监视窗口:
刚开始时子进程运行5秒,父进程休眠10秒,然后子进程退出,变成僵尸状态,父进程再休眠5秒后调用waitpid方法将子进程自动回收,子进程僵尸状态瞬间消失了,最后打印wait success,说明等待成功了。
下面我们再对waitpid的第二个参数int *? status进行进一步理解:
下面我们以如下这段代码来进行测试
运行之后:
我们发现status被改成了2560,那么为什么会是2560呢?这是怎么来的呢?
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
?
status是一个int的整数,32bit位,但是我们只考虑低16位,而这16位里面又分为两种情况,一种是正常终止的情况,那么高8位就是进程的退出状态,低8位是终止信号,如果正常终止那么默认是0.
如果是被信号所杀,那么退出状态就不会使用到了,因为这属于异常终止,这时只有低8位有效,其中有一位时core dump标志,我们暂且不谈,还有七位使用了表示终止信号的。
我们上面设置的退出状态是exit(10),也就是10,而10用二进制来表示是1010,放在高8位上则是
0000 1010 0000 0000,而该二进程算出来就是我们上面打印出来的status的值:2560.
下面我们对status这16位通过如下的代码来进行测验
通过位运算取到status各个位上的信息进行打印。
运行结果:
exit sig: 0 代表的信号是0,而信号中并没有0信号,说明运行是成功的:
但是退出码是1,代表结果不正确,运行正常和结果正确必须exit sig和exit code都为0.如果我们的exit sig不为0,也就是代表代码运行异常,此时exit code退出码则没有意义了。
下面我们给代码创建几个异常进行测验:
除0错误:
运行结果:
我们发现收到的信号是8号信号,对应的信号是SIGFPE,除0错误。
空指针异常:
运行结果:
我们发现变成了11号信号,对应的SIGSEGV.也就是段错误。
也就是父进程已经收到了子进程异常退出的信号。
下面我们针对刚刚的现象来提出几个问题:
1.当一个进程异常了(收到信号),exit code退出码还有意义吗? 没有意义。
2.有没有收到信号怎么判定? exit sig: 0
信号我们目前还不是很懂,但是我们可以通过kill -l 命令知道它是有一个数字对应一个信号的名称,这个名称是一个大写的宏,当我们查看信号列表的时候我们可以发现信号列表里是没有0号信号的,换句话说有没有收到信号就是我们的exit sig是不是0.如果是0那就是没有收到信号,如果不是0那就说明收到了信号。
3.我通过手动杀掉进程会有什么现象呢?
下面我们把子进程调用的Worker函数改成死循环通过手动杀掉这个死循环的子进程让其停下来
运行结果:
我们发现通过发送9号信号手动杀掉子进程6857让其进程终止。
那么父进程又是如何得知子进程的退出信息呢? wait? ?waitpid?
可是我们不想从系统调用的方面去谈了,而是从操作系统的层面上来谈:
上述我们只是说了父进程等待一个子进程的案例,下面我们展示下父进程等待多个子进程的案例:
下面我们创建一个多进程的代码:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
//int status = 0;
void Worker(int number)
{
int *p = NULL;
int cnt = 10;
while(cnt)
{
printf("I am child process, pid: %d, ppid: %d, cnt: %d, number: %d\n", getpid(), getppid(), cnt--, number);
sleep(1);
//*p = 100;
//int a = 10;
//a /= 0;
}
}
const int n = 10;
int main()
{
for(int i = 0;i < n; i++)
{
pid_t id = fork();
if(id == 0)
{
Worker(i);
//status = i;
exit(0);
}
}
//等待多个子进程?
for(int i = 0; i < n; i++)
{
int status = 0;
pid_t rid = waitpid(-1, &status, 0); // pid>0, -1:任意一个退出的子进程
if(rid > 0){
printf("wait child %d success, exit code: %d\n", rid, WEXITSTATUS(status));
}
}
return 0;
}
运行之后:
监视窗口:
最后代码的退出结果是:
我们为什么不用全局变量获取子进程的退出信息?而用系统调用?
原因:进程具有独立性,父进程无法直接获得子进程的退出信息。
下面我们来谈waitpid的最后一个参数:int options
当options为0时:阻塞等待
WNOHANG:等待的时候,以非阻塞的方式等待。
下面我们用一个故事来进行理解一下
故事时刻:
故事一:假设你的名字叫小明,在学校里呢是一个放荡不羁的公子哥,反正就是在学校就是上课经常不去,考试能不能过呢完全是看你努不努力,或者看运气,你呢有你自己的努力方式。小明你呢有一个同学叫做小张,小张是你们班的努力型学霸,经常稳定在班上排前十名,然后上课笔记也做得特别详细。然后呢,突然有一天要考c语言了,你呢一开始并不知道,突然在宿舍听到你室友说过两天要考c语言,你心想:"这下坏了,啥也不知道咋过呢"。然后你突然想到你班上有个跟你玩的还可以的努力型学霸小张,知道他做笔记做的好,肯定做了老师划重点的笔记,然后就打算找他带你好好复习一下,然后你走到他的宿舍楼下,就跟小张打电话:"小张啊,在不在?过两天是不是要考c语言了啊?" 小张说:"是啊" 然后你说:"那你等会有没有时间啊?" 小张就说:"有时间啊,不过得再等我30来分钟把这个知识点复习完才有空"? 那么此时,你听到说有空,那你就又说:"行,那你一会把你的c语言笔记带上,咱们一会去食堂吃个饭,我请你,我想借你的c语言复习笔记拿来复习一下。" 然后小张听说有人请吃饭,给他复习,顺便也可以给我再复习一遍,然后就说:"行吧,那你等一等我"。然后你呢就把电话挂了,准备在下面刷了会短视频等着。然后你等了一会呢,就有点着急了,这怎么还没好啊,然后呢你又给小张来了个电话:"小张,你好了没?"小张说:"我不跟你说了吗,我还得 一会呢,你在等我把这个知识点弄清楚了。" 然后呢你说:"好吧。"然后你把电话一挂,挂了之后呢,你又闲着没事干,然后又玩了玩手机,给家里面打了个电话,聊了会天,打了会游戏,你发现小张还没下来,又继续打电话:"小张,你还没好呢嘛?" 小张说:"快了,快了。"? ?然后你挂电话继续玩你的手机,又过了好一会,你发现还没下来,又打了个电话:"小张,你好了没?" 小张说:"好了,在下楼了。" 然后过了好一会儿,终于小张看到你,你两愉快的去食堂吃个饭顺便复习去了,然后过了两天你以60分的c语言成绩冒险过了。
故事二:
又过了几天突然又说要考数据结构了,发现比c语言更难了,然后你又再次想到了小张,照样走到小张宿舍楼下跟他打电话说:"过两天又要考试了,跟上次一样,我请你吃饭你给我划重点复习呗!"小张听到就回复说:"好"。 然后你又说:"这次电话别挂了,你就跟我保持着吧,搞完了早点下来啊,到时候你弄好了直接跟我说一声再挂电话"? 小张说:"行吧" 然后你等了十来二十分钟也不知道小张在干嘛,最后下来了。然后看到你之后在电话里跟你说了句话,然后挂了。
进程的阻塞式等待方式:
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
} else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(257);
} else{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("this is test for wait\n");
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
运行结果:
[root@localhost linux]# ./a.out
child is run, pid is : 45110
this is test for wait
wait child 5s success, child return code is :1.
进程的非阻塞式等待:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if(pid < 0){
printf("%s fork error\n",__FUNCTION__);
return 1;
}else if( pid == 0 ){ //child
printf("child is run, pid is : %d\n",getpid());
sleep(5);
exit(1);
} else{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
if( ret == 0 ){
printf("child is running\n");
}
sleep(1);
}while(ret == 0);
if( WIFEXITED(status) && ret == pid ){
printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
}else{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}