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


前引:Linux 信号是进程间通信的核心机制,更是保障程序稳定性的关键 —— 当进程收到中断、终止等信号时,如何正确保存信号状态、避免丢失,又如何按预期执行处理逻辑,直接决定了程序的可靠性。本文将从内核底层逻辑出发,拆解信号保存的核心机制,结合实际代码案例,带你掌握 Linux 信号处理的完整流程与实操技巧!
目录
【一】信号的保存
在上篇我们已经学习了什么是信号、信号的产生,信号从产生到被处理不是立刻被执行的:

下面我们先来认识几个概念:
信号递达:实际处理信号的动作
信号末决:从产生到处理(递达)之间的过程
信号阻塞:进程可以选择阻塞某个信号,阻塞的信号会保持在末决状态(产生但是不执行),只 有解决对该信号的阻塞,才会执行递达
信号忽略:区分“信号阻塞”(产生但是不执行),“忽略”属于执行处理动作的一种(选择忽略处理)

(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集
#include <signal.h> int sigpending(sigset_t *set);参数:指向一个
sigset_t变量,函数成功时自动将当前未决的信号集填充到set中未决信号的特点:
- 已被发送(如通过
kill()或系统事件产生)。- 处于阻塞状态(在 block 信号集中),因此未被处理。
- 一旦信号从 block 集中移除(解除阻塞),会立即被处理
#include <signal.h> int sigismember(const sigset_t *set, int signum);参数:
set:指向要检查的信号集(sigset_t类型)
signum:要检查的信号编号(按对应标志位检查)for(int i=32;i>0;i--) { d = sigismember(&set, i); if(d==1) { std::cout<<"1"; } else { std::cout<<"0"; } }返回值:
- 若信号
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)
更多推荐




所有评论(0)