Linux信号——保存信号

目录

一信号其他相关常见概念

二信号在内核中的表示

三sigset_t

[3.1 总结](#3.1 总结)

四信号集操作函数

五sigprocmask

六sigpending

七演示代码

Linux 信号是系统用来通知进程事件的异步方式。很多时候进程暂时没空处理信号,系统就需要先把信号暂时"存起来",避免丢失或乱序。日常开发中遇到的信号不生效、延迟触发、莫名丢失等问题,大多都出在信号的保存和状态管理上。本文就聚焦信号如何被内核保存、如何暂存、状态如何切换,通俗讲清未决、阻塞、递达的区别,以及内核位图、信号集和信号屏蔽的底层逻辑,并结合代码演示完整过程。

当前内容:

一信号其他相关常见概念

• 实际执行信号的处理动作称为信号递达(Delivery)

• 信号从产生到递达之间的状态,称为信号未决(Pending)。

• 进程可以选择阻塞 (Block )某个信号。

• 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作.

• 注意,阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动
作。

二信号在内核中的表示

在 Linux 内核中,每一个运行的进程(无论是 bash、nginx 还是你的 hello_world)都对应一个名为 task_struct 的庞大结构体。可以把它想象成进程的"身份证"和"档案袋"。

这张图的核心在于,它提取了 task_struct 中关于信号管理的三个最关键的数据成员:

  1. block (阻塞位图) 这是一个位图(bitmap),每一位对应一个信号编号。例如位 2 对应 SIGINT。如果该位为 1,表示进程屏蔽/阻塞了这个信号。内核收到这个信号后,不会立即处理它,而是会将其"挂起"。

  2. pending (待处理位图) 这也是一个位图,记录了已经发给进程,但尚未被处理 的信号。当内核收到一个信号时,首先查看 block 位图:

    • 如果 block 位为 0 :直接将对应的 pending 位设为 1。

    • 如果 block 位为 1 :将对应的 pending 位设为 1,然后将信号丢入"等待队列"。

  3. handler (函数处理映射表)

    这是信号处理流程的最终出口。当进程决定真正处理 一个信号时(即 pending 中有信号,且对应的 block 被解除),内核会查找这张表。

    表中每一项(对于每个信号)提供了三种处理选项:SIG_DFL (Default) :执行系统默认动作。对于大多数信号,默认动作就是 终止进程SIG_IGN (Ignore)彻底忽略 这个信号。即便 pending 位为 1,内核也会直接清空它,不执行任何动作。用户自定义函数 :此时指向一个内存地址。当信号处理时,内核会从内核态切入到用户空间 ,跳转到这个地址,执行用户写的 void sighandler(int signo) { ... } 代码。

这三者就像三道关卡,决定了信号从产生到最终执行的命运。

源码:

struct sighand_struct定义

sigset_t定义:

struct sigpending定义:

三sigset_t

定义:

cpp 复制代码
typedef struct {
    unsigned long sig[_NSIG_WORDS];
} sigset_t;

其中:

  • _NSIG_WORDS 是一个宏,定义为 (_NSIG / _NSIG_BPW)

  • _NSIG 是信号总数(通常是 64,对应 1~64 号信号)。

  • _NSIG_BPWunsigned long 的位数(在 32 位系统上是 32)。

  • 所以 sigset_t 本质就是一个 unsigned long 类型的数组

对于 32 位系统,sigset_t 展开后就是:

cpp 复制代码
typedef struct {
    unsigned long sig[2];  // 64 / 32 = 2
} sigset_t;
  • sig[0] 存储信号 1~32 的位图。

  • sig[1] 存储信号 33~64 的位图。

每一位(bit)对应一个信号:

  • 0:信号未产生/未阻塞。

  • 1:信号已产生(pending)或被阻塞(block)。

从上图来看,每个信号只有一个bit的未决标志, 非0即1, 不记录该信号产生了多少次,阻塞标志也是这样表示的。因此, 未决和阻塞标志可以用相同的数据类型sigset_t来存储, , 这个类型可以表示每个信号的"有效"或"无效"状态, 在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞, 而在未决信号集中"有 效"和"无效"的含义是该信号是否处于未决状态。下一节将详细介绍信号集的各种操作。塞信号集也叫做当前进程的 这里的"屏蔽"应该理解为阻塞而不是忽略。sigset_t称为信号集信号屏蔽字(Signal Mask),

3.1 总结

  1. 本质sigset_t 是一个位图 ,用数组 unsigned long sig[N] 实现,每个 bit 代表一个信号。

  2. 作用 :它被用于 task_struct 中的 blocked(阻塞)和 pending(待处理)位图。

四信号集操作函数

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。

五sigprocmask

调用函数sigprocmask 可以读取或更改进程的信号屏蔽字(阻塞信号集)。

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
参数 说明 取值
how 指定如何修改信号屏蔽字 SIG_BLOCKSIG_UNBLOCKSIG_SETMASK
set 指向要设置的信号集 非空:用于修改;NULL:忽略
oldset 保存旧的信号屏蔽字 非空:保存旧值;NULL:不保存

how参数的可选值:

如果调用sigprocmask解除了对当前若干个未决信号的阻塞,则在sigprocmask返回前,至少将其中一

个信号递达。

六sigpending

sigpending可以读取当前进程的未决信号集,通过set参数传出。

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);
读取当前进程的未决信号集,通过set参数传出。
调用成功则返回0,出错则返回-1

