Linux进程通信---6.1---进程信号屏蔽

信号屏蔽(Signal Mask)

信号屏蔽是 Linux 进程主动掌控信号处理时机的核心机制,也是进程信号知识点中最易混淆、最贴近实战的部分。以下从「本质→实现→操作→规则→场景→避坑」层层拆解,覆盖所有核心细节:

信号屏蔽的本质与核心价值

1. 核心定义

信号屏蔽(也叫 "信号阻塞")是进程通过设置「信号屏蔽字(Signal Mask)」,告诉内核:

"以下这些信号,我暂时不想处理;如果它们产生了,先帮我存在 pending 表(未决信号集)里,等我解除屏蔽后再处理。"

2. 关键误区纠正(新手必看)

错误认知 正确事实
屏蔽信号 = 阻止信号产生 屏蔽不阻止信号产生 (内核仍会接收信号),仅阻止信号递达处理(执行默认 / 捕捉 / 忽略逻辑);信号产生后会暂存到 pending 表
屏蔽 = 忽略 屏蔽是 "暂存"(解除后仍会处理),忽略(SIG_IGN)是 "直接丢弃"(信号产生后不进 pending 表)
所有信号都能屏蔽 SIGKILL (9)、SIGSTOP (19) 是内核 "终极控制信号",无法屏蔽 / 捕捉 / 忽略,保证管理员能随时控制进程

3. 核心价值

信号是异步的(进程无法预测信号何时到达),而进程执行「临界区代码」(如操作全局数据、调用非异步安全函数)时,被信号打断会导致数据错乱 / 程序崩溃。信号屏蔽的核心价值就是:

把 "随机时机到达的信号",推迟到 "进程安全的时机" 处理,保证程序稳定性。

信号屏蔽的底层实现(PCB 中的核心结构)

信号屏蔽的实现依赖进程 PCB(进程控制块) 中的两个核心结构,二者通过 sigset_t(信号集,64 位位图)关联:

结构名称 类型 核心作用 与信号屏蔽的关系
信号屏蔽字(signal mask) sigset_t 位图 存储 "进程当前要屏蔽的信号清单" 进程通过 sigprocmask 主动修改该结构,是 "屏蔽规则"
未决信号集(pending 表) sigset_t 位图 + 实时信号队列 存储 "已产生但未递达的信号" 信号产生后,若在屏蔽字中,则存入该结构,是 "屏蔽的结果"

信号屏蔽的核心流程(可视化)

信号屏蔽的核心操作(函数 + 标准流程)

| 函数原型 | 核心用途 | 关键说明 |
| int sigprocmask( int how, const sigset_t *set, sigset_t *oldset); | 设置 / 查询进程的信号屏蔽字 | how=SIG_BLOCK(添加屏蔽)/SIG_UNBLOCK(解除屏蔽)/SIG_SETMASK(替换屏蔽集);oldset 保存旧屏蔽集 |
| int sigpending( sigset_t *set); | 查询进程的未决信号集(pending 表) | 拿到的是 pending 表的用户态镜像,可查哪些信号已产生但未处理 |

int sigsuspend( const sigset_t *mask); 原子操作:切换屏蔽集 + 暂停进程等待信号 替代 pause(无竞态);收到信号后恢复原屏蔽集

所有信号屏蔽的操作都围绕 sigprocmask 函数展开(进程级),结合 sigset_t 完成 "编辑屏蔽清单→应用屏蔽规则→恢复旧规则" 的完整流程。

1. 核心函数:sigprocmask

函数原型
cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数详解(重点!)
参数 取值 / 说明 实战场景
how 控制 "如何修改屏蔽字",仅 3 种合法取值: ① SIG_BLOCK:将 set 中的信号添加到 当前屏蔽字(新增屏蔽,保留原有规则)② SIG_UNBLOCK:将 set 中的信号 当前屏蔽字删除(解除屏蔽)③ SIG_SETMASK:用 set 替换整个屏蔽字(全量覆盖,慎用) - 临时屏蔽少数信号:用 SIG_BLOCK- 解除特定信号屏蔽:用 SIG_UNBLOCK- 恢复旧屏蔽字:用 SIG_SETMASK
set 要操作的信号集(sigset_t 类型),需先通过 sigemptyset/sigaddset 编辑 必选,除非 howSIG_SETMASKset 为 NULL(仅查询当前屏蔽字)
oldset 保存修改前的旧屏蔽字(用于后续恢复),传 NULL 则不保存 建议必传!避免修改后无法恢复原有屏蔽规则
返回值
  • 成功:返回 0
  • 失败:返回 -1(仅参数无效时失败,如传入非法信号编号)。

