详谈进程等待

2024-08-26 00:44
文章标签 进程 等待 详谈

本文主要是介绍详谈进程等待,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

  • 前言
  • 1. 进程等待的必要性
    • 1.1 进程等待的定义
  • 2. 如何进行进程等待
    • 2.1 wait 单进程
    • 2.2 wait 多进程
    • 2.3 status && 退出情况
      • 2.3.1 status 参数构成
      • 2.3.2 简证 status 参数构成
      • 2.3.3 进程等待失败
      • 2.3.4 宏调用查看退出信息
  • 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 中,而用户是无法直接越过操作系统 访问 操作系统所管理的内核数据结构对象的,操作系统不允许任何用户访问它的底层数据。


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

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

感谢各位观看!

这篇关于详谈进程等待的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1107066

相关文章

[Linux]:进程(下)

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 进程终止 1.1 进程退出的场景 进程退出只有以下三种情况: 代码运行完毕,结果正确。代码运行完毕,结果不正确。代码异常终止(进程崩溃)。 1.2 进程退出码 在编程中,我们通常认为main函数是代码的入口,但实际上它只是用户级

java 进程 返回值

实现 Callable 接口 与 Runnable 相比,Callable 可以有返回值,返回值通过 FutureTask 进行封装。 public class MyCallable implements Callable<Integer> {public Integer call() {return 123;}} public static void main(String[] args

C#关闭指定时间段的Excel进程的方法

private DateTime beforeTime;            //Excel启动之前时间          private DateTime afterTime;               //Excel启动之后时间          //举例          beforeTime = DateTime.Now;          Excel.Applicat

linux中使用rust语言在不同进程之间通信

第一种:使用mmap映射相同文件 fn main() {let pid = std::process::id();println!(

Golang进程权限调度包runtime

关于 runtime 包几个方法: Gosched:让当前线程让出 cpu 以让其它线程运行,它不会挂起当前线程,因此当前线程未来会继续执行GOMAXPROCS:设置最大的可同时使用的 CPU 核数Goexit:退出当前 goroutine(但是defer语句会照常执行)NumGoroutine:返回正在执行和排队的任务总数GOOS:目标操作系统NumCPU:返回当前系统的 CPU 核数量 p

如何保证android程序进程不到万不得已的情况下,不会被结束

最近,做一个调用系统自带相机的那么一个功能,遇到的坑,在此记录一下。 设备:红米note4 问题起因 因为自定义的相机,很难满足客户的所有需要,比如:自拍杆的支持,优化方面等等。这些方面自定义的相机都不比系统自带的好,因为有些系统都是商家定制的,难免会出现一个奇葩的问题。比如:你在这款手机上运行,无任何问题,然而你换一款手机后,问题就出现了。 比如:小米的红米系列,你启用系统自带拍照功能后

flume系列之:记录一次flume agent进程被异常oom kill -9的原因定位

flume系列之:记录一次flume agent进程被异常oom kill -9的原因定位 一、背景二、定位问题三、解决方法 一、背景 flume系列之:定位flume没有关闭某个时间点生成的tmp文件的原因,并制定解决方案在博主上面这篇文章的基础上,在机器内存、cpu资源、flume agent资源都足够的情况下,flume agent又出现了tmp文件无法关闭的情况 二、

C++编程:ZeroMQ进程间(订阅-发布)通信配置优化

文章目录 0. 概述1. 发布者同步发送(pub)与订阅者异步接收(sub)示例代码可能的副作用: 2. 适度增加缓存和队列示例代码副作用: 3. 动态的IPC通道管理示例代码副作用: 4. 接收消息的超时设置示例代码副作用: 5. 增加I/O线程数量示例代码副作用: 6. 异步消息发送(使用`dontwait`标志)示例代码副作用: 7. 其他可以考虑的优化项7.1 立即发送(ZMQ_IM

[轻笔记] ubuntu Shell脚本实现监视指定进程的运行状态,并能在程序崩溃后重启动该程序

根据网上博客实现,发现只能监测进程离线,然后对其进行重启;然而,脚本无法打印程序正常状态的信息。自己通过不断修改测试,发现问题主要在重启程序的命令上(需要让重启的程序在后台运行,不然会影响监视脚本进程,使其无法正常工作)。具体程序如下: #!/bin/bashwhile [ 1 ] ; dosleep 3if [ $(ps -ef|grep exe_name|grep -v grep|

[Linux]:环境变量与进程地址空间

✨✨ 欢迎大家来到贝蒂大讲堂✨✨ 🎈🎈养成好习惯,先赞后看哦~🎈🎈 所属专栏:Linux学习 贝蒂的主页:Betty’s blog 1. 环境变量 1.1 概念 **环境变量(environment variables)**一般是指在操作系统中用来指定操作系统运行环境的一些参数,具有全局属性,可以被子继承继承下去。 如:我们在编写C/C++代码的时,在链接的时候,我们并不知