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

devtools/2025/2/5 14:30:28/




在这里插入图片描述




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.ppmy.cn/devtools/156293.html

相关文章

Google C++ Style / 谷歌C++开源风格

文章目录 前言1. 头文件1.1 自给自足的头文件1.2 #define 防护符1.3 导入你的依赖1.4 前向声明1.5 内联函数1.6 #include 的路径及顺序 2. 作用域2.1 命名空间2.2 内部链接2.3 非成员函数、静态成员函数和全局函数2.4 局部变量2.5 静态和全局变量2.6 thread_local 变量 3. 类3.…

VLAN 基础 | 不同 VLAN 间通信实验

注&#xff1a;本文为 “ Vlan 间通信” 相关文章合辑。 英文引文&#xff0c;机翻未校。 图片清晰度限于原文图源状态。 未整理去重。 How to Establish Communications between VLANs? 如何在 VLAN 之间建立通信&#xff1f; Posted on November 20, 2015 by RouterSwi…

Java 性能优化与新特性

Java学习资料 Java学习资料 Java学习资料 一、引言 Java 作为一门广泛应用于企业级开发、移动应用、大数据等多个领域的编程语言&#xff0c;其性能和特性一直是开发者关注的重点。随着软件系统的规模和复杂度不断增加&#xff0c;对 Java 程序性能的要求也越来越高。同时&a…

Linux网络 应用层协议 HTTP

概念 在互联网世界中&#xff0c; HTTP &#xff08;HyperText Transfer Protocol &#xff0c;超文本传输协议&#xff09;是一个至关重要的协议。它定义了客户端&#xff08;如浏览器&#xff09;与服务器之间如何通信&#xff0c;以交换或传输超文本&#xff08;如 HTML 文…

基于Hadoop实现气象分析大屏可视化项目【源码+LW+PPT+解析】

作者简介&#xff1a;Java领域优质创作者、CSDN博客专家 、CSDN内容合伙人、掘金特邀作者、阿里云博客专家、51CTO特邀作者、多年架构师设计经验、多年校企合作经验&#xff0c;被多个学校常年聘为校外企业导师&#xff0c;指导学生毕业设计并参与学生毕业答辩指导&#xff0c;…

使用windows笔记本让服务器上网

使用windows笔记本让服务器上网 前言准备工具开始动手实践1.将手机热点打开&#xff0c;让Windows笔记本使用无线网卡连接上网2.使用网线将Windows笔记本的有线网卡和服务器的有线网卡直连3.在Windows笔记本上按winR输入ncpa.cpl打开网卡设置界面4.在Windows笔记本上右键“无线…

Spring AI 与企业级应用架构的结合

随着 AI 技术的不断发展&#xff0c;越来越多的企业开始将 AI 模型集成到其业务系统中&#xff0c;从而提升系统的智能化水平、自动化程度和用户体验。在此背景下&#xff0c;Spring AI 作为一个企业级 AI 框架&#xff0c;提供了丰富的工具和机制&#xff0c;可以帮助开发者将…

深度学习 Pytorch 神经网络的损失函数

本节开始将以分类神经网络为例&#xff0c;展示神经网络的学习和训练过程。在介绍PyTorch的基本工具AutoGrad库时&#xff0c;我们系统地介绍过数学中的优化问题和优化思想&#xff0c;我们介绍了最小二乘法以及梯度下降法这两个入门级优化算法的具体操作&#xff0c;并使用Aut…