目录
[一 保存信号](#一 保存信号)
[1 补充概念](#1 补充概念)
[2 在内核中的表现](#2 在内核中的表现)
[3 sigset_t](#3 sigset_t)
[4 信号集操作函数](#4 信号集操作函数)
[(1)sigprocmask:读取或更改进程的信号屏蔽字(block表)](#(1)sigprocmask:读取或更改进程的信号屏蔽字(block表))
[5 代码演示](#5 代码演示)
[二 信号处理](#二 信号处理)
[1 内核态 用户态](#1 内核态 用户态)
[2 信号处理的流程](#2 信号处理的流程)
一 保存信号
当前阶段

信号处理不一定会被立即执行。如果操作系统此时正处理优先级更高的任务,该信号会被暂时保存,等待合适时机再进行处理。
信号保存本质是延后处理
1 补充概念
实际执行信号的处理动作称为信号递送 (Delivery)
信号从产生到递送之间的状态,称为信号未决 (Pending)。
进程可以选择阻塞 (Block) 某个信号。
被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递送的动作.
注意,阻塞和忽略是不同的,只要信号被阻塞就不会递送,而忽略是在递送之后可选的一种处理动作
进程阻塞信号:信号可以被未决(收到信号),但是永远不会被递达,直到解除阻塞;进程阻塞信号是会提前做好的
2 在内核中的表现
在操作系统内部,每一个进程与信号强相关的,一共涉及三张表:

(1)pending表-->位图结构 ,每一位对应一个信号;作用是记录信号的到达状态(是否已产生但是未处理)
位含义:
1:表示该信号已产生 (送达进程),但暂未被处理,处于 未决 (Pending) 状态。
0:表示该信号未产生,或已被处理
(2)handler表--->函数指针数组(下标是信号编号),作用是定义进程收到信号后该做什么
常见取值:
SIG_DFL :默认处理(Default)。例如终端输入Ctrl+C产生的SIGINT信号,默认动作是终止进程。
SIG_IGN :忽略处理(Ignore)。进程收到该信号后,直接丢弃不做任何响应。
自定义函数指针:如图中void sighandler(int signo),用户通过signal()或sigaction()系统调用注册的自定义信号处理函数
我们以前学过的signal(3,handler)方法(设定对特定信号的自定义捕捉动作),signal的这个方法作用是找到handler表,在3号下标处,把用户对应的地址设定起来,这样信号就能保存
(3)block表,支持进程阻塞 block位图:a.比特位的位置,表示的是信号编号(同pending表) b.比特位的内容,表示是否阻塞对应信号(pending表示的是是否收到)
位含义:
1:表示阻塞对应编号的信号。如果信号产生,会被挡在防火墙外,保持 pending 状态,不允许递达。
0:表示不阻塞该信号。如果 pending 位为1,系统会立即尝试递送
这三张表赋予进程能识别并处理信号的能力
3 sigset_t
sigset_t 是 Linux 系统定义的信号集数据类型,本质为位图结构。
我们可在用户空间(全局变量、堆内存等)定义 sigset_t 类型变量,通过信号集操作函数配置其内部的位图状态;再通过 sigprocmask 等系统调用,将该位图同步到进程内核的 block(阻塞)或 pending(未决)表中,实现对信号阻塞 / 未决状态的管理。
从上图来看,每个信号只有一个 bit 的未决标志,非 0 即 1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集,这个类型可以表示每个信号的 "有效" 或 "无效" 状态,在阻塞信号集中 "有效" 和 "无效" 的含义是该信号是否被阻塞,而在未决信号集中 "有效" 和 "无效" 的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。阻塞信号集也叫做当前进程的信号屏蔽字 (Signal Mask), 这里的 "屏蔽" 应该理解为阻塞而不是忽略
4 信号集操作函数
sigset_t 类型对于每种信号用一个 bit 表示 "有效" 或 "无效" 状态,至于这个类型内部如何存储这些 bit 则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作 sigset_t 变量,而不应该对它的内部数据做任何解释,比如用 printf 直接打印 sigset_t 变量是没有意义的。
cpp
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
函数 sigemptyset 初始化 set 所指向的信号集,使其中所有信号的对应bit 清零 ,表示该信号集不包含任何有效信号。
函数sigfillset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 置位 ,表示该信号集的有效信号包括系统支持的所有信号。
注意,在使用 sigset_t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。
这四个函数都是成功返回 0, 出错返回 - 1。sigismember 是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回 1, 不包含则返回 0, 出错返回 - 1。
| 函数名 | 作用 | 返回值 |
|---|---|---|
sigemptyset |
清空信号集(所有 bit 置 0,无有效信号) | 成功 0,失败 - 1 |
sigfillset |
填满信号集(所有 bit 置 1,包含全部信号) | 成功 0,失败 - 1 |
sigaddset |
向信号集添加指定信号(对应 bit 置 1) | 成功 0,失败 - 1 |
sigdelset |
从信号集删除指定信号(对应 bit 置 0) | 成功 0,失败 - 1 |
sigismember |
判断信号集是否包含指定信号 | 包含返回 1,不包含返回 0,失败 - 1 |
(1)sigprocmask:读取或更改进程的信号屏蔽字(block表)
cpp
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
参数说明:
| 参数名 | 类型 | 作用 | 取值说明 |
|---|---|---|---|
how |
int |
决定如何修改信号屏蔽字(block 表) | 1. SIG_BLOCK:将set中的信号添加 到阻塞集2. SIG_UNBLOCK:将set中的信号从阻塞集移除 3. SIG_SETMASK:直接覆盖 设置阻塞集为set |
set |
const sigset_t * |
传入要操作的信号集 | 指向sigset_t类型变量,存放需要阻塞 / 解除的信号;传NULL表示不修改屏蔽字,仅读取 |
oset |
sigset_t * |
输出参数 ,保存修改前的旧屏蔽字 | 用于备份原阻塞状态,方便后续恢复;不关心可直接传NULL |
第一个参数是一个输入型参数 ,通过sigpromask设置block位图;第三个参数是输出型参数,把block修改前的内容带出去,将来想恢复时,可以再设置
(2)sigpending:获取pending位图
cpp
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调⽤成功则返回0,出错则返回-1
参数也是sigset_t类型,是输出型参数,作用是获取当前调用进程的pending信号
我们怎么不见pending信号集的写入 && 修改操作?
其实已经见过了:在信号产生的5种方式种,例如:kill,alarm....,本质就是在修改pending位图
5 代码演示
cpp
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <signal.h>
void PrintPending(sigset_t &pending)
{
// 0000 0000... 0000 -> 0000 0000... 0010
for (int signum = 31; signum >= 1; signum--)
{
if (sigismember(&pending, signum))
{
printf("1");
}
else
{
printf("0");
}
}
printf("\n");
}
void handler(int signum)
{
std::cout << "get a signal: " << signum << std::endl;
// 在处理2号信号期间,2号信号被自动block了!
while(true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
// exit(0);
}
int main()
{
struct sigaction act, old_act;
act.sa_handler = handler;
act.sa_flags = 0;
sigemptyset(&(act.sa_mask)); //???, 如果进程收到了大量的重复信号???
sigaddset(&(act.sa_mask), 1);
sigaddset(&(act.sa_mask), 3);
sigaddset(&(act.sa_mask), 4);
sigaddset(&(act.sa_mask), 5);
sigaction(2, &act, &old_act); //signal
while(true)
{
pause();
}
}
在上述代码中,我们看到有struct sigaction,那它是什么呢?
sigaction() 是比 signal() 更强大、更稳定的信号注册函数 。
可以设置:
sa_handler:处理函数
sa_mask:在处理信号期间额外阻塞的信号集
sa_flags:一般填 0
结论:
信号处理时,自身信号会被自动阻塞
当进程正在执行某个信号的处理函数(handler)时,内核会自动把该信号阻塞。
此时再发送相同信号 → 不会递达,只会进入 pending 状态。
表现:pending 位图对应位 = 1
Linux 中 9 号(SIGKILL)和 19 号(SIGSTOP)信号是 "绝对特权信号",不可被屏蔽、不可被捕捉、不可被忽略
为什么要这么设计?(系统安全)
防止恶意进程 / 死循环程序屏蔽所有信号,导致管理员无法杀死、无法控制
是系统保留的 "终极手段",保证 root 永远能管控所有进程
二 信号处理
当前阶段

信号收到后,不一定会立即递达,而是在合适的时候
那什么是合适的时候呢?(1)核心工作做完了(2)进程从内核态返回用户态的时候,进行信号的检测和处理
1 内核态 用户态
用户态:执行用户代码,访问用户数据--->收到管控的,权限级别低
内核态:访问操作系统的代码,数据--->权限级别高
2 信号处理的流程

上半部分是用户态,下半部分是内核态
通过对block和pending的检查,判断是否要处理
步骤 1:用户态执行,触发内核态切换
「在执行主控制流程的某条指令时,因为中断、异常或系统调用进入内核」
进程正常运行 main 主流程(用户态),当发生硬件中断(如键盘输入 Ctrl+C)、异常(如除零错误)、系统调用(如 read/write 时,CPU 会从用户态切换到内核态,执行内核代码。
这是信号处理的起点:只有进入内核态,内核才有机会检查 pending 信号。
步骤 2:内核处理完任务,准备返回用户态前,先处理信号
「内核处理完异常准备回用户模式之前,先处理当前进程中可以递送的信号」
内核完成中断 / 异常 / 系统调用的处理后,不会立刻返回用户态,而是先执行 do_signal() 函数:
检查进程的 pending 表(未决信号集),找出未被阻塞、可以递达的信号。
读取 handler 表,确认信号的处理动作(默认 / 忽略 / 自定义)。
步骤 3:自定义信号处理函数,切换回用户态执行
「如果信号的处理动作是自定义的信号处理函数,则回到用户模式执行信号处理函数(而不是回到主控制流程)」
如果信号的 handler 是自定义函数(不是默认 / 忽略),内核会:
把当前进程的上下文(寄存器、程序计数器等)保存起来。
修改栈帧,让返回用户态时,跳转到自定义的 sighandler 函数,而不是回到 main 主流程被打断的位置。
切换回用户态,执行用户写的信号处理函数。
这一步是信号异步性的核心:用用户态函数处理内核态检测到的信号。
步骤 4:信号处理函数返回,再次进入内核态
「信号处理函数返回时,执行特殊的系统调用 sigreturn 再次进内核」
当 sighandler 函数执行完毕、return 时,会触发特殊的系统调用 sigreturn,CPU 再次从用户态切换回内核态。
作用:让内核恢复之前保存的进程上下文,为回到主流程做准备。
步骤 5:内核恢复上下文,最终返回用户态主流程
「返回用户模式,从主控制流程中上次被中断的地方继续向下执行」
内核执行 sys_sigreturn(),恢复步骤 3 保存的寄存器、栈帧等上下文。
最后切换回用户态,从 main 主流程上次被中断的那条指令的下一条继续执行,信号处理流程彻底结束。
执行用户的捕捉方法,是用户态执行,还是内核态执行handler方法?
谁的代码谁来跑;执行代码是你的进程在执行
涉及用户态和内核态的切换:一共四次状态切换
