【Linux系统】SIGCHLD 信号(选学了解)




在这里插入图片描述




SIGCHLD 信号


使用waitwaitpid函数可以有效地清理僵尸进程。父进程可以选择阻塞等待,直到子进程结束;或者采用非阻塞的方式,通过轮询检查是否有子进程需要被回收。

然而,无论是选择阻塞等待还是非阻塞的轮询方式,父进程与子进程之间都无法实现真正的异步执行,因为父进程仍需“分心”来管理子进程的状态。

当子进程终止时,会向父进程发送一个SIGCHLD信号,这个信号的默认行为是被忽略。不过,父进程可以通过设置自定义的SIGCHLD信号处理函数来改变这一行为。这样,父进程就可以专注于自己的任务,无需直接管理子进程的状态。一旦子进程终止,它会通知父进程,父进程只需要在其信号处理函数中调用waitwaitpid来清理子进程即可。

因此,我们能够通过自定义处理子进程发出的SIGCHLD信号,在接收到该信号时利用waitpid回收子进程资源。这种方法避免了主动等待子进程的结束,使得父子进程之间能够更加高效地异步执行。



演示代码如下:因为需要在函数 handler 中使用子进程 id,因此定义了一个全局变量 id

其实还有一种方法不用传id也不用定义全局变量:waitpid(-1, nullptr, 0);

-1 表示回收该父进程下的任意一个子进程

pid 参数为 -1 时,waitpid 函数会等待任何一个子进程的状态变化。这意味着它会捕获任何已经终止的子进程,并回收其资源。这对于处理多个子进程的情况非常有用,因为父进程不需要知道具体是哪个子进程终止了。

#include<iostream>  // 引入输入输出流库
#include<signal.h> // 引入信号处理库
#include<sys/wait.h> // 引入等待子进程状态改变的函数库
#include<sys/types.h> // 引入系统类型定义
#include<unistd.h> // 引入Unix标准函数库pid_t id; // 定义全局变量id,用于存储子进程ID// 定义信号处理函数
void handler(int signum)
{waitpid(id, nullptr, 0); // 等待子进程结束,回收子进程资源std::cout << "子进程退出, 我也退出了" << '\n'; // 输出子进程已退出的信息// 当接收到信号时,调用raise给自己发送9号信号(SIGKILL),强制终止进程raise(9);
}int main()
{id = fork(); // 创建子进程if(id < 0){perror("fork"); // 如果fork失败,输出错误信息return 1; // 返回错误码1}// 子进程逻辑if(id == 0){std::cout << "I am 子 process" << '\n'; // 子进程输出标识信息sleep(2); // 子进程暂停2秒exit(0); // 子进程正常退出}// 父进程逻辑else if (id > 0){std::cout << "I am 父 process" << '\n'; // 父进程输出标识信息signal(SIGCHLD, handler); // 设置SIGCHLD信号的处理函数为handlerint cnt = 0; // 初始化计数器while(1){   sleep(1); // 每秒暂停1秒std::cout << "cnt = " << cnt++ << '\n'; // 输出当前计数值}}   return 0; // 程序正常结束
}


运行结果如下:


在这里插入图片描述




问题一:如果同时多个子进程退出,是否会全部回收


但是这样通过信号回收子进程是有一定风险的!

因为信号是通过 pending 位图保存的,当一个父进程同时有多个子进程同时退出,同时发送 SIGCHLD 信号,则位图不能记录信号接收数量,就大概率会遗漏处理某些子进程,导致多个子进程僵尸的情况

验证如下:

#include <iostream>      // 引入输入输出流库
#include <signal.h>      // 引入信号处理库
#include <sys/wait.h>    // 引入等待子进程状态改变的函数库
#include <sys/types.h>   // 引入系统类型定义
#include <unistd.h>      // 引入Unix标准函数库// 定义信号处理函数
void handler(int signum)
{pid_t id = waitpid(-1, nullptr, 0); // 等待任意一个子进程结束,回收其资源std::cout << "回收子进程 id : " << id << '\n'; // 输出回收的子进程ID
}int main()
{pid_t id; // 定义变量id,用于存储子进程ID// 循环创建15个子进程for (int i = 1; i <= 15; ++i){id = fork(); // 创建子进程if (id < 0){perror("fork"); // 如果fork失败,输出错误信息return 1; // 返回错误码1}// 子进程逻辑if (id == 0){std::cout << "I am 子 process" << '\n'; // 子进程输出标识信息sleep(2); // 子进程暂停2秒exit(0); // 子进程正常退出}}// 父进程逻辑if (id > 0){std::cout << "I am 父 process" << '\n'; // 父进程输出标识信息signal(SIGCHLD, handler); // 设置SIGCHLD信号的处理函数为handler,当子进程结束时会触发此函数int cnt = 0; // 初始化计数器while (1){sleep(1); // 每秒暂停1秒std::cout << "cnt = " << cnt++ << '\n'; // 输出当前计数值}}return 0; // 程序正常结束
}


