Linux 信号管理

Linux 信号管理

七月 22, 2025 次阅读

信号的状态

信号的三种状态

状态 说明 类比手机消息
递达 信号已到达进程并完成处理(执行默认动作、自定义处理函数或忽略)。 消息被打开并阅读(已读)。
未决 信号已产生但尚未递达(可能因阻塞而暂时无法处理)。 消息收到但未读(通知栏显示未读红点)。
阻塞 进程主动屏蔽某个信号,即使信号产生也不会递达,直到解除阻塞。 消息被设为“免打扰”(不提醒但未读)。

2. 阻塞 vs 忽略

行为 说明 结果
阻塞 信号被加入未决集合,但暂时不递达(即使收到多次也只记录一次)。 信号状态保持为未决,直到解除阻塞。
忽略 信号已递达,但处理动作为显式忽略(通过 SIG_IGN 或捕获函数中不处理)。 信号直接丢弃,不会未决。

关键区别

  • 阻塞影响的是信号从产生到递达的过程(未决阶段)。
  • 忽略是信号递达后的处理方式之一。

3. 内核中的实现

信号的阻塞和未决状态通过位图( sigset_t)管理:

  • 未决集合(Pending):记录哪些信号已产生但未递达。
  • 阻塞掩码(Block):记录哪些信号被阻塞(屏蔽)。

信号在内核中的表示示意图

signal

每个信号在内核中由以下三部分表示:

  • 阻塞标志 (block):布尔值,表示信号是否被屏蔽
  • 未决标志 (pending):布尔值,表示信号是否已产生但未递达
  • 处理函数指针:指向信号处理函数(可以是默认动作/SIG_IGN/用户自定义函数)

信号产生时,内核在进程PCB中设置该信号的未决标志
信号递达时(执行处理动作后),内核才会清除未决标志

示例如下:

信号 阻塞状态 未决状态 处理动作 说明
SIGHUP 未阻塞 未产生 默认处理动作 正常递达
SIGINT 阻塞 已产生 忽略(SIG_IGN) 即使动作为忽略,仍需先解除阻塞才会处理
SIGQUIT 将阻塞 未产生 用户自定义函数(sighandler) 一旦产生会被阻塞,直到解除阻塞后执行sighandler

多次信号产生处理规则:

当信号在解除阻塞前多次产生时:

  • 常规信号(1-31):

    • 只记录一次(后到的信号会被丢弃)
    • 示例:连续发送3次SIGINT,解除阻塞后只处理1次
  • 实时信号(34-64):

    • 会排队保存多次信号
    • 示例:发送3次SIGRTMIN,解除阻塞后会处理3次

POSIX.1标准允许系统自行选择实现方式,Linux采用上述策略

流程

  1. 信号产生 → 检查是否被阻塞:
    • 若阻塞:加入未决集合,保持未决状态。
    • 未阻塞:立即递达(执行处理动作)。
  2. 解除阻塞后 → 检查未决集合中是否有该信号,若有则递达。

信号管理核心数据结构:信号集 sigset_t

信号标志的存储方式

  • 每个信号使用 1个bit 表示状态
    • 未决标志 (pending)0=未产生,1=已产生(不记录次数)
    • 阻塞标志 (block)0=未阻塞,1=已阻塞
  • 两种标志均使用 相同数据类型 sigset_t 存储

信号集的语义

信号集类型 “有效” (1) 的含义 “无效” (0) 的含义
未决信号集 信号处于未决状态 信号未产生或已递达
阻塞信号集 信号被阻塞 信号未被阻塞

关键说明

  • 阻塞信号集又称 信号屏蔽字 (Signal Mask)
  • “屏蔽”在此语境中特指 阻塞(拦截信号递达),与忽略(递达后的处理方式)有本质区别

