《从内核到应用:Linux 信号的保存逻辑与捕捉过程》

《从内核到应用:Linux 信号的保存逻辑与捕捉过程》
前引:Linux 信号是进程间通信的核心机制,更是保障程序稳定性的关键 —— 当进程收到中断、终止等信号时,如何正确保存信号状态、避免丢失,又如何按预期执行处理逻辑,直接决定了程序的可靠性。本文将从内核底层逻辑出发,拆解信号保存的核心机制,结合实际代码案例,带你掌握 Linux 信号处理的完整流程与实操技巧!

目录

【一】信号的保存

(1)信号在内核的保存

(2)信号集

(3)信号集操作

(1)sigemptyset()

(2)sigfillset()

(3)sigaddset()

(4)sigdelset()

(5)sigismember()

(6)sigprocmask()

函数使用总结:

【二】二谈进程地址空间

【三】内核|用户态的切换

【四】信号的捕捉过程

(1)sigaction()

(2)信号自动屏蔽

(3)如何获取pending集

【五】可重入函数

【六】volatile关键字

【七】子进程的“遗嘱”信号


【一】信号的保存

在上篇我们已经学习了什么是信号、信号的产生,信号从产生到被处理不是立刻被执行的:

下面我们先来认识几个概念:

信号递达:实际处理信号的动作

信号末决:从产生到处理(递达)之间的过程

信号阻塞:进程可以选择阻塞某个信号,阻塞的信号会保持在末决状态(产生但是不执行),只                      有解决对该信号的阻塞,才会执行递达

信号忽略:区分“信号阻塞”(产生但是不执行),“忽略”属于执行处理动作的一种(选择忽略处理)

(1)信号在内核的保存

在内核中,信号其实通过三个指针被保存在task_struct中的:这涉及到三张表,我们逐一来看:

block:信号的标志位,对应状态信号阻塞状态(是否正常执行):0表示非阻塞,1表示阻塞

pending:信号的标志位,对应信号的接收:比如接收了2号信号(00000000.....00000010)

handler:信号的处理方式(默认处理、忽略处理、自定义处理),替换指针即替换了执行方法

当一个信号产生到执行,会根据三张表的查询来解决:pending—>block—>handler

(2)信号集

每个信号都有末决标志:非0即1,包括其中的阻塞状态也是0和1,对应该信号“有效”or“无效”

因此该状态的存储类型都可以表示为:sigset_t类型

sigset_t:称为信号集,用来表示信号的一种状态,比如:

阻塞信号集:表示阻塞或者非阻塞状态;末决信号集:末决状态或者非末决状态

(3)信号集操作

我们可以操作下面的接口来实现控制 sigset_t 类型的变量,从而改变信号末决(过程):

#include <signal.h>
(1)sigemptyset()

原型:

int sigemptyset(sigset_t *set);

参数:指向 sigset_t 类型的指针(参数名自己设置)

返回值:成功返回 0;失败返回 -1,并设置 errno

作用:代表要初始化的信号集

例如:

(2)sigfillset()

原型:

int sigfillset(sigset_t *set);

参数:指向 sigset_t 类型的指针

返回值:成功返回 0;失败返回 -1,并设置 errno

作用:将信号集 set 初始化为全包含集合(可包含所有信号)

例如:

(3)sigaddset()

原型:

int sigaddset(sigset_t *set, int signo);

参数:

  • set:指向 sigset_t 类型的指针,代表要操作的信号集
  • signo:要添加的信号编号

返回值:成功返回 0;失败返回 -1,并设置 errno

作用:将信号添加到信号集

例如:

(4)sigdelset()

原型:

int sigdelset(sigset_t *set, int signo);

参数:

  • set:指向 sigset_t 类型的指针
  • signo:要删除的信号编号

返回值:成功返回 0;失败返回 -1,并设置 errno

作用:从信号集 set 中删除指定信号

(5)sigismember()

原型:

int sigismember(const sigset_t *set, int signo);

参数:

  • set:指向 const sigset_t 类型的指针
  • signo:要检查的信号编号

返回值:

  • 若信号 signo 在集合 set 中,返回 1
  • 若不在集合中,返回 0
  • 失败返回 -1,并设置 errno(如 signo 无效或 set 指针为空)

作用:检查指定信号 signo 是否为信号集 set 的成员

(6)sigprocmask()

原型:

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

参数:

第一个参数:SIG_BLOCK:将 set 中的信号添加到当前屏蔽集

                      SIG_UNBLOCK:将set中的信号从当前屏蔽集移除

                      SIG_SETMASK:将当前屏蔽集直接设置为set的值

第二个参数:指向 sigset_t 类型的指针。若为 NULL,则 how 的操作被忽略,仅用于 “获取旧屏蔽                       集”

第三个参数:指向 sigset_t 类型的指针,用于保存 “修改前的旧屏蔽集”。若为 NULL,则不保存旧                        屏蔽集

返回值:

  • 成功:返回 0
  • 失败:返回 -1,并设置 errno

作用:sigprocmask 直接修改进程 task_struct 中的 block 集合,从而控制信号的 “阻塞状态”

