从内核到应用层:深度剖析信号捕捉技术栈(含sigaction系统调用/SIGCHLD回收/volatile内存屏障)

Linux系列


文章目录

  • Linux系列
  • 前言
  • 一、进程对信号的捕捉
    • 1.1 内核对信号的捕捉
    • 1.2 sigaction()函数
    • 1.3 信号集的修改时机
  • 二、可重入函数
  • 三、volatile关键字
  • 四、SIGCHLD信号


前言

Linux系统中,信号捕捉是指进程可以通过设置信号处理函数来响应特定信号。通过信号捕捉机制,进程可以对异步事件做出及时响应,从而提高程序的健壮性和灵活性。


一、进程对信号的捕捉

图中内容及执行流程我已在Linux系列上上篇博客中介绍了,这里就不重复了。
在这里插入图片描述

1.1 内核对信号的捕捉

当信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,这称为捕捉信号。由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:
1、用户程序注册(对指定信号捕捉)了SIGQUIT信号的处理函数sighandler

2、 当前正在执行main函数,这时发生中断或异常切换到内核态

3、 在中断处理完毕后要返回用户态的main函数之前检查到有信号SIGQUIT递达。

4、 内核决定返回用户态后,不是恢复main函数的上下文继续执行,而是执行sighandler函数,sighandlermain函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程

5、 sighandler函数返回后自动执行特殊的系统调用sigreturn再次进入内核态。

6、 再次检测sigpending位图,如果没有新的信号要递达,这次再返回用户态就是恢复main函数的上下文继续执行了。

1.2 sigaction()函数

#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

功能:捕捉指定信号,并读取和修改与指定捕捉信号相关联的处理动作。

参数

signum:指定捕捉信号的编号。

act: 输入型参数,act若非空,则根据act来修改信号的处理动作。

oldact:输出型参数,oldact若非空,则获取信号原来的处理动作。

struct sigaction:系统为用户提供的结构体类型,帮助用户访问内核级结构体:
在这里插入图片描述
今天我们主要使用,上面两个成员对象。

  1. 信号处理方法,该方法需要一个整形变量,函数指针类型
  2. act.sa_mask 所代表的是在信号处理函数执行期间需要阻塞的信号集合。也就是说,当 指定信号被捕获并且处理函数handler开始执行时,sa_mask 里的信号会被阻塞,一直到处理函数执行完毕。

下面我们通过两个场景来认识他们:

例一

#include<iostream>
#include<signal.h>
#include<unistd.h>
using namespace std;void handler(int signum)
{cout<<"I catch a signal:"<<signum<<endl;return;
}
int main()
{struct sigaction act;struct sigaction olact;memset(&act,0,sizeof(act));//初始化内存空间memset(&olact,0,sizeof(olact));sigaction(2,&act,&olact);//对二号信号捕获,并修改处理方法while(true){cout<<"I am process,Pid:"<<getpid()<<endl;sleep(1);}return 0;
}

在这里插入图片描述
可以看到这样我们,就完成了对二号进程的捕获并修改执行方法为自定义的行为。

例二

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;void handler(int signum)
{cout<<"I catch a signal:"<<signum<<endl;sleep(10);//在执行handler方法期间,blocksig阻塞信号集中的信号被阻塞return;
}
int main()
{struct sigaction act;struct sigaction olact;memset(&act,0,sizeof(act));//初始化内存空间memset(&olact,0,sizeof(olact));sigset_t blocksig;sigaddset(&blocksig,2);//将二信号添加进blocksigact.sa_handler=handler;//将处理方法添加到act对象中act.sa_mask=blocksig;//将想要阻塞的信号位图赋值给actsigaction(2,&act,&olact);//对二号信号捕获,并修改处理方法while(true){cout<<"I am process,Pid:"<<getpid()<<endl;sleep(1);}return 0;
}

