文章目录
- 一、进程等待的必要性
- 1.进程等待是什么
- 2.进程等待的必要性
- 3.为什么要进程等待呢?
- 二、进程等待的方法
- 1.问题
- 2.wait
- 3.waitpid
- 4.status的原理
- 5.等待失败
- 6.与status有关的两个宏
- 7.options
一、进程等待的必要性
1.进程等待是什么
通过系统调用wait/waitpid,来进行对子进程状态检测与回收的功能!
2.进程等待的必要性
- 子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.为什么要进程等待呢?
答案已经在前面回答了
就是因为僵尸进程无法被杀死,需要通过进程等待来杀掉它,进而解决内存泄漏问题----必须解决的
我们要通过进程等待,获得子进程的退出情况,要能够知道布置给子进程的任务,它完成的怎么样了----要么关心,也可能不关心
二、进程等待的方法
1.问题
我们使用如下代码
#include<unistd.h>
#include<stdlib.h>
#include<stdio.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(0);
}
else
{
while(1)
{
printf("I am parent,pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
}
return 0;
}
上述代码的功能不难理解。然后我们使用监控去运行一下
while :; do ps ajx | head -1 && ps ajx | grep -v grep |grep waitTest; echo "---------------------------------"; sleep 1;done
运行结果如下所示
为了解决上面的问题,我们可以让父进程通过调用wait/waitpid进行僵尸进程的回收问题!
2.wait
我们先来看wait函数
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int*status);
返回值:
- 成功返回被等待进程pid,失败返回-1。
参数:
- 输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
这个系统调用的作用就是等待一个进程,直到它的状态发生改变。
对于wait函数,我们可以看到它需要传入一个指针,但是在wait函数,我们先不关心它的这个参数。也就是我们先给他一个NULL指针
这个wait它会返回一个值,这个值就是对应等待的子进程的pid
#include<stdio.h>
#include<unistd.h>
#include<stdlib.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(0);
}
else
{
int cnt = 10;
while(cnt)
{
printf("I am parent,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);
cnt--;
sleep(1);
}
pid_t ret = wait(NULL);
if(ret == id)
{
printf("wait success:,ret : %d\n",ret);
}
sleep(5);
}
return 0;
}
最终它的运行结果为
程序一共经历了三个5秒。第一个五秒钟,是父子进程都存在,第二个五秒,子进程进入僵尸状态。第三个五秒,子进程的僵尸状态被回收了。
所以说,到目前为止,进程等待是必须的。因为这个进程等待最重要的作用就是回收僵尸进程
那么如果我们这个程序有很多个子进程呢?应该等待哪一个呢?其实wait是等待任意一个子进程退出。
所以如果我们要等待多个进程退出,我们要这样做
如下代码所示
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define N 10
void RunChild()
{
int cnt = 5;
while(cnt)
{
printf("I am a 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(0);
}
printf("create child process: %d success\n",id);
}
sleep(10);
for(int i = 0; i < N; i++)
{
pid_t id = wait(NULL);
if(id > 0)
{
printf("wait %d success\n",id);
}
}
sleep(5);
return 0;
}
运行结果如下
上面都是子进程会退出的情况,那么如果子进程都不退出呢?
即我们将上面的子进程都改为死循环
那么最终的运行结果是一个也不退出,父进程也不退出,在那里阻塞等待。
所以这说明,如果子进程不退出,父进程默认在wait的时候,调用这个系统调用的时候,也就不返回,默认就叫做阻塞状态!
所以说阻塞不仅仅只是像我们之前要等待scanf的时候,需要等待硬件,还有可能等待软件
3.waitpid
现在我们已经知道对于子进程的僵尸进程问题是如何解决的了,就是使用wait即可。这解决了进程等待中最为重要的一点,那么如果父进程需要获得子进程的退出时的状态,得知子进程任务完成的怎么样了,那么应该如何解决呢?
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:
- pid:
Pid=-1,等待任一个子进程。与wait等效。
Pid>0.等待其进程ID与pid相等的子进程。
- status:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
- options:
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。
从这里的描述,我们就可以知道,wait其实就相当于waitpid的一个子集
即在这段代码中,wait与waitpid是等价的
在我们这个wait和waitpid的函数中,他们都有一个status,这个就是用来获取退出时的状态的。如果我们不想用,直接设置为空即可,这两个函数的这个参数是一样的
对于这个status,它是一个输出型参数。其次,这个int是被当作几部分来使用的。
我们可以先来使用一下,为了展示出效果,我们让子进程的退出设置为1
最终运行结果为
我们可以发现,我们退出信息本应该是1,但是结果却是256
这里我们就有如下几个问题
- 子进程退出,一共会有几种退出场景呢?
对于这个问题,我们在前面已经知道了:总共三种场景
①:代码运行完毕,结果正确 ②:代码运行完毕,结果不正确 ③:代码异常终止
- 父进程等待,期望获得子进程退出的哪些信息呢?
①:子进程代码是否异常?②:没有异常的话,结果对吗?这里可以用exitcode退出码来获取,不对是什么原因呢?可以用不同的退出码表示原因
所以我们可以看到,这个status需要做的事情其实挺多的,所以这个变量注定了不能被看作一个,而是要划分为几个部分
对于这个status它一共有32位,但是我们目前只考虑它的低16位
低八位,用于表示是否出异常了。其中有一共比特位是core dump标志位,我们后面再谈
我们在前面说过,一共进程异常了,本质就是该进程收到了一个信号。
对于它的次低八位,代表的就是退出的状态,即退出码
比如我们刚刚所说的明明是退出码是1,但是打印结果是256,其实就是退出码的最低位被置为了1
对于第七位,我么可以看到总共有64种信号,但是我们会发现没有0号信号,是因为0要代表正常终止。所以总共65种状态,就需要七位来表示。
那么在这里我们可能会有一个问题,就是我们觉得可能没有必要这样做,因为完全可以设置一个全局变量,然后再子进程退出的时候,修改这个全局变量来处理状态就可以了,不需要用wait来处理?
其实这是因为,进程具有独立性
即便子进程中将这个全局变量给修改了,但是父进程也是看不到的。所以必须得通过wait系统调用,让操作系统去拿这个数据。
我们可以这样做,就可以分别拿出两种码了
运行结果为
如果我们的退出码是11
那么运行结果的退出码就是11
我们也可以模拟当他出现异常的时候
可以看到,子进程第一次就发生了除0错误,直接进入了僵尸状态。
并且最终就是8号信号浮点数错误
我们也可以让他进入死循环,然后我们使用kill去杀掉这个信号
4.status的原理
在下图种,意思是,父进程再某行种调用了waitpid这个函数,cpu去调度父进程,当子进程执行完毕的时候。子进程为了保证自己的结果可以被上层获取,子进程可以允许把代码和数据释放掉,但是子进程对应的进程控制块不能被释放掉。我们需要将子进程退出时的信息放入进程控制块中
所以子进程中一定有sigcode,exitcode
如下在linux内核中就可以看到这两个
当他退出时,会将值写入exit_code中,当他异常终止时,会将信号写入exit_signal中。然后waitpid就可以读取这里面的数据了
waitpid一方面可以检测当前进程是否退出了,比如说z状态。是z状态,就直接读取这两个值,通过位运算,让上层拿到
所以waitpid的核心工作就是读取子进程的task_struct,内核数据结构对象,并且将进程的Z状态改为X状态
那么为什么不让我们写代码时候直接访问子进程的pcb中呢,而是必须要通过系统调用呢?
我们必须要通过管理者拿到被管理者的数据,不能直接拿被管理者的数据,因为操作系统不相信任何人
5.等待失败
前面我们知道,wait/waitpid函数在等待失败的时候,会返回-1,那么什么时候会失败呢?
这里有一个很经典的场景,那就是等待的进程不存在或者等待的进程不是该进程的子进程
这就说明了,等待的进程必须是该进程的子进程
6.与status有关的两个宏
其实在我们的代码中,如果要让我们去写这两个的话是比较麻烦的
所以linux提供了两个宏
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
我们可以这样用
我们可以在试一下多进程的
运行结果为
不过要注意的是,我们这个具有一定的偶然性,因为多进程的话不一定越早的就一定是最快的。取决于CPU的调度
所以现在我们就知道了前面所说的进程等待的原因之二了:我们要通过进程等待,获得子进程的退出情况,要能够知道布置给子进程的任务,它完成的怎么样了----要么关心,也可能不关心。我们也就知道了main函数的返回到底是什么了
就好比,当我们正在运行我们上面的代码的时候,bash也在等待这个进程退出。因为这个进程也是bash的子进程。
7.options
我们知道,wait本身会等待子进程,但是当子进程一直停不下来的时候,父进程只能等待,wait会导致父进程阻塞调用,导致父进程陷入阻塞状态。options可以指定等待方式。如果是0,则是阻塞等待方式(即父进程要等待子进程,子进程一直处于R,父进程就一直处于S,然后将父进程投入到等待队列中,投入到了子进程的等待队列。当子进程结束的时候,再从这个等待队列中将父进程唤醒)
我们在等待的时候,还可以选择另外一种等待方式:非阻塞轮询
pid_ t waitpid(pid_t pid, int *status, int options);
在这个函数中,如果options是0,那么就是阻塞等待方式
还有一种选项是WNOHANG (wait no hang, hang可以理解为夯住了,类似于服务器宕机了,意思是等待的时候不要夯住,也就是非阻塞)
类似于小明有事找小王
如果小明在楼下给小王隔一会就打一下电话,因为小王一直说忙着等会马上到。这就是非阻塞轮询(小明是在不打电话的时候是非阻塞的,而且是一直循环的打电话)
如果小明在楼下给小王打电话,然后不挂了,知道小明事情做完才挂电话,这就是阻塞等待(因为小明啥也干不了了)
如果小明在楼下给小王隔一会就打一下电话,在这中间的间隙做一些自己的事情,就是非阻塞轮询+做自己的事情
与之对应的就是pid_t的返回值其实应该有三种
大于0:等待成功,即返回子进程的pid
小于0:等待失败
等于0:等待的条件还没有就绪。
如下就是非阻塞轮询的示例
运行结果如下所示:
注意在非阻塞轮询中,最好加上sleep,否则的话频繁的printf,可能会对系统压力比较大,导致卡住了。达不到预期的结果。
那么这里我们可能会疑惑父进程具体要具体做什么样子的工作?
我们可以用下面这个例子
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
#define TASK_NUM 10
typedef void(*task_t)();
task_t tasks[TASK_NUM];
void task1()
{
printf("这是一个执行打印日志的任务,pid:%d\n",getpid());
}
void task2()
{
printf("这是一个执行检测网络健康状况的任务,pid:%d\n",getpid());
}
void task3()
{
printf("这是一个绘制图形界面的任务,pid:%d\n",getpid());
}
void InitTask()
{
for(int i = 0; i < TASK_NUM; i++) tasks[i] = NULL;
}
int AddTask(task_t t)
{
int pos = 0;
for(pos = 0; pos < TASK_NUM; pos++)
{
if(!tasks[pos]) break;
}
if(pos == TASK_NUM) return -1;
tasks[pos] = t;
return 0;
}
void DelTask()
{}
void CheckTask()
{}
void UpdateTask()
{}
void ExecuteTask()
{
for(int i = 0; i < TASK_NUM; i++)
{
if(!tasks[i]) continue;
tasks[i]();
}
}
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 status = 0;
InitTask();
AddTask(task1);
AddTask(task2);
AddTask(task3);
while(1) //轮询
{
pid_t ret = waitpid(-1, &status,WNOHANG); //非阻塞
if(ret > 0)
{
if(WIFEXITED(status))
{
printf("进程是正常跑完的,退出码为:%d\n",WEXITSTATUS(status));
}
else
{
printf("进程出异常了\n");
}
break;
}
else if(ret < 0)
{
printf("wait fail\n");
break;
}
else
{
ExecuteTask();
usleep(500000);
//printf("子进程还没有退出,在等等\n");
//sleep(1);
}
}
sleep(3);
}
return 0;
}
运行结果为
在这里我们需要注意的是,在这里,我们等待子进程才是最核心的任务,这些其他的任务都是一些顺带的事情。
这些顺带的任务不可以太重了,应该得是轻量化的任务。比如打印日志,检测网络状况等。而且在这里,我们这里也只是延迟回收了一会子进程,而不是说不回收子进程了。
在上面的代码中,我们只是单进程的等待任务,如果我们想要改为多进程的等待任务的话,那么我们只需要将这里稍作修改即可,不让他直接break,而是设置一个计数器,计数子进程的个数,当一个子进程结束后,计数器减减即可。只有减到0以后,才是break。还有就是在出错的时候,也break即可
最后一点需要注意的是
waitpid在回收子进程的时候,它可以保证父进程一定是最后一个被回收的。因为子进程可以全部被waitpid给回收掉。