例如:

函数使用总结:

这些函数看起来很杂乱,但是它们的功能其实都是在执行一件事:改变当前进程的 block 信号集

(1)(2)(3)(4)(5):你自己创建一个信号集,将你想统一操作的信号放在信号集

(6):根据选项选择统一屏蔽/解决屏蔽信号集里面的信号

             它的第三个参数:用来一键恢复修改之前的信号集,目前简单场景用不到~

【二】二谈进程地址空间

如果有50个完全不同进程,那么可能有50个页表,对应50个该进程在内存的分布空间,而我们可以看到上面有1G的内核空间,那对应50个内核代码和数据吗?内核空间也有自己的内核级页表

答案:内核的代码和数据只有一份,被50个进程重复使用,因为里面是系统调用的相关的方法

                                         

我们看下面的进程地址空间与、页表、内存的大概联系图:

操作系统来做软硬件的管理者,那么操作系统本身也是软件资源,谁让它一直跑的?

时钟中断是由硬件定时器(如 CPU 的时钟芯片)周期性触发的中断信号,类似操作系统的心脏

操作系统内核会不断执行 “等待时钟中断 → 处理中断 → 回到等待” 的循环~

【三】内核|用户态的切换

在这里需要两个概念储备:然后我们直接看表:

用户态:允许你访问用户的代码和数据

内核态:允许你访问操作系统的代码和数据

状态的切换是需要每次更改CPU中ecs寄存器的标志位:00(用户态)-><-11(内核态)

(1)用户执行系统调用,就进入了内核态->(2)

(2)完成系统调用之后准备返回用户态,此时发送信号检测,如果进程中有信号,且信号的block标志位对应1,说明该信号不用管,直接进入(3);否则执行该信号:默认执行方法就执行完返回用户态直接进入(3);自定义信号执行方法需要返回用户态执行用户指定的方法,然后进入内核态,再返回用户态直接进入(3)

(3)开始下一轮系统调用

因此:信号不是立刻被执行的,有一个检测的过程

【四】信号的捕捉过程

(1)sigaction()

我们来看一个函数:(signal()+sigprocmask()==sigaction()【狗头】)

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

参数:

第一个参数:要操作的信号编号

第二个参数:指向 struct sigaction 结构体的指针,用于设置新的信号处理规则(如果为 NULL,则只获取当前规则,不修改)

第三个参数:指向 struct sigaction 结构体的指针,用于保存旧的信号处理规则(如果为 NULL,则不保存)

struct sigaction:

struct sigaction { void (*sa_handler)(int); // 简单信号处理函数(参数为信号编号) void (*sa_sigaction)(int, siginfo_t *, void *); // 高级处理函数(带更多信息) sigset_t sa_mask; // 处理信号时需要阻塞的信号集 int sa_flags; // 控制信号处理行为的标志 void (*sa_restorer)(void); // 已废弃(无需关注) };

返回值:成功返回0;失败返回-1,设置errno

作用:为指定的信号(比如 SIGINT 中断信号、SIGTERM 终止信号)设置新的处理规则,或获取当             前已有的处理规则

使用说明:一般对于刚创建的结构体可以使用 memset()接口来置空成员

#include <string.h> void *memset(void *s, int c, size_t n); 参数说明: s:指向要操作的内存区域的指针(对于结构体,就是结构体变量的地址,用 & 取地址) c:要设置的值(通常传 0,表示清零) n:要设置的内存字节数(对于结构体,就是 sizeof(结构体变量) 或 sizeof(结构体类型))
(2)信号自动屏蔽

说明:当一个信号正在执行执行方法时,系统会自动将该信号屏蔽,避免多次出现该信号,在执               行方法完成时,再解除对该信号的自动屏蔽。如果你想在执行某个信号方法时,额外屏蔽               其它信号,可以使用 sa_flags 标志位(这是sigaction()的成员,一般带选项设值)

(3)如何获取pending集


参数:指向一个 sigset_t 变量,函数成功时自动将当前未决的信号集填充到 set 中

未决信号的特点:已被发送(如通过 kill() 或系统事件产生)。处于阻塞状态(在 block 信号集中),因此未被处理。一旦信号从 block 集中移除(解除阻塞),会立即被处理


参数:

   set
:指向要检查的信号集(sigset_t 类型)