七演示代码

1)这个实验演示了信号的阻塞、未决和递达三态转换

阻塞SIGINT后发送的信号会进入未决状态,解除阻塞后信号才被递达处理。

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

int main() {
    sigset_t set;
    
    // 创建信号集并阻塞SIGINT
    sigemptyset(&set);
    sigaddset(&set, SIGINT);
    sigprocmask(SIG_BLOCK, &set, NULL);
    
    printf("SIGINT被阻塞,发送信号测试:\n");
    kill(getpid(), SIGINT);  // 发送信号,但会被阻塞
    
    // 检查未决信号
    sigset_t pending;
    sigpending(&pending);
    printf("未决信号中是否有SIGINT: %s\n", 
           sigismember(&pending, SIGINT) ? "是" : "否");
    
    // 解除阻塞
    sigprocmask(SIG_UNBLOCK, &set, NULL);
    printf("解除阻塞\n");
    
    return 0;
}

2)这个实验演示了信号屏蔽、未决状态和递达的过程

核心流程:

  1. 屏蔽2号信号(SIGINT)

  2. 期间发送的2号信号会处于未决状态(pending位图置1)

  3. 15秒后解除屏蔽 ,未决信号被递达,执行自定义处理函数

cpp 复制代码
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#include <cstdio>
#include <iostream>
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 << "\n";
}
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);  // 我们有没有修改当前进行的内核block表呢???10
  // 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);
  }
}

八结语

简单来说,Linux 内核就是靠位图和信号集来统一管理信号的保存状态。通过阻塞位图、未决位图配合信号处理函数表,系统可以精准记住哪些信号被挡住、哪些信号已经产生但还没来得及处理,完美解决了异步信号容易丢失、混乱的问题。搞懂这套信号保存机制,就能彻底理解信号阻塞、延迟、未处理的本质,平时排查程序异常、信号失效问题也能更加得心应手。

相关推荐
AOwhisky22 分钟前
Python 学习笔记(第十五期)——运维自动化(下·后篇):堡垒机实战——paramiko高阶篇
运维·python·学习·云原生·自动化·运维开发
重生的黑客32 分钟前
Linux 进程优先级、切换与调度:从孤儿进程到 O(1) 调度模型
linux·运维·服务器·进程优先级·nice
Urbano35 分钟前
夹克与商务单西口袋工序自动化技改研究报告
运维·自动化
骑上单车去旅行2 小时前
MD5校验对比脚本
linux·服务器·windows
平生幻2 小时前
Linux 常用命令
linux
期待着20132 小时前
docker 安装 ,在centos7.9
运维·docker·容器
踏月的造梦星球2 小时前
DM8 DSC 单机双实例部署
运维·开发语言·数据库
MXsoft6183 小时前
平台化是智能运维发展的必然趋势吗?
运维
ShirleyWang0123 小时前
让headlamp控制台能访问
linux·服务器·python·k8s·k3s
liuccn3 小时前
Linux 存储系统:LVM 与直接分区
linux·运维