详谈进程等待

server/2024/9/20 3:54:22/ 标签: linux, 进程等待

目录

前言

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


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.ppmy.cn/server/106559.html

相关文章

android aar适配uniapp

最近有商户需要接入我们sdk&#xff0c;但是我们都是android或者ios原生的&#xff0c;直接用又不能用&#xff0c;需要做适配&#xff0c;本文就教你一步步实现android aar适配uniapp。 官方参考教程&#xff1a;开发者须知 | uni小程序SDK 但是官方写的比较繁琐&#xff0c;好…

Windows安装nexus 私服仓库(6)

1.私服 安装 配置nexus 下载你需要的环境 Windows 第一个文件夹是运行程序 第二个文件夹是工作空间 去运行exe 这个端口是可以改 点击进去 修改完重启就可以了 2.私服资源获取 然后登录nexus 账号是admin 密码在这个文件里 然后登录后提醒你修改密码 只能修改密码 名称不…

协议汇总 TCP、UDP、Http、Socket、Web Scoket、Web Service、WCF、API

TCP&#xff1a;   &#xff08;1&#xff09;位于OSI传输层&#xff0c;基于soap&#xff08;信封&#xff09;协议&#xff1b; &#xff08;2&#xff09;数据格式是xml、Json&#xff1b; &#xff08;3&#xff09;是面向连接的&#xff0c;需要先建立连接&#xff1b…

CRUD的最佳实践,联动前后端,包含微信小程序,API,HTML等(二)

CRUD老生常谈&#xff0c;但是我搜索了一圈&#xff0c;发觉几乎是着重在后端&#xff0c;也就是API部分&#xff01; 无外乎2个思路 1.归总的接口&#xff0c;比如一个接口&#xff0c;实现不同表的CRUD 2.基于各自的表&#xff0c;使用代码生成器实现CRUD 个人来说是推荐2&am…

Linux下单网卡配置多个路由ip方法

Linux下配置网卡ip别名何谓ip别名 用windows的话说&#xff0c;就是为一个网卡配置多个ip。 什么场合增加ip别名能派上用场&#xff1f; 布网需要、多ip访问测试、特定软件对多ip的需要 下面通过几个例子简单介绍一下如何使用ifconfig命令给网卡配置ip别名。 一、首先为服务器…

C#实现数据采集系统-数据反写(2)消息内容处理和写入通信类队列

C#实现数据采集系统-数据反写 实现步骤 MQTT订阅&#xff0c;接收消息 链接-MQTT订阅接收消息反写内容写入通信类&#xff0c;添加到写入队列中实现Modbustcp通信写入 具体实现 2. 消息内容写入通信类&#xff0c;添加到写入队列中 在服务类DAqService中添加通信集合_modb…

MyBatis框架学习

系列文章目录 第一章 基础知识、数据类型学习 第二章 万年历项目 第三章 代码逻辑训练习题 第四章 方法、数组学习 第五章 图书管理系统项目 第六章 面向对象编程&#xff1a;封装、继承、多态学习 第七章 封装继承多态习题 第八章 常用类、包装类、异常处理机制学习 第九章 集…

NRC-SIM:基于Node-RED的多级多核缓存模拟器

整理自&#xff1a; 《NRC-SIM: A NODE-RED Based Multi-Level, Many-Core Cache Simulator》&#xff0c;由 Ezequiel Trevio 撰写&#xff0c;作为他在德克萨斯大学里奥格兰德河谷分校攻读电气工程硕士学位的部分成果。以下是论文的详细主要内容&#xff1a; 摘要(Abstract…

day41.动态规划

