Linux进程退出与等待
@[toc]
零.前言
进程终止有多种可能的原因,也有多种方式可以导致进程终止。
父进程需要等待子进程结束从而来回收子进程的资源。
1.进程退出
(1)进程退出的本质
进程退出时会销毁PCB以及进程地址空间,释放掉页表以及其中的各种映射关系,代码段与数据段所占用的空间也要被还给系统。
(2)进程退出的三种情况
进程的退出有三种情况:
1.代码运行完毕,结果正确。
2.代码运行完毕,结果不正确。
3.代码异常终止。
其中代码异常终止指的是程序崩溃。比如我们进行了除0操作等,会引发程序崩溃。
(3)退出码
退出码的概念
当一个进程退出后,就会产生一个退出码。注意,我们研究的退出码的范围是第一种和第二种情况,即程序进行正常的退出,当程序由于崩溃而退出时,退出码没有意义。
查看退出码
在命令行使用:
echo $?
来查看最近的一个进程退出后的退出码。
比如我们随意写一份C语言的代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello world!\n");
return 0;
}
执行后查看退出码:
可以观察到退出码是0,此时查看的是刚刚结束的进程mytest的退出码。
如果我们在命令行继续输入echo $?,我们会发现退出码依然是0,注意这个退出码指的是上一个进程即上一个echo $?命令的退出码(每一个命令也是一个进程)。
通常而言,命令行指令的退出码都是0。
查看所有退出码
C语言提供了退出码与退出原因对应函数:sterror(i),其中i表示退出码,sterror返回的是i所对应的错误的字符串。我们可以根据这一点来打印所有退出码代表的含义:
int i=0;
for(i=0;i<140;i++)
{
printf("%d:%s\n",i,strerror(i));
}
我们先假定有140种退出码,这段代码打印的结果是:
我们发现一共有134种退出码,超过133的数字就已经未知了。
同时我们发现:
当退出码为0时,才表示程序正常运行,其他的数字都代表一个程序没有正常运行的原因。
而通过这一点我们还可以验证一下,程序崩溃时退出码为什么没有意义:
int a=10;
a/=0;
此时我们观察到退出码是136:
对比上表,是没有意义的。
(4)控制进程退出的方式
我们了解了进程退出的原因,那么如何控制进程退出呢?
return
在书写C语言代码的时候,我们习惯在末尾加上return 0,来表示代码执行完毕。这就是一种进程的退出方式。其中0就代表该进程退出的退出码。因此我们是可以随意地修改C语言进程的退出码的。比如我们可以return 10,那么该进程的退出码就变成了10。
int main()
{
return 10;
}
但是由于退出码都有特定的含义,所以不建议随便修改。
exit()与_exit()
exit()和exit()也可以控制进程退出,其中括号中的内容为退出码。两者的区别在于:exit()退出后缓冲区会被刷新,而_exit退出后缓冲区不会进行刷新。
printf("hello exit");
sleep(5);
exit(1);
printf("wrong\n");
printf("hello exit");
sleep(5);
_exit(1);
printf("wrong\n");
其中使用exit退出可以打印出hello exit,因为刷新了缓冲区;而使用_exit退出没有打印出该字符串,因为没有刷新缓冲区。
这里将进程的退出码设为了1,可以在终端自行验证。
注意,虽然没有刷新缓冲区,操作系统也一定将缓冲区中的内容释放掉了,否则会造成内存泄漏。
2.进程等待
(1)进程等待的原因
进行进程等待的通常是父进程,父进程需要等待子进程执行结束之后再进行进程退出。
1.通过获取子进程退出的信息,来获得子进程退出的结果。
2.可以保证时序问题,子进程先退出,父进程后退出。
3.为避免子进程进入僵尸状态,父进程需要等子进程结束后回收子进程的资源。
(2)wait函数
我们在父进程中使用wait函数来进行进程的等待。下面我们将介绍两个函数来实现进程的等待。
返回值与参数
等待成功,wait将返回子进程的id,如果等待失败,wait会返回-1。
wait的参数status和waitpid的status表示一个意思,在下面会西索。
举例
父进程可以通过进程等待来达到回收僵尸进程的目的:
pid_t id=fork();
if(id==0)
{
int cnt=5;
while(cnt)
{
printf("child:%d is running,cnt is %d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(0);
}
else
{
sleep(10);
printf("father wait begin\n");
pid_t ret=wait(NULL);
if(ret>0)
{
printf("father wait %d\n",ret);
}
else
{
printf("wait fail");
}
printf("wait success\n");
sleep(10);
}
这段代码的目的是:先让子进程运行5s,父进程休眠10s,这就导致了子进程在前5s是运行态,在后5s是僵尸状态(父进程在休眠,无法进行资源的回收)。当父进程进行进程等待的时候,回收了子进程的资源,此时子进程死亡。父进程在10s再退出。
当我们不进行进程等待的时候,父进程执行结束,子进程会变成孤儿进程被操作系统领养。
如果进行进程等待,子进程就不会被领养,资源被父进程回收后再死亡。
通过简单的命令行脚本我们可以进行观察。
wait其实有两种作用,1是等待子进程结束后回收,2是通过status获得子进程的信息。
(3)waitpid函数
返回值与参数
wait的返回值也是一个pid_t类型:
1.正常情况下,返回子进程的pid。
2.如果设置了选项WRONGHANG,而调用waitpid发现没有子进程可以被回收,则返回0。
3.如果嗲破蛹出错则返回-1。
waitpid有三个参数:
pid
一个进程可以有多个子进程,wait函数等待的是所有子进程执行结束,而waitpid可以通过参数pid来指定要结束的进程。
当pid为-1时,代表等待所有的进程结束,与wait相同。
pid_t ret=waitpid(id,NULL,0);
只需要将上面进程等待的代码改成这样,由于父进程中的fork返回值是子进程的id,赋值给了变量id,所以这里表示的是等待子进程执行结束。
status
status的值
status是一个指针类型,是一个输出型参数。输出型参数的意义在于在waitpid中修改了status的值是通过指针修改的,对函数外界是有影响的。我们其实就是通过status来获得进程退出的结果的。
因此我们需要自己定义一个int类型的数据,并向status传入它的地址,然后通过该数据来获得进程退出的结果。
我们可以将子进程的退出码改为20,然后打印一下status的值来进行观察:
pid_t id=fork();
if(id==0)
{
int cnt=5;
while(cnt)
{
printf("child:%d is running,cnt is %d\n",getpid(),cnt);
cnt--;
sleep(1);
}
exit(10);
}
else
{
// sleep(10);
int status=0;
printf("father wait begin\n");
pid_t ret=waitpid(id,&status,0);
if(ret>0)
{
printf("father wait %d\n",ret);
printf("status is %d\n",status);
}
else
{
printf("wait fail");
}
printf("wait success\n");
// sleep(10);
}
此时我们发现status的值是2560,那么这个数字代表什么呢?
首先我们要明确,status是反映子进程退出时的状态的,而进程退出有三种情况:正常退出,结果正确;正常退出,结果错误;程序崩溃退出。所以status其实是反映这三种情况的。
status的构成
status指向的内容有32个比特位,但是只有低的16位有意义,高的16位没有意义。
程序崩溃即代码异常终止,本质上是收到了一个终止的信号,waitpid会首先判断是否是接收到终止信号导致进程结束,如果是则直接对status指向的内容进行修改。
其中终止信号占7位,coredump占一位。
如果不是程序异常终止,则status进行如下赋值:
因此我们就可以知道2560的由来了,退出码是10,status就是0000 1010 0000 0000的值刚好是2560。
通过status获得退出码
知道了status的构成,我们就可以通过移位操作,通过status来获得程序的退出码和退出信号。
(status>>8)&0xFF//获得退出码
status&(0x7F)//获得退出信号
了解了这一原理,我们还会得出这样的结论:bash就是通过wait方法获得的各个退出码,因此我们可以才能通过echo $?来获得各个进程的退出码。
我们发现如果要获得退出码还需要进行移位操作,非常的不方便,因此大佬们又引入了两个宏,来帮助我们获取退出码:
WIFEXITED(status):若为正常退出,则返回非0。
WEXITSTATUS(status):如果WIFEXITED为非0,则提取子进程的退出码。
if(WIFEXITED(status))
{
printf("father wait %d\n",ret);
printf("statusexit is %d\n",WEXITSTATUS(status));
}
此时可以打印到退出码10:
options
waitpid的第三个选项options有两种取值:一种是0,一种是WNOHANG。
其中0表示的是阻塞等待,而WNOHANG表示的是非阻塞等待。
阻塞等待指的是父进程在等待子进程结束的期间内,不进行任何操作。而非阻塞等待指的是父进程在等待子进程结束的期间内不断调用waitpid的过程。(注意返回值条件,当设置WNOHANG且子进程没有结束时,返回值为0,否则返回子进程的pid)。
当取值为0,即为阻塞等待时,本质上其实就是将父进程由运行队列转移到等待队列。等子进程执行结束之后再将父进程由等待队列转移到运行队列。之前的例子都是阻塞赋值的例子。
我们也可以验证一下非阻塞赋值:
while(1)
{
pid_t ret=waitpid(id,&status,WNOHANG);
if(ret==0)
{
printf("do my things\n");
}
else if(ret>0)
{
printf("wait success\n");
break;
}
else
{
printf("wait failed\n");
}
sleep(1);
}
我们可以通过循环来使用非阻塞等待,帮助父进程完成其他内容。
3.总结
1.进程结束有三种原因。使进程结束有两种主要的方法。
2.进程等待有两个主要的函数,并且等待分为阻塞等待和非阻塞等待。
3…三连叭球球了。
- 点赞
- 收藏
- 关注作者
评论(0)