运行结果如下:不少子进程没有被回收,而是变成了僵尸进程


在这里插入图片描述




解决办法:循环等待回收子进程,否则退出


演示代码如下:

#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>void handler(int signum)
{while (true){pid_t id = waitpid(-1, nullptr, 0);if(id > 0){std::cout << "回收子进程 id : " << id << '\n';}else if(id < 0){std::cout << "回收完毕, 暂时结束回收\n";break;}}
}int main()
{pid_t id;for (int i = 1; i <= 15; ++i){id = fork();if (id < 0){perror("fork");return 1;}// 子进程if (id == 0){std::cout << "I am 子 process" << '\n';sleep(2);exit(0);}}// 父进程if (id > 0){std::cout << "I am 父 process" << '\n';signal(SIGCHLD, handler);int cnt = 0;while (1){sleep(1);std::cout << "cnt = " << cnt++ << '\n';}}return 0;
}


运行结果如下:自己可以去查询,可以确定当前没有僵尸子进程


在这里插入图片描述




问题二:如果有子进程不退出,问题一中的循环wait,是否会退出循环


演示代码:

// 创建一个不退出的子进程
id = fork();
if (id == 0)
{std::cout << "I am 不退出的子进程" << '\n';sleep(6);
}

结果就是 循环没退出,因为 waitpid 是阻塞式等待,会等待子进程退出,因为该子进程不退出则循环不退出一直阻塞等待


因此需要换成非阻塞式等待,同时当 waitpid 的返回值为 0,说明当前没有退出的子进程,则此时可以主动退出循环

pid_t id = waitpid(-1, nullptr, WNOHANG);

#include <iostream>
#include <signal.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <unistd.h>void handler(int signum)
{while (true){pid_t id = waitpid(-1, nullptr, WNOHANG);if (id > 0){std::cout << "回收子进程 id : " << id << '\n';}else if(id == 0) // 表示没有子进程退出了(注意是没有退出的子进程了, 不是没有子进程){std::cout << "暂时没有子进程退出\n";break;}else if (id < 0) // 表示没有子进程了{std::cout << "waitpid error\n";break;}}
}int main()
{pid_t id;for (int i = 1; i <= 15; ++i){id = fork();if (id < 0){perror("fork");return 1;}// 子进程if (id == 0){std::cout << "I am 子 process" << '\n';sleep(2);exit(0);}}// 创建一个不退出的子进程id = fork();if (id == 0){std::cout << "I am 不退出的子进程" << '\n';sleep(6); // 时间长点, 模拟短时间内不退出}// 父进程if (id > 0){std::cout << "I am 父 process" << '\n';signal(SIGCHLD, handler);int cnt = 0;while (1){sleep(1);std::cout << "cnt = " << cnt++ << '\n';}}return 0;
}


运行结果如下


在这里插入图片描述



waitpid 系统调用的工作原理如下:

  • 阻塞等待:当 waitpid 被调用时,如果当前没有符合条件的已退出子进程,内核会让父进程进入阻塞状态。这意味着父进程会被挂起,不再占用CPU时间,直到有子进程的状态发生变化(通常是退出)。
  • 非阻塞等待:如果 waitpid 调用时传递了 WNOHANG 选项,内核会立即返回,即使没有子进程退出。在这种情况下,waitpid 不会阻塞父进程。
  • 状态变化通知:当一个子进程退出时,内核会检查该子进程的父进程是否正在等待子进程的状态变化。如果是,内核会唤醒父进程,使其从 waitpid 调用中返回,并传递子进程的退出状态。
  • 资源回收:父进程通过 waitpid 获得子进程的退出状态后,内核会释放子进程占用的资源,防止子进程变成僵尸进程。

意思是:父进程使用waitpid 系统调用时,若为阻塞等待,则OS将该父进程挂起(即阻塞),当目标子进程退出时,若该父进程正处于等待子进程退出的状态,则OS会传递子进程退出状态信息并使父进程退出阻塞状态(即使其从 waitpid 调用中返回)



子进程退出,OS是如何知道的,是因为OS需要轮询子进程的状态吗

当然不是OS轮询,前面讲解过 OS 就是一个躺在中断向量表上的一个代码块,OS的运行基本靠中断,因此进程退出也是通过中断通知OS,使其执行相应的后续”善后“工作


意思是子进程退出时,会向OS发送软件中断,此时进入内核态,执行该中断对应的中断处理例程:即更新子进程的 PCB,将子进程的状态标记为“已退出”(Zombie 状态),生成一个 SIGCHLD 信号并发送给父进程


子进程退出的详细过程

  1. 子进程调用 exitexit_group 系统调用
    • 子进程在调用 exitexit_group 系统调用时,会进入内核态。
      • 不是子进程退出子进程发送的软件中断,而是子进程在调用 exit 或 exit_group 系统调用触发的软件中断
  2. 进入内核态
    • 当子进程调用 exitexit_group 时,控制权转移到内核,进入内核态。
    • 内核会执行相应的中断处理例程(中断服务程序)。
  3. 中断处理例程
    • 内核的中断处理例程会执行以下操作:
      • 更新子进程的 PCB:内核会更新子进程的进程控制块(PCB),将子进程的状态标记为“已退出”(Zombie 状态)。
      • 生成 SIGCHLD 信号:内核会生成一个 SIGCHLD 信号并发送给父进程。
  4. 父进程接收 SIGCHLD 信号
    • 父进程接收到 SIGCHLD 信号后,会调用预先注册的信号处理函数(如 handler)默认为忽略


主动忽略子进程的 SIGCHLD

Linux下,将SIGCHLD的处理动作置为SIG IGN,这样fork出来的子进程在终止时会自动清理掉

由于UNIX 的历史原因,要想不产⽣僵⼫进程还有另外⼀种办法:⽗进程调 ⽤sigaction将
SIGCHLD的处理动作置为SIG_IGN,这样fork出来的⼦进程在终⽌时会⾃动清理掉,不 会产⽣僵⼫进程,

也不会通知⽗进程。系统默认的忽略动作和⽤⼾⽤sigaction函数⾃定义的忽略 通常是没有区别的,但这
是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可 ⽤。

signal(SIGCHLD, SIG_IGN);


底层原理:

父进程未调用 waitpid 的情况

  1. 子进程退出
    • 子进程调用 exitexit_group 系统调用,进入内核态。
    • 内核更新子进程的 PCB,将其状态标记为“已退出”(Zombie 状态)。
    • 内核生成 SIGCHLD 信号并发送给父进程。
  2. 父进程处理 SIGCHLD 信号
    • 如果父进程注册了 SIGCHLD 信号处理函数(如 handler),内核会调用该处理函数。
    • 在信号处理函数中,父进程可以调用 waitpid 来获取子进程的退出状态并释放资源。
  3. 父进程忽略 SIGCHLD 信号
    • 如果父进程将 SIGCHLD 信号的处理动作设置为 SIG_IGN,内核会自动回收子进程的资源,子进程不会变成僵尸进程。
    • 这意味着父进程不需要显式调用 waitwaitpid 来回收子进程的资源。


问题:父进程忽略了该信号,内核如何知道父进程忽略了,然后进行的自动回收子进程的资源

  1. 内核记录信号处理动作

    • 内核会记录每个进程的信号处理动作。当父进程调用 signalsigaction 设置 SIGCHLD 信号的处理动作时,内核会更新父进程的信号处理表。

    • 内核会记录 SIGCHLD 信号的处理动作为 SIG_IGN

子进程调用退出

  1. 内核生成 SIGCHLD 信号
    • 内核生成 SIGCHLD 信号并准备发送给父进程。
    • 内核会检查父进程的信号处理表,查看 SIGCHLD 信号的处理动作。
  2. 检查信号处理动作
    • 如果父进程的信号处理动作是 SIG_IGN,内核会知道父进程忽略了 SIGCHLD 信号。
    • 内核会自动回收子进程的资源,子进程不会变成僵尸进程。


问题:系统对该信号的默认处理不就是忽略吗,为什么我们还要自己主动忽略

系统默认的忽略动作和⽤⼾⽤sigaction函数⾃定义的忽略 通常是没有区别的,但这
是⼀个特例。此⽅法对于Linux可⽤,但不保证在其它UNIX系统上都可 ⽤。

其实,因为位图本身一次只能记录一个进程退出信号,因此即使循环等待等操作,还是会有极小概率处理不了某些退出子进程

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/67942.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【R语言】获取数据

R语言自带2种数据存储格式&#xff1a;*.RData和*.rds。 这两者的区别是&#xff1a;前者既可以存储数据&#xff0c;也可以存储当前工作空间中的所有变量&#xff0c;属于非标准化存储&#xff1b;后者仅用于存储单个R对象&#xff0c;且存储时可以创建标准化档案&#xff0c…

Vim的基础命令

移动光标 H(左) J(上) K(下) L(右) $ 表示移动到光标所在行的行尾&#xff0c; ^ 表示移动到光标所在行的行首的第一个非空白字符。 0 表示移动到光标所在行的行首。 W 光标向前跳转一个单词 w光标向前跳转一个单词 B光标向后跳转一个单词 b光标向后跳转一个单词 G 移动光标到…

11. 9 构建生产级聊天对话记忆系统:从架构设计到性能优化的全链路指南

构建生产级聊天对话记忆系统:从架构设计到性能优化的全链路指南 关键词: 聊天对话记忆系统、多用户会话管理、LangChain生产部署、Redis记忆存储、高并发对话系统 一、服务级聊天记忆系统核心需求 多用户隔离:支持同时处理数千个独立对话持久化存储:对话历史不因服务重启丢…

Block Blaster Online:免费解谜游戏的乐趣

Block Blaster Online 是一款免费的在线解谜游戏&#xff0c;它将挑战你的思维和反应能力&#xff01;在这里&#xff0c;你可以匹配五彩缤纷的方块&#xff0c;创造出令人惊叹的组合&#xff0c;享受无尽的解谜乐趣。无需安装&#xff0c;点击即可开始&#xff0c;加入全球数百…

Guided Decoding (借助FSM,有限状态自动机)

VLLM对结构化输出的支持&#xff1a; vllm/docs/source/features/structured_outputs.md at main vllm-project/vllm GitHub VLLM对tool call的支持&#xff1a; vllm/docs/source/features/tool_calling.md at main vllm-project/vllm GitHub 以上指定输出格式&#xf…

IFeatureWorkspace.CreateFeatureClass(),报错对COM组件的调用返回了错误 HRESULT E_FAIL

1、问题描述&#xff1a;在AE开发中&#xff0c;新增一个空的shpfile文件的时候&#xff0c;报错&#xff0c;如下图&#xff1a; 2、原因分析&#xff1a;产生此问题的原因是未设置默认字段的默认参数&#xff0c;特别是未设置IGeometryDef 参数。 3、解决方案&#xff1a;在…

算法题(48):反转链表

审题&#xff1a; 需要我们将链表反转并返回头结点地址 思路&#xff1a; 一般在面试中&#xff0c;涉及链表的题会主要考察链表的指向改变&#xff0c;所以一般不会允许我们改变节点val值。 这里是单向链表&#xff0c;如果要把指向反过来则需要同时知道前中后三个节点&#x…

内存的介绍

1、程序运行为什么需要内存 1.1、计算机程序运行的目的 (1)程序的目的是为了去运行&#xff0c;程序运行是为了得到一定的结果。 (2)计算机程序 代码 数据。计算机程序运行完得到一个结果&#xff0c;就是说 代码 数据 (经过运行后) 结果。 (3)从宏观上来理解&#xff…

【NLP百面百过】大模型算法面试高频面题(全面整理 ʘ‿ʘ)

目录 一、大模型面试指南 重点面题精讲 【LLM面题精讲 - RAG系统面】 查看答案 【LLM面题精讲 - 实体识别面】 查看答案 【LLM面题精讲 - 文本分类面】 查看答案 【LLM面题精讲 - 分布式训练面】 查看答案 【LLM面题精讲 - 大模型微调面】 查看答案 【LLM面题精讲 - 大…

Java 大视界 -- Java 大数据在智能医疗影像诊断中的应用(72)

💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也期待你毫无保留地分享独特见解,愿我们于此携手成长,共赴新程!💖 一、…

基于 docker 的mysql 5.7 主主集群搭建

创建挂载目录和配置文件 主节点1 mkdir -p /mysql_master_1/mysql/log mkdir -p /mysql_master_1/mysql/data mkdir -p /mysql_master_1/mysql/conf vim /mysql_master_1/mysql/conf/my.cnf[mysqld] datadir/var/lib/mysql #MySQL 数据库文件存放路径 server_id 1 #指定数据…

list容器(详解)

list的介绍及使用&#xff08;了解&#xff0c;后边细讲&#xff09; 1.1 list的介绍&#xff08;双向循环链表&#xff09; https://cplusplus.com/reference/list/list/?kwlist&#xff08;list文档介绍&#xff09; 1. list是可以在常数范围内在任意位置进行插入和删除的序…

MapReduce分区

目录 1. MapReduce分区1.1 哈希分区1.2 自定义分区 2. 成绩分组2.1 Map2.2 Partition2.3 Reduce 3. 代码和结果3.1 pom.xml中依赖配置3.2 工具类util3.3 GroupScores3.4 结果 参考 本文引用的Apache Hadoop源代码基于Apache许可证 2.0&#xff0c;详情请参阅 Apache许可证2.0。…

kamailio-ACC_JSON模块详解【后端语言go】

要确认 ACC_JSON 模块是否已经成功将计费信息推送到消息队列&#xff08;MQueue&#xff09;&#xff0c;以及如何从队列中取值&#xff0c;可以按照以下步骤进行操作&#xff1a; 1. 确认 ACC_JSON 已推送到队列 1.1 配置 ACC_JSON 确保 ACC_JSON 模块已正确配置并启用。以下…

网件r7000刷回原厂固件合集测评

《网件R7000路由器刷回原厂固件详解》 网件R7000是一款备受赞誉的高性能无线路由器&#xff0c;其强大的性能和可定制性吸引了许多高级用户。然而&#xff0c;有时候用户可能会尝试第三方固件以提升功能或优化网络性能&#xff0c;但这也可能导致一些问题&#xff0c;如系统不…

【C++STL标准模板库】二、STL三大组件

文章目录 1、容器2、算法3、迭代器 二、STL三大组件 1、容器 容器&#xff0c;置物之所也。 研究数据的特定排列方式&#xff0c;以利于搜索或排序或其他特殊目的&#xff0c;这一门学科我们称为数据结构。大学信息类相关专业里面&#xff0c;与编程最有直接关系的学科&…

基于 Java 开发的 MongoDB 企业级应用全解析

基于Java的MongoDB企业级应用开发实战 目录 背景与历史MongoDB的核心功能与特性企业级业务场景分析MongoDB的优缺点剖析开发环境搭建 5.1 JDK安装与配置5.2 MongoDB安装与集群配置5.3 开发工具选型 Java与MongoDB集成实战 6.1 项目依赖与驱动选择6.2 连接池与客户端配置6.3…

需求分析应该从哪些方面来着手做?

需求分析一般可从以下几个方面着手&#xff1a; 业务需求方面 - 与相关方沟通&#xff1a;与业务部门、客户等进行深入交流&#xff0c;通过访谈、问卷调查、会议讨论等方式&#xff0c;明确他们对项目的期望、目标和整体业务需求&#xff0c;了解项目要解决的业务问题及达成的…

算法题(57):找出字符串中第一个匹配项的下标

审题: 需要我们根据原串与模式串相比较并找到完全匹配时子串的第一个元素索引&#xff0c;若没有则返回-1 思路&#xff1a; 方法一&#xff1a;BF暴力算法 思路很简单&#xff0c;我们用p1表示原串的索引&#xff0c;p2表示模式串索引。遍历原串&#xff0c;每次遍历都匹配一次…

求组合数(递推法、乘法逆元、卢卡斯定理、分解质因数)

文章目录 递推法 10^4代码 乘法逆元 10^6代码 卢卡斯定理 1 0 18 m o d 1 0 6 10^{18}mod 10^6 1018mod106代码 分解质因数 常规的解法就不多加赘述了&#xff0c;如&#xff08;分子/分母&#xff0c;边乘边除&#xff09;&#xff0c;本文讲述以下方法&#xff1a; 递推法 了…