信号集特点

  1. 二进制记录

    • 不统计信号产生次数(常规信号多次触发仅记1次)
    • 实时信号需通过其他机制实现队列化
  2. 高效存储

    • 使用位图结构(一般为 unsigned long[])紧凑存储所有信号状态
  3. 内核操作

    • 通过 sigprocmask() 修改阻塞信号集
    • 通过 sigpending() 读取未决信号集

图示表示

sigset_t 结构示例(假设系统有8种信号):
位序号: [7][6][5][4][3][2][1][0]
未决集: 0  1  0  0  1  0  0  1  → 信号1/4/7未决
阻塞集: 1  0  1  0  0  0  0  0  → 信号0/2被阻塞

信号集操作函数

1. sigemptyset

该函数用于清空信号集,即将所有信号标志设置为无效(0)。

int sigemptyset(sigset_t *set);

2. sigfillset

该函数用于填充信号集,即将所有信号标志设置为有效(1)。

int sigfillset(sigset_t *set);

3. sigaddset

该函数用于在信号集 set 中添加指定信号 signo

int sigaddset(sigset_t *set, int signo);

使用实例:

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);  // 添加 SIGINT 信号
sigaddset(&set, SIGTERM); // 添加 SIGTERM 信号

4. sigdelset

该函数用于在信号集 set 中删除指定信号 signo

int sigdelset(sigset_t *set, int signo);

使用实例:

sigset_t set;
sigfillset(&set);
sigdelset(&set, SIGINT);  // 删除 SIGINT 信号
sigdelset(&set, SIGTERM); // 删除 SIGTERM 信号

5. sigismember

该函数用于检查信号集 set 中是否包含指定信号 signo

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

使用实例:

sigset_t set;
sigemptyset(&set);
sigaddset(&set, SIGINT);
if (sigismember(&set, SIGINT)) {
    printf("SIGINT is in the set\n");
} else {
    printf("SIGINT is not in the set\n");
}

信号状态相关函数详解

1. sigpending - 获取未决信号集

函数原型

#include <signal.h>
int sigpending(sigset_t *set);

功能说明

  • 获取当前进程的未决信号集(Pending Signal Set)
  • 未决信号指已产生但尚未递达的信号(可能因阻塞而滞留)

参数

参数 类型 说明
set sigset_t* 输出参数,用于存储未决信号集

返回值

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

使用示例

sigset_t pending_set;
if (sigpending(&pending_set) == -1) {
    perror("sigpending failed");
    exit(EXIT_FAILURE);
}

2. sigprocmask - 设置/获取阻塞信号集

函数原型

#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

功能说明

  • 修改或获取当前进程的信号屏蔽字(Blocked Signal Set)
  • 信号屏蔽字决定哪些信号会被阻塞(阻止递达)

参数详解

参数 类型 说明
how int 指定操作方式(见下表)
set const sigset_t* 新信号集(NULL表示不修改)
oldset sigset_t* 输出旧信号集(NULL表示不需要获取)
how 取值说明
宏定义 行为
SIG_BLOCK 0 set 中的信号加入当前屏蔽字(阻塞新增信号)
SIG_UNBLOCK 1 从当前屏蔽字中移除 set 中的信号(解除阻塞)
SIG_SETMASK 2 直接将当前屏蔽字替换set(完全覆盖旧值)

返回值

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

使用示例

sigset_t new_set, old_set;

// 初始化信号集
sigemptyset(&new_set);
sigaddset(&new_set, SIGINT);  // 阻塞SIGINT

// 添加阻塞信号
if (sigprocmask(SIG_BLOCK, &new_set, &old_set) == -1) {
    perror("sigprocmask failed");
    exit(EXIT_FAILURE);
}

// 恢复旧屏蔽字
if (sigprocmask(SIG_SETMASK, &old_set, NULL) == -1) {
    perror("sigprocmask restore failed");
}

3. 注意事项

  1. 信号屏蔽字继承

    • 子进程会继承父进程的信号屏蔽字
    • 但未决信号集不会被继承
  2. 原子操作建议

修改信号屏蔽字时应确保操作的原子性,避免竞态条件

