三、进程等待
1.进程等待的必要性
1.子进程退出,父进程如果不获取到子进程的退出信息,就可能造成 僵尸进程 的问题,进而造成内存泄漏。
2.进程一旦变成僵尸状态,所谓的 kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
3.父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
4.父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
2.进程等待方法
1.wait方法
函数原型以及所需头文件
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status);
返回值:等待成功则返回等待进程的PID,等待失败,返回-1;
参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> /* 代码含义:通过fork创建子进程,实现5次打印,然后终止掉子进程,子进程便处于僵尸状态。同时父进程是处于等> 待状态的,在10秒后开始对子进程进行处理(也就是获取子进程的pid),处理结束,子进程退出。父进程此时再次等 待10秒后退出。 */ int main() { pid_t id = fork(); if(id == 0){ int ret = 5; while(ret){ printf("child[%d] is running:ret is %d\n", getpid(), ret); ret--; sleep(1); } exit(0); } sleep(10); printf("father wait begin..\n"); pid_t cur = wait(NULL); if(cur > 0){ printf("father wait:%d success\n", cur); } else{ printf("father wait failed\n"); } sleep(10); }
对以上代码编译之后,编写一个shell脚本,进行进程的持续检测。
[mlg@VM-20-8-centos lesson4-进程控制]$ while :; do ps axj | head -1 && ps axj | grep mytest | grep -v grep; sleep 1; echo "**********************"; done
2.waitpid方法
函数原型以及所需头文件
#include <sys/types.h> #include <sys/wait.h> 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,不予以等待。若正常结束,则返回该子进程的PID。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> /* 代码含义:通过fork创建子进程,实现5次打印,然后终止掉子进程,子进程便处于僵尸状态。同时父进程是处于等> 待状态的,在10秒后开始对子进程进行处理(也就是获取子进程的pid),处理结束,子进程退出。父进程此时再次等 待10秒后退出。 */ int main() { pid_t id = fork(); if(id == 0){ int ret = 5; while(ret){ printf("child[%d] is running:ret is %d\n", getpid(), ret); ret--; sleep(1); } exit(0); } sleep(10); printf("father wait begin..\n"); //pid_t cur = waitpid(id, NULL, 0);//等待指定一个子进程 pid_t cur = waitpid(-1, NULL, 0);//等待任意一个子进程 if(cur > 0){ printf("father wait:%d success\n", cur); } else{ printf("father wait failed\n"); } sleep(10); }
结果和wait一样
3.获取子进程status
1.什么是status
int* status:它是一种输出型的参数
所谓获取子进程的status,就是获取子进程退出时的退出信息;
首先,在子进程中分别用exit(0)和exit(10)来中断子进程,父进程获取status值,判断进程的退出状态
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0){ int ret = 3; while(ret){ printf("child[%d] is running:ret is %d\n", getpid(), ret); ret--; sleep(1); } exit(0);//比较exit(10)或任意值 } printf("father wait begin..\n"); int status = 0; pid_t cur = waitpid(id, &status, 0); if(cur > 0){ printf("father wait:%d success,status:%d\n", cur, status); } else printf("father wait failed\n"); } }
通过上面的运行结果,我们本来以为status应该是0和10,但和预期的结果却有所不同。这里我们仔细思考一下: 父进程拿到什么样的status结果,一定是和子进程如何退出强相关的。子进程退出问题不就是进程退出嘛。
进程退出不就是三种情况:
1.代码运行完毕,结果正确
2.代码运行完毕,结果不正确
3.代码异常终止
也就是父进程只要通过status反馈出这三种情况,做出相应的决策。
所以代码中的 int status 就不能简单的理解为单纯的整数了!!!!!!
2.status的构成
在上文中,我们对status有了一定的了解后,接下来谈一谈status的构成。
status是由32个比特位构成的一个整数,目前阶段我们只使用低16个位来表示进程退出的结果,如下图所示,就是status低16位的表示图;
进程正常退出有两种,与退出码有关,异常退出与信号有关;(结合上文的进程退出的概念)所以这里我们就需要获取到两组信息:退出码与信号;如果没有收到信号,就表明我们所执行的代码是正常跑完的,然后在判断进程的退出码,究竟是何原因使进程结束的;反之则是异常退出,也就不需要关心退出码了;
3.如何获取status
结合下图,我们用次低8位表示进程退出时的退出状态,也就是退出码;用低7位表示进程终止时所对应的信号;
此时,我们想要拿到这个退出码和信号的值,我们是不是只要拿到了这低16个比特位中的次低8位和低7位就可以了;具体操作如下图所示
status exit_code = (status >> 8) & 0xFF; //退出码 status exit_code = status7 & 0x7F; //退出信号
接下来将上面的代码重新整理如下,测试子进程的退出状态时的退出码及是否获取到了信号
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0){ int ret = 3; while(ret){ printf("child[%d] is running:ret is %d\n", getpid(), ret); ret--; sleep(1); } exit(10); } printf("father wait begin..\n"); int status = 0; pid_t cur = waitpid(id, &status, 0); if(cur > 0){ printf("father wait:%d success, status exit_code:%d, status exit_signal:%d\n", cur, (status >> 8)& 0xFF, status & 0x7F); } else printf("father wait failed\n"); } }
通过运行结果可以看出,刚好与我们所给的退出码对应,并且没有接受到信号,意味着正常退出,但是这里我们所给的退出码是10,至于退出码的含义可以自由设定。
综上所述,我们理解一下进程退出的三种情况:
1.代码运行完毕,结果正确;对应如下:
2.代码运行完毕,结果不正确;对应如下:
3.代码异常终止;对应如下:
接下来我们在看一段代码以及运行结果:
我们通过这三张图,可以发现,命令行解释器(bash)能够获取到退出码,并且bash是命令行启动的所有进程的父进程(不难看出,我们通过进程查看7120,相应的进程就是bash)所以,bash也一定是通过子进程去执行这段程序,也一定通过wait方式得到子进程的退出结果,刚好我们能看到echo $?能够查到子进程的退出码!
其实这里就是想说,系统也是有自带的,能够获取退出码与退出信号的宏
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0){ int ret = 3; while(ret){ printf("child[%d] is running:ret is %d\n", getpid(), ret); ret--; sleep(1); } exit(1); } printf("father wait begin..\n"); int status = 0; pid_t cur = waitpid(id, &status, 0); if(cur > 0){ if(WIFEXITED(status)){//没有收到任何退出信号的 //正常结束,获取对应的退出码 printf("exit code:%d\n",WEXITSTATUS(status)); } else{ printf("error:get a signal!\n"); } } }
4.阻塞等待与非阻塞等待
这里我们所讲的阻塞等待和非阻塞等待,其实就是waitpid函数的第三个参数,我们之前并未提及,直接给的是0,这种是默认行为,阻塞等待;如果设置为WNOHANG,表示的是非阻塞等待方式。
阻塞等待:父进程一直在等待子进程,什么事都不干,直到子进程正常退出。
非阻塞等待:父进程的PCB由运行队列转变为等待队列,直达子进程结束,操作系统获取到子进程退出的信号时,再将父进程从等待队列中调度到运行队列,由父进程去获取子进程的退出码以及退出信号。
//基于阻塞等待的轮询访问 #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0){ int ret = 10; while(ret){ printf("child[%d] is running:ret is %d\n", getpid(), ret); ret--; sleep(1); } exit(1); } int status = 0; while(1){ pid_t cur = waitpid(id, &status, WNOHANG); if(cur == 0){ //子进程没有退出,但是waitpid等待是成功的,需要继续重复进行等待 printf("Do father things!\n"); } else if(cur > 0){ //子进程退出了,waipid也成功了,获取到了对应的结果 printf("father wait:%d success, status exit_code:%d, status exit_signal:%d\n", cur, (status >> 8)& 0xFF, status & 0x7F); break; } else{ //等待失败了 perror("waitpid"); break; } sleep(1); } }
从运行结果可以看出,父进程在不断的查看子进程是否退出,若未退出,则父进程先去忙自己的事情,过一段时间再来查看,直到子进程退出后读取子进程的退出信息。