Linux进程信号详解(三):信号保存

当前阶段

一、 信号递达、未决与阻塞

在深入代码之前,必须先搞清楚三个容易混淆的概念:

术语 英文 含义
递达 Delivery 实际执行信号的处理动作**(默认、忽略或自定义)**
未决 Pending 信号从产生之后递达之前所处的状态
阻塞 Block 进程可以选择阻止某个信号递达。被阻塞的信号产生后将保持在未决状态,直到解除阻塞

⚠️ 重点区分:阻塞 ≠ 忽略

  • 阻塞信号还没被处理,只是因为"被屏蔽"而暂时无法递达。解除阻塞后,信号仍然会递达。

  • 忽略信号已经递达了,只是处理动作选择了"忽略"而已。

注:信号未决。信号在位图中,还没来得及处理

打个比方:阻塞就像你把快递员拒之门外,快递还在门口等着;忽略就像你收了快递但直接扔掉了。一个是不让进来,一个是进来后不处理。

二、在内核中的表示

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

  • 每个信号都有两个标记位分别表示 阻塞 (block) 和 未决(pending),还有一个函数指针表示处理动作。信号产生时 , 内核在进程控制块中设置该信号的未决标志 , 直到信号递达才清除该标志 。 上图 , SIGHUP信号未阻塞也未产生过 , 当它递达时执行默认处理动作
  • SIGINT信号产生过,但正在被阻塞,所以暂时不能递达 。虽然它的处理动作是忽略 ,但在没有解除阻塞之前,不能忽略这个信号,因为进程仍然有机会改变动作之后再解除阻塞 。
  • SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义含函数sighandler。

每个进程的PCB(task_struct)中,与信号相关的核心数据结构有三部分:

  • blocked位图 :记录了哪些信号被阻塞(1表示阻塞)。

  • pending位图 :记录了哪些信号已经产生但尚未递达(1表示未决)。

  • handler数组:记录了每个信号的处理函数指针(SIG_DFL、SIG_IGN或用户函数地址)。

2.1 handler

复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional> 
#include <vector>
 
void handler(int sig)
{
    std::cout << "hello sig :" << sig << std::endl;
    signal(2,SIG_DFL); //2默认动作是终止
    std::cout << "恢复处理动作" << std::endl; 
}
int main()
{
    //signal(2,handler); //自定义捕捉
    //signal(2,SIG_IGN); //忽略信号

    while(true)
    {
        sleep(1);
        std::cout << ". " << std::endl;
    }

    return 0;
}

2.2 相关知识解释

Q1:block 和 pending 有没有什么关系 ?

没有对应的约束关系;如果你收到了消息,当时阻塞了,消息就显示不出来了

三、信号集 ------ sigset_t 类型

验证信号保存的话题: Linux提供信号操作:一定是围绕着这三张表展开的

3.1 附加话题 - 位图如何设置的

sigset_t是信号集类型,本质是一个位图,我们不能直接用位操作(&/|/~)修改它 ,必须使用 Linux 提供的信号集操作函数,这是操作信号阻塞的核心工具。

3.2 信号集操作函数

  • sigset_t 类型对于每种信号用一个 bit 表示 "有效" 或 "无效"状态 ,**至于这个类型内部如何存储这些 bit 则依赖于系统实现 , 从使用者的角度是不必关心的,**使用者只能调用一下函数来操作sigset_t 变量 , 而不应该对它的内部数据做任何解释 , 比如用printf直接打印sigset_t 变量是没有意义的 。

  • 为了操作blockedpending位图,Linux提供了sigset_t类型(通常是一个整数或结构体,用于表示多个信号的集合)。我们可以使用以下函数来操作信号集:

复制代码
#include <signal.h>

int sigemptyset(sigset_t *set);      // 清空集合(所有信号位=0)
int sigfillset(sigset_t *set);       // 填满集合(所有信号位=1)
int sigaddset(sigset_t *set, int signo);   // 将signo加入集合
int sigdelset(sigset_t *set, int signo);   // 将signo从集合中删除
int sigismember(const sigset_t *set, int signo); // 测试signo是否在集合中
  • 所有函数成功返回0,出错返回-1。

  • sigismember返回1表示是成员,0表示不是,-1表示出错。

⚠️ 重要 :使用sigset_t变量前,必须先用sigemptysetsigfillset初始化,否则结果不可预料。

四、 更改信号屏蔽字 ------ sigprocmask

sigprocmask函数可以读取或更改进程的信号屏蔽字 (也就是blocked位图)。

复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
how 含义
SIG_BLOCK set中的信号添加到当前屏蔽字中(blocked |= set)
SIG_UNBLOCK set中的信号从当前屏蔽字中移除(blocked &= ~set)
SIG_SETMASK 直接将当前屏蔽字设置set
  • 如果oset非空,则之前的屏蔽字被备份到oset中。

  • 如果setNULL,则how被忽略,仅用于获取当前屏蔽字。

核心使用场景

  1. 阻塞某个信号 :用SIG_BLOCK将信号添加到 block 位图。
  2. 解除某个信号的阻塞 :用SIG_UNBLOCK将信号从 block 位图中移除。
  3. 重置阻塞信号集 :用SIG_SETMASK直接替换整个 block 位图。
  4. 获取当前阻塞信号集 :set传NULL,oldset传信号集指针,获取当前 block 位图。

五、获取未决信号集 ------ sigpending

sigpending函数用于获取当前进程的未决信号集(pending位图)。

复制代码
#include <signal.h>
int sigpending(sigset_t *set);
  • 将当前的pending位图通过set参数传出。

  • 成功返回0,出错返回-1。

