🔥🔥 欢迎来到小林的博客!!
🛰️博客主页:✈️小林爱敲代码
🛰️博客专栏:✈️Linux之路
🛰️社区 :✈️ 进步学堂
🛰️欢迎关注:👍点赞🙌收藏✍️留言
文章目录
💖进程的退出场景
进程的退出的三种场景。
- 代码运行完毕,结果正确。
- 代码运行完毕,结果错误。
- 代码异常终止。
想必我们在写C语言的时候,在main函数后面都会写上 return 0 ,那么为什么偏偏是return 0呢?而不是return 其他数字呢? 因为main函数的return值是进程的退出码!
而退出码分两者情况:
0 : 正常退出。
!0 : 结果不正确。
那么为什么 0是正常的,非0是不正确的? 打个比方,假如你小时候有次考试,考了100分。你回家后,你爸爸会问你为什么考了100分吗?那肯定是因为你认真努力对不对?这是正确的结果,所以你父亲不会问你为什么考100分。但如果此时你考了20分回家,你爸会问你为什么考了20分?这时候就有很多种可能性,可能是昨晚没睡好,也可能是你发烧了… 然后你父亲会根据这些可能性对症下药。所以C进程也有很多的退出码,而strerror函数可以通过退出码来获取对应的字符串。让我们遍历一下这些退出码,来看看这些退出码对应的错误可能。
#include<stdio.h>#include<string.h>intmain(){for(int i =0; i <140; i++)printf("%d:%s\n",i,strerror(i));return0;}
我们可以看到退出码的数量,有135个之多。而每个退出码都对应着一种错误。
exict
而还有一种退出方式,就是exict()强制终止。而这个函数要传的参数也是一个退出码。我们可以用 echo $?查看程序的退出码。我们写一个这样的程序。
#include<stdio.h>
#include<string.h>
void fun()
{
exit(100);
}
int main()
{
fun(); return 0; }
我们执行这个程序,然后查看退出码。
所以,exit是一个强制终止的函数,传进去的参数则是这个进程的退出码
_exit
我们先来看看两段代码:
#include<stdio.h>#include<stdlib.h>intmain(){printf("hello");return0;}
#include<stdio.h>#include<stdlib.h>intmain(){printf("hello");exit(0);}
我们会发现这两段代码都会输出hello
当这个进程的时候,hello还在缓冲区当中,为什么可以输出hello?这是因为main函数return和exit都会在进程结束之前刷新缓冲区。所以我们可以看到hello。那么我们换成_exit试试呢?
#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){printf("hello");_exit(12);}
程序运行后,我们可以发现退出码是12,但程序并没有打印hello,这是因为**_exit终止进程不会刷新缓冲区!!**
exit,return和_exit的区别
return:在main函数中代表程序退出,返回值为退出码,退出前会刷新缓冲区。在非main函数中是一个函数调用。
exit:在任何地方使用都代表进程退出,并会刷新缓冲区,参数就是退出码。
_exit:在任何地方使用都代表进程退出,但不会进行程序后序的收尾工作,比如刷新缓冲区… 参数也是退出码。
进程的退出
进程退出的时候,操作系统做了什么?在操作系统层面,就是少了一个进程:free PCB , free mm_struct 地址空间,free 页表和各种映射关系以及代码和数据。
💖进程wait
我们从三个问题来剖析进程wait,进程wait是什么?为什么要有进程wait?怎么解决僵尸问题?
进程wait是什么?
我们都知道fork()可以创建一个子进程。那么创建这个子进程是为了什么?当然是为了帮助父进程完成某种任务。但有没有这样一种情况,那就是父进程比子进程先挂掉?所以为了避免这个问题,我们就需要进程wait,也就父进程等待子进程。
我们先来看下面一段代码:
#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){int sum =0;int pid =fork();if(pid ==0){//childfor(int i =1; i <5;i++){
sum += i;sleep(1);}exit(0);//子进程结束退出。}printf("i am father\n");return0;}
如上这个代码,父进程比子进出先挂掉了,那么为了不让父进程挂掉,我们需要用wait来等待子进程。而wait函数在一个 sys/wait.h的头文件中。
#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>intmain(){int sum =0;int pid =fork();if(pid ==0){//childfor(int i =1; i <5;i++){
sum += i;printf("i am child,sum is %d\n",sum);sleep(1);}exit(0);//子进程结束退出。}wait(NULL);//等待子进程printf("i am father\n");return0;}
现在,父进程就会等子进程结束,才会继续执行。
wait() 的返回值
wait()也有返回值,如果wait等待失败,那么wait会返回-1。
为什么要让父进程等待?
1.通过获取子进程退出的信息,能够得知子进程执行结果。
2.可以保证:时序问题,子进程先退出,父进程后退出。
3.进程退出的时候会进入僵尸状态,会造成内存泄漏的问题,需要通过父进程wait,释放子进程占用的资源。
另外,进程一旦变成僵尸状态,那么它将刀枪不入,即使是"杀人不眨眼的" kill -9命令,也无法奈何它,因为谁也没有办法杀死一个已经死去的进程。
如何让父进程进行等待?
我们先来看下面这一段代码。
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>intmain(){int pid =fork();if(pid ==0){int cmt =5;while(cmt--){printf("i am child\n");sleep(1);}exit(0);//子进程结束}sleep(10);printf("father wait begin\n");//父亲开始等待wait(NULL);sleep(10);return0;}
然后我们来测试一下。
刚开始的时候,我们的子进程处于S状态,也就是休眠状态。
当子进程结束,但父进程还在休眠时,子进程变成了僵尸状态
当父进程结束休眠,开始wait时。我们会发现子进程消失了。
所以我们可以发现:父进程等待,是可以回收掉僵尸进程的。
waitpid
waitpid介绍
- 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,子进程存在且正常运行,则进程可能阻塞。
- 如果不存在该子进程,则立即出错返回。
waitpid(pid_t pid, int status, int options) 的第一个参数,可以是进程的Pid,这样是指定等待某个进程。也可以是-1,*-1是等待任意控制的一个进程**。而第二个参数 *status 是一个整形的指针,我们需要传一个整形的地址给它,它代表着子进程的退出情况。所以最终一定要通过status来获取子进程的退出结果。
获取子进程的status
- wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
- status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特 位):
status是一个整形,可以用来反馈程序退出的三种状,程序退出结果正确,程序退出结果不正确,程序异常崩溃。 而它的三种状态都在它的低16位上进行表示。而高16位我们暂且无视。0 - 7 位用来表示终止信号,8-15位用来表示退出码。当程序正常退出时,我们只看8-15位。当程序异常退出时,我们只看0-8位。
异常信号
那代码异常终止的本质是什么呢?是因为进程收到了某种信号。如果进程没有收到信号,那么进程是正常的跑完的,此时再回有退出码。如果进程收到信号,那么此时就不关心退出码。
如何获取子进程的退出码和信号呢?
还是这张图,它的前8位是退出码,后七位是收到信号。当然,如果正常退出,则收到的信号为0。如果收到信号,那么前8位就无法使用。
所以我们要求出退出码很简单,直接
(status>>8)& 1111 1111
。也就获取前8位,获取退出信号就是后七位,我们直接
status& 0111 1111
即可。那么我们写一段代码来测试一下。
子进程正常退出的情况
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
int main()
{
int pid = fork();
if(pid == 0)
{
sleep(1);
exit(100);//退出码
}
int status = 0;
waitpid(pid,&status,0);
printf("child pid : %d, status code : %d, status signal:%d\n",pid,status>>8&0xFF,status&0x7F); //打印退出码及信号
return 0;
}
进程正常结束的话,不会收到信号,所以信号值为0。
子进程异常终止的情况
那么我们试试异常退出的情况呢?我们在子进程退出前加上一句代码
int a = 5 /0;
,除于0,这个异常够大吧!
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>intmain(){int pid =fork();if(pid ==0){sleep(1);int a =5/0;//故意制造异常exit(100);//退出码}int status =0;waitpid(pid,&status,0);printf("child pid : %d, status code : %d, status signal:%d\n",pid,status>>8&0xFF,status&0x7F);//打印退出码及信号return0;}
我们执行后发现退出码变成0了,可是我们的退出码之前设置的是100。这就意味着这个程序根本就没有退出码!因为退出码是程序正常结束的时候才有。而异常终止则不会有退出码,而我们可以看到,我们的进程收到了一个为8的信号。
然后我们用 kill -l 来查看信号。
当然,这样子位运算计算退出码或者状态码是不是太麻烦了呢?我们可以直接用对应的宏即可。
- WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
- WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
代码:
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>intmain(){int pid =fork();if(pid ==0){sleep(1);exit(100);}int status =0;waitpid(pid,&status,0);// printf("child pid : %d, status code : %d, status signal:%d\n",pid,status>>8&0xFF,status&0x7F);if(WIFEXITED(status)){//如果进来说明正常退出printf("exit code:%d\n",WEXITSTATUS(status));}else{printf("get a signal!");}return0;}
阻塞等待与非阻塞等待
我们的waitpid 还有第三个参数,而这第三个参数如果为0则是阻塞等待,如果为 WNOHANG(宏) ,那么则是非阻塞等待。
什么是阻塞等待和非阻塞等待?
举个例子,假如你有事情要请张三帮忙,你呢给他打电话。这时候张三说:我现在有事,你等我一下啊。然后就让你等他,那么你怎么等呢?如果是阻塞等待,那么就是你让他电话别挂电话,要观测他的一举一动。如果是非阻塞等待,那么就是挂掉电话,过一会再来问。还没好就再挂掉电话,过一会再过来问…
简单来说,阻塞等待就是父进程一直在那干等着,什么也不做。而非阻塞等待就是父进程隔一段时间来监测一下子进程的状态,子进程还没结束就再隔一段时间来看一下子进程。直到子进程结束。
阻塞等待的代码演示:
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>intmain(){int pid =fork();if(pid ==0){int cmt =5;while(cmt--){printf("i am child,time = %d\n",cmt);sleep(1);}exit(0);}//parentint status =0;waitpid(pid,&status,0);//阻塞等待printf("father wait end\n");return0;}
非阻塞等待的代码演示:
#include<stdio.h>#include<string.h>#include<stdlib.h>#include<unistd.h>#include<sys/wait.h>intmain(){int pid =fork();if(pid ==0){int cmt =5;while(cmt--){printf("i am child,time = %d\n",cmt);sleep(1);}exit(0);}//parentint status =0;int ret =0;while(ret==0){
ret =waitpid(pid,&status,WNOHANG);//返回值为0则正常退出printf("i am father\n");sleep(1);//每1秒监测一下子进程}return0;}
阻塞的本质,起始就是进程的PCB被放入了等待队列,并讲进程设置为S状态。返回的本质就是把进程的PCB从等待队列拿到运行队列。从而被CPU调度。
版权归原作者 林 子 所有, 如有侵权,请联系我们删除。