目录
前言
一、进程终止
1、进程终止的几种可能
2、exit 与 _exit
二、进程等待
1、为什么要进程等待
2、如何进行进程等待
(1)wait函数
(2)waitpid函数
3、再次深刻理解进程等待
前言
我们前面介绍进程时说子进程退出,父进程不对子进程进行资源回收,子进程会进入僵尸状态,对于操作系统来说,这是一种资源泄漏,而且还是操作系统层面的资源泄漏,除非父进程退出,否则子进程将一直处于僵尸状态,本章就介绍父进程如何回收子进程;
一、进程终止
1、进程终止的几种可能
在介绍回收子进程之前,我们必须对进程终止有一定的了解;所谓进程终止,就是进程的退出,这里我们首先要直到进程退出有以下三种可能;
1、程序正常运行结束,结果正确;
2、程序正常运行结束,结果不正确;
3、程序崩溃,结果不重要;
可能我们之前对进程退出并没有什么概念,我们是如何区分以上三种情况呢?比如我们以前在virtual studio上运行我们的C/C++程序,我们总会写一个main函数,而一般我们都会在main函数的最后写一个return 0;实际上,这个0就是退出码,我们通过这个判定结果是否正确,退出码有对应的解释含义,我们可以通过 strerror 函数将退出码含义打印出来;这是区分情况一和情况二的方法,对于情况三,程序崩溃,我们的软件 virtual studio 一般会出来一个弹窗,告诉你是哪里引发了程序的崩溃,最常见的就是除零错误、空指针解引用等等,都会导致程序崩溃;下面我们来打印退出码;如下代码;
我们编译运行上述程序,结果如下;
我们发现错误码的信息最多编辑到了133号,前面几项我们也很熟悉,其中第一项0就是成功且结果正确;
2、exit 与 _exit
前面我们说过,在main函数中,我们可以通过return语句让进程退出,并返回返回值;那么要是我们不在main函数呢?那么我们难道要返回main函数再调用return语句?那也太麻烦了吧,实际上,我们也可以通过exit函数和 _ exit函数来使进程终止;如下代码;
我们编译代码,结果如下所示;这里介绍一条命令 echo $?;可以查看最近运行的一个程序的返回值,我们发现我们输入除-1以外的值时,返回值为0,也就是main函数中的return 语句,而我们输入-1时,返回值为14,是我们调用exit函数的返回值;
上述的退出函数exit换成_exit也可以实现相同功能,那么其区别在哪呢?看如下代码;
当我们使用exit函数后,运行结果如下;
当我们使用_exit函数后,运行结果如下;
我们已经看不到you can see me;这是因为我们的打印的内容还在C语言的缓冲区内,而我们的_exit是系统调用,在退出前并不能刷新缓冲区;而我们的exit为C语言库函数,会刷新我们的缓冲区;
补充:C语言的缓冲区刷新机制为行刷新,因为在打印时没有换行符,所以我们的使用exit函数时会打印,而使用_exit函数时不会打印;
二、进程等待
1、为什么要进程等待
其一,这个原因早在我们前面就已经进行了阐述,当子进程退出时,父进程不对子进程进行资源回收,子进程将一直处于僵尸状态,而这种状态会造成系统资源泄漏,这时我们需要通过进程等待的方式,给子进程 “收尸” ;
其二,我们让子进程去完成任务是否需要子进程完成的如何?对于某些时候,就有这样的需求,因此进程等待另一作用是获取子进程任务完成情况;
2、如何进行进程等待
(1)wait函数
关于如何进行进程等待,我们通常是通过系统调用wait和waitpid来实现;我们首先看啊可能wait的函数声明;
这个函数只有一个参数,是一个输出型参数,所谓输出型参数就是我们传一个指针,函数内会给这个指针执行的值进行赋值返还给我们;这个输出型参数就是子进程的退出码和终止信号,这里我们暂时设置为NULL,等待会介绍waitpid时再做介绍;
该函数的返回值,若调用成功,返回子进程pid,若失败返回-1.错误码被设置,我们写出如下代码;查看父进程是否回收了子进程;
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
int id = fork();
if(id == -1)
{
// fork调用失败
exit(1);
}
else if(id == 0)
{
// 子进程
int cnt = 5;
while(cnt--)
{
printf("我是子进程,我的pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
exit(14); // 退出码
}
else
{
// 父进程
sleep(7);
wait(NULL); // 进程等待
sleep(2);
}
return 0;
}
我们再在命令行输入以下脚本命令对这两个进程进行监控;
while :; do ps -axj | head -1 && ps -axj | grep test | grep -v grep; sleep 1; echo "-------------------"; done
我们发现前面几秒确实都在运行,接着中间有两秒子进程处于僵尸状态,因为父进程比子进程多sleep两秒,正如我们所料;接着最后只剩父进程;子进程成功被父进程回收;
(2)waitpid函数
下面为我们通过man手册查询结果;
参数pid:
pid | 作用 |
pid < -1 | 等待进程组号为pid绝对值的任何子进程。 |
pid = -1 | 等待任何子进程,此时的waitpid()函数就退化成了普通的wait()函数。 |
pid = 0 | 等待进程组号与目前进程相同的任何子进程,也就是说任何和调用waitpid()函数的进程在同一个进程组的进程。 |
pid > 0 | 等待进程号为pid的子进程。 |
注意:这里进程组号的暂不提及,我们用的也不多,平常用的较多的就是2和4;
参数status:
这个参数为输出型参数,与我们wait函数相同;关于这个参数的使用,我们不能将里面的int值整体使用,得按比特位分开使用;
情况一:正常退出
此时,我们使用第7到第15比特位,当作退出码;我们可以通过 (status >> 8) & 0xFF来获得这个退出码,还可以使用宏函数 WEXITSTATUS 来获取,通过宏函数 WIFEXITED 来获取进程是否正常退出,若正常退出返回真,否则返回假;
情况二:信号终止退出(异常退出)
这里我们用后面0到6的比特位来表示信号终止,我们可以使用 status & 0x7F来得到这个终止信号;至于这里的 core dump 标志位暂不讲解,这又是另一个话题了;
参数options:
这个参数默认填0就好,表示阻塞等待,若填WNOHANG,则表示非阻塞等待;
返回值:
waitpid的返回值略比wait复杂一些,有三种情况;
1、正常返回,此时返回子进程pid;
2、若设置WNOHANG,且子进程还未退出,则返回0,子进程退出了,返回子进程pid;
3、调用失败,返回-1,错误码被设置;
代码实践:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
int id = fork();
if(id == -1)
{
// fork调用失败
exit(1);
}
else if(id == 0)
{
// 子进程
int cnt = 5;
while(cnt--)
{
printf("我是子进程,我的pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
exit(14); // 退出码
}
else
{
// 父进程
sleep(7);
int status = 0;
// 进程等待
int ret = waitpid(id, &status, 0); // 此时与我们的wait函数功能相同
if(WIFEXITED(status))
{
printf("子进程正常退出,退出码为%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程异常退出,收到信号%d\n", (status) & 0x7F);
}
sleep(2);
}
return 0;
}
代码输出结果如下;
若我们将代码加上一个除零错误;
运行结果如下;
我们再通过kill -l查看信号;8号信号正是我们的浮点数计算问题;
我们再将代码改一下,将我们的waitpid改成非阻塞等待的情况,如下;
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{
int id = fork();
if(id == -1)
{
// fork调用失败
exit(1);
}
else if(id == 0)
{
// 子进程
int cnt = 5;
// 除零错误展示
// int a = 10/ 0;
while(cnt--)
{
printf("我是子进程,我的pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
exit(14); // 退出码
}
else
{
// 父进程
int status = 0;
// 进程等待
while(true)
{
int ret = waitpid(id, &status, WNOHANG);
if(ret == -1)
{
printf("waitpid调用失败\n");
exit(-1);
}
else if(ret == 0)
{
printf("子进程还未退出,我再干点别的\n");
sleep(1);
}
else
{
printf("等待成功\n");
break;
}
}
if(WIFEXITED(status))
{
printf("子进程正常退出,退出码为%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程异常退出,收到信号%d\n", (status) & 0x7F);
}
sleep(2);
}
return 0;
}
运行结果如下,此时我们的父进程就不用阻塞等待子进程结束了,父进程只需要通过轮询的方式来来回收子进程;
3、再次深刻理解进程等待
上述内容为进程等待的实操部分,我们现在再次回到理论部分,我有如下问题;
问题一:我们是否可以通过一个全局变量来获取子进程的退出码呢?
不可以,虽然父进程和子进程共用一段代码,但是都有各自的进程地址空间,当我们使用全局变量时,子进程往这个全局变量里写入时,会发生写时拷贝,因此无法获得退出码,这也是进程的独立性;
问题二:既然进程具有独立性,那么wait和waitpid是如何获取子进程的退出码的呢?
我们的wait和waitpid属于系统调用,既然是系统调用,当然是属于操作系统的一部分,我们在回收子进程时,实际上是销毁进程PCB等内核数据的过程,而PCB(task_struct)中有一个退出码和退出信号的字段,以下为Linux源码截图;
我们在task_struct里确实发现了这几个字段,如果有兴趣的,可以去官网下载一份源码,task_strcut结构体在 include/linux/sche.h 中;
回到正题,既然我们task_struct中有这些字段,那么我们父进程回收子进程的时候是否可以获取这些字段的信息呢?答案当然是肯定的,我们的wait和waitpid为系统调用,当然有资格获取这些字段,我们的父进程也就可以通过这两个系统调用拿到了子进程的退出码了;