一.动态规划 121.买卖股票的最佳时机I 思路:dp[i][1] 表示第i天不持有股票所得最多现金 dp[i][0] 表示第i天持有股票所得最多现金 class Solution { public:int maxProfit(vector<int>& prices) {int len prices.size();if (len 0) return 0;vector<vector&…

强化学习第九章:策略梯度方法

强化学习第九章&#xff1a;策略梯度方法 思路优化函数优化函数的梯度求解 Monte Carlo policy gradient (REINFORCE)总结参考资料 思路 与上一章的思路类似&#xff0c; 状态-动作 对下标索引获取概率π(s, a)的方式转换为状态-动作对 或者状态输入到神经网络中&#xff0c;两…

DevOps实现CI/CD实战(三)- 集成Sonar Qube

七、集成Sonar Qube 1. SonarQube介绍 Sonar Qube是一个开源的代码分析平台&#xff0c;支持Java、Python、PHP、JavaScript、CSS等25种以上的语言&#xff0c;可以检测出重复代码、代码漏洞、代码规范和安全性漏洞的问题。 Sonar Qube可以与多种软件整合进行代码扫描&#…

TCP keepalive和HTTP keepalive区别

TCP 的 Keepalive 在传输层 是内核态实现的&#xff0c;是TCP的保活机制 当两端的TCP连接一直没有数据交互&#xff0c;就达到了触发TCP保活机制的条件&#xff0c;那么内核里的TCP协议栈就会发送探测报文。 如果对端程序是正常工作的&#xff0c;当TCP保活的探测报文发送到对…

wp-autopost-pro 3.7.8最新完美版

插件简介&#xff1a; 插件是wp-autopost-pro 3.7.8最新版本。 采集插件适用对象 1、刚建的wordpress站点内容比较少&#xff0c;希望尽快有比较丰富的内容&#xff1b; 2、热点内容自动采集并自动发布&#xff1b; 3、定时采集&#xff0c;手动采集发布或保存到草稿&#xff…

mysql数据表管理

数据表管理 如果将数据库管理系统与之前的文件管理系统做类比的话&#xff1a; 数据库管理系统文件管理系统数据库文件夹数据表文件夹下的文件 数据表的常见操作指令 进入数据库use 数据库&#xff0c;查看当前所有表:show tables 创建表结构 idnameemailage创建表的基础语…

Leetcode面试经典150题-13.罗马数字转整数

解法都在代码里&#xff0c;不懂就留言或者私信&#xff0c;这个是相对简单点的&#xff0c;感觉会在低职级面试的时候考 class Solution {/**罗马数字转整数还是比较简单的&#xff0c;基本思路&#xff1a;把罗马数字字符串转成字符数组同时创建一个int型数组&#xff0c;遍…

「C#」EF Core的“迁移”(Migration)

1、“迁移”是什么 “迁移”&#xff08;Migration&#xff09;我觉得可以理解为将实体类的变化 转换为对数据库修改的方案&#xff0c;应用迁移就是将这个修改方案应用到数据库。其次&#xff0c;迁移也记录了数据库的版本历史等信息。 2、添加迁移 2.1、dotnet cli tool …

设计模式之简单工厂模式

一 、定义 简单工厂模式是一种创建型设计模式&#xff0c;它提供一个统一的接口来创建对象&#xff0c;而不需要客户端直接实例化对象。简单工厂模式通过封装创建对象的逻辑&#xff0c;简化了对象的创建过程&#xff0c;同时也提高了代码的可维护性和扩展性。缺点是&#xff…

浅谈【数据结构】图-图的存储

目录 1、图的存储 2、邻接表 3、十字链表 谢谢帅气美丽且优秀的你看完我的文章还要点赞、收藏加关注 没错&#xff0c;说的就是你&#xff0c;不用再怀疑&#xff01;&#xff01;&#xff01; 希望我的文章内容能对你有帮助&#xff0c;一起努力吧&#xff01;&#xff01;…

计算机毕业设计pyspark+django+scrapy租房推荐系统 租房大屏可视化 租房爬虫 hadoop 58同城租房爬虫 房源推荐系统

用到的技术: 1. python 2. django后端框架 3. django-simpleui&#xff0c;Django后台 4. vue前端 5. element-plus&#xff0c;vue的前端组件库 6. echarts前端可视化库 7. scrapy爬虫框架 基于大数据的租房信息推荐系统包括以下功能&#xff1a…

选择排序【详解】

本期介绍&#x1f356; 主要介绍&#xff1a;排序中的选择排序。 文章目录 1. 前言2. 选择排序3. 优化选择排序 1. 前言 相信只要接触过C语言的同学都或多或少了解排序问题&#xff0c;其中最基本&#xff0c;且最为人所熟知的排序是&#xff1a;选择排序。下面我会带着大家重新…