Linux进程控制(二)---进程等待

目录

什么是进程等待

为什么要进行进程等待?

wait()

waitpid()

status的使用★

options★

问题:既然进程具有独立性,进程退出码不也是子进程数据吗,父进程凭什么拿到呢?wait/waitpid究竟做了什么呢?

什么是进程等待

进程等待指父进程等待其子进程终止的一种机制。在多进程系统中,父进程创建子进程并且可能需要等待子进程执行完毕,以便获取子进程的退出状态或执行其他操作。

需要注意的是:父进程等待子进程终止是一个阻塞操作,即父进程会暂停自己的执行,直到子进程终止。

为什么要进行进程等待?

1.回收僵尸进程

2.获取子进程退出状态.

这是对上面两句话详细说明:

1.之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
另外,进程一旦变成僵尸状态,那就刀枪不入,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
2.最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
结论:父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

接下来我们要制造一种僵尸状态.

输入以下代码:

1 #include<stdio.h>
  2 #include<stdlib.h>
  3 #include<unistd.h>
  4 #include<sys/types.h>
  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接口.

wait()

我们man 2 wait查看一下其用法.

它的作用是等待一个进程,直到其状态发生变化. 

这个参数status是用来获取子进程结果的,这个在后面waitpid会讲的,都是一样的.目前写NULL即可.

然后看一下返回值:

 陈工,返回子进程的pid,否则返回-1.

此时我们使用它,把父进程模块里的代码做如下改动:

先输出一次,然后再等待子进程,等待成功的话,会输出“等待子进程成功”.

退出vim,make编译.同时还是右边窗口监视.

这样父进程就一直会等子进程从运行直到死亡,状态发生变化时,将其回收.

 我们发现,此时子进程在5秒后没有产生僵尸状态,而是直接没有了,只剩下父进程在继续运行。

这个时候,子进程就是被父进程回收了,wait等到了状态变化.

那么waitpid和这个wait又有什么区别呢?

waitpid()

同样地我们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的使用★

那我们来使用一下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,代表浮点数计算错误。

此时由于程序都没有正常跑完,退出码便没有了意义。因为程序都没有跑完,此时再判断结果正确已经没意义了.

而且程序异常,不只是内部代码有问题,也有可能是外部原因(如信号等).

options★

我们之前写的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,便输出子进程的退出信息.然后父进程循环同时也结束了,结束进程.

当然可以让父进程具体处理一些任务,比如在函数最前面加上这些代码:

在等待期间的这个模块:

 

 然后我们再次执行:

这个时候,父进程在等待子进程推出的同时也可以执行对应的任务了. 

问题:既然进程具有独立性,进程退出码不也是子进程数据吗,父进程凭什么拿到呢?wait/waitpid究竟做了什么呢?

我们需要从僵尸进程谈起:它是一个死亡的进程,但是至少要保留该进程的PCB信息,里面保留了任何进程退出时的退出结果信息!

僵尸进程保留自己退出信息的目的就是让别的进程来读取的!

wait和waitpid本质上是读取子进程的task_struct结构。

我们看一下内核源代码:

 wait/waitpid本质是读取到这两个字段,然后位操作设置到status这个输出型变量里,我们就拿到了.

那么wait和waitpid有权限去读取这个内核数据结构内容吗?

答案是一定有,因为wait和waitpid是系统调用!系统调用就是操作系统在调用。

父进程没有权限读取,但我可以调用wait和waitpid让操作系统帮我去拿.

总结来说:父进程没有权限,但是wait/waitpid有权限,父进程可以调用wait/waitpid获取子进程的退出状态.

wait/waitpid本质上是把读取的子进程的退出状态 通过位操作设置到status这个输出型变量中.

所以到这里,进程的等待就结束了.

猜你喜欢

转载自blog.csdn.net/weixin_47257473/article/details/131802018