进程等待
子进程退出,父进程不管子进程,子进程处于僵尸状态——若不回收会导致内存泄漏
父进程如何得知子进程状况?
上面这些问题都需要进程等待来完成
进程等待的必要性:
之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
wait验证
子进程跑完后进入僵尸状态,没有被回收
我们使用wait回收它,wait是等待一个进程直到这个进程的状态发生变化(如有R或S变为Z),如果回收成功,返回子进程PID,失败了返回-1
参数 status 保存着子进程退出时的一些状态(包括 task_struct、thread_info及内核栈等)它是一个指向 int 类型的指针;如果不在意子进程的结束状态值,只想把这个僵尸进程消灭掉(实际上,大多数时候都是这样做的),则可以将这个参数设为 NULL,即
pid = wait(NULL); // 不管子进程的结束状态,直接杀死进程
使用wait,由于前面子进程sleep5秒钟,这个时候父进程调用wait是在阻塞式的等待
通过对比我们发现,子进程已经被回收了,子进程由S切换到Z,立马被回收
进程的一般写法就是fork+wait/wait pid
wait pid
第一个参数:某进程的PID,若是-1,则代表任意一个进程,与wait等效,若是0,表示等待指定进程
第二个参数:输出型参数和wait的status一模一样
第三个参数:默认为0,表示阻塞等待
返回值>0代表等待成功,<0代表等待失败
waitpid(pid,NULL,0)=wait(NULL),验证这一结论,我们发现结果和上面一致
获取子进程退出的结果
wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
exit(15),status是3840
status并不是按照整数来整体使用的。而是按照比特位的方式,将32个比特位进行划分,其中我们学习的是低16个
status构成:
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特
位):
15-8叫次低8位表示子进程退出时的退出码
如果想得到子进程退出结果,右移8位,然后和1111 1111按位与,此时拿到了退出码15
父进程可以通过子进程的退出码来查看子进程的具体情况
程序异常退出或者崩溃,本质是操作系统杀掉了该进程
操作系统如何杀掉?
本质是通过发送信号的方式!下图都是信号
其实没有0信号,如果收到的信号是0,说明我们的进程是正常跑完的,测试异常
8号信号是,8号信号是浮点数错误,这里的0代表退出码无意义,因为我们的退出码是15
如果我们用kill -9 ,子进程也会收到9号信号
程序异常,不仅是内部代码有问题,也可能是外部杀掉的
总结
父进程通过wait/waitpid可以拿到子进程的退出结果(退出码和推出信号), 不能用全局变量来代替退出码
如果父进程在子进程结束之前退出,则子进程将由init接管。init将会以父进程的身份对僵尸状态的子进程进行处理。
使用wait或waitpid,当子进程退出的时候,父进程还存在,从某种意义上说wait/waitpid可以让进程退出具有一定的顺序性,这让设置的目的让父进程将来可以有更多的收尾工作
僵尸进程:至少要保留该进程的PCB信息,task_struct里面保留了任何进程退出时的退出结果信息。wait和waitpid本质是读取子进程的task_struct的结构
wait和waitpid有权利访问是task_struct(内核数据结构)因为wait和waitpid是系统调用的
waitpid详解
如果用位运算获取进程编号和退出码会比较麻烦,系统给我们提供了宏,我们可直接拿来使用
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
当waitpid()第三个参数设置为WNOHANG的时候 ,父进程就成了非阻塞状态
Linux是用C语言写的->系统调用接口->OS自己提供的接口->就是C语言函数->系统提供的一般大写的标记位如WNOHANG,其实就是宏
#define WNOHANG 1
WNOHANG其实就是wait no hang(夯住了),夯住了在系统层面上就是这个进程没被CPU调度。此时要么是在阻塞队列中,要么等待被调度。
非阻塞等待:如果父进程检测子进程的退出状态,发现子进程没有退出,我们的父进程通过调用waitpid来进行等待,如果子进程没有退出,waitpid这个系统调用,立马返回。
阻塞等待一般都是在内核中阻塞,阻塞队列会等待被唤醒,一般一个进程被阻塞了就会被切换
伪代码:
非阻塞等待会有一个基于非阻塞调用的轮询检测方案(就是不停的进行访问,当条件不满足时会做其他事情)
#include <iostream> #include <vector> #include<stdio.h> #include <stdlib.h> #include <unistd.h> #include <sys/wait.h> typedef void (*handler_t)(); //函数指针类型 std::vector<handler_t> handlers; //函数指针数组 void fun_one() { printf("这是一个临时任务1\n"); } void fun_two() { printf("这是一个临时任务2\n"); } // 设置对应的方法回调 // 以后想让父进程闲了执行任何方法的时候,只要向Load里面注册,就可以让父进程执行对应的方法喽! void Load() { handlers.push_back(fun_one); handlers.push_back(fun_two); } int main() { pid_t id = fork(); if(id == 0) { // 子进程 int cnt = 5; while(cnt) { printf("我是子进程: %d\n", cnt--); sleep(1); } exit(11); // 11 仅仅用来测试 } else { int quit = 0; while(!quit) { int status = 0; pid_t res = waitpid(-1, &status, WNOHANG); //以非阻塞方式等待 if(res > 0) { //等待成功 && 子进程退出 printf("等待子进程退出成功, 退出码: %d\n", WEXITSTATUS(status)); quit = 1; } else if( res == 0 ) { //等待成功 && 但子进程并未退出 printf("子进程还在运行中,暂时还没有退出,父进程可以在等一等, 处理一下其他事情??\n"); if(handlers.empty()) Load(); for(auto iter : handlers) { //执行处理其他任务 iter(); } } else { //等待失败 printf("wait失败!\n"); quit = 1; } sleep(1); } } return 0; }