2. 信号集操作函数(编辑屏蔽清单)

sigset_t 是 "信号清单容器"(64 位位图),必须通过以下函数操作(禁止手动位运算):

函数 作用 备注
sigemptyset(sigset_t *set) 清空信号集(所有位设 0) 初始化必备!未初始化的 sigset_t 位值随机
sigaddset(sigset_t *set, int sig) 向信号集添加指定信号(对应位设 1) 最常用,如添加 SIGINT/SIGTERM
sigdelset(sigset_t *set, int sig) 从信号集删除指定信号(对应位设 0) 解除部分信号屏蔽时用
sigismember(const sigset_t *set, int sig) 判断信号是否在信号集中 返回 1 = 存在,0 = 不存在,-1 = 错误
sigfillset(sigset_t *set) 填满信号集(所有位设 1) 临时屏蔽所有可屏蔽信号时用

3. 信号屏蔽的标准操作流程(必背!)

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// 临界区函数:模拟操作全局数据(不能被信号打断)
void critical_work() {
    static int global_count = 0;
    printf("开始执行临界区代码,操作全局变量...\n");
    for (int i = 0; i < 5; i++) {
        global_count++;
        sleep(1); // 模拟耗时操作
    }
    printf("临界区代码执行完成,global_count = %d\n", global_count);
}

int main() {
    // ========== 步骤1:初始化信号集,编辑要屏蔽的信号 ==========
    sigset_t mask, old_mask;
    sigemptyset(&mask);                // 初始化(必须!)
    sigaddset(&mask, SIGINT);          // 添加要屏蔽的信号:SIGINT(Ctrl+C)
    sigaddset(&mask, SIGTERM);         // 可选:添加更多信号

    // ========== 步骤2:保存旧屏蔽字,应用新屏蔽规则 ==========
    // SIG_BLOCK:新增屏蔽(保留原有屏蔽字,仅添加 SIGINT/SIGTERM)
    int ret = sigprocmask(SIG_BLOCK, &mask, &old_mask);
    if (ret == -1) {
        perror("sigprocmask failed");
        return -1;
    }
    printf("已屏蔽 SIGINT/SIGTERM,按 Ctrl+C 无反应(5秒内)\n");

    // ========== 步骤3:执行临界区代码(核心保护逻辑) ==========
    critical_work();

    // ========== 步骤4:恢复旧屏蔽字(避免永久屏蔽) ==========
    sigprocmask(SIG_SETMASK, &old_mask, NULL);
    printf("已恢复原有屏蔽规则,现在按 Ctrl+C 可终止进程\n");

    sleep(3);
    return 0;
}
运行效果
  • 执行临界区代码时,按 Ctrl+C 无反应(SIGINT 被屏蔽,存入 pending 表);
  • 临界区代码执行完成后,恢复屏蔽规则,pending 表中的 SIGINT 会立即处理(默认终止进程)。

信号屏蔽的关键规则(避坑核心)

1. 仅可屏蔽 "可屏蔽信号"

Linux 中仅 SIGKILL(9)SIGSTOP(19) 无法屏蔽 ------ 即使将它们加入 sigset_tsigprocmask 也会直接忽略该操作。

原因:这两个信号是内核的 "终极控制手段",保证管理员能随时终止 / 暂停任意进程(哪怕进程屏蔽了所有其他信号)。

2. 屏蔽是 "进程级 / 线程级" 行为

  • 单进程:sigprocmask 作用于整个进程,所有线程共享同一套屏蔽字(实际是 "主线程的屏蔽字");
  • 多线程:sigprocmask 仅影响调用线程 的屏蔽字(线程级屏蔽),若要设置进程级屏蔽,需用 pthread_sigmask(POSIX 线程函数)。