// 推荐先获取旧值再修改
sigset_t old_mask;
sigprocmask(SIG_BLOCK, &new_mask, &old_mask);
/* 临界区代码 */
sigprocmask(SIG_SETMASK, &old_mask, NULL);  // 恢复
  1. 不可阻塞信号

    • SIGKILL 和 SIGSTOP 无法被阻塞(即使尝试设置也会被系统忽略)
  2. 多线程环境

    • 在多线程中应使用 pthread_sigmask() 而非 sigprocmask()
    • sigprocmask() 的行为在多线程中是未定义的

使用示例

下面我们利用信号状态相关函数来模拟一下阻塞二号信号以及解除阻塞的条件下给进程通过键盘发送二号信号:

#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <cstring>

using namespace std;

// SIGINT信号处理函数
void myhandler(int signal)
{
    sleep(1);  // 模拟处理耗时
    cout << "I get a signal: " << strsignal(signal) << endl;  // 打印信号信息
}

// 闹钟信号处理函数
void alarm_handler(int signal)
{
    raise(SIGINT);  // 给自己发送SIGINT信号
    cout << "I have sent myself signal SIGINT" << endl;
}

// 打印当前未决信号集
void print_pending_map()
{
    sigset_t sigset;
    sigpending(&sigset);  // 获取未决信号集
    
    // 从31到1遍历所有可能的信号
    for(int i = 31; i >= 1; --i)
    {
        if(sigismember(&sigset, i)) 
            cout << 1 << " ";  // 信号未决
        else 
            cout << 0 << " ";   // 信号未产生
    }
    cout << endl << endl;
}

int main() {
    // 设置SIGINT信号处理函数
    signal(SIGINT, myhandler);

    // 初始化信号集
    sigset_t sigset, old_sigset;
    sigemptyset(&sigset);      // 清空信号集
    sigemptyset(&old_sigset);  // 清空旧信号集

    // 将SIGINT添加到信号集
    sigaddset(&sigset, SIGINT);
    // 阻塞SIGINT信号,并保存旧的信号屏蔽字
    sigprocmask(SIG_BLOCK, &sigset, &old_sigset);

    // 设置1秒后触发SIGALRM信号
    alarm(1);
    // 设置SIGALRM信号处理函数
    signal(SIGALRM, alarm_handler);
    
    int cnt = 0;  // 循环计数器

    while(true)
    {
        cnt++;
        if(cnt == 5){
            // 第5次循环时解除对SIGINT的阻塞
            cout << "signal SIGINT unblocked" << endl;
            sigprocmask(SIG_UNBLOCK, &sigset, &old_sigset);
        }
        else if(cnt == 10) {
            cout << "Bye!" << endl;
            abort();  // 第10次循环时终止程序
        }
        
        print_pending_map();  // 打印当前未决信号状态
        sleep(1);            // 休眠1秒
    }
    return 0;
}
  1. 信号处理部分:

    • myhandler处理SIGINT信号,打印接收到的信号信息
    • alarm_handler处理SIGALRM信号,会主动发送SIGINT信号
  2. 信号阻塞控制:

    • 程序开始时阻塞SIGINT信号
    • 第5次循环时解除阻塞
    • 第10次循环时终止程序
  3. 未决信号监控:

    • print_pending_map函数会打印所有信号的未决状态
    • 1表示信号未决,0表示信号未产生
  4. 主要流程:

    • 程序启动后立即阻塞SIGINT
    • 1秒后闹钟触发,发送SIGINT(但由于阻塞会进入未决状态)
    • 第5秒解除阻塞,未决的SIGINT会被递达
    • 第10秒程序终止

结果如下:

╭─ljx@VM-16-15-debian ~/linux_review/sign
╰─➤  ./signal.o
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

I have sent myself signal SIGINT
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0

signal SIGINT unblocked
I get a signal: Interrupt
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0

Bye!
[1]    424187 IOT instruction  ./signal.o