在这里插入图片描述
从执行结构可以得到,当二号信号被捕获执行处理方法,到该方法执行结束,二号信号一直被阻塞,当解除阻塞后,二号信号再次递达。这里也可以使用SIG_IGN(忽略信号)、SIG_DFL(执行默认方法),来设定act.sa_handler。测试时建议尝试其他信号,因为即使我们不手动的将二号信号添加到阻塞信号集,系统在执行二号信号时也会将它先阻塞,下面我们来详细探讨。

1.3 信号集的修改时机

当我们完成对指定信号的捕捉并执行对应处理方法时,操作系统会在执行该方法前,先将pending位图中对应信号的标志位由1置为0,并将该信号添加到对应的阻塞信号集中。具体来说,在二号信号处理方法执行期间,即便进程再次收到二号信号,该信号也不会被递达。只有当上一个信号处理方法执行完毕并返回后,操作系统解除对二号信号的阻塞,新收到的二号信号才会被递达。

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;void printsig()
{sigset_t set;sigemptyset(&set);sigpending(&set);for(int i=1;i<=31;i++)//依次检测信号集{if(sigismember(&set,i))cout<<1;else cout<<0;}cout<<endl;return ;
}
void handler(int signum)
{int cnt=5;while(cnt--){printsig();sleep(1);}cout<<"I catch a signal:"<<signum<<endl;sleep(5);//在执行handler方法期间,blocksig阻塞信号集中的信号被阻塞return;
}
int main()
{struct sigaction act;struct sigaction olact;memset(&act,0,sizeof(act));//初始化内存空间memset(&olact,0,sizeof(olact));act.sa_handler=handler;//将处理方法添加到act对象中sigaction(2,&act,&olact);//对二号信号捕获,并修改处理方法while(true){cout<<"I am process,Pid:"<<getpid()<<endl;sleep(1);}return 0;
}

在这里插入图片描述
从程序执行结果可以得出,当方法被执行时,操作系统会先将pending信号集1--->0,并将该信号阻塞,知道上次执行结束才会完成递达。

二、可重入函数

结合图中展示,分析函数调用链

在程序运行过程中,main函数调用insert函数,打算向链表head中插入节点node1insert函数的插入操作分为两个步骤,当main函数调用的insert函数刚完成第一步时,硬件中断出现,进程被切换到内核态。在从内核态再次返回用户态之前,系统检测到有信号需要处理,于是进程转而执行sighandler函数。在sighandler函数中,同样调用了insert函数,并且向同一个链表head中插入节点node2sighandler函数中的insert操作顺利完成了两个步骤,之后从sighandler函数返回内核态,接着再次回到用户态时,恢复上下文数据,程序从main函数调用的insert函数中断处继续执行,完成了剩余的第二步操作。原本main函数和sig handler函数先后尝试向链表中插入两个不同的节点,但最终链表中实际上仅成功插入了一个节点。 在这里插入图片描述

在上述执行流程中,insert函数被main和handler两条执行流重复调用,这一情况引发了结点丢失问题,并进而导致内存泄漏。像insert函数这种在被重复调用时可能出错或已经出错的函数,我们称之为不可重入函数;与之相对应的,则被称为可重入函数。

不可重入函数的特点:
调用了malloc或free,因为malloc也是用全局链表来管理堆的。
调用了标准I/O库函数。标准I/O库的很多实现都以不可重入的方式使用全局数据结构。

三、volatile关键字

接下来会通过这个关键字,拓展部分知识

例一

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
using namespace std;int flag=0;void handler(int signum)
{cout<<"I captured a signal:"<<signum<<endl;flag=1;
}
int main()
{struct sigaction act;memset(&act,0,sizeof(act));act.sa_handler=handler;sigaction(2,&act,nullptr);while(!flag);cout<<"process quit "<<endl;return 0;
}
mytest:mytest.ccg++ -o $@ $^ -std=c++11

在这里插入图片描述

相信这个执行结果大家都能理解,我就不对上面代码作解释了。
这里将flag设为全局变量,是因为main和sighandler是两个独立的执行流

例二
代码同上