3. 执行信号处理函数时,内核自动屏蔽同类型信号

进程执行某信号的自定义处理函数时,内核会临时屏蔽该类型信号 (除非设置 sigactionSA_NODEFER 标志)。

目的:避免 "信号重入"------ 比如处理 SIGINT 时,再次收到 SIGINT 打断处理函数,导致逻辑错乱。

4. 屏蔽不改变信号的处理方式

信号屏蔽仅控制 "何时处理",不改变 "如何处理":

  • 若进程设置 signal(SIGINT, SIG_IGN)(忽略 SIGINT),即使先屏蔽 SIGINT,解除屏蔽后 pending 表中的 SIGINT 也会被直接丢弃;
  • 若进程设置了自定义处理函数,解除屏蔽后会执行该函数。

5. pending 表中的信号仅保存 "未处理状态"

  • 非实时信号(1-31):重复产生的信号在 pending 表中仅保存 1 次(合并丢失);
  • 实时信号(34-64):重复产生的信号在 pending 表中排队保存(每个实例 + 附加数据,不丢失)。

信号屏蔽与易混概念的区分

1. 屏蔽(block)vs 忽略(SIG_IGN)

维度 屏蔽(block) 忽略(SIG_IGN)
核心逻辑 信号暂存 pending 表,解除屏蔽后处理 信号产生后直接丢弃,不进 pending 表
操作方式 通过 sigprocmask 设置 通过 signal/sigaction 设置
信号状态 未决(pending) 无状态(直接丢弃)
示例 sigaddset(&mask, SIGINT); sigprocmask(SIG_BLOCK, &mask, NULL); signal(SIGINT, SIG_IGN);

2. 屏蔽(block)vs 捕捉(自定义 handler)

维度 屏蔽(block) 捕捉(自定义 handler)
核心逻辑 控制信号处理时机 定义信号的处理逻辑
操作方式 通过 sigprocmask 设置 通过 signal/sigaction 设置
关联关系 捕捉不影响屏蔽,屏蔽仅推迟捕捉的执行 解除屏蔽后,pending 表中的信号会执行捕捉函数

信号屏蔽的典型实战场景

1. 保护临界区代码(最常用)

场景:进程操作全局数据、共享内存、文件句柄等 "不能被打断" 的逻辑时,屏蔽信号避免数据错乱。

cpp 复制代码
// 示例:修改全局配置时屏蔽信号
void update_global_config() {
    sigset_t mask, old_mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, &old_mask); // 屏蔽信号

    // 临界区:修改全局配置(不能被打断)
    global_config.flag = 1;
    global_config.time = time(NULL);

    sigprocmask(SIG_SETMASK, &old_mask, NULL); // 恢复屏蔽字
}

2. 避免非异步安全函数重入

场景:信号处理函数中只能调用「异步安全函数」(如 write_exit),若进程正在执行 malloc/printf(非异步安全),屏蔽信号可避免重入崩溃。

cpp 复制代码
// 示例:调用 malloc 时屏蔽信号
void alloc_large_memory() {
    sigset_t mask, old_mask;
    sigfillset(&mask); // 屏蔽所有可屏蔽信号
    sigprocmask(SIG_BLOCK, &mask, &old_mask);

    // 调用非异步安全函数:malloc
    char *buf = (char*)malloc(1024 * 1024);
    if (buf == NULL) perror("malloc failed");

    sigprocmask(SIG_SETMASK, &old_mask, NULL); // 恢复屏蔽字
}

3. 批量处理信号

场景:进程需要完成批量任务(如文件写入、网络发包)后,统一处理所有待决信号,保证任务完整性。

cpp 复制代码
// 示例:批量写入文件后处理信号
void write_file_batch() {
    sigset_t mask, old_mask;
    sigfillset(&mask);
    sigprocmask(SIG_BLOCK, &mask, &old_mask); // 屏蔽所有可屏蔽信号

    // 批量写入文件(不会被信号打断)
    for (int i = 0; i < 100; i++) {
        write(fd, &data[i], sizeof(data[i]));
    }

    sigprocmask(SIG_UNBLOCK, &mask, NULL); // 解除屏蔽,统一处理信号
}

