进程等待
进程等待的必要性
我们之前提到过僵尸进程,僵尸进程就是子进程先于父进程退出,子进程的退出状态信息发送给父进程但是父进程忽略处理,子进程就变成了僵尸进程,解决僵尸进程我们有三种办法, 第一就是杀死父进程,子进程就变成了孤儿进程,会被一号进程领养,一号进程会回收子进程的退出状态信息;第二种就是关机,操作系统一关,所有进程都没了;第三种就是今天的要介绍的进程等待。
父进程进行进程等待,等待子进程退出之后,回收子进程的退出状态信息,防止子进程变成僵尸进程。
wait函数
函数原型:
pid_t wait(int *status);
返回值:等待成功返回被等待进程的pid,等待失败返回-1.
参数:输出型参数,获取子进程的退出状态信息,如果不关心子进程的退出状态信息可以设置为NULL。
status是wait函数填充的,程序员在wait函数调用结束之后,可以通过参数获得退出进程的退出状态信息。
wait函数是一个阻塞调用函数(一定等待到被等待进程的退出状态信息才会结束函数调用)
我们来验证一下wait函数的阻塞属性:
上面的代码有两个进程,父进程和子进程,父子进程相互独立,抢占式执行,所以就有了两种可能性,第一就是父进程先执行,子进程后执行,此时子进程有可能变成僵尸进程,如果子进程没有变成僵尸进程,那就说明父进程的wait函数在等待中,是具有阻塞属性的;第二种是子进程先执行,父进程再执行,这种情况,wait等到到子进程的退出状态信息就很顺理成章了。
没有产生僵尸进程,说明两种情况下,wait都等待到了。
我们再让子进程睡眠十秒,来看一下效果:
运行结果:
子进程并没有变成僵尸进程,我们查看父进程的调用堆栈,能看到父进程正在苦苦等待子进程结束。
pstack:查看进程的调用堆栈,也就是说,用来查看进程正在运行什么代码。
我们回到wait函数的参数,是一个指向int类型的指针,我们说它是输出型参数,它指向的那个int类型的数字包含了wait向程序员传递的退出进程的退出状态信息。它一共有四个字节,不过,包含进程退出状态信息的只有后面两个字节,其中一个字节包含了退出码,两外一个字节的第一个比特位是coredump标志位,后七个比特位是退出信号位。
那如何通过进程退出状态信息判断进程是否是正常退出呢?
标准是看退出信号是否有值,如果退出信号等于0,那就是正常终止,只有正常终止才会有退出码,如果退出信号大于0,那就是异常终止,我们上次提到的那个段错误,它前面就有一个信号11,这种就是异常终止。
coredump:标志着是否产生核心转储文件,不标志进程是否异常终止。如果该比特位是1,那就是产生了核心转储文件,程序确实是异常终止。程序异常终止的时候,理论上是要产生核心转储文件的,不过此时还要关心core file size的值,如果该值是0,就有一个软限制,不允许产生核心转储文件,所以此时该比特位值是0,但是进程依然是异常终止,修改为unlimited就可以在异常终止的时候产生coredump文件,比特位就会变成1了。
我们如何通过wait的参数得到退出信号呢?
退出信号:status & 0x7f
coredump标志位:(status>>7) & 0x1
退出码:(status>>8) & 0xff
我们通过下面这段代码来举例说明一下:
运行结果:
运行结果:
waitpid函数
函数原型:
pid_t waitpid(pid_t pid, int *status, int options);
参数:
- pid:
- pid==-1,等待任意一个子进程(一个父进程可以有多个子进程),与wait等效
- pid>0,等待进程ID与pid相等的进程。
- status,进程退出状态信息,和wait函数的参数相同
- options:waitpid函数可以有非阻塞属性,该属性就是通过该参数设置的。如果option参数为WNOHANG,就是设置waitpid函数为非阻塞状态,如果pid指定的子进程没有结束,则waitpid()函数返回0,不会一直等待,如果pid指定的子进程正常结束,就返回该子进程的ID。如果设置为0,那就是阻塞的,和wait函数的阻塞属性一样。
如果是非阻塞调用函数,有一个很明显的问题就是我们不知道函数是否完成了既定的功能,所以 非阻塞调用一定要搭配循环使用 。
waitpid函数返回值:
- 正常返回的时候waitpid返回收集到的子进程的进程号
- 如果设置了选项WNOHANG,调用中waitpid发现没有已经退出的子进程可以收集,则返回0
- 如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在。
验证waitpid函数的阻塞以及waitpid函数的等待功能:
waitpid函数被设置了阻塞属性,一直等待到子进程结束之后等待到子进程的退出状态信息才结束调用,且我们查看进程状态的时候,父进程确实在等待子进程。且子进程并没有变成僵尸进程,说明子进程的退出状态信息被父进程成功回收。
验证waitpid可以有的非阻塞属性:
子进程变成了僵尸进程,父进程陷入睡眠当中,验证了waitpid的非阻塞属性。
我们前面提到,在调用非阻塞属性的函数时,要使用循环,循环可以保证非阻塞函数完成我们想要的功能。
我们来验证一下waitpid函数的非阻塞属性,加上循环,通过循环来让它持续等待。
#include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<sys/wait.h>
5 int main(){
6 pid_t ret = fork();
7 if(ret < 0){
8 return 0;
9 }else if(ret == 0){
10 //child;
11 printf("I am child.\n");
12 sleep(30);
13 printf("I am child,I exited....\n");
14 }else{
15 //father
16 while(1){
17 int w_ret = waitpid(-1,NULL,WNOHANG);
18 //返回值:
19 //正常情况:
20 // >0:返回等待到的子进程的PID
21 // =0:没有等待到
22 //不正常情况下返回-1
23 if(ret == w_ret){
24 break;
25 }else if(w_ret == -1){
26 return 0;
27 }
28 }
29 printf("I am father.\n");
30 while(1){
31 sleep(1);
32 }
33 }
34 return 0;
35 }
父进程成功回收到子进程的退出状态信息,在等待到子进程的退出状态信息之前父进程一直在执行waitpid函数,最后子进程也没有变成僵尸进程,说明waitpid函数成功完成函数功能。
当非阻塞遇到循环,不执行完功能就会一直循环,那其他功能要怎么实现呢?比如父进程的守护,这个要用到进程信号的知识,我们后面再给出答案。