博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
【Linux】进程控制(三):进程等待 wait 和 waitpid
阅读量:4155 次
发布时间:2019-05-25

本文共 9358 字,大约阅读时间需要 31 分钟。

【Linux】进程控制(三):进程等待 wait 和 waitpid

文章目录


一、 进程等待的必要性

  ● 子进程退出,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而造成内存泄漏。

  ● 进程一旦变成僵尸状态,就会刀枪不入,kill -9 强杀也无能为力,

  ● 父进程派给子进程的任务完成的如何,父进程得知道,

  ● 父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。


二、 进程等待的方法

2.1 wait 方法

#include 
#include
pid_t wait(int *status);

  返回值 pid_t : 成功返回被等待进程的 PID,失败返回 -1。

  参数 status: 输出型参数,获取子进程的退出状态,不关心则可以设置为 NULL。调用函数时不用传入具体的值,在函数内部赋予值,在函数返回之后,调用者就可以拿到函数当中赋予的值。


2.2 waitpid 方法

#include 
#include
pid_t waitpid(pid_t pid, int *status, int options);

  返回值 pid_t: 正常返回子进程的进程 PID,功能完成。

         等于 0,非阻塞,功能没有完成。

         等于 -1,函数调用中出错。

  参数 pid: 告诉 waitpid 函数需要等待的子进程的进程 pid。pid == -1 表示等待任意进程,pid > 0 表示等待指定子进程,子进程的进程号就为传入的 pid 的值。

  参数 status: 输出型参数,获取子进程的退出状态。和 wait 函数中的 status 参数含义一致。

  参数 options: 设置 waitpid 函数是阻塞还是非阻塞,0 表示阻塞,WNOHANG 表示非阻塞。如果调用非阻塞,当子进程没有退出时,waitpid 函数会报错返回。

  waitpid (pid>0, status, 0),相当于 wait 函数。wait 函数就是调用 waitpid 函数实现的。


  补充:

  (1) 函数的参数逻辑类型

  输入型参数 &: 调用函数的时候,传入具体的值,在函数的内部进行使用。一般在代码当中使用 & 传参,不发生值拷贝。

  输出型参数 : 调用函数的时候不用传入具体的值,在函数内部赋予值,在函数返回之后,调用者就可以拿到函数当中赋予的值。一般在代码中使用 * 传参,传入参数的地址。

  输入输出型参数 : 既需要传入具体的值,也需要在函数内部赋予值,之后再返回给调用者。一般在代码中也是使用 * 传参。

  (2) 阻塞与非阻塞

  阻塞: 当调用函数需要等待一定条件成熟的时候,条件成熟则返回,条件不成熟,则一直等待。

  非阻塞: 当调用函数需要等待一定条件成熟的时候,条件成熟则返回,条件不成熟,则返回报错信息。


三、获取子进程 status

  wait 和 waitpid 都有 status 参数,该参数是 int 型且是输出型参数,由操作系统填充。如果传递 NULL 表示不关心子进程的退出状态,否则,操作系统会根据 status 参数,将子进程的退出信息给父进程。

  int 类型为 4 个字节,但是 status 只用到了后面 2 个字节(16 个比特位)。

image-20210701175109855

  获取退出码: (status >> 8) & 0xFF

  获取 coredump 标志位: (status >> 7) & 0x01

​              等于 1,有 coredump 标志位;

              等于 0,没有 coredump 标志位。

  获取进程终止信号: status & 0x7f

  判断一个进程是否正常退出,只需要判断是否接收到进程终止信号。大于0,异常退出,有进程终止信号;等于0,正常退出。


  验证:

  (1) 获取退出码

