1. 进程退出场景
进程退出一般有三种场景:
。代码运行完毕,结果正确
。代码运行完毕,结果错误【比如,我们要对某个文件进行写入,但写入的文件路径出错,代码运行完毕,可是结果出错】
。代码异常终止【这种情况的原因有很多种,我们在写C/C++代码时肯定遇到过】
一般进程的退出结果是交给父进程的,因为父进程创建子进程是为了让子进程帮父进程完成某种任务,所以父进程肯定需要拿到子进程的退出结果。
那父进程是如何拿到子进程的退出结果呢?我们下面慢慢说。
2. 进程常见退出方法
2.1 从main函数返回
我们以往在写C/C++程序时,通常会在main函数的结尾返回0,但至于这个返回值的作用是什么,我们不知道,并且我们也不关心!但是,我们现在有必要知道了:
main函数的返回值通常表明你程序的执行情况!
。return 0;表明代码运行完毕,结果正确
。return 非0;表面代码运行完毕,结果错误【不同的值表明不同的出错原因】
下面,我们来写一段代码来见一见main函数的不同返回值:
以上代码,我们打开一个文件进行读的操作,但是在该目录下,我并没有这个文件,所以肯定是打开失败的。所以函数会返回1。
但是,我们怎么知道这个进程的退出结果呢?
这里补充一个概念:退出码【退出码(退出状态)可以告诉我们最后⼀次执行的命令的状态。在命令结束以后,我们可以知道命令 是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码0 时表示执行成功,没有问题。 代码1 或0 以外的任何代码都被视为不成功】。
每个进程退出后进入僵尸状态,其退出码都会写入自己的task_struct中【exit_code】。
我们只要在命令行上敲下以下指令就可以查看到最近一个进程退出时的退出码:
其实,在C标准中,规定了每个退出码都有自己对应的错误信息,strerror就可以把退出码转化成对应的错误信息!
下面我们就来看一下C标准规定了多少错误信息:
可以看到从0开始一共有134个退出码!
最后,如果进程异常终止,退出码就不重要了,就好比你考试作弊被发现了,你的结果也就不重要了。
2.2 exit函数
其实除了从main中用return返回结束进程,我们还可以用exit函数结束进程。
并且在程序的任何地方调用该函数,进程都会直接退出并返回退出码给父进程!
只打印“fun begin”验证了这一点!
2.3 _exit函数
如果exit退出的时候,进程退出的时候,会进行缓冲区刷新。
如果_exit退出的时候,进程退出的时候,不会进行缓冲区刷新。
以上这一点本身并不重要,了解即可。
我们知道库函数和系统调用是上下级关系,即exit的底层还是_exit,但为什么_exit却不会刷新缓冲区呢?
因为缓冲区是库级别的【C语言提供的】,也就是库缓冲区,这里只点出来,更多细节,后面再说。
3. 进程等待
3.1 为什么要有进程等待
• 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
• 另外,进程⼀旦变成僵尸状态,那就刀枪不入,“杀⼈不眨眼”的kill-9也无能为力,因为谁也没有办法杀死⼀个已经死去的进程。
• 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是 不对,或者是否正常退出。
• 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
3.2 进程等待是什么
要搞清楚这个问题,我们先来见一见进程等待先简单使用一下!
a. wait方法
wait其中的参数wstatus其实是输出型参数,这点我会在waitpid处再详细说明。
这里我们先使用wait方法解决僵尸进程的问题:
以下代码我先创建了一个子进程,在子进程运行3秒后退出,而父进程先休眠5秒后,再使用wait等待子进程,最后让父进程运行10秒后退出。这样做的原因是方便我们检测时,先看到子进程的僵尸状态,父进程等待成功后,我们可以看到子进程从僵尸状态被回收的过程。
1#include <stdio.h>2 #include <sys/types.h>3 #include <unistd.h>4 #include <wait.h>5 #include <stdlib.h>6 7 int main()8 {9 pid_t id=fork();//创建子进程10 if(id==0)//子进程11 {12 int cnt=3;13 while(cnt--)14 {15 printf("子进程pid->%d\n",getpid());16 sleep(1);17 }18 exit(0);19 }20 //父进程等待子进程21 sleep(5);//父进程先等待5秒,可以看到子进程的僵尸状态22 int exit_code=0; 23 pid_t rid=wait(&exit_code);24 if(rid>0) printf("等待子进程成功!rid->%d\n",rid);25 sleep(10);26 return 0;27 }
运行结果:
下面是检测写的简单shell脚本:
while :; do ps axj | head -1 && ps axj | grep code; sleep 1;done
检测结果:
结果也确实和我们预期的一样!
这里还有两个细节 ,如果父进程在等待子进程的中,子进程没有退出,那么父进程就在wait处等待【可以理解为scanf】!父进程wait时会等待它任意一个子进程退出并获得其pid。
b. waitpid方法
waitpid有3个参数,其中第一个参数有以下四种填写方法,目前我们先不管<-1和=0,-1表明等待任意子进程退出,>0则表明等待指定子进程退出。
第二个参数为输出型参数,我们之前便说过,父进程创建子进程是为了让子进程帮父进程完成某些任务。所以父进程应该有能力知道【不管它想不想知道】子进程的退出结果【即退出信息】,而第二个指针参数就是为了做到这一点而存在的,而wstatus获得的正好就是子进程的退出码!
第三个参数可以控制阻塞【我们先暂时不管】
下面我们来看看waitpid到底能不能获得子进程的退出码:
按照我们的预期,exit_code应该是1!
但是结果似乎和我们预期的不太一样!
当实际情况和预期不一样时,那一定是我们的认知出了问题!
这里就直说了,整形的exit_code接收退出码时,只用了32个比特位中的8~15这8个比特位来记录退出码【我们可以通过位图来理解】!而16~32这16个比特位没有被使用,至于1~8这8个比特位在子进程正常退出时都默认是0,而只有在子进程异常退出时,这八个比特位才会被使用,这和信号有关,细节我们以后再说!
既然如此,那我们拿到记录退出码的那八个比特位就可以看到正确的退出码了!
修改这一句代码即可!
if(rid>0) printf("等待子进程成功!rid->%d,exit_code->%d\n",rid,(exit_code>>8)&0xFF);
还有一个问题,我们为何不定义一个全局变量来记录子进程的退出码呢,在子进程退出时,修改这个变量!原因也很简单,你子进程修改它,发生写时拷贝,父进程和子进程看到的全局变量根本就不是同一个!
最后,如果进程异常退出,退出码就没有意义了,但是,我们还是可以通过waitpid的输出型参数拿到退出信号。
稍微修改一下代码,做一个实验:
手动杀掉进程!父进程得到相应的退出信号!
我们可以通过指令kill -l来查找所有的信号:
这里只是简单的见了见信号,具体细节还没有说!
3.3 进程等待怎么办到
现在,我们理解了什么是进程等待,为什么要有进程等待,但是我们还是不知道,父进程通过进程等待是如何拿到子进程的退出码和退出信号的!
要明白这个问题,我们首先需要知道这些信息存放到哪里!我们都知道一个进程退出后,它的进程虚拟空间 | 页表 | 代码和数据 都会被系统回收和释放!但是,他的PCB【task_struct】却会被保存起来,进入僵尸状态!所以,子进程的退出码和退出信号势必在它的PCB中!所以,进程退出时,它的退出信息会被记录进它的PCB中的某些变量中!
事实也是如此:在源码中记录信息的变量如下所示
所以,父进程通过系统调用的方式让系统帮它去子进程的PCB中拿到子进程的退出信息返回给父进程。至此,我们也就明白了,进程等待的原理了。而且,我们现在也更加明白了,为什么进程要有僵尸状态【1. 为了方便父进程回收子进程 2. 为了父进程方便拿到子进程的退出信息】
3.4 阻塞与非阻塞等待
上面我们提到过waitpid的第三个参数使用来控制阻塞和非阻塞调用的,在默认情况下【不填参数】,waitpid进行的是阻塞调用,什么是阻塞调用呢?
阻塞调用:父进程在waitpid处等待子进程退出,如果子进程一直不退出,那父进程则会一直等待,并且只做这一件事情!
如果,我们将参数填写为WNOHANG,则为非阻塞调用,非阻塞调用:父进程每隔一段时间去看一看子进程是否退出,如果退出,则返回值大于0,如果调用结束,子进程没有退出,则返回0,如果返回值小于0,则调用失败。在子进程未退出之前,父进程可以利用其他时间做一些其他的事情,所以非阻塞调用一般效率更高【并不是指子进程退出的效率高】。
如何做到非阻塞等待,其实也很简单,我仅们需要做一次非阻塞轮询,说白了,就是循环!
下面一个例子就很好的体现了非阻塞调用下父进程利用等待时间完成其他的任务。
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <vector>
typedef void (*handler_t)(); // 函数指针类型
std::vector<handler_t> handlers; // 函数指针数组
void fun_one() {printf("这是⼀个临时任务1\n");
}
void fun_two() {printf("这是⼀个临时任务2\n");
}
void Load() {handlers.push_back(fun_one);handlers.push_back(fun_two);
}
void handler() {if (handlers.empty())Load();for (auto iter : handlers)iter();
}
int main() {pid_t pid;pid = fork();if (pid < 0) {printf("%s fork error\n", __FUNCTION__);return 1;}else if (pid == 0) { // childprintf("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");}handler();} 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;
}