4. 实现优雅退出

场景:捕获 SIGTERM 后,屏蔽该信号,先释放资源(关闭文件、断开连接),再解除屏蔽处理退出逻辑。

cpp 复制代码
void sigterm_handler(int sig) {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL); // 屏蔽 SIGTERM,避免重复触发

    // 释放资源
    close(fd);
    disconnect_network();
    printf("资源已释放,准备退出...\n");

    _exit(0); // 异步安全的退出函数
}

int main() {
    signal(SIGTERM, sigterm_handler);
    while (1) sleep(1);
    return 0;
}

5. 实战示例:直观看到 "信号保存" 的效果

下面的代码演示:屏蔽 SIGINT 后触发多次 SIGINT,观察内核如何保存信号,解除屏蔽后看进程收到几次信号。

cpp 复制代码
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// SIGINT 处理函数
void sigint_handler(int sig) {
    printf("收到 SIGINT 信号(已处理)\n");
}

int main() {
    sigset_t block_set, pending_set;
    // 1. 注册 SIGINT 处理函数(避免默认终止进程)
    signal(SIGINT, sigint_handler);

    // 2. 初始化屏蔽集,添加 SIGINT(屏蔽该信号)
    sigemptyset(&block_set);
    sigaddset(&block_set, SIGINT);
    sigprocmask(SIG_BLOCK, &block_set, NULL);
    printf("已屏蔽 SIGINT,接下来 5 秒内按多次 Ctrl+C...\n");

    // 3. 等待 5 秒,期间可多次按 Ctrl+C(触发 SIGINT,内核会保存)
    sleep(5);

    // 4. 查看当前保存的未决信号
    sigpending(&pending_set);
    if (sigismember(&pending_set, SIGINT)) {
        printf("检测到内核保存的未决信号:SIGINT\n");
    } else {
        printf("无未决信号\n");
    }

    // 5. 解除 SIGINT 屏蔽(保存的信号会立即递达处理)
    printf("解除 SIGINT 屏蔽,处理保存的信号...\n");
    sigprocmask(SIG_UNBLOCK, &block_set, NULL);

    printf("程序继续运行\n");
    return 0;
}
运行结果分析:
  1. 运行程序后,快速按 3 次 Ctrl+C;
  2. 5 秒后,程序检测到 pending 中保存了 SIGINT(位图位为 1);
  3. 解除屏蔽后,进程仅收到 1 次 SIGINT(非实时信号仅保存一次),处理函数打印一次 "收到 SIGINT 信号";
  4. 若将示例中的 SIGINT 换成实时信号 SIGRTMIN,并多次用 sigqueue() 发送,解除屏蔽后会收到所有发送的信号(逐个保存)。
相关推荐
郑泰科技2 小时前
SpringBoot项目实践:之前war部署到服务器好用,重新打包部署到服务器报404
服务器·spring boot·后端
一颗青果2 小时前
五种IO模型
linux·服务器·网络
宋军涛2 小时前
SqlServer性能优化
运维·服务器·性能优化
rocksun2 小时前
Neovim,会是你的下一款“真香”开发神器吗?
linux·python·go
郝学胜-神的一滴2 小时前
Linux线程属性设置分离技术详解
linux·服务器·数据结构·c++·程序人生·算法
知识分享小能手2 小时前
Ubuntu入门学习教程,从入门到精通, Ubuntu 22.04中的进程管理详解(15)
linux·学习·ubuntu
zfj3213 小时前
Linux内核和发行版的的区别、职责
linux·运维·服务器·内核·linux发行版
leoufung3 小时前
LeetCode 120. Triangle:从 0 分到 100 分的思考过程(含二维 DP 与空间优化)
linux·算法·leetcode
`林中水滴`3 小时前
Linux Shell 命令:nohup、&、>、bg、fg、jobs 总结
linux·服务器·microsoft