#include 
#include
#include
#include
#include
int main(){
pid_t pid = fork(); if(pid < 0) {
perror("创建失败\n"); return 0; } else if(pid == 0) {
//子进程 int count = 10; while(1) {
if(count <= 0) {
break; } printf("i am child pid=[%d] ppid=[%d]\n",getpid(),getppid()); count--; sleep(1); } exit(10); } else {
//父进程pid > 0 printf("begin ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); int status; wait(& status); //父进程在等待子进程的退出 printf("exit_code : %d\n",(status >> 8) & 0xFF); //获取退出码 //父进程应该在等待,等待子进程退出,退出后才会进入下面这个循环 while(1) {
printf("end ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); sleep(1); } } return 0;}

  可以看到父进程打印完 begin ---> i am father pid=[2469] ppid=[6971] 后阻塞,子进程循环打印,直至退出,退出码为 10。

image-20210702101924817


  (2)获取进程终止信号

#include 
#include
#include
#include
#include
int main(){
pid_t pid = fork(); if(pid < 0) {
perror("创建失败\n"); return 0; } else if(pid == 0) {
//子进程 while(1) {
printf("i am child pid=[%d] ppid=[%d]\n",getpid(),getppid()); sleep(1); } } else {
//父进程pid > 0 printf("begin ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); int status; wait(& status); //父进程在等待子进程的退出 printf("sig_code : %d\n",status & 0x7F); //获取进程终止信号 //父进程应该在等待,等待子进程退出,退出后才会进入下面这个循环 while(1) {
printf("end ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); sleep(1); } } return 0;}

  可以看到父进程在打印完 begin ---> i am father pid=[4559] ppid=[6971] 后阻塞,子进程循环打印,在另一终端 kill -9 4560 强杀子进程,打印退出码 9,ps aux | grep wait查看到子进程已经退出。

在这里插入图片描述


  (3) 获取 coredump 标志位

#include 
#include
#include
#include
#include
int main(){
pid_t pid = fork(); if(pid < 0) {
perror("创建失败\n"); return 0; } else if(pid == 0) {
//子进程 int * p = NULL; *p = 20; //解引用空指针,这里会异常退出 while(1) {
printf("i am child pid=[%d] ppid=[%d]\n",getpid(),getppid()); sleep(1); } } else {
//父进程pid > 0 printf("begin ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); int status; wait(& status); //父进程在等待子进程的退出 printf("sig_code : %d\n",status & 0x7F); //获取进程终止信号 printf("coredump_code:%d\n",(status >> 7) & 0x1); //获取 coredump 标志位 //父进程应该在等待,等待子进程退出,退出后才会进入下面这个循环 while(1) {
printf("end ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); sleep(1); } } return 0;}

  可以看到父进程在打印完 begin ---> i am father pid=[3148] ppid=[6944] 后阻塞,子进程解引用空指针,子进程异常退出,终止信号 11 SIGSEGV,coredump 标志位为1,产生 core.3149 文件。 这里的 coredup 文件的原理可参考下面这篇文章详细了解。

  

image-20210703142513405


四、wait 的使用

  常见一个子进程,子进程正常逻辑,父进程调用 wait 函数进行进程等待,当子进程退出时,父进程由于在等待,所以子进程不会变成僵尸进程。

  代码来验证一下

#include 
#include
#include
#include
int main(){
pid_t pid = fork(); if(pid < 0) {
perror("创建失败\n"); return 0; } else if(pid == 0) {
//子进程 pid == 0 int count = 10; while(1) {
if(count <= 0) {
break; } printf("i am child pid=[%d] ppid=[%d]\n",getpid(),getppid()); count--; sleep(1); } } else {
//父进程pid > 0 printf("begin ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); int status; wait(& status); //父进程在等待子进程的退出 //父进程应该在等待,等待子进程退出,退出后才会进入下面这个循环 while(1) {
printf("end ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); sleep(1); } } return 0;}

  可以看到父进程的 PID 是 14240,子进程的 PID 14241。父进程打印 begin ---> i am father pid=[14240] ppid=[6971] 之后,就在等待子进程的退出,子进程循环 10 次退出,父进程才进入循环。

image-20210701210128487

  通过 ps aux | grep wait 查看进程信息,可以看到子进程最后退出,没有变成僵尸进程正常退出,父进程在前台运行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XJ6aC8QJ-1625298398053)(https://raw.githubusercontent.com/ACosine/PicGo/master/img/20210701211907.png)]


  而如果将 int status; wait(&status); 注释掉的结果是:父进程和子进程都进入各自的循环中,父子进程交替打印,子进程循环 10 次之后退出,父进程继续循环打印。ps aux | grep wait 可以看到 PID = 25028 的子进程 由 S+ 变成 Z+。

在这里插入图片描述


  结论:

  (1) 父进程中的 wait 函数等待到了子进程,所以子进程没有变成僵尸进程。

  (2) 父进程当中一开始调用 wait 函数,父进程被阻塞在 wait 函数当中,看到的现象是:当父进程执行的 wait 函数之后,父进程似乎卡死在 wait 函数当中。

  (3) 子进程不退出,wait 函数调用不返回。


  问题: 子进程和父进程是两个独立的进程,子进程退出的时候,父进程是如何知道的呢?子进程又是如何通知父进程的?

  回答: 子进程在退出的时候,会给父进程发送一个 SIGCHLD 信号,但是父进程对 SIGCHLD 信号默认是忽略处理的。

  那么,这里就可以解释清楚僵尸进程是如何产生的?其实是子进程退出时,父进程收到了子进程的退出信号 SIGCHLD,但是父进程对于该信号是忽略处理的,所以导致子进程的资源无法释放。而进程等待(wait 函数)将父进程收到了子进程的退出信号 SIGCHLD 进行处理,回收子进程的资源,子进程得以正常退出就不会变成僵尸进程。


五、waitpid的使用

  (1) waitpid 的阻塞等待

#include 
#include
#include
#include
#include
int main(){
pid_t pid = fork(); if(pid < 0) {
perror("创建失败\n"); return 0; } else if(pid == 0) {
//子进程 int count = 10; while(1) {
if(count <= 0) {
break; } printf("i am child pid=[%d] ppid=[%d]\n",getpid(),getppid()); count--; sleep(1); } } else {
//父进程pid > 0 printf("begin ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); int status; waitpid(pid, &status, 0); //阻塞等待 printf("exit_code : %d\n",(status >> 8) & 0xFF); //获取退出码 printf("sig_code : %d\n",status & 0x7F); //获取进程终止信号 //父进程应该在等待,等待子进程退出,退出后才会进入下面这个循环 while(1) {
printf("end ---> i am father pid=[%d] ppid=[%d]\n",getpid(),getppid()); sleep(1); } } return 0;}

  可以看到父进程的 PID 是 8556,子进程的 PID 8557。父进程打印 begin ---> i am father pid=[8556] ppid=[6944] 之后阻塞,在等待子进程的退出,子进程循环 10 次退出,父进程这才等待到子进程的退出,方可进入循环。可以看到子进程是正常退出,退出码和终止信号都为 0。

在这里插入图片描述


  (2) waitpid 的非阻塞等待

int status;waitpid(pid, &status, WNOHANG); //非阻塞等待

  以看到父进程的 PID 是 11892,子进程的 PID 11893。父进程打印 begin ---> i am father pid=[11892] ppid=[6944] ,调用 waitpid 函数(非阻塞,WNOHANG)父子进程交替打印进行各自的循环,大概 10s 之后子进程,ps aux | grep waitpid 可以看到子进程由 S+ 变成 Z+。

在这里插入图片描述

  如果想让非阻塞 waitpid 达到功能实现子进程才退出,也是可以的,只需要给 waitpid 加上一个while 循环。(非阻塞 waitpid 函数配合循环使用)

int status;while(waitpid(pid, &status, WNOHANG) == 0);

六、守护进程和 nginx 反向代理器

  (1) 什么是守护进程?

  守护进程(Daemon Process),也就是通常说的 Daemon 进程(精灵进程),是 Linux 中的后台服务进程。它是一个生存期较长的进程,通常独立于控制终端并且周期性地执行某种任务或等待处理某些发生的事件。

  守护进程是个特殊的孤儿进程,这种进程脱离终端,为什么要脱离终端呢?之所以脱离于终端是为了避免进程被任何终端所产生的信息所打断,其在执行过程中的信息也不在任何终端上显示。由于在 Linux 中,每一个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端就称为这些进程的控制终端,当控制终端被关闭时,相应的进程都会自动关闭。

  Linux 的大多数服务器就是用守护进程实现的。比如,Internet 服务器 inetd,Web 服务器 httpd 等。


  举例:服务端在运行程序时,有可能遇到 bug 崩溃掉,那么就会导致后台程序不能提供服务,APP 就不能正常使用。为了解决这个问题,比较常用的还是守护进程

  父进程是一个守护程序,第一负责启动 wechat_svr 服务,第二判断 wechat_svr 服务是否运行正常,如果运行不正常,则需要重启 wechat_svr,也就是让守护进程再次创建子进程,立即拉起 wechat_svr,提高用户使用 wechat_svr 的体验效果。

image-20210702085958033


  (2) 假如 2s 宕机,提供不了服务,有什么解决方案?

  如果服务进程挂掉,守护进程再次创建服务进程,这之间也是有时差的,为了极致的体验效果。假设我们有多个守护进程和服务进程,倘若一个断掉,服务器还可以让另一个守护进程和服务进程启动,让用户在体验过程无宕机感觉。

  nginx 反向代理器代理服务端应答,第一个作用就是负载均衡,第二个作用就是路由转发。负载均衡就是它可能把用户所发的消息分发给一个指定的守护进程和服务进程去处理。路由转发是假如一个一个守护进程和服务进程挂掉了,那分发时就不给它分发,会分发至其它正常进程进行处理。 这就解决了守护进程再次创建服务进程之间的时间差。

image-20210702092839854


转载地址:http://uiwxi.baihongyu.com/

你可能感兴趣的文章
Netconsole to capture the log
查看>>
Build GingerBread on 32 bit machine.
查看>>
How to make SD Card world wide writable
查看>>
Detecting Memory Leaks in Kernel
查看>>
Linux initial RAM disk (initrd) overview
查看>>
Timestamping Linux kernel printk output in dmesg for fun and profit
查看>>
There's Much More than Intel/AMD Inside
查看>>
CentOS7 安装MySQL 5.6.43
查看>>
使用Java 导入/导出 Excel ----Jakarta POI
查看>>
本地tomcat 服务器内存不足
查看>>
IntelliJ IDAE 2018.2 汉化
查看>>
Openwrt源码下载与编译
查看>>
我和ip_conntrack不得不说的一些事
查看>>
Linux 查看端口使用情况
查看>>
文件隐藏
查看>>
两个linux内核rootkit--之二:adore-ng
查看>>
两个linux内核rootkit--之一:enyelkm
查看>>
关于linux栈的一个深层次的问题
查看>>
rootkit related
查看>>
配置文件的重要性------轻化操作
查看>>