Q:为什么只给我提供输出型参数 ? 难道不给我们修改pending位图吗?

六、阻塞SIGINT并观察pending变化

复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>
#include <cstdio>

void PrintfPending(sigset_t &pending)
{
    printf("我是一个进程(%d),pending: ",getpid());
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

int main()
{
    // 1.屏蔽2号信号
    sigset_t block, oblock;
    // 先把位图全部清空
    sigemptyset(&block);
    sigemptyset(&oblock);
    // 2号信号添加到集合里
    sigaddset(&block, SIGINT);
    int n = sigprocmask(SIG_SETMASK, &block, &oblock);
    (void)n;

    // 4.重复获取/打印过程
    while (true)
    {
        // 2.获取pending信号集合
        sigset_t pending;
        int m = sigpending(&pending);

        // 3.打印
        PrintfPending(pending);
        sleep(1);
    }
    return 0;
}

6.1 不可被捕捉信号

要是把所有的信号都给屏蔽了,所有信号都不可以递达 , 程序不久金刚不坏了???

6.2 0->1->0

复制代码
#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <functional>
#include <vector>
#include <cstdio>

void handler(int sig)
{
    std::cout << "递达2号信号" << std::endl;
}

void PrintfPending(sigset_t &pending)
{
    printf("我是一个进程(%d),pending: ", getpid());
    for (int signo = 31; signo >= 1; signo--)
    {
        if (sigismember(&pending, signo))
        {
            std::cout << "1";
        }
        else
        {
            std::cout << "0";
        }
    }
    std::cout << std::endl;
}

int main()
{
    signal(SIGINT,handler);
    // 1.屏蔽2号信号
    sigset_t block, oblock;
    // 先把位图全部清空
    sigemptyset(&block);
    sigemptyset(&oblock);
    // 2号信号添加到集合里
    sigaddset(&block, SIGINT);

    //把所有信号都屏蔽了?这ok???
    // for (int i = 1; i < 32; i++)
    //     sigaddset(&block, i);

    int n = sigprocmask(SIG_SETMASK, &block, &oblock);
    (void)n;

    int cnt = 0;
    // 4.重复获取/打印过程
    while (true)
    {
        // 2.获取pending信号集合
        sigset_t pending;
        int m = sigpending(&pending);

        // 3.打印
        PrintfPending(pending);
        if(cnt == 10)
        {
            sigprocmask(SIG_SETMASK,&oblock,nullptr); 
            std::cout << "解除对2号的屏蔽" << std::endl;
        }
        sleep(1);
        cnt++;
    }
    return 0;
}

6.3 解除屏蔽 pending的变化

  • 递达之后,才清0 ?
  • 递达之前,清理?

结论:当我们准备递达的时候,要首先清空pending 信号集中对应的位图 1-> 0

七、为什么阻塞时信号不会递达?

在进程从内核态返回用户态时(例如系统调用结束、中断处理完毕),内核会执行以下逻辑:

检查 pending 位图 & (~blocked 位图)

如果存在非零位,说明有信号没有被阻塞且处于未决状态,内核就会处理该信号(执行对应的handler或默认动作)。而被阻塞的信号,即使pending=1,也因为blocked对应位为1而被掩码过滤掉,不会递达。

当解除阻塞后,内核再次检查时,该信号就会满足条件,从而递达。

八、 常见误区与注意事项

8.1 阻塞 ≠ 忽略

操作 效果 信号是否会丢失?
阻塞(block) **信号暂不递达,**解除阻塞后会递达 不会(常规信号多次产生只记一次)
忽略(ignore) 信号递达后什么都不做 信号已经递达,只是动作是忽略

8.2 常规信号的pending是位图,不是队列

如果进程阻塞了SIGINT,然后在阻塞期间**连续按下10次Ctrl+C最终解除阻塞后,handler只会被调用一次。因为pending位图只能记录"有"或"没有",不能记录次数。 这就是常规信号(不可靠信号)**的特点。实时信号(34~64)则支持队列,可以记录多次。

8.3 SIGKILL和SIGSTOP无法被阻塞

**这两个信号是"终极"信号,用于强制终止或停止进程,不受阻塞和自定义捕捉的影响。**这是系统设计的安全底线。

8.4 sigprocmask的使用约束

  • 在多线程程序中,sigprocmask的行为是未定义的,应该使用pthread_sigmask

  • 不能阻塞SIGKILLSIGSTOP但函数不会报错(会被内核忽略)。

相关推荐
2402_881319302 小时前
跨服务通信兜底机制-Java 回传失败无持久重试队列,报告可能静默丢失。
java·开发语言·python
2401_892070982 小时前
算法与数据结构精讲:最大子段和(暴力 / 优化 / 分治)+ 线段树从入门到实战
c++·算法·线段树·最大子段和
memcpy02 小时前
LeetCode 904. 水果成篮【不定长滑窗+哈希表】1516
算法·leetcode·散列表
格林威2 小时前
SSD 写入速度测试命令(Linux)(基于工业相机高速存储)
linux·运维·开发语言·人工智能·数码相机·计算机视觉·工业相机
老四啊laosi2 小时前
[双指针] 8. 四数之和
算法·leetcode·四数之和
汀、人工智能2 小时前
[特殊字符] 第24课:反转链表
数据结构·算法·链表·数据库架构··反转链表
田梓燊2 小时前
leetcode 41
数据结构·算法·leetcode
暴力求解3 小时前
C++ ---- String类(一)
开发语言·c++
_深海凉_3 小时前
LeetCode热题100-三数之和
算法·leetcode·职场和发展