mytest:mytest.ccg++ -o $@ $^ -std=c++11 -O2

在这里插入图片描述

从程序执行结果可知,当将g++编译器的优化级别设置为-O2时,即便通过发送二号信号(SIGINT)将flag变量修改为1,循环仍无法终止。这一现象的根源在于:当使用-O2这类高级优化级别编译代码时,编译器会对代码进行多维度优化以提升执行效率。针对while(!flag);这一循环结构,编译器通过静态代码分析发现,循环体内部不存在对flag变量的修改操作,因此推断该变量的值在循环过程中不会发生变化。

基于“内存访问速度相对较慢”这一特性,编译器为减少对内存的频繁访问,会将flag变量的值从内存加载至CPU寄存器中缓存。此后,在循环条件判断时,CPU会直接从寄存器中读取flag的值,而非重新从内存中获取最新数据,这就导致flag内存不可见了。然而,信号处理机制对flag变量的修改是直接作用于内存的,由于寄存器中的缓存值未及时刷新,导致循环条件判断始终基于寄存器中的旧值,最终造成循环无法终止的现象。

对于上面的结果我们可以,将 flag 声明为 volatile 类型,即 volatile int flag = 0;volatile 关键字的作用是保存flag的内存可见性,告诉编译器,这个变量的值可能会被意外地改变,例如被硬件或者其他线程、信号处理函数等修改,因此编译器不能对其进行优化,这里就不展示了。

四、SIGCHLD信号

之前我们探讨过使用 waitwaitpid 函数来清理僵尸进程。在处理子进程结束的问题上,父进程有两种选择:一是进行阻塞等待,直至子进程结束;二是采用非阻塞的轮询方式,周期性地检查是否有子进程结束,以便及时清理。然而,这两种方式都存在明显的弊端。若采用阻塞等待的方式,父进程在等待期间会被阻塞,无法处理自身的任务,这会极大地降低父进程的工作效率。而采用轮询方式,虽然父进程可以在处理自身工作的同时检查子进程的状态,但这要求父进程时刻记得进行轮询操作,无疑增加了程序实现的复杂度,也容易出现疏漏。实际上,当子进程终止时,它会向父进程发送 SIGCHLD 信号。该信号的默认处理方式是被忽略,但我们可以对其进行优化。父进程可以自定义 SIGCHLD 信号的处理函数,这样一来,父进程就能够专注于自身的工作,无需时刻关注子进程的状态。当子进程终止时,会自动通知父进程,父进程只需在信号处理函数中调用 wait 函数,即可完成子进程的清理工作,既高效又便捷。 下面我们通过这样的方式实现一下:

#include<iostream>
#include<signal.h>
#include<unistd.h>
#include<cstring>
#include<sys/wait.h>
#include<sys/types.h>
using namespace std;void handler(int signum)
{pid_t wid=waitpid(0,nullptr,WNOHANG);if(wid)cout<<"child quit success"<<endl;return;
}
int main()
{signal(SIGCHLD,handler);pid_t id=fork();if(id==0){sleep(5);//模拟子进程工作exit(0);}while(true){cout<<"I am father process"<<endl;sleep(1);}return 0;
}

在这里插入图片描述
从执行结果可以得出,子进程在退出时给父进程发送了SIGCHLD信号。

当然还有一种防止僵尸进程的方法:父进程调 用sigactionSIGCHLD的处理动作置为SIG_IGN,这样fork出来的子进程在终止时会自动清理掉,不会产生僵尸进程,也不会通知父进程:

int main()
{struct sigaction act;memset(&act,0,sizeof(act));act.sa_handler=SIG_IGN;sigaction(SIGCHLD,&act,nullptr);pid_t id=fork();if(id==0){sleep(5);exit(0);}while(true){cout<<"I am father process"<<endl;sleep(1);}return 0;
}

这个结果不方便展示,你自己尝试一下。
本篇就分享到这里了,如果文章的知识,或代码有错误请您联系我,不胜感激!!!

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

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

相关文章

DDD领域驱动与传统CRUD

DDD 是一套 应对复杂业务系统 的设计方法论&#xff0c;核心是 让代码直接映射业务逻辑&#xff0c;避免技术实现与业务需求脱节。 关键区别&#xff1a; 传统开发&#xff1a;根据数据库表写 CRUD&#xff08;技术驱动&#xff09;。DDD&#xff1a;根据业务行为建模&#xf…

20. git diff

基本概述 git diff的作用是&#xff1a;比较代码差异 基本用法 1.工作区 VS 暂存区 git diff [file]2.暂存区 VS 最新提交 git diff --staged [file] # 或 git diff --cached [file]3.工作区 VS 最新提交 git diff HEAD [file]高级用法 1.比较两个提交间的差异 git dif…

大模型面经 | 春招、秋招算法面试常考八股文附答案(五)

大家好,我是皮先生!! 今天给大家分享一些关于大模型面试常见的面试题,希望对大家的面试有所帮助。 往期回顾: 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题一) 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题二) 大模型面经 | 春招、秋招算法…