   signum:要检查的信号编号(按对应标志位检查)

返回值:若信号 signum 在 set 中:返回 1若信号 signum 不在 set 中:返回 0失败(如 signum 无效):返回 -1,并设置 errno=EINVAL

【五】可重入函数

简单理解:一个工程没有执行完立刻执行另外一个工程,此时容易发生事故。例如:

如果⼀个函数符合以下条件之⼀则是不可重⼊的:

(1)调⽤了malloc或free,因为malloc也是⽤全局链表来管理堆的

(2)调⽤了标准I/O库函数。标准I/O库的很多实现都以不可重⼊的⽅式使⽤全局数据结构

【六】volatile关键字

我们看下面的代码:flag为全局变量;如果出现2号信号会执行自定义方法;执行while的条件是真

#include <stdio.h> #include <signal.h> int flag = 0; void handler(int sig) { printf("chage flag 0 to 1\n"); flag = 1; } int main() { signal(2, handler); while(!flag); printf("process quit normal\n"); return 0; } 

代码情况:当出现2号信号flag=1,而main里面循环的条件是为真(即一直循环),但是当你按下Ctrl+C,循环也一直就行不退出,按理说:flag会被设为1,不进入循环

理解:内存读写比寄存器慢得多,编译器会尽量把变量 “缓存” 到寄存器中。编译器会分析 “谁会修改flag”。在main函数内部,没有任何代码修改flag;而编译器默认不会跨函数分析 “其他函数是否修改了全局变量”,因此就需要volatile去修饰变量,告诉编译器该变量需要从内存读取

解释:编译器为了提高程序性能,会对代码进行优化(比如将变量缓存到寄存器,减少内存访问次数)。但如果变量的值可能被程序外部的因素修改(如多线程、硬件、信号等),这种优化就会导致程序逻辑错误。volatile 就像给变量贴了个 “警示牌”:每次用它都得去内存里查最新值

【七】子进程的“遗嘱”信号

如下直接出结论:

子进程在退出的时候会给父进程发生17号信号

而waitpid是可以在信号执行函数中回收子进程的
如果你想要子进程在退出的时候不捕获信号:signal (SIGCHLD, SIG_IGN)

那么内核会在子进程终止后直接清理其进程控制块(PCB),无需父进程调用 wait/waitpid
如果你想让子进程变成僵尸进程:不捕获也不忽略:signal(SIGCHLD, SIG_DFL)

那么内核会在子进程终止后不会清理其进程控制块(PCB)

Read more

Python文本为什么会乱码?从根源到解决方案的深度解析

“乱码”是每个Python开发者,尤其是处理中文、日文等非ASCII字符时,都会遇到的“噩梦”。明明代码逻辑正确,文件也存在,但打印出来或保存的文件却是一堆莫名其妙的符号(如éÂ\x87Â\x91éÂ\x9eÂ\x93)。 这篇文章将带你彻底理解乱码产生的根本原因,并提供一套行之有效的解决方案和最佳实践。 一、乱码的本质:编码与解码的“鸡同鸭讲” 要理解乱码,首先必须明白两个核心概念:字符集(Charset) 和 字符编码(Character Encoding)。 1. 字符集(Charset):是一个系统支持的所有抽象字符的集合。比如: * ASCII:包含128个字符(英文字母、数字、符号),用1个字节(8位)表示。 * GBK/GB2312:中国国家标准,包含汉字、符号等,用1或2个字节表示。 * Unicode:

By Ne0inhk

B站充电视频下载器(需配合会员Cookie使用,仅供学习交流,Python)

这个程序是一个用于下载B站充电视频的工具,依赖于用户提供的会员Cookies。如何获取B站cookie请参考本站cookie登录b站获取cookie登录billbill教程。 程序主要功能:加载和验证Cookies,从文件中读取Cookies,并验证其有效性。获取视频信息,通过B站API获取视频的详细信息。获取视频播放地址,通过B站API获取视频的实际播放地址。 下载视频,从播放地址下载视频文件,并显示下载进度。 首先,类定义和初始化。初始化时从 cookie_file 中加载Cookies,并设置HTTP请求头。 class ChargeVideoDownloader: def __init__(self, cookie_file): self.headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/

By Ne0inhk

python addict的用法

addict 是一个流行的 Python 第三方库,主要用来让你像访问属性一样访问字典里的数据(即点操作符访问属性,而不是方括号),从而使字典操作更方便和美观。常用的核心类为Dict。 基本用法 安装 addict pip install addict 基本示例 from addict import Dict data = Dict() data.name ="Tom"# 直接赋值属性 data.age =21print(data)# {'name': 'Tom', 'age': 21}print(data.name)# 'Tom'print(data[

By Ne0inhk

FMPy完全指南:Python中FMU仿真的终极解决方案

FMPy完全指南:Python中FMU仿真的终极解决方案 【免费下载链接】FMPySimulate Functional Mockup Units (FMUs) in Python 项目地址: https://gitcode.com/gh_mirrors/fm/FMPy 在当今复杂的工程仿真领域,功能性模型单元(FMU)已成为系统建模和仿真的标准格式。FMPy作为一款强大的Python库,为工程师和研究人员提供了在Python环境中轻松模拟FMU文件的完整工具链。这款免费开源工具支持从FMI 1.0到3.0的全系列标准,无论是Co-Simulation还是Model Exchange模式,都能在Windows、Linux和macOS系统上无缝运行。 为什么选择FMPy进行FMU仿真? 多平台兼容性保障 FMPy采用纯Python实现,确保了跨平台的兼容性。无论您使用的是哪种操作系统,都能获得一致的仿真体验。这种设计理念使得团队协作更加顺畅,无需担心环境差异带来的问题。 完整的FMI标准支持 从基础到高级,FMPy全面覆盖FMI标准的所有版本。这意味着您可以导入和使用市面

By Ne0inhk