详谈进程等待

目录

  • 前言
  • [1. 进程等待的必要性](#1. 进程等待的必要性)
    • [1.1 进程等待的定义](#1.1 进程等待的定义)
  • [2. 如何进行进程等待](#2. 如何进行进程等待)
    • [2.1 wait 单进程](#2.1 wait 单进程)
    • [2.2 wait 多进程](#2.2 wait 多进程)
    • [2.3 status && 退出情况](#2.3 status && 退出情况)
      • [2.3.1 status 参数构成](#2.3.1 status 参数构成)
      • [2.3.2 简证 status 参数构成](#2.3.2 简证 status 参数构成)
      • [2.3.3 进程等待失败](#2.3.3 进程等待失败)
      • [2.3.4 宏调用查看退出信息](#2.3.4 宏调用查看退出信息)
  • [3. 进程等待的原理](#3. 进程等待的原理)

前言

本篇文章继上一篇文章 进程的创建、终止 ,继续介绍关于进程控制中的进程等待,从理解进程等待的必要性,进而理解什么是进程等待,以及如何进行进程等待。


1. 进程等待的必要性

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题(也就是 Z 状态进程),进而造成内存泄漏。 所以进行进程等待的其中一个原因就是为了读取子进程状态,解决内存泄漏问题。
  • 另外,进程一旦变成僵尸状态,不管是 ctrl + c,还是 kill -9 命令都无法杀死这个进程,只能通过进程等待,将这个进程进行回收。所以进程等待的第二个原因是僵尸进程不可被 "杀死"。
  • 子进程的出现就需要回归到创建子进程的本质:为了帮助用户完成某些任务。所以既然是完成任务,用户怎么知道子进程完成的如何了,当子进程退出了,用户又该如何得知任务办完没有?结果是什么?结果正不正确?或者中间异常中止了?所以进程等待的第三个原因是为了获取子进程任务执行的结果,也即退出情况。

僵尸进程造成的内存泄露问题是必须解决的!而至于要不要关心子进程的退出情况,则是可选项,不一定每个子进程的退出可能都要关心。

1.1 进程等待的定义

通过系统调用 wait/waitpid,来对子进程进行状态检测与回收的功能。


2. 如何进行进程等待

如何进程等待呢? ----- 调用系统调用 wait/waitpid(即等待一个进程,直到进程状态发生改变)

2.1 wait 单进程

wait 就代表只有父进程有子进程,并且子进程退出了,父进程就可以通过 wait 等待子进程的退出,其中的 status 参数代表子进程的退出情况,如果不关心其退出情况,设置为 NULL 即可。返回值 > 0,代表的是等待的子进程的 pid,如果返回值 < 0,等待失败。

接下来,我们先简单看看进程等待是什么样子的。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

int main()
{
    pid_t id = fork();
    if(id < 0)
    {
     	perror("fork");
        return 1;
    }
    else if(id == 0)
    {
     	int cnt = 5;
        while(cnt)
        {
            printf("I am child, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt$
            cnt--;
            sleep(1);
        }
	exit(11);
    }
    else
    {
     	int cnt = 10;
        while(cnt)
        {
            printf("I am father, pid:%d, ppid:%d, cnt: %d\n", getpid(), getppid(), cn$
            cnt--;
            sleep(1);
        }
	pid_t ret = wait(NULL);
        if(ret == id)
        {
             printf("wait success! ret: %d\n", ret);
        }
	sleep(5);
    }
    return 0;
}

当子进程退出后,父进程通过系统调用 wait 进行进程等待,回收了子进程,因此监控中的子进程也由原来的 Z 状态变为 X 状态(看不见了),再经过 3s 睡眠后,父进程也退出了(由 bash 进行等待回收)。

如果是多进程的进程等待呢?? 又该如何进程等待?


2.2 wait 多进程

void RunChild()    
{    
    int cnt = 5;    
    while(cnt)    
    {    
        printf("I am Child Process, pid: %d, ppid:%d\n", getpid(), getppid());    
        sleep(1);    
        cnt--;    
    }    
}    
    
int main()    
{    
     for(int i = 0; i < N; i++)    
     {    
         pid_t id = fork();    
         if(id == 0)    
         {    
             RunChild();    
             exit(i);    
         }    
         printf("create child process: %d success\n", id); 	// 只有父进程才会执行         
     }    
    
     // 等待    
     for(int i = 0; i < N; i++)    
     {    
         pid_t id = wait(NULL);    
         if(id > 0)
         {
             printf("wait %d success\n", id);
         }
     }
     sleep(3);
}

借助循环结构,我们顺利的创建出多进程,并且对多个子进程进行等待回收。也即当任意一个进程退出时,wait 会回收子进程。

那如果任意一个子进程都不退出呢?----- 如果父进程在等待的子进程(一个或多个)不退出时,那么父进程也不退出,父进程会在 wait 处进行阻塞等待!换言之,wait 等待时,如果子进程不退出,父进程调用 wait 不返回,处于一直等待的状态,直到子进程退出时,父进程 wait 返回。

所以阻塞状态不一定就是等待硬件资源,这里的父进程阻塞在系统调用 wait 处,也即阻塞状态,只不过等待的不是硬件资源,而是子进程(即软件资源)。


2.3 status && 退出情况

pid_t waitpid(pid_t pid, int *status, int options);   	// 其中的 status 与 wait 一样可以置为 NULL(不关心)

返回值:
	 当正常返回的时候 waitpid 返回收集到的子进程的进程 ID;
	 如果设置了选项 WNOHANG,而调用中 waitpid 发现没有已退出的子进程可收集,则返回 0;
	 如果调用中出错,则返回-1,这时 errno 会被设置成相应的值以指示错误所在;
参数:
	 pid:
		 Pid = -1 : 等待任一个子进程。与 wait 功能等效。
		 Pid > 0 : 等待指定进程
	 status:
		 WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
		 WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
	 options:
		 WNOHANG: 若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该子进程的ID。

2.3.1 status 参数构成

  • status 是一个输出型参数,用于将子进程的退出结果带出给父进程

  • 其 int 是被当作几个部分使用的(4字节)

    // 修改部分代码:

      else if(id == 0)                                               
      {                                                              
      	......                                                                                    
          exit(1);    
      }                
      else    
      {        
      	......	
          int status = 0;              
          pid_t ret = waitpid(id, &status, 0);    
          if(ret == id)                           
          {                
               printf("wait success! ret: %d, status: %d\n", ret, status);                                                                   
          }    
          ......   
      }    
    

运行结果的 status 为 256,我们之前说过 status 即子进程的退出结果,但是子进程中明明是 eixt(1),退出码是 1 啊,怎么 waitpid 返回的退出结果是 256 呢??

这就需要我们弄清楚几个问题。

  1. 子进程婆出,一共会有几种退出场景呢? ------ 代码运行完毕,结果正确或者不正确;代码异常终止
  2. 父进程等待,期望获得子进程退出的哪些信息呢? ----- 子进程代码是否异常?没有异常,结果是否正确? 即退出码 exitcode,如果结果不正确,又是因为什么呢?(不同的退出码即可表面不同的错误信息)换言之,父进程所关心的问题,就是子进程的退出情况。

正是因为父进程所关心的内容不只一点,因此 wait / waitpid 中的 status 才要被划分成多个部分,以此兼顾到父进程关心的全部信息。父进程希望通过 waitpid 等待子进程,获得子进程的退出结果(代码是否异常中止,如果不是,结果是否正确)。

我们只考虑 status 的低 16 位,其中的低7位用来表示进程的异常情况,第 8 位是 core dump 标志位(信号章节介绍),接下来的次低 8 位 用于表示进程的退出状态。

因此虽然子进程中 exit(1),但最终整体的 status 打印出来为 256,就是因为,代码没有异常中止,status 的低7位为0,而退出码为1,因此次低8位为 000000001,结合起来就是 256。

换言之,想要检查一个进程执行时是否发生异常,只需要检查 status 的低7位即可,如果为0,说明没有异常中止,如果异常中止了,不同的位结合起来也可以涵盖所有的异常情况(异常中止的本质就是收到了某种信号,这也是为什么 kill -l 查看信号编号时,没有所谓的 0 编号的信号请求,因为 0 代表没有异常中止);低7位为0之后,再检查次低8位的退出状态即可确定子进程的退出结果是否正确!

  • 拓展问题:status 能不能直接定义成全局变量,而不使用系统调用 waitpid 获取?
    不行,因为要保证进程独立性,status是用于存储子进程的执行结果的,无论子进程如何修改,进程独立性需要保证父进程的视角是无感的, 而如果是全局变量,那么无法做到这一点。换言之,只要是一个进程想要获取另一个进程的信息,因为进程独立性,所以这件事,进程自己无法做到,需要通过操作系统(即系统调用)来完成获取。

2.3.2 简证 status 参数构成

if(ret == id)
{
	//status&0x7F: status的低7位,即终止信号
	//(status>>8)&0xFF: status的次低8位,即退出状态
    printf("wait success, ret: %d, exit sig: %d, exit code: %d\n", ret, status & 0x7F, (status >> 8) & 0xFF);
}

因为子进程退出时 exit(1),代码执行完毕,因此退出信号为 0 表无异常中止,退出码为 1.

else if(id == 0)                                               
{                                                              
	......     
	int a = 10;
	a /= 10;                                                                               
	......
}      

除 0 错误,父进程 waitpid 等待子进程,返回的 status 中的终止信号 8,即 kill -l 信号中的 8) SIGFPE,因为代码中途异常终止了,所以就没有退出码,因此退出状态就为 0。再者,只要你愿意,当你访问野指针,运行结果的 exit sig 就会是 11,当你 kill -9 杀掉一个死循环的进程时,exit sig 就会是 9 号信息,这不仅印证了 status 参数的构成,也再一次印证了,进程异常终止,其本质就是收到了某种信号!

2.3.3 进程等待失败

关于 status 的返回值:如果调用中出错,则返回 -1,这时 errno 会被设置成相应的值以指示错误所在;

pid_t ret = waitpid(id + 4, &status, 0);    
if(ret == id)    {    // 不变    }    
else    
{    
    printf("wait failed!\n");    
} 

如果父进程等待的并不是自己的子进程,那么就一定会等待失败。换言之,父进程在进行等待时,只能等待自己的子进程。

2.3.4 宏调用查看退出信息

status:
	WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
	WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)

pid_t ret = waitpid(id, &status, 0);    
if(ret == id)    
{    
    if(WIFEXITED(status))    
    {    
        printf("process is normal, exit_code: %d\n", WEXITSTATUS(status));    
    }    
    else    
    {    
        printf("the process terminated abnormally! \n");                                                              
    }    
}    
else    
{    
    printf("wait failed!\n");    
} 

有了系统提供的宏,就不再需要我们自己通过位运算来获取进程的退出情况了。


3. 进程等待的原理

子进程执行完毕时,为了保证其退出结果被上层获取,它的代码和数据是允许被释放的,只不过需要将退出信息保存在子进程的 PCB 中而已。当进程收到信号时,会写入到 pcb 中的 exit_code,进程的退出码写入到 exit_signal 中,父进程再通过系统调用 wait / waitpid 检测子进程是否退出了,如果退出了,再读取子进程的退出信息,将退出信息合并成 status 传递给上层用户。

为什么不让上层用户直接访问子进程的退出信息呢?? ----- 与之前讲述的系统管理一样,因为操作系统不信任用户,子进程的退出信息就存储在子进程的 PCB 中,而用户是无法直接越过操作系统 访问 操作系统所管理的内核数据结构对象的,操作系统不允许任何用户访问它的底层数据。


关于进程等待,本篇文章就介绍到这里,后续还会介绍非阻塞轮询,并且非阻塞轮询的同时,是如何执行其它任务的。

如果感觉该篇文章给你带来了收获,可以 点赞👍 + 收藏⭐️ + 关注➕ 支持一下!

感谢各位观看!

相关推荐
无为之士5 分钟前
Linux自动备份Mysql数据库
linux·数据库·mysql
岑梓铭22 分钟前
(CentOs系统虚拟机)Standalone模式下安装部署“基于Python编写”的Spark框架
linux·python·spark·centos
努力学习的小廉23 分钟前
深入了解Linux —— make和makefile自动化构建工具
linux·服务器·自动化
MZWeiei26 分钟前
Zookeeper基本命令解析
大数据·linux·运维·服务器·zookeeper
7yewh41 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
小张认为的测试1 小时前
Linux性能监控命令_nmon 安装与使用以及生成分析Excel图表
linux·服务器·测试工具·自动化·php·excel·压力测试
打鱼又晒网1 小时前
linux网络套接字 | 深度解析守护进程 | 实现tcp服务守护进程化
linux·网络协议·计算机网络·tcp
良许Linux1 小时前
0.96寸OLED显示屏详解
linux·服务器·后端·互联网
蜜獾云1 小时前
docker 安装雷池WAF防火墙 守护Web服务器
linux·运维·服务器·网络·网络安全·docker·容器
小屁不止是运维1 小时前
麒麟操作系统服务架构保姆级教程(五)NGINX中间件详解
linux·运维·服务器·nginx·中间件·架构