Sql刷题日志(day5)

面试&#xff1a; 1、从数据分析角度&#xff0c;推荐模块怎么用指标衡量&#xff1f; 推荐模块主要目的是将用户进行转化&#xff0c;所以其主指标是推荐的转化率推荐模块的指标一般都通过埋点去收集用户的行为并完成相应的计算而形成相应的指标数据&#xff0c;而这里的驱动…

封装 element-ui 二次弹框

author 封装 element-ui 弹框 param text 文本内容 &#xff08;不传默认显示 确定执行此操作吗&#xff1f; &#xff09; param type 弹框类型&#xff08;不传默认warning类型&#xff09; param title 弹框标题&#xff08;不传默认显示 提示 &#xff09; export fun…

【Rust 精进之路之第12篇-生命周期·入门】为何需要与显式标注 (`‘a`):让编译器读懂引用的“有效期”

系列: Rust 精进之路:构建可靠、高效软件的底层逻辑 作者: 码觉客 发布日期: 2025-04-20 引言:悬垂引用的“幽灵”与编译器的“侦探” 在前面的章节中,我们深入学习了 Rust 的所有权系统,以及如何通过引用 (& 和 &mut) 进行借用,从而在不转移所有权的情况下安…

[密码学实战]CTF竞赛高频加密与解密技术详解

CTF竞赛高频加密与解密技术详解 一、CTF加密体系全景图 在CTF密码学挑战中&#xff0c;加解密技术主要分为四大战域&#xff1a; #mermaid-svg-lmm07BXqYAGYjymI {font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}#mermaid-svg-lm…

docker.desktop下安装普罗米修斯prometheus、grafana并看服务器信息

目标 在docker.desktop下先安装这三种组件,然后显示当前服务的CPU等指标。各种坑已踩,用的是当前时间最新的镜像 核心关系概述 组件角色依赖关系Prometheus开源监控系统,负责 数据采集、存储、查询及告警。依赖 Node-Exporter 提供的指标数据。Node-Exporter专用的 数据采集…

《MySQL:MySQL表的内外连接》

表的连接分为内连接和外连接。 内连接 内连接实际上就是利用where子句对两种表形成的笛卡尔积进行筛选&#xff0c;之前的文章中所用的查询都是内连接&#xff0c;也是开发中使用的最多的连接查询。 select 字段 from 表1 inner join 表2 on 连接条件 and 其他条件&#xff1…

实现SpringBoot底层机制【Tomcat启动分析+Spring容器初始化+Tomcat 如何关联 Spring容器】

下载地址&#xff1a; https://download.csdn.net/download/2401_83418369/90675207 一、搭建环境 创建新项目 在pom.xml文件中导入依赖 &#xff08;一定要刷新Maven&#xff09;排除内嵌的Tomcat&#xff0c;引入自己指定的Tomcat <?xml version"1.0" enco…

从零开始构建微博爬虫:实现自动获取并保存微博内容

