【Linux】进程信号(二):信号的保存与阻塞机制
文章目录
Linux进程信号(二):信号的保存与阻塞机制
💬 承上启下:在上一篇中,我们学习了信号的五种产生方式,理解了信号是进程间事件异步通知的一种机制。但是,信号产生后并不会立即处理,而是在"合适的时候"处理。那么问题来了:信号产生后到被处理之前,是如何被保存的?进程如何控制哪些信号可以被处理,哪些信号需要被暂时阻塞? 本篇将深入内核,揭开信号保存和阻塞机制的神秘面纱。
👍 点赞、收藏与分享:本篇包含大量内核数据结构图示、位图操作、完整代码实验,内容深入且实用,如果对你有帮助,请点赞、收藏并分享!
🚀 循序渐进:建议先复习第一篇的内容,理解信号的基本概念和产生方式,再学习本篇。
一、信号相关核心概念
1.1 三个重要概念
在深入学习信号保存机制之前,我们必须先理解三个核心概念:
1. 信号递达(Delivery)
定义:
- 实际执行信号的处理动作称为信号递达
理解:
- 递达 = 信号真正被处理
- 可以是执行默认动作
- 可以是执行忽略动作
- 可以是执行自定义处理函数
2. 信号未决(Pending)
定义:
- 信号从产生到递达之间的状态,称为信号未决
理解:
- 未决 = 已经产生,但还没有被处理
- 就像快递已经到了楼下,但你还没有去取
- 进程需要记住这个信号,等待合适的时候处理
3. 信号阻塞(Block)
定义:
- 进程可以选择阻塞某个信号
- 被阻塞的信号产生时将保持在未决状态
- 直到进程解除对此信号的阻塞,才执行递达的动作
理解:
- 阻塞 = 暂时不想处理这个信号
- 就像你设置了"勿扰模式",快递到了也不通知你
- 但信号会被记录下来,等你取消"勿扰模式"后再处理
三者关系图:
时间线:
────────────────────────────────────────────────────→
t1: 信号产生
│
├─ 检查是否被阻塞?
│
├─ 如果被阻塞 ─→ 设置pending位,保持未决状态
│ │
│ ├─ 等待解除阻塞
│ │
│ └─ 解除阻塞后 ─→ 信号递达
│
└─ 如果未被阻塞 ─→ 设置pending位,等待递达
│
└─ 合适时机 ─→ 信号递达
t2: 信号递达(处理)
1.2 阻塞 vs 忽略
很多初学者容易混淆"阻塞"和"忽略",这里重点区分:
| 对比项 | 阻塞(Block) | 忽略(Ignore) |
|---|---|---|
| 时机 | 信号产生时 | 信号递达时 |
| 状态 | 保持未决状态 | 递达后丢弃 |
| pending位 | 置1 | 先置1,递达后清0 |
| 能否解除 | 可以解除阻塞后处理 | 已经丢弃,无法处理 |
| 处理动作 | 还未到处理阶段 | 处理动作是"忽略" |
举例说明:
快递例子:
阻塞:
- 你设置了"勿扰模式"
- 快递员打电话,但被屏蔽了
- 但你的手机记录了"未接来电"
- 等你关闭"勿扰模式",看到未接来电,还可以回拨
忽略:
- 你没有设置"勿扰模式"
- 快递员打通了电话
- 你说"我不要这个快递,退回去吧"
- 快递被退回,永远拿不到了
代码验证:
#include <iostream>
#include <unistd.h>
#include <signal.h>
int main()
{
std::cout << "进程PID: " << getpid() << std::endl;
// 场景1:阻塞2号信号(后面会学习如何阻塞)
// 此时按Ctrl+C,信号产生但不处理,保持未决状态
// 场景2:忽略2号信号
signal(SIGINT, SIG_IGN);
// 此时按Ctrl+C,信号递达但处理动作是忽略,直接丢弃
while(true) {
std::cout << "running..." << std::endl;
sleep(1);
}
return 0;
}
📌 重点理解:
1. 阻塞和忽略是不同的
- 阻塞:信号还没递达,暂时不处理
- 忽略:信号已经递达,处理动作是"什么都不做"
2. 只要信号被阻塞就不会递达
3. 忽略是在递达之后可选的一种处理动作
4. 阻塞可以解除,解除后信号会递达
忽略是已经处理完了,无法恢复
二、信号在内核中的表示
2.1 核心数据结构
要理解信号如何被保存,我们必须看看内核是如何表示信号的。
Linux内核2.6.18的task_struct结构(简化):
struct task_struct {
// ... 其他字段
/* signal handlers */
struct sighand_struct *sighand; // 信号处理函数表
sigset_t blocked; // 阻塞信号集(block位图)
struct sigpending pending; // 未决信号集(pending位图)
// ... 其他字段
};
sighand_struct结构:
struct sighand_struct {
atomic_t count;
struct k_sigaction action[_NSIG]; // #define _NSIG 64
spinlock_t siglock;
};
k_sigaction结构:
struct k_sigaction {
struct __new_sigaction sa;
void __user *ka_restorer;
};
struct __new_sigaction {
__sighandler_t sa_handler; // 信号处理函数
unsigned long sa_flags;
void (*sa_restorer)(void);
__new_sigset_t sa_mask;
};
/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
sigpending结构:
struct sigpending {
struct list_head list;
sigset_t signal; // pending位图
};
2.2 信号在PCB中的完整表示
核心图示:
task_struct (PCB)
│
├─ blocked (sigset_t) ← 阻塞信号集(block位图)
│ ┌──┬──┬──┬──┬──┬──┬──┬──┐
│ │ 0│ 1│ 0│ 1│ 0│ 0│ 0│ 0│ ... (共64位)
│ └──┴──┴──┴──┴──┴──┴──┴──┘
│ ↑ ↑ ↑ ↑
│ │ │ │ └─ 4号信号被阻塞
│ │ │ └──── 3号信号未阻塞
│ │ └─────── 2号信号被阻塞
│ └────────── 1号信号未阻塞
│
├─ pending (sigpending) ← 未决信号集(pending位图)
│ ┌──┬──┬──┬──┬──┬──┬──┬──┐
│ │ 0│ 1│ 0│ 1│ 0│ 0│ 0│ 0│ ... (共64位)
│ └──┴──┴──┴──┴──┴──┴──┴──┘
│ ↑ ↑ ↑ ↑
│ │ │ │ └─ 4号信号已产生,未决
│ │ │ └──── 3号信号未产生
│ │ └─────── 2号信号已产生,未决
│ └────────── 1号信号未产生
│
└─ sighand->action[64] ← 信号处理函数数组
┌───────────────────────┐
│ [0] 未使用 │
│ [1] SIG_DFL │ ← 1号信号默认处理
│ [2] handler函数地址 │ ← 2号信号自定义处理
│ [3] SIG_IGN │ ← 3号信号忽略
│ [4] SIG_DFL │ ← 4号信号默认处理
│ ... │
│ [63] SIG_DFL │
└───────────────────────┘
2.3 具体例子
假设进程当前状态如下:
信号 block pending handler
────────────────────────────────────
SIGHUP 0 0 SIG_DFL ← 未阻塞,未产生,默认处理
SIGINT 1 1 handler ← 已阻塞,已产生,自定义处理
SIGQUIT 1 0 handler ← 已阻塞,未产生,自定义处理
SIGILL 0 0 SIG_DFL ← 未阻塞,未产生,默认处理
...
分析:
1. SIGHUP(1号):
- block=0:未阻塞
- pending=0:未产生
- handler=SIG_DFL:默认处理动作
- 状态:一旦产生,立即递达,执行默认动作
2. SIGINT(2号):
- block=1:已阻塞
- pending=1:已产生
- handler=handler:自定义处理函数
- 状态:虽然已产生,但被阻塞,暂时不能递达
- 注意:虽然处理动作是自定义函数,但在解除阻塞之前不能执行
因为进程仍有机会改变处理动作之后再解除阻塞
3. SIGQUIT(3号):
- block=1:已阻塞
- pending=0:未产生
- handler=handler:自定义处理函数
- 状态:一旦产生,将被阻塞,保持未决状态
重要问题:
Q: 如果在进程解除对某信号的阻塞之前,这种信号产生过多次,将如何处理?
A: POSIX.1允许系统递送该信号一次或多次。
Linux的实现:
- 常规信号:在递达之前产生多次只计一次
原因:pending位图只有一个bit,非0即1
- 实时信号:在递达之前产生多次可以依次放在一个队列里
原因:实时信号有专门的队列机制
本章只讨论常规信号(1-31号)。
三、sigset_t信号集
3.1 sigset_t类型
从上面的数据结构可以看到,每个信号只有一个bit的未决标志,非0即1,不记录该信号产生了多少次。阻塞标志也是这样表示的。
因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储。
typedef struct {
unsigned long sig[_NSIG_WORDS]; // _NSIG_WORDS = 64/32 = 2
} sigset_t;
// 在32位系统上:
// sig[0]: 表示信号1-32
// sig[1]: 表示信号33-64
sigset_t称为信号集,这个类型可以表示每个信号的"有效"或"无效"状态:
在阻塞信号集(blocked)中:
- "有效" = 该信号被阻塞
- "无效" = 该信号未被阻塞
在未决信号集(pending)中:
- "有效" = 该信号处于未决状态
- "无效" = 该信号未产生
阻塞信号集也叫做当前进程的信号屏蔽字(Signal Mask)
📌 注意:
这里的"屏蔽"应该理解为阻塞而不是忽略!
屏蔽(mask) = 阻塞(block)
3.2 为什么需要信号集操作函数?
问题:
sigset_t类型内部如何存储这些bit依赖于系统实现,
从使用者的角度是不必关心的,也不应该关心。
比如:
- 你不能用printf直接打印sigset_t变量
- 你不能直接操作sigset_t变量的bit位
- 你必须使用系统提供的函数来操作信号集
原因:
- 跨平台兼容性
- 封装内部实现
- 提供统一接口
四、信号集操作函数
4.1 基本操作函数
#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清零 | 成功0,失败-1 |
| sigfillset | 初始化set,使所有信号的bit置位 | 成功0,失败-1 |
| sigaddset | 在信号集中添加某个信号 | 成功0,失败-1 |
| sigdelset | 在信号集中删除某个信号 | 成功0,失败-1 |
| sigismember | 判断某信号是否在信号集中 | 包含返回1,不包含返回0,出错-1 |
📌 重要提示:
在使用sigset_t类型的变量之前,
一定要调用sigemptyset或sigfillset做初始化,
使信号集处于确定的状态!
初始化之后,才可以调用sigaddset和sigdelset
在该信号集中添加或删除某种有效信号。
代码示例:
#include <iostream>
#include <signal.h>
int main()
{
sigset_t set;
// 1. 初始化为空集
sigemptyset(&set);
// 2. 添加2号信号
sigaddset(&set, SIGINT);
// 3. 添加3号信号
sigaddset(&set, SIGQUIT);
// 4. 检查信号是否在集合中
if(sigismember(&set, SIGINT)) {
std::cout << "SIGINT在信号集中" << std::endl;
}
if(sigismember(&set, SIGTERM)) {
std::cout << "SIGTERM在信号集中" << std::endl;
} else {
std::cout << "SIGTERM不在信号集中" << std::endl;
}
// 5. 删除2号信号
sigdelset(&set, SIGINT);
if(!sigismember(&set, SIGINT)) {
std::cout << "SIGINT已被删除" << std::endl;
}
return 0;
}
$ g++ test.cc -o test
$ ./test
SIGINT在信号集中
SIGTERM不在信号集中
SIGINT已被删除
4.2 sigprocmask函数
调用sigprocmask函数可以读取或更改进程的信号屏蔽字(阻塞信号集)。
函数原型:
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
参数说明:
oset参数:
- 如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出
set参数:
- 如果set是非空指针,则更改进程的信号屏蔽字
- 参数how指示如何更改
如果oset和set都是非空指针:
- 先将原来的信号屏蔽字备份到oset里
- 然后根据set和how参数更改信号屏蔽字
how参数的可选值:
| how参数 | 含义 | 公式 |
|---|---|---|
| SIG_BLOCK | 添加信号到阻塞集 | mask = mask | set |
| SIG_UNBLOCK | 从阻塞集中移除信号 | mask = mask & ~set |
| SIG_SETMASK | 直接设置阻塞集 | mask = set |
图示说明:
假设当前mask为:
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 0│ 1│ 0│ 1│ 0│ 0│ 0│ 0│
└──┴──┴──┴──┴──┴──┴──┴──┘
1 2 3 4 5 6 7 8
set为:
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 1│ 0│ 1│ 0│ 0│ 0│ 0│ 0│
└──┴──┴──┴──┴──┴──┴──┴──┘
1 2 3 4 5 6 7 8
SIG_BLOCK (mask = mask | set):
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 1│ 1│ 1│ 1│ 0│ 0│ 0│ 0│ ← 添加1号和3号
└──┴──┴──┴──┴──┴──┴──┴──┘
SIG_UNBLOCK (mask = mask & ~set):
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 0│ 1│ 0│ 1│ 0│ 0│ 0│ 0│ ← 移除1号和3号
└──┴──┴──┴──┴──┴──┴──┴──┘
SIG_SETMASK (mask = set):
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 1│ 0│ 1│ 0│ 0│ 0│ 0│ 0│ ← 直接覆盖
└──┴──┴──┴──┴──┴──┴──┴──┘
📌 重要特性:
如果调用sigprocmask解除了对当前若干个未决信号的阻塞,
则在sigprocmask返回前,至少将其中一个信号递达。
4.3 sigpending函数
函数原型:
#include <signal.h>
int sigpending(sigset_t *set);
// 读取当前进程的未决信号集,通过set参数传出
// 调用成功则返回0,出错则返回-1
五、完整实验:观察信号的pending和block
下面用刚学的几个函数做一个完整实验。
5.1 实验代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
void PrintPending(sigset_t &pending)
{
std::cout << "curr process[" << getpid() << "] pending: ";
for(int signo = 31; signo >= 1; signo--)
{
if(sigismember(&pending, signo)) {
std::cout << "1";
} else {
std::cout << "0";
}
}
std::cout << std::endl;
}
void handler(int signo)
{
std::cout << signo << " 号信号被递达!!!" << std::endl;
std::cout << "-------------------------------" << std::endl;
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
std::cout << "-------------------------------" << std::endl;
}
int main()
{
// 0. 捕捉2号信号
signal(2, handler); // 自定义捕捉
// signal(2, SIG_IGN); // 忽略一个信号
// signal(2, SIG_DFL); // 信号的默认处理动作
// 1. 屏蔽2号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, SIGINT); // 准备阻塞2号信号
// 1.1 设置进入进程的Block表中
sigprocmask(SIG_BLOCK, &block_set, &old_set);
// 真正修改当前进程的内核block表,完成了对2号信号的屏蔽!
int cnt = 15;
while(true)
{
// 2. 获取当前进程的pending信号集
sigset_t pending;
sigpending(&pending);
// 3. 打印pending信号集
PrintPending(pending);
cnt--;
// 4. 解除对2号信号的屏蔽
if(cnt == 0)
{
std::cout << "解除对2号信号的屏蔽!!!" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, &block_set);
}
sleep(1);
}
return 0;
}
5.2 实验过程
$ g++ sig.cc -o sig
$ ./sig
curr process[448336] pending: 0000000000000000000000000000000
curr process[448336] pending: 0000000000000000000000000000000
^C ← 按Ctrl+C,发送2号信号
curr process[448336] pending: 0000000000000000000000000000010 ← pending的第2位变成1
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
^C ← 再按Ctrl+C
curr process[448336] pending: 0000000000000000000000000000010 ← 还是1,不会变成2
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
curr process[448336] pending: 0000000000000000000000000000010
解除对2号信号的屏蔽!!! ← cnt==0,解除阻塞
2 号信号被递达!!! ← 信号立即递达
-------------------------------
curr process[448336] pending: 0000000000000000000000000000000 ← pending清0
-------------------------------
curr process[448336] pending: 0000000000000000000000000000000
^C ← 再按Ctrl+C
2 号信号被递达!!! ← 没有阻塞,立即递达
-------------------------------
curr process[448336] pending: 0000000000000000000000000000000
-------------------------------
curr process[448336] pending: 0000000000000000000000000000000
5.3 实验分析
阶段1:阻塞期间(cnt > 0)
1. 进程启动,阻塞2号信号
- block表的第2位 = 1
2. 按Ctrl+C,产生2号信号
- OS检查block表,发现第2位=1,说明被阻塞
- 设置pending表的第2位 = 1
- 信号保持未决状态
3. 再按Ctrl+C,又产生2号信号
- pending表的第2位已经是1了
- 不会变成2(只有一个bit)
- 这就是"常规信号产生多次只计一次"的原理
4. pending位图持续显示第2位为1
- 说明信号一直处于未决状态
- 等待解除阻塞
阶段2:解除阻塞(cnt == 0)
1. 调用sigprocmask(SIG_SETMASK, &old_set, ...)
- 恢复原来的信号屏蔽字
- block表的第2位 = 0
2. 信号立即递达
- OS检测到pending第2位=1,block第2位=0
- 调用handler函数
- pending第2位清0
3. 之后再按Ctrl+C
- 没有阻塞,信号立即递达
- pending位图显示全0(递达后立即清除)
5.4 验证"忽略"与"阻塞"的区别
现在我们修改代码,对比"忽略"和"阻塞"的不同效果。
实验1:忽略信号
int main()
{
signal(2, SIG_IGN); // 忽略2号信号
while(true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
sleep(1);
}
return 0;
}
$ ./sig
curr process[448512] pending: 0000000000000000000000000000000
^C ← 按Ctrl+C
curr process[448512] pending: 0000000000000000000000000000010 ← pending第2位短暂为1
curr process[448512] pending: 0000000000000000000000000000000 ← 立即清0(已递达并忽略)
^C
curr process[448512] pending: 0000000000000000000000000000010
curr process[448512] pending: 0000000000000000000000000000000
实验2:阻塞信号
# 使用5.1的完整代码
$ ./sig
curr process[448336] pending: 0000000000000000000000000000000
^C
curr process[448336] pending: 0000000000000000000000000000010 ← pending第2位为1
curr process[448336] pending: 0000000000000000000000000000010 ← 持续为1(未递达)
curr process[448336] pending: 0000000000000000000000000000010
...
对比总结:
| 对比项 | 忽略 | 阻塞 |
|---|---|---|
| 信号产生后 | pending位短暂置1 | pending位持续为1 |
| 信号递达 | 立即递达,处理动作是忽略 | 不会递达,等待解除阻塞 |
| pending位变化 | 递达后立即清0 | 解除阻塞后才清0 |
| 能否恢复处理 | 不能,已经丢弃 | 能,解除阻塞后递达 |
5.5 多信号实验
#include <iostream>
#include <unistd.h>
#include <signal.h>
void PrintPending(sigset_t &pending)
{
std::cout << "curr process[" << getpid() << "] pending: ";
for(int signo = 31; signo >= 1; signo--)
{
if(sigismember(&pending, signo)) {
std::cout << "1";
} else {
std::cout << "0";
}
}
std::cout << std::endl;
}
void handler(int signo)
{
std::cout << "捕捉到信号: " << signo << std::endl;
}
int main()
{
// 捕捉2、3、4号信号
signal(SIGINT, handler); // 2号
signal(SIGQUIT, handler); // 3号
signal(SIGILL, handler); // 4号
// 阻塞2、3、4号信号
sigset_t block_set, old_set;
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, SIGINT); // 2号
sigaddset(&block_set, SIGQUIT); // 3号
sigaddset(&block_set, SIGILL); // 4号
sigprocmask(SIG_BLOCK, &block_set, &old_set);
std::cout << "进程PID: " << getpid() << std::endl;
std::cout << "已阻塞2、3、4号信号,请在另一个窗口发送信号测试" << std::endl;
int cnt = 20;
while(true)
{
sigset_t pending;
sigpending(&pending);
PrintPending(pending);
cnt--;
if(cnt == 0)
{
std::cout << "\n解除阻塞!!!\n" << std::endl;
sigprocmask(SIG_SETMASK, &old_set, nullptr);
}
sleep(1);
}
return 0;
}
测试过程:
# 窗口1
$ ./sig
进程PID: 449123
已阻塞2、3、4号信号,请在另一个窗口发送信号测试
curr process[449123] pending: 0000000000000000000000000000000
# 窗口2
$ kill -2 449123 ← 发送2号信号
$ kill -3 449123 ← 发送3号信号
$ kill -4 449123 ← 发送4号信号
# 窗口1继续显示
curr process[449123] pending: 0000000000000000000000000011110 ← 2、3、4号信号pending
curr process[449123] pending: 0000000000000000000000000011110
curr process[449123] pending: 0000000000000000000000000011110
...
解除阻塞!!!
捕捉到信号: 2 ← 信号递达顺序不确定
捕捉到信号: 3
捕捉到信号: 4
curr process[449123] pending: 0000000000000000000000000000000 ← pending全部清0
📌 重要结论:
1. 多个信号可以同时被阻塞
- 每个信号占用pending位图的一个bit
2. 多个信号同时未决时,解除阻塞后会依次递达
- 但递达的顺序是不确定的
- 不要依赖信号的处理顺序
3. 每个信号递达时,pending位图对应的bit会被清0
4. 常规信号不会排队
- 同一信号产生多次,pending位只记录一次
六、从内核视角理解信号递达的时机
6.1 信号何时被递达?
前面我们一直说信号在"合适的时候"被递达,那么到底什么时候才是"合适的时候"?
答案:从内核态返回用户态时检查信号

