目录
引用:进程等待讲解-优快云博客
什么是进程等待!
进程等待:
进程等待就是通过系统调用wait/waitpid的方式,让父进程对子进程进行资源回收的等待过程!
为什么要进程等待
两个原因:
1.防止僵尸进程的产生!(必要的!)因为当子进程退出之后,如果父进程不及时回收子进程的资源时,会导致子进程一直处于僵死状态,就连kill-9也拿他没有任何办法,所以引进了进程等待来解决对操作系统资源的浪费!这一点原因是进程等待存在的必要!
2.既然通过了父进程创建了子进程,那么我们需要知道子进程将任务完成的如何了,此时就引进了进程等待!这也是进程等待存在的一种原因,但是这种原因不是必须的!
既然上面讲到了为什么要有进程等待,那么就不得不介绍进程等待是如何做到的,能满足上述的两种情况。
进程等待的流程
想要了解进程等待是如何做到的,必须要了解有关进程等待的两个系统调用!
首先验证进程等待可以将僵尸进程进行回收!可以写一段代码,让子进程跑起来,然后退出,此刻让父进程先睡会儿,然后通过监视进程窗口观察子进程状态的变化即可验证!
while : ; do ps ajx |head -1&&ps ajx |grep mytest|grep -v grep; echo "-----------------------------"; sleep 1;done
代码如下:
我们做了一个什么工作呢?
结论:子进程在跑,父进程就要进行阻塞等待。等子进程跑完,父进程才wait sucess,总结如下:
wait()
首先来介绍wait函数,此函数只有一个参数,是一个返回型参数!最后返回进程的状态!其返回值类型是pid_t 类型,如若进程等待成功,那么将会返回子进程的pid!!,如果在fork之前调用wait函数,那么就会出现结果为-1的情况!!
作用:进程一旦调用了wait,就会立刻阻塞自己,由wait分析当前进程中的某个子进程是否已经退出了,如果让它找到这样一个已经变成僵尸进程的子进程,wait会收集这个子进程的信息,并将它彻底销毁后返回;如果没有找到这样一个子进程,wait会一直阻塞直到有一个出现。参数stat_loc用来保存被收集进程退出时的一些状态,它是一个指向int型的指针。但如果对这个子进程是如何死掉的不在乎,咱们可以将它设置为NULL:pid = wait(NULL);如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用会失败,wait返回-1,同时errno会被设置为ECHILD。
waitpid()
用法:
第二个参数:
status是输出型参数,所谓输出型参数需要用户自己定义变量,然后传参,操作系统接收这个参数经过操作之后再返回给用户级变量。
第三个参数:
第三个参数option代表的是进程的等待方式!其中默认0是阻塞等待方式!
还有一种方式是WNOHANG,其代表的状态是非阻塞式等待!其中其返回值有三种类型!
当正常返回的时候waitpid返回收集到的子进程的进程ID;而调用中waitpid发现没有已退出的子进程可收集,则返回0;如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;其中要想看见具体的出错信息可以利用strerror函数来进行打印!
如下,定义一个int型变量,变量名为status,取地址传参,然后等如果父进程等待子进程之后就把status的值打印出来看看:
结果如下:
status是一个整形,有32位,分为高八位和低八位:
如果进程正常退出,就返回低八位的0,高八位的退出状态。
如果异常退出,就返回低七位的终止信号,为什么不是低八位呢? 低八位的第一位是core dump标志,所以只返回低七位。
如下,我们把子进程的退出码改为exit(10),再让父进程去等待:运行结果:
status为2560。
因为退出码10的二进制为1010,又因为异常所以在高八位,如下:
转化为十进制就是2560;
exit sign为stautus的信号退出码,exit code为status的进程退出码:
为什么exit sign是 status&0x7F?
因为信号退出码在低七位 7是3个1,F是4个1,合起来就是7个1,&7个1就把低七位保留,其他位全变0
同理,exit code为高八位
运行结果:
退出信号为0,退出码为10。
如果我们把子进程的退出码改为正常退出码,即0,再跑,就会退出信号为0,退出码为0:
我们给子进程写一个除0错误:
退出信号会显示8:
我们让子进程出现空指针错误:
退出信号为11:
11就是段错误:
第二个实验 手动退出
我们让子进程不要退出了,一直运行:
此时运行之后子进程就会一直跑,然后我们输入kill -9 pid终止进程,进程退出信号会显示9:
小结
父进程得到子进程的退出结果实际上是调用stautus这个整型变量,可以用一个指针解引用即可得到status。
至此相关进程等待的知识,介绍的大致差不多了,也有一些同学可能会问到,为什么要通过系统调用来判断进程的状态呢?而不是直接使用一个全局变量status直接来观察呢?
这里我们就来解释一下为什么不能通过一个全局变量来判断进程状态,因为进程是独立的,所以对于每一个变量,都有一个自己的stauts,他们不能共享相同的status,因为进程要想修改一个共享的数据时,会引起写时拷贝的发生,所以他们各自都有一份自己的statues,所以不能仅仅通过一个简单的变量来观察进程的状态!
WIFEXITED WEXITSTAUS
一种更简单的方式来获得进程退出码的信息呢?而不是每次都通过这些按位操作获取:
用这两个宏就我们就可以不用关注status返回值,宏会自己获取子进程的返回值。
调用WIFEXITED获取是否正常退出,如果为假,直接输出else里面的异常。
如果为真,就调用WEXITSTAUS获取子进程的退出码并打印出来。
我们可以先搞一个异常出来,把子进程死循环:
运行:
我们再把子进程改为正常的再运行:
非阻塞轮巡/非阻塞等待
与阻塞式等待相对。
阻塞式等待父进程什么事情也不干,就在那等着子进程返回值。
阻塞式等待可以干自己的事情,等子进程返回的时候接收一下就可以了。
非阻塞等待案例
fork()
函数创建一个子进程,子进程在循环中调用work()
函数并逐秒递减计数器。当计数器达到0时,子进程通过exit(0)
结束。父进程则使用waitpid()
函数等待子进程的结束,并打印相关信息。子进程会运行10次work()
函数然后退出。
等待失败的情况
pid写错就会等待失败:
这里故意写错。
那么父亲节就检测到子进程不是它的,父进程就退了,子进程还在继续:
非阻塞等待实验
因为是非阻塞式等待,所以父进程在等待期间可以做其他事情。
我们让父进程进程三个任务,下载任务,打印日志任务,展示信息任务,任务时长为5秒:
然后我们定义一个函数指针数组,然后把它tyepdef为一个函数指针类型:
也就是说 task_t就是我们的任务类型。
然后我们开始执行任务,tasks[TASK_NUM]也就是有五个任务。然后对五个任务进行初始化,最后添加任务。
调用:
定义:
对五个任务进行初始化,把每个任务初始化为NULL;
对任务数组添加任务,如果任务数组为NULL,那就把 t 这个任务添加到任务数组中;
用return 1表示添加任务成功,return 0表示任务数组内全为有效内容,不再添加,退出。
void IninTasks(task_t tasks[],int num)
{
for(int i=0;i<num;i++) tasks[i]=NULL;
}
void AddTask(tasks_t tasks[],task_t t)
{
int i=0;
for(;i<TASK_NUM;i++)
{
if(tasks[i]==NULL)
{
//添加
tasks[i]=t;
return 1;
}
}
return 0;
}
接下来我们就开始添加执行任务了,把download,PrinfLog,show三个任务添加:
AddTask(tasks,download);
AddTask(tasks,PrintLog);
AddTask(tasks,show);
执行任务:
void executeTask(task_t task[],int num)
{
//安全检查
for(int i=0;i<num;i++)
{
if(task[i]) task[i](); //如果某个任务不为空,那么就执行该任务
}
}
然后把executeTask()添加到父进程等待那里,让父进程可以在等待期间移除,添加,执行任务:
else if(rid==0)
{
//走到这里,父进程等子进程成功,但是子进程还未退出。
printf("father to do other things\n");
executeTask(tasks,TASK_NUM);//父进程在等待期间也可以移除,添加等任务
}
我们可以看见子进程在执行work(),父进程在等待期间做其他事情,下载,打印日志,展示信息:
#include <stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
#include<unistd.h>
#define TASK_NUM 5
typedef void (*task_t)();
//
void download()
{
printf("this is a dowm;load task is running\n");
}
void PrintLog()
{
printf("this is a wirte log task\n");
}
void show()
{
printf("this is ashow info task is running\n");
}
void InitTasks(task_t tasks[],int num)
{
for(int i=0;i<num;i++) tasks[i]=NULL;
}
int AddTask(task_t tasks[],task_t t)
{
int i=0;
for(;i<TASK_NUM;i++)
{
if(tasks[i]==NULL)
{
//添加
tasks[i]=t;
return 1;
}
}
return 0;
}
void executeTask(task_t task[],int num)
{
//安全检查
for(int i=0;i<num;i++)
{
if(task[i]) task[i](); //如果某个任务不为空,那么就执行该任务
}
}
void work(int cnt)
{
printf("i am child process ,pid:%d,cnt:%d\n",getpid(),cnt);
}
int main()
{
task_t tasks[TASK_NUM];
InitTasks(tasks,TASK_NUM);
AddTask(tasks,download);
AddTask(tasks,PrintLog);
AddTask(tasks,show);
//
// int status=0;
//子进程
pid_t id=fork();
if(id==0)//说明子进程创建成功
{
int cnt=10;
while(cnt )
{
// printf("child process creat sucess\n");
work(cnt);
sleep(1);
cnt--;
}
exit(0);
}
//父进程
while(1)
{
int status=0;
pid_t rid=waitpid(id,&status,WNOHANG);
if(rid>0)
{
//走到这里,说明父进程等待子进程成功
printf("child quit sucess,exit code:%d,exit sigin:%d\n ",(status<<8)&0xFF,status&0x7F);
}
else if(rid==0)
{
printf("#########################################################");
//走到这里,父进程等子进程成功,但是子进程还未退出。
printf("father to do other things\n");
executeTask(tasks,TASK_NUM);//父进程在等待期间也可以移除,添加等任务
sleep(1);
}
else
{
//走到这里就只有最后一种情况:rid<0,说明有异常,一般都是pid写错了
printf("wait fail\n");
break;
}
}
return 0;
}
总结:非阻塞等待可以让父进程在等待期间做其他任务