从零开始构建微博爬虫&#xff1a;实现自动获取并保存微博内容 前言 在信息爆炸的时代&#xff0c;社交媒体平台已经成为信息传播的重要渠道&#xff0c;其中微博作为中国最大的社交媒体平台之一&#xff0c;包含了大量有价值的信息和数据。对于研究人员、数据分析师或者只是…

Uniapp微信小程序:轻松获取用户头像和昵称

参考文献&#xff1a;Uniapp微信小程序&#xff1a;轻松获取用户头像和昵称-百度开发者中心 (baidu.com) uni.login({ provider: weixin, success: function (loginRes) { console.log(loginRes.authResult); // 打印登录凭证 // 使用登录凭证获取用户信息 uni.getUserInfo({ …

【自然语言处理与大模型】大模型(LLM)基础知识③

&#xff08;1&#xff09;大模型的“7B”是什么意思&#xff1f; "B"通常代表“Billion”&#xff0c;即十亿。因此&#xff0c;当提到7B时&#xff0c;指的是该模型拥有7 billion&#xff08;70亿&#xff09;个参数。 &#xff08;2&#xff09;模型后面标的“ins…

聊聊自动化用例的维护

自动化测试中的农药悖论&#xff1a;为何长期维护至关重要 自动化测试常被视为"一次编写&#xff0c;永久有效"的解决方案&#xff0c;但随着时间的推移&#xff0c;即使设计最精良的测试套件也会逐渐失效。这种现象被称为农药悖论&#xff08;Pesticide Paradox&am…

微帧Visionular斩获NAB Show 2025年度产品奖

在本月刚结束的NAB Show 2025展会上&#xff0c;全球领先的视频编码与AI超高清服务提供商微帧Visionular大放异彩&#xff0c;其核心产品AI-driven Video Compression&#xff08;AI视频智能编码引擎&#xff09;不仅在展会中吸引了众多行业目光&#xff0c;更凭借其卓越的编码…

idea中运行groovy程序报错

我的项目是使用的 gradle 构建的。 在 idea 中运行Groovy的面向对象程序报错如下&#xff1a; Execution failed for task :Person.main(). > Process command G:/Program Files/jdk-17/jdk-17.0.12/bin/java.exe finished with non-zero exit value 1* Try: Run with --s…

【自然语言处理与大模型】个人使用LLaMA Factory微调的记录

一、魔塔社区免费服务器如何使用webui微调&#xff1f; 一上来我就得先记录一下&#xff0c;使用魔塔社区的免费服务器的时候&#xff0c;因为没有提供ssh而导致无法看到webui的遗憾如何解决的问题&#xff1f; 执行命令 如果点这个链接无法弹出微调的webui&#xff0c;则可以在…

【官方正版,永久免费】Adobe Camera Raw 17.2 win/Mac版本 配合Adobe22-25系列软

Adobe Camera Raw 2025 年 2 月版&#xff08;版本 17.2&#xff09;。目前为止最新版新版已经更新2个月了&#xff0c;我看论坛之前分享的还是2024版&#xff0c;遂将新版分享给各位。 Adobe Camera Raw&#xff0c;支持Photoshop&#xff0c;lightroom等Adobe系列软件&#…

leetcode:1295. 统计位数为偶数的数字(python3解法)

难度&#xff1a;简单 给你一个整数数组 nums&#xff0c;请你返回其中位数为 偶数 的数字的个数。 示例 1&#xff1a; 输入&#xff1a;nums [12,345,2,6,7896] 输出&#xff1a;2 解释&#xff1a; 12 是 2 位数字&#xff08;位数为偶数&#xff09; 345 是 3 位数字&…

使用Handsontable实现动态表格和下载表格

1.效果 2.实现代码 首先要加载Handsontable&#xff0c;在示例中我是cdn的方式引入的&#xff0c;vue的话需要下载插件 let hot null;var exportPlugin null;function showHandsontable(param) {const container document.getElementById("hot-container");// 如果…