三、进程等待
3.1 进程等待必要性
- 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
- 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
- 最后,父进程派给子进程的任务完成的如何,我们需要知道。子进程运行完成,结果对还是不对,或者是否正常退出。
- 父进程通过进程等待的方式,获取子进程退出信息,回收子进程资源。
3.2 进程等待的方法
测试程序:
#include <iostream>
#include <unistd.h>
#include <sys/wait.h>
using namespace std;
int main(){
pid_t id = fork();
if(id < 0)
{
perror("fork");
exit(10);
}
else if(id == 0)
{
//子进程执行流
int cnt = 3;
while(cnt--){
cout << "I'm child process!";
cout << " pid:" << getpid();
cout << " ppid:" << getppid();
cout << " " << cnt+1 << endl;
sleep(1);
}
exit(122); //为了便于测试,将子进程退出码设为122
}
else{
//父进程等待回收子进程
//此处插入下面的测试代码
//..........
//..........
while(1){
cout << "I'm father process!";
cout << " pid:" << getpid();
cout << " ppid:" << getppid() << endl;
sleep(1);
}
}
}
运行结果:父进程不等待回收子进程,子进程变僵尸。
3.2.1 wait方法
- 参数:输出型参数status,用于获取子进程退出状态,如果不关心也设置为NULL。
- 返回值:等待成功返回子进程pid,失败返回-1。
注意:wait是一种阻塞式的等待,也就是说父进程会停下来等待子进程结束,然后wait才会返回。
测试代码1:
//wait
sleep(5);
pid_t ret = wait(NULL);
if(ret > 0)
{
cout << "等待子进程成功!ret:" << ret << endl;
}
else{
cout << "等待子进程失败!" << endl;
}
运行结果:父进程等待回收子进程,僵尸进程消失。
进程监视:
提示:以后我们编写多进程,基本写法就是fork+wait/waitpid
3.2.2 waitpid方法
参数:
-
pid:
- Pid = -1,等待任一个子进程,与wait等效。
- Pid > 0,等待指定的子进程。
-
status:
-
status输出型参数status,用于获取子进程退出状态,如果不关心也设置为NULL。
-
可以直接通过位运算解析status,得到退出信号和退出码。还可以通过系统提供的宏进行解析:
-
WIFEXITED(status)宏: 如果子进程正常退出,则返回真。(查看进程是正常退出还是运行崩溃)
-
WEXITSTATUS(status)宏: 若WIFEXITED为真,则返回子进程退出码。(查看进程的退出码)
-
-
-
options:
- 默认为0,表示阻塞等待。也就是说父进程会停下来等待子进程结束,然后waitpid才会返回,父进程才能继续执行。
- WNOHANG宏(值为1),表示非阻塞等待。 若pid指定的子进程没有结束,则waitpid()函数直接返回0,不予以等待。若正常结束,则返回该子进程的PID。
返回值:
- 当正常返回的时候waitpid返回收集到的子进程的进程ID;
- 如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
提示:
waitpid(-1, NULL, 0)
的效果与wait(NULL)
相同- Linux内核是C语言写的,所有的系统调用接口也都是C函数。所以系统提供的大写标记一般都是宏定义。
测试代码2:
//waitpid阻塞等待
sleep(5);
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
cout << "等待子进程成功!ret:" << ret;
cout << " status:" << status << endl;
}
else{
cout << "等待子进程失败!" << endl;
}
运行结果:父进程等待子进程成功,并且获取到了子进程的退出状态。
进程监视:
等等,为什么获取到的退出码不是我们要的122呢?这是因为退出状态status并不是整体使用的,进程退出码只是退出状态的一部分!
3.3 获取子进程status
退出状态status并不是整体使用的,而是按照比特位的方式,将32个比特位进行划分,我们暂时只研究低16位。
- 当进程正常退出时,status的次低8位是进程退出码。因此要获得退出码,要先将status右移8位,再判断位低8位(&0xFF)。
- 进程异常退出或者崩溃,本质是操作系统通过向该进程发送信号终止了该进程。
- 当进程异常退出时,status的低7位是进程收到的退出信号。因此要获得退出信号,直接判断位低7位(&0x7F)。
- 退出状态的正确打开方式:先检查退出信号,退出信号为0表示进程正常退出;再检查退出码,退出码为0表示进程运行结果正确。
正常退出
测试代码3:
sleep(5);
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0)
{
cout << "等待子进程成功!ret:" << ret;
cout << " SIGNUM:" << (status&0x7F);
cout << " ExitCode:" << ((status>>8)&0xFF) << endl;
}
else{
cout << "等待子进程失败!" << endl;
}
运行结果:
进程正常退出,不会收到退出信号,因此信号值为0。此时的退出码有意义!
异常退出
-
测试一:子进程中故意加上除0操作,导致进程崩溃。
-
测试二:子进程中故意访问野指针,导致进程崩溃。
-
测试三:向子进程发送9号信号,使进程异常退出。
-
父进程测试代码同上面的正常退出相同。
运行结果:
-
除0操作崩溃,进程接受到8号信号,通过查表可知是浮点数错误。
-
访问野指针崩溃,进程接受到11号信号,通过查表可知是段错误。
-
进程异常退出,不光光是因为内部代码有问题,还有可能是外力直接杀掉,如发送9号信号。
-
进程异常退出,此时的退出码无意义,因为进程的return/exit未执行。
kill -l
显示信号列表:
利用系统提供的宏处理退出状态status
-
WIFEXITED(status): 如果子进程正常退出,则返回真。(查看进程是否是正常退出)
-
WEXITSTATUS(status): 若WIFEXITED为真,则返回子进程退出码。(查看进程的退出码)
测试代码4:
sleep(5);
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(WIFEXITED(status)) //判断子进程是否正常退出
{
cout << "子进程正常退出!child_pid:" << ret;
cout << " exit_code:" << WEXITSTATUS(status) << endl; //查看子进程的退出码
}
else{
cout << "子进程运行崩溃!child_pid:" << ret;
cout << " exit_signal:" << (status&0x7F) << endl; //查看子进程的退出信号
}
运行结果:
既然进程具有独立性,父进程又凭什么能拿到子进程的退出状态呢?
- 父进程并不能直接获取子进程的退出状态,需要通过系统调用wait/waitpid来获取。
- 系统调用,即操作系统有获取进程信息的权利。
wait/waitpid是如何获取到进程的退出状态的?
-
子进程终止,其占用的资源会被释放(包括代码和数据)但会保留进程控制块task_sturct等待父进程回收,这就是僵尸进程。而task_struct结构体中保留了进程的退出状态。
-
task_struct结构体中定义了
int exit_code, exit_signal;
字段,分别表示退出码和退出信号。 -
wait/waitpid实际上就是将子进程task_struct结构中的
exit_code, exit_signal
字段通过位操作写入status,最后回收子进程的进程控制块。
3.4 阻塞等待 VS 非阻塞等待
waitpid的第三个参数options:
- 默认为0,表示阻塞等待。也就是说父进程会停下来等待子进程结束,然后waitpid才会返回,父进程才能继续执行。
- WNOHANG宏(值为1),表示非阻塞等待。 若pid指定的子进程没有结束,则waitpid()函数直接返回0,不予以等待。若正常结束,则返回该子进程的PID。
3.4.1 阻塞等待
-
父进程调用wait/waitpid阻塞式等待子进程,让进程退出具有一定的顺序性,将来可以让父进程进行更多的收尾工作。
-
进程阻塞本质上是进程阻塞在系统调用函数的内部(包括进程等待,IO操作等的相关系统调用)。程序计数器记录阻塞位置方便进程唤醒后继续向后执行,进程PCB被调度到阻塞队列中等待阻塞事件的发生。
-
我们一般说进程“Hang住了”或者“卡住了”,感觉到明显的卡顿。是因为进程在阻塞队列中等待被唤醒或是在运行队列中等待被调度执行(CPU过于繁忙)。
3.4.2 非阻塞等待
测试代码5:
typedef void(*pf)(); //函数指针类型
vector<pf> arrpf; //函数指针数组
void func1(){
cout << "父进程临时任务1" << endl;
}
void func2(){
cout << "父进程临时任务2" << endl;
}
//想让父进程在等待子进程过程中执行临时任务,只需要在Load中注册任务即可。
void Load(){ //加载临时任务(回调函数)
arrpf.push_back(func1);
arrpf.push_back(func2);
}
int main(){
//.......
int status = 0;
while(1) //基于非阻塞等待的轮询检测方案
{
pid_t ret = waitpid(-1, &status, WNOHANG); //以非阻塞方式等待
if(ret > 0) //子进程退出
{
if(WIFEXITED(status)) //判断子进程是否正常退出
{
cout << "子进程正常退出!child_pid:" << ret;
cout << " exit_code:" << WEXITSTATUS(status) << endl; //查看子进程的退出码
}
else{
cout << "子进程运行崩溃!child_pid:" << ret;
cout << " exit_signal:" << (status&0x7F) << endl; //查看子进程的退出信号
}
break;
}
else if(ret == 0) //子进程未退出
{
cout << "子进程正在运行..." << endl;
if(arrpf.empty()) Load();
for(auto iter : arrpf)
{
//等待子进程的过程中执行其他任务
iter();
}
}
else{ //等待子进程失败
cout << "等待子进程失败!" << endl;
break;
}
sleep(1);
}
//.......
}
运行结果:
可以看到,在子进程运行过程中,父进程采用基于非阻塞等待的轮询检测方案,不断检查子进程是否退出。如果子进程没有退出,父进程不会阻塞等待,而是可以同时执行一些临时任务。