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位图)
│   ┌──┬──┬──┬──┬──┬──┬──┬──┐
│   │ 01010000... (共64位)
│   └──┴──┴──┴──┴──┴──┴──┴──┘
│    ↑  ↑  ↑  ↑
│    │  │  │  └─ 4号信号被阻塞
│    │  │  └──── 3号信号未阻塞
│    │  └─────── 2号信号被阻塞
│    └────────── 1号信号未阻塞
│
├─ pending (sigpending)        ← 未决信号集(pending位图)
│   ┌──┬──┬──┬──┬──┬──┬──┬──┐
│   │ 01010000... (共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为:
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 01010000│
└──┴──┴──┴──┴──┴──┴──┴──┘
  1  2  3  4  5  6  7  8

set为:
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 10100000│
└──┴──┴──┴──┴──┴──┴──┴──┘
  1  2  3  4  5  6  7  8

SIG_BLOCK (mask = mask | set):
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 11110000│  ← 添加1号和3号
└──┴──┴──┴──┴──┴──┴──┴──┘

SIG_UNBLOCK (mask = mask & ~set):
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 01010000│  ← 移除1号和3号
└──┴──┴──┴──┴──┴──┴──┴──┘

SIG_SETMASK (mask = set):
┌──┬──┬──┬──┬──┬──┬──┬──┐
│ 10100000│  ← 直接覆盖
└──┴──┴──┴──┴──┴──┴──┴──┘

📌 重要特性:

如果调用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 (阻塞信号集)                      │
│     ┌──┬──┬──┬──┬──┬──┬──┬──┐               │
│     │ 01010000... (64)     │
│     └──┴──┴──┴──┴──┴──┴──┴──┘               │
│     含义:bit=1表示该信号被阻塞                │
│                                               │
│  2. pending (未决信号集)                      │
│     ┌──┬──┬──┬──┬──┬──┬──┬──┐               │
│     │ 01010000... (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 核心知识点

  1. 三个重要概念

    • 信号递达(Delivery):实际执行信号的处理动递达的状态
    • 信号阻塞(Block):暂时不处理某个信号
  2. 阻塞 vs 忽略

    • 阻塞:信号产生时决定,保持未决状态,可以解除后处理
    • 忽略:信号递达时决定,处理动作是"什么都不做"
  3. 信号在内核中的表示

    • blocked位图:记录哪些信号被阻塞
    • pending位图:记录哪些信号处于未决状态
    • handler数组:记录每个信号的处理动作
  4. 信号集操作

    • sigemptyset/sigfillset:初始化
    • sigaddset/sigdelset:添加/删除信号
    • sigismember:查询信号
    • sigprocmask:修改阻塞信号集
    • sigpending:读取未决信号集
  5. 信号递达时机

    • 从内核态返回用户态时检查
    • 检查pending和block位图
    • 执行对应的处理动作

8.2 完整流程图

信号生命周期:

产生 ────→ 未决 ────→ 递达
│          │          │
│          │          └─ 执行handler
│          │
│          └─ 如果被阻塞:保持未决
│             如果未阻塞:等待递达时机
│
└─ OS设置pending位

8.3 下一篇预告

在下一篇中,我们将学习:

1. 信号捕捉的深入机制
   - sigaction函数
   - 用户态与内核态的切换

2. ★★★ 操作系统是怎么运行的 ★★★
   - 硬件中断机制
   - 时钟中断与进程调度
   - 系统调用原理
   - 软中断、陷阱、异常
   - 中断向量表

3. 内核态与用户态
   - CPL权限级别
   - ring 0 ~ ring 3
   - 用户态与内核态切换流程
   - TSS任务状态段

4. 可重入函数与volatile关键字

5. SIGCHLD信号实战
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