6.2 为什么是从内核态返回用户态时检查?
原因分析:
1. 权限原因:
- 信号的pending、block、handler都存储在PCB中
- PCB是内核数据结构,只有内核能访问
- 所以必须在内核态才能检查和处理信号
2. 时机原因:
- 进程运行在用户态时,无法访问内核数据
- 只有发生以下情况才会进入内核态:
a. 系统调用(主动进入内核态)
b. 硬件中断(被动进入内核态)
c. 异常/陷阱(被动进入内核态)
3. 效率原因:
- 不可能无时无刻都在检查信号
- 选择从内核态返回用户态的时机检查,开销最小
- 这个时机天然就在内核态,可以访问PCB
4. 安全原因:
- 用户态代码不可信任
- 必须由内核来决定是否递达信号
- 防止用户态代码篡改信号处理流程

6.3 示例说明
#include <iostream>
#include <unistd.h>
#include <signal.h>
void handler(int signo)
{
std::cout << "处理信号: " << signo << std::endl;
}
int main()
{
signal(SIGINT, handler);
while(true)
{
std::cout << "running..." << std::endl; // ← 这里会发生系统调用(write)
sleep(1); // ← 这里会发生系统调用(nanosleep)
}
return 0;
}
执行流程:
时刻t1: 进程在用户态执行main函数
↓
时刻t2: 执行std::cout,调用write系统调用
↓ 进入内核态
时刻t3: 内核执行write
↓
时刻t4: 用户按下Ctrl+C,产生SIGINT信号
↓ 键盘中断进入内核
时刻t5: 内核的中断处理程序:
- 识别为Ctrl+C
- 设置进程PCB的pending位图第2位 = 1
↓ 返回继续执行write
时刻t6: write执行完毕,准备返回用户态
↓
时刻t7: ★★★ 检查信号 ★★★
- 发现pending第2位=1
- 检查block第2位=0(未阻塞)
- 查找handler数组第2项 = handler函数
- 清除pending第2位
↓ 不是直接返回用户态,而是先执行信号处理
时刻t8: 执行handler(2)
↓
时刻t9: handler执行完毕,返回内核
↓
时刻t10: 再次检查信号(可能有其他信号)
↓
时刻t11: 真正返回用户态,继续执行main函数
七、信号递达的完整过程总结
7.1 OS向进程发送信号的完整流程
第一步:信号产生(5种方式之一)
│
├─ 键盘产生:Ctrl+C触发硬件中断 → OS识别 → 产生信号
├─ 系统调用:kill(pid, signo) → OS发送信号
├─ 软件条件:alarm超时 → OS产生信号
├─ 硬件异常:除0 → CPU异常 → OS产生信号
└─ ... 其他方式
↓
第二步:OS修改目标进程的PCB
│
├─ 找到进程的task_struct
│
├─ 修改pending位图:
│ pending.signal |= (1 << (signo - 1))
│ // 将第signo位置1
│
└─ 信号进入"未决"状态
↓
第三步:等待信号递达
│
├─ 进程继续运行在用户态
│
├─ 直到某个时刻进入内核态:
│ - 系统调用
│ - 硬件中断
│ - 异常
│
└─ 准备从内核态返回用户态时...
↓
第四步:检查信号(★核心★)
│
├─ 内核检查pending位图和block位图
│
├─ for(signo = 1; signo <= 31; signo++)
│ {
│ if (pending第signo位 == 1 && block第signo位 == 0)
│ {
│ // 找到了一个需要递达的信号
│ break;
│ }
│ }
│
├─ 如果没有需要递达的信号 → 直接返回用户态
│
└─ 如果有需要递达的信号 → 继续下一步
↓
第五步:信号递达
│
├─ 查找handler数组:
│ handler = task->sighand->action[signo].sa_handler
│
├─ 清除pending位:
│ pending.signal &= ~(1 << (signo - 1))
│
├─ 根据handler的值执行不同动作:
│ │
│ ├─ SIG_DFL(默认):
│ │ - SIGTERM → 终止进程
│ │ - SIGQUIT → 终止进程并core dump
│ │ - SIGCHLD → 忽略
│ │ - ... 不同信号的默认动作不同
│ │
│ ├─ SIG_IGN(忽略):
│ │ - 什么都不做
│ │ - 直接返回
│ │
│ └─ 自定义函数:
│ - 保存当前上下文
│ - 切换到用户态执行handler
│ - handler执行完毕返回内核
│ - 恢复上下文
│
└─ 继续检查是否还有其他信号...
↓
第六步:返回用户态
│
└─ 继续执行用户代码
7.2 关键数据结构的配合
PCB中的三张表:
┌───────────────────────────────────────────────┐
│ task_struct (PCB) │
├───────────────────────────────────────────────┤
│ │
│ 1. blocked (阻塞信号集) │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │
│ │ 0│ 1│ 0│ 1│ 0│ 0│ 0│ 0│ ... (64位) │
│ └──┴──┴──┴──┴──┴──┴──┴──┘ │
│ 含义:bit=1表示该信号被阻塞 │
│ │
│ 2. pending (未决信号集) │
│ ┌──┬──┬──┬──┬──┬──┬──┬──┐ │
│ │ 0│ 1│ 0│ 1│ 0│ 0│ 0│ 0│ ... (64位) │
│ └──┴──┴──┴──┴──┴──┴──┴──┘ │
│ 含义:bit=1表示该信号已产生但未递达 │
│ │
│ 3. sighand->action[64] (处理函数数组) │
│ ┌────────────────────────┐ │
│ │ [1] → SIG_DFL │ │
│ │ [2] → handler函数地址 │ │
│ │ [3] → SIG_IGN │ │
│ │ [4] → SIG_DFL │ │
│ │ ... │ │
│ └────────────────────────┘ │
│ 含义:存储每个信号的处理动作 │
│ │
└───────────────────────────────────────────────┘
三张表的协作:
检查流程伪代码:
for (signo = 1; signo <= 31; signo++)
{
// 第一步:检查pending位图
if (pending位图第signo位 == 0)
continue; // 该信号未产生,跳过
// 第二步:检查block位图
if (block位图第signo位 == 1)
continue; // 该信号被阻塞,跳过
// 第三步:查找handler数组
handler = action[signo].sa_handler;
// 第四步:清除pending位
pending位图第signo位 = 0;
// 第五步:执行处理动作
if (handler == SIG_DFL)
执行默认动作;
else if (handler == SIG_IGN)
什么都不做;
else
调用handler(signo);
}
八、本篇总结
8.1 核心知识点
-
三个重要概念
- 信号递达(Delivery):实际执行信号的处理动递达的状态
- 信号阻塞(Block):暂时不处理某个信号
-
阻塞 vs 忽略
- 阻塞:信号产生时决定,保持未决状态,可以解除后处理
- 忽略:信号递达时决定,处理动作是"什么都不做"
-
信号在内核中的表示
- blocked位图:记录哪些信号被阻塞
- pending位图:记录哪些信号处于未决状态
- handler数组:记录每个信号的处理动作
-
信号集操作
- sigemptyset/sigfillset:初始化
- sigaddset/sigdelset:添加/删除信号
- sigismember:查询信号
- sigprocmask:修改阻塞信号集
- sigpending:读取未决信号集
-
信号递达时机
- 从内核态返回用户态时检查
- 检查pending和block位图
- 执行对应的处理动作
8.2 完整流程图
信号生命周期:
产生 ────→ 未决 ────→ 递达
│ │ │
│ │ └─ 执行handler
│ │
│ └─ 如果被阻塞:保持未决
│ 如果未阻塞:等待递达时机
│
└─ OS设置pending位
8.3 下一篇预告
在下一篇中,我们将学习:
1. 信号捕捉的深入机制
- sigaction函数
- 用户态与内核态的切换
2. ★★★ 操作系统是怎么运行的 ★★★
- 硬件中断机制
- 时钟中断与进程调度
- 系统调用原理
- 软中断、陷阱、异常
- 中断向量表
3. 内核态与用户态
- CPL权限级别
- ring 0 ~ ring 3
- 用户态与内核态切换流程
- TSS任务状态段
4. 可重入函数与volatile关键字
5. SIGCHLD信号实战
更多推荐


所有评论(0)