目录
什么是进程等待
为什么要进行进程等待?
wait()
waitpid()
status的使用★
options★
问题:既然进程具有独立性,进程退出码不也是子进程数据吗,父进程凭什么拿到呢?wait/waitpid究竟做了什么呢?
进程等待是指父进程等待其子进程终止的一种机制。在多进程系统中,父进程创建子进程并且可能需要等待子进程执行完毕,以便获取子进程的退出状态或执行其他操作。
需要注意的是:父进程等待子进程终止是一个阻塞操作,即父进程会暂停自己的执行,直到子进程终止。
1.回收僵尸进程
2.获取子进程退出状态.
这是对上面两句话详细说明:
1.之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
2.最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
结论:父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息
接下来我们要制造一种僵尸状态.
输入以下代码:
1 #include
2 #include
3 #include
4 #include
5 int main()
6 {
7 pid_t id = fork();
8 if(id < 0)
9 {
10 perror("fork");
11 exit(1);//标识进程运行完毕,结果不正确
12 }
13 else if(id == 0)
14 {
15 //子进程
16 int cnt = 5;
17 while(cnt)
18 {
19 printf("cnt: %d, 我是子进程,pid: %d,ppid: %d\n",cnt,getpid(),getppid());
20 sleep(1);
21 cnt--;
22 }
23 exit(0);
24
25 }
26 else
27 {
28 while(1)
29 {
30 printf("我是父进程,pid: %d,ppid: %d\n",getpid(),getppid());
31 sleep(1);
32 }
33 }
34
35 return 0;
36 }
这样子进程循环5次后退出,而父进程一直在循环,无法退出,造成了子进程无法被回收的情况.
然后退出vim,make编译并执行,同时再创建一个窗口,检测该进程的状态,在新窗口输入:
while :; do ps axj | head -1 && ps ajx | grep myproc | grep -v grep;sleep 1; echo "-------------------------------"; done
5秒后子进程退出,只有父进程在运行了.
此时观察进程状态也知,5秒后子进程的状态变成了Z+,僵尸状态.
所以此时我们如何回收僵尸进程呢,便用到了wait接口.
我们man 2 wait查看一下其用法.
它的作用是等待一个进程,直到其状态发生变化.
这个参数status是用来获取子进程结果的,这个在后面waitpid会讲的,都是一样的.目前写NULL即可.
然后看一下返回值:
陈工,返回子进程的pid,否则返回-1.
此时我们使用它,把父进程模块里的代码做如下改动:
先输出一次,然后再等待子进程,等待成功的话,会输出“等待子进程成功”.
退出vim,make编译.同时还是右边窗口监视.
这样父进程就一直会等子进程从运行直到死亡,状态发生变化时,将其回收.
我们发现,此时子进程在5秒后没有产生僵尸状态,而是直接没有了,只剩下父进程在继续运行。
这个时候,子进程就是被父进程回收了,wait等到了状态变化.
那么waitpid和这个wait又有什么区别呢?
同样地我们man 2 waitpid查看用法.
第一个参数pid, 是用来等待特定pid的子进程。
有如下两个选择:
1.Pid=-1,等待任一个子进程。与wait等效。
2.Pid>0.等待其进程ID与pid相等的子进程。
第三个参数options 默认为0,表示阻塞等待.
第二个参数status,是一个指针类型,它是一个输出型参数.
比如我们在函数外定义一个int status = 0,
然后把这个status传入到waitpid中的第二个参数中。
然后 函数结束的时候,会把子进程的退出结果自动填充到这个 status里面去,这便是输出型参数.
所以来说,waitpid(-1,NULL,0)等价于wait(NULL).
我们把父进程中的wait改成waitpid:
make编译.并运行
结果是同样的.
那我们来使用一下status吧.
我们手动的在子进程模块添加一个退出码69,然后父进程模块中先定义好status变量,然后再将其传入到waitpid中,最后再将其输出.
退出来,make编译并运行.
我们发现,退出码不是69,而是这么大一个数,这是什么情况呢?
其实status并不是按照整数来整体使用的,而是按照比特位的方式,将32个比特位进行划分.
我们只学习低16位.这个低16位就基本上满足我们需求了.
其中,这次低8位标识子进程的退出码.相当于第8-15位.我们想拿到它,首先得先将其右移8位,然后&0xFF,0xFF是只有后8位是1,其余位都是0,而任何&1是它本身,&0都是0,这样就拿到了这8位.
此时我们便拿到了我们想要的结果69了.
每次这么写感觉很麻烦,那么有没有更快一点的方法呢?
C标准库提供了一些宏:
WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(status):若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
我们具体看一下如何用:
运行:
可以看到此时子进程正常结束,并获得了我们返回的退出码
如果此时还是在子进程加上一个除0错误,然后再次输出.
可以看到子进程异常退出,而 WIFEXITED这个返回了一个值0.
相对来说也是比较简单的.
终止信号
再来说进程异常退出或者崩溃,本质是操作系统杀掉了进程.
那OS是如何杀掉的呢?本质是通过发送信号的方式杀掉进程.
这是Linux下的所有信号.
所以那个最低7个比特位,即终止信号那里,表示的是进程收到的信号.
其中code dump在后面讲解信号的时候会说明。
我们同样的输出一下最后7个比特位的值.
这个时候需要获取最后7个比特位,那就要&上0111 1111,这个十六进制表示0x7 F.
运行:
子进程最后收到的信号是0,说明是正常跑完的.
而退出码是在正常跑完的基础上,用来判断结果正确还是不正确的.
那我们此时写一个除0错误,让子进程异常结束.
运行:
我们发现子进程收到的信号是8,信号8是SIGFPE,代表浮点数计算错误。
此时由于程序都没有正常跑完,退出码便没有了意义。因为程序都没有跑完,此时再判断结果正确已经没意义了.
而且程序异常,不只是内部代码有问题,也有可能是外部原因(如信号等).
我们之前写的waitpid,父进程一直在阻塞等待直至子进程结束,也就是说子进程结束之前父进程就一直在等待,什么事情也做不了,这好像有点怪,那么我们能不能让父进程非阻塞等待呢?
这里就用到了waitpid的第三个参数.
WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,立马返回。若正常结束,则返回该子进程的ID。
这个其实是个宏,内部其实是#define WNOHANG 1,也就是说你第3个参数填WNOHANG可以,填1也可以.
下面是waitpid的一段伪代码,当检测到子进程没有退出时,若检测到options是0,则父进程会直接被阻塞,本质是阻塞在系统函数内部,等待子进程被唤醒.
而如果是1,即WNOHANG,会直接返回,而不进行阻塞等待.
那么问题来了,当options==0,子进程被唤醒时,是从if后面继续向下执行,还是重新执行waitpid呢?
但是是继续执行if后面的语句,因为寄存器EIP保存的是下一行代码的地址.
我们换做代码来理解一下它.
如果子进程没有退出,那么就一直做检测,相当于轮询检测.
可以看到,子进程运行期间,父进程每隔一秒都会轮询检测一下,当子进程结束时,waitpid也检测到子进程退出,此时res>0,便输出子进程的退出信息.然后父进程循环同时也结束了,结束进程.
当然可以让父进程具体处理一些任务,比如在函数最前面加上这些代码:
在等待期间的这个模块:
然后我们再次执行:
这个时候,父进程在等待子进程推出的同时也可以执行对应的任务了.
我们需要从僵尸进程谈起:它是一个死亡的进程,但是至少要保留该进程的PCB信息,里面保留了任何进程退出时的退出结果信息!
僵尸进程保留自己退出信息的目的就是让别的进程来读取的!
wait和waitpid本质上是读取子进程的task_struct结构。
我们看一下内核源代码:
wait/waitpid本质是读取到这两个字段,然后位操作设置到status这个输出型变量里,我们就拿到了.
那么wait和waitpid有权限去读取这个内核数据结构内容吗?
答案是一定有,因为wait和waitpid是系统调用!系统调用就是操作系统在调用。
我父进程没有权限读取,但我可以调用wait和waitpid让操作系统帮我去拿.
总结来说:父进程没有权限,但是wait/waitpid有权限,父进程可以调用wait/waitpid获取子进程的退出状态.
wait/waitpid本质上是把读取的子进程的退出状态 通过位操作设置到status这个输出型变量中.
所以到这里,进程的等待就结束了.