【Linux】进程信号(2)_信号保存

hello~ 很高兴见到大家! 这次带来的是Linux系统中关于信号这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页 : 默|笙


文章目录


一、信号保存

1.1 信号其他相关常见概念

  1. 信号递达:实际执行信号处理的动作称作信号递达---信号处理。
  2. 信号未决:从信号产生到信号递达中间的这段时间叫做信号未决。也就是信号保存。
  3. 信号阻塞:进程可以选择阻塞(屏蔽)某个信号。被阻塞的信号产生后,会一直维持在未决状态,无法走到递达处理的步骤;直到进程解除对该信号的阻塞,信号才会被递达。
  4. 阻塞!= 忽略 :忽略是信号已经递达后的一种处理方式(进程选择不做任何操作);而阻塞是在递达前就拦截(它是不让信号递达的方式),让信号始终停留在未决状态,二者发生的阶段和本质完全不同。
  5. 信号需要被保存,是因为信号产生后通常不会立即被进程处理(递达),因此需要先将其暂存。
  6. 对于信号递达、信号未决、信号阻塞,进程的task_struct 里面有三张表分别对应:handler表、pending表和block表。

1.2 三张表

sigset_t类型(信号集)

  1. pending 表和 block 表都是 sigset_t 类型,而 sigset_t 本质是 unsigned long 数组实现的位图(bitmask);位图的第 1 位对应 1 号信号,第 2 位对应 2 号信号,以此类推,信号编号与位图的位编号一一对应。例如:第 2 位 对应 SIGINT(2 号信号)。

pending表(未决信号集)

  1. pending 表的本质是一个位图,每一个比特位与信号编号一一对应;信号产生后,OS 会将该表中对应信号编号的比特位置为 1 并保存,因为 OS 不会立即处理该信号。因此,表中比特位为 1 的信号,其状态就是信号未决状态(比特位:信号编号,比特位的内容:是否收到该信号)。
  2. 产生信号后,信号的状态会保存在 pending 表中;信号递达并处理完成后,pending 表中该信号对应的比特位会被置为 0。

block表(信号屏蔽字)

  1. block 表与 pending 表本质相同,均为位图结构,每一个比特位与信号编号一一对应;比特位为 1 代表该信号被阻塞,为 0 则代表该信号未被阻塞(比特位:信号编号,比特位的内容:该信号是否被阻塞)。
  2. 修改 block 表的阻塞状态不会影响信号在 pending 表中的保存;被阻塞的信号会一直滞留在 pending 表中,除非解除阻塞,否则 pending 表中对应的比特位会始终保持为 1。
  3. 如果信号在阻塞解除之前产生多次,常规信号在递达之前产生多次只计⼀次。
  4. 特殊信号 9 号和 19 号无法被屏蔽,这两个信号的阻塞位永远为1。内核会拒绝所有想要屏蔽它们的操作。

handler表

  1. handler 表并非 sigset_t 类型,它是一个函数指针数组,数组中存储着各个信号的处理函数指针;注意:数组下标 + 1 = 信号编号

  2. 以 signal(2, myhandler) 为例,底层会将 2 号信号 对应的数组下标 1 处的元素,替换为我们自定义的 myhandler 函数指针。这样下次在处理 2 号信号时,就会使用我们提供的自定义处理方法。

  3. 什么是信号的忽略和默认处理?忽略与默认是信号的两种基础处理方式,它们在 handler 表中存储的内容分别为 SIG_IGN 和 SIG_DFL;这两个宏的本质,就是将整数 1 和 0 强制转换成函数指针类型后存入数组

1.3 信号集操作函数

  1. C 标准库(遵循 POSIX 标准)提供了一组专用的信号集操作函数,这些函数专门用于对 sigset_t 类型的信号集变量进行初始化、添加信号、删除信号、判断信号是否存在等操作;它们仅在用户态工作,不直接与内核交互,是我们操作信号集的基础工具函数。用户态和内核态这两个概念下一篇博客会讲到。
  1. sigemptyset:清空信号集,所有比特位置 0。
  2. sigfillset:填满信号集,所有比特位置 1。
  3. sigaddset:向信号集中添加一个指定信号。
  4. sigdelset:从信号集中删除一个指定信号。
  5. sigismember:判断指定信号是否在信号集中。

这 5 个信号集操作函数的底层本质都是位运算;理论上我们可以手动通过位运算实现相同功能,但绝不建议这样做。

这 5 个信号都是成功返回 0,失败返回 -1。

  1. 操作系统允许用户控制信号的核心本质,就是访问和操作 block、pending、handler 这三张内核维护的表;系统为我们提供了专用的系统调用接口,例如 sigprocmask(操作 block 表)、sigpending(查询 pending 表),配合相关接口可完成对三张表的修改与管理。


  1. sigprocmask 函数用于修改进程的 block 表(信号屏蔽字):第一个参数 how:指定修改 block 表的方式,包含三种取值:SIG_BLOCK:将 set 中的信号追加屏蔽到 block 表;SIG_UNBLOCK:对 set 中的信号解除屏蔽;SIG_SETMASK:直接用 set 替换当前的 block 表。第二个参数 set:传入我们提前准备好的 sigset_t 信号集(待操作的目标集合);传入 NULL 表示不修改 block 表。第三个参数 oldset:输出型参数,用于保存修改前的旧 block 表;传入 NULL 表示不保存旧值
  1. sigpending 函数用于获取当前进程的未决信号集(pending 表);它的参数是一个输出型参数,内核会将进程的 pending 表拷贝到该参数指向的变量中,该函数仅用于读取 pending 表,无法修改。
  2. 修改 pending 表我们之间就已经讲了5种方法,也就是信号产生的5种方法。
  3. 使用 sigprocmask 和 sigpending 这两个函数,必须包含标准头文件 <signal.h>。接下来对这些函数进行测试:
cpp 复制代码
#include <iostream>
#include <signal.h>
#include <unistd.h>

void PrintPending(sigset_t& pending)
{
    for (int signo = 31; signo > 0; 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()
{
    // 0.修改信号处理方法
    signal(2, handler);

    // 1.屏蔽信号
    sigset_t block_set, old_set;
    sigemptyset(&block_set);
    sigemptyset(&old_set);

    sigaddset(&block_set, 2);
    int n = sigprocmask(SIG_BLOCK, &block_set, &old_set);
    (void)n;
    
    int cnt = 8;
    while (true)
    {
        sigset_t pending;
        int n = sigpending(&pending);
        (void)n;
        // 2. 查看pending表
        PrintPending(pending);

        //3. 解除屏蔽
        if (cnt == 0)
        {
            int n = sigprocmask(SIG_SETMASK, &old_set, nullptr);
            (void)n;
        }
        cnt--;
        sleep(1);
    }
    

    return 0;
}
  1. 这个代码就是屏蔽 2 号信号,然后使用2号信号,查看pending表是否有被阻塞,信号对应比特位是否为1,5秒之后解除阻塞,查看pending表信号递达成功之后对应比特位是否为0。
  2. pending 信号对应比特位是在 handler 之前还原为0,还是在之后?是在之前,我们可以通过修改 handler 函数进行证明。
cpp 复制代码
void handler(int signo)
{
    std::cout << "------------------------------" << std::endl;
    std::cout << "处理完成:" << signo << std::endl;
    sigset_t pending;
    sigpending(&pending);
    PrintPending(pending);
    std::cout << "------------------------------" << std::endl;
}
  1. 可以看到,在执行 handler 函数体的时候信号2对应比特位就已经变回0了。所以不可能在hanler函数调用之后,而是在之前,且 handler 函数本身没有改 pending 表能力。

  2. 被阻塞的普通信号,在解除阻塞的一瞬间,信号就会被立即递达,执行处理函数


今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!

相关推荐
CaracalTiger1 小时前
Windows 环境下 OpenClaw 的安装与千问Qwen、Kimi、MiniMax、GLM国产大模型配置完全指南
运维·ide·windows·开源·github·aigc·ai编程
youyoulg1 小时前
opencode在Linux终端中无法复制文字的解决方法
linux·服务器·人工智能
2301_807367192 小时前
Linux(CentOS)安装 Nginx
linux·nginx·centos
yy_xzz2 小时前
【Linux开发】 05 Linux 多进程并发服务器
linux·服务器·github
minji...2 小时前
Linux 进程间通信(四)System V共享内存
linux·运维·服务器
艾莉丝努力练剑2 小时前
【Linux信号】Linux进程信号(中):信号保存、信号处理(含“OS是如何运行的?”)
大数据·linux·运维·服务器·数据库·c++·mysql
泡沫·2 小时前
docker的基本认识
运维·docker·容器
山峰哥2 小时前
《解锁SQL高效查询:从索引设计到执行计划优化》
服务器·数据库·sql·oracle·性能优化
FatHonor2 小时前
Nginx作用以及应用场景
运维·nginx