Linux进程信号:内核数据结构与捕捉递达全流程

上篇文章:Linux信号机制:从键盘到内核、进阶实战硬核剖析

导语

在上一篇文章中,我们探讨了"信号是如何产生的",并且扒开了硬件异常(除零、野指针)触发操作系统发送信号强制终结进程的底层逻辑。

但这仅仅是信号机制的冰山一角。很多开发者对信号的理解只停留在 signal(SIGINT, handler) 这句简单的 API 上。但这会引来无数灵魂拷问:

  • 一个信号产生后,如果被进程阻塞(Block)了,它会消失还是被保存在哪里?

  • 操作系统底层究竟用什么样的数据结构在管理这些信号?

  • 当信号处于未决(Pending)状态时,它到底是如何翻转和递达的?

本文将带你直接掀开 Linux 内核的源码底裤!我们不仅会深入剖析 task_struct 中的底层信号位图机制,更会手写硬核代码可视化呈现 pending 位图的转变过程,并亲自验证 9 号与 19 号信号的绝对特权。

目录

导语

1.信号的生命周期与三大核心概念

[1.1内核探秘:信号在 PCB 中的真实模样](#1.1内核探秘:信号在 PCB 中的真实模样)

1.2深入内核:四大底层术语硬核释义

[底层源码验证:内核结构(Linux 2.6.18)](#底层源码验证:内核结构(Linux 2.6.18))

3.信号集sigset_t

4.信号集操作函数

[4.1int sigemptyset(sigset_t *set);](#4.1int sigemptyset(sigset_t *set);)

[4.2int sigfillset(sigset_t *set);](#4.2int sigfillset(sigset_t *set);)

[4.3int sigaddset(sigset_t *set, int signo);](#4.3int sigaddset(sigset_t *set, int signo);)

[4.4int sigdelset(sigset_t *set, int signo);](#4.4int sigdelset(sigset_t *set, int signo);)

[4.5int sigismember(const sigset_t *set, int signo);](#4.5int sigismember(const sigset_t *set, int signo);)

5.核心系统调用:更改/读取信号集

5.1sigprocmask

5.2sigpending

[6.硬核实战:验证 Pending 位的转变时机](#6.硬核实战:验证 Pending 位的转变时机)

6.1pending过程可视化

[6.2Pending 标志位是在递达前清 0,还是递达后清 0?](#6.2Pending 标志位是在递达前清 0,还是递达后清 0?)

6.3验证9号和19号信号不能被屏蔽

结语


1.信号的生命周期与三大核心概念

在信号的处理流程中,有几个至关重要的状态和概念。理解了它们,你就懂了信号在底层的流转逻辑。

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

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

  • 信号阻塞 (Block):进程可以选择阻塞某个信号。被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

注意阻塞与忽略的区别 :阻塞(Block)是拦截 ,只要信号被阻塞就不会递达,信号还在未决队列里攒着;忽略(Ignore)是信号已经递达并处理了,只是处理动作是可选的一种:"什么都不干"。在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

1.1内核探秘:信号在 PCB 中的真实模样

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

每个信号都有两个标志位分别表示阻塞(block)和未决(pending),还有一个函数指针表示处理动作。信号产生时,内核在进程控制块中设置该信号的未决标志,直到信号递达才清除该标志。

在上图例子中, SIGHUP信号未阻塞也未产生过,当它递达时执行默认处理动作。

SIGINT信号产生过,但正在被阻塞,所以暂时不能递达。虽然它的处理动作是忽略,但在没有解除阻塞之前不能忽略这个信号,因为进程仍有机会改变处理动作之后再解除阻塞。

SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。

1.2深入内核:四大底层术语硬核释义

  1. pending (未决状态 / 未决信号集)

    • 硬核释义 :信号已经产生 ,但还没有被处理的这种"中间状态"。

    • 底层机制 :在内核中,它对应着一个位图(pending 表)。当操作系统给进程发信号时,本质上就是把这个位图里对应的比特位置为 1。只要这个位是 1,就说明有一个信号正停留在进程的 PCB 里,等着被处理。

    • 生活隐喻:这就像是你微信里收到了消息,但你还没点开看。这个红色的未读角标,就是 pending。

  2. block (阻塞状态 / 信号屏蔽字)

    • 硬核释义:允许进程拦截某些信号。

    • 底层机制 :它在内核中也是一个位图(block 表,也叫信号屏蔽字 Signal Mask)。如果某个信号在 block 位图中对应的位被置为 1,那么该信号就被阻塞了,操作系统绝对不会把它递达(处理)给进程。

    • 生活隐喻:这相当于你把某个群设为了"消息免打扰"。群里依然可以发消息(依然可以 pending),但系统不会弹窗震动提醒你。直到你取消免打扰(解除 block),你才会去集中处理这些消息。

  3. handler (处理动作 / 处理方法表)

    • 硬核释义 :当信号终于突破重重阻碍,成功递达给进程时,进程具体要怎么做

    • 底层机制 :在内核中,它是一个函数指针数组handler 表)。数组的下标是信号编号,数组里存的是处理方法的地址。通常有三种:SIG_DFL(系统默认)、SIG_IGN(忽略)、或者指向用户自定义函数的指针。

  4. sighandler / sighandler_t (自定义处理函数指针类型)

    • 硬核释义 :它是 glibc 库中 signal 函数里用到的一个类型重命名,代表用户自定义的信号处理函数

    • 底层机制 :在 C 语言中,定义为 typedef void (*sighandler_t)(int);。当你调用 signal(2, my_func) 时,你就是把 my_func 这个类型的指针,写到了内核 handler 表的第 2 号位置上。

底层源码验证:内核结构(Linux 2.6.18)

cpp 复制代码
struct task_struct
{
    ...
        /* signal handlers */
        struct sighand_struct *sighand;
    sigset_t blocked struct sigpending pending;
    ...
}

struct sighand_struct
{
    atomic_t count;
    struct k_sigaction action[_NSIG]; // #define _NSIG 64
    spinlock_t siglock;
};

struct __new_sigaction
{
    __sighandler_t sa_handler;
    unsigned long sa_flags;
    void (*sa_restorer)(void); /* Not used by Linux/SPARC */
    __new_sigset_t sa_mask;
};

struct k_sigaction
{
    struct __new_sigaction sa;
    void __user *ka_restorer;
};

/* Type of a signal handler. */
typedef void (*__sighandler_t)(int);
struct sigpending
{
    struct list_head list;
    sigset_t signal;
};

3.信号集sigset_t

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

4.信号集操作函数

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

4.1int sigemptyset(sigset_t *set);

功能清空信号集 ,将集合中所有信号的位都置为0

  • 参数set - 指向要初始化的信号集的指针
  • 返回值 :成功返回0,失败返回-1并设置errno

使用场景 :信号集在使用前必须初始化,这是最常用的初始化方式。

复制代码
sigset_t block;
sigemptyset(&block); // 现在block集合中不包含任何信号

4.2int sigfillset(sigset_t *set);

功能填满信号集 ,将集合中所有信号的位都置为1

  • 参数:同上
  • 返回值:同上

使用场景:需要阻塞所有信号时使用。

复制代码
sigset_t all_signals;
sigfillset(&all_signals); // 现在all_signals包含了系统支持的所有信号

4.3int sigaddset(sigset_t *set, int signo);

功能向信号集中添加一个信号 ,将对应信号的位置为1

  • 参数
    • set - 目标信号集指针
    • signo - 要添加的信号编号(如SIGINT即 2 号信号)
  • 返回值:同上

使用场景:精确控制要阻塞或处理的信号。

复制代码
sigaddset(&block, SIGINT); // 向block集合中添加2号信号
sigaddset(&block, SIGQUIT); // 再添加3号信号

4.4int sigdelset(sigset_t *set, int signo);

功能从信号集中删除一个信号 ,将对应信号的位置为0

  • 参数:同上
  • 返回值:同上

使用场景:从一个填满的信号集中排除特定信号。

复制代码
sigfillset(&block); // 先填满所有信号
sigdelset(&block, SIGKILL); // 删除9号信号(SIGKILL无法被阻塞)
sigdelset(&block, SIGSTOP); // 删除19号信号(SIGSTOP无法被阻塞)

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

功能检查一个信号是否在信号集中

  • 参数:同上
  • 返回值
    • 1:信号集合中
    • 0:信号不在集合中
    • -1:调用失败(如signo无效)

使用场景:检测 pending 信号集或阻塞信号集的状态。

复制代码
if(sigismember(&pending, SIGINT)) {
    printf("2号信号处于pending状态\n");
}

5.核心系统调用:更改/读取信号集

5.1sigprocmask

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

cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset); 
返回值:若成功则为0,若出错则为-1 

如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset,然后根据set和how参数更改信号屏蔽字。假设当前的信号屏蔽字为mask,下表说明了how参数的可选值。

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

5.2sigpending

cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);

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

调⽤成功则返回0,出错则返回-1

6.硬核实战:验证 Pending 位的转变时机

6.1pending过程可视化

我们先屏蔽 2 号信号(SIGINT),然后不断获取 pending 信号集并打印出来。此时如果我们按下 Ctrl+C,由于信号被阻塞,它会一直停留在 pending 状态!

代码:

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

void PrintPending(sigset_t &pending)
{
    for(int signum = 31; signum >= 1; signum--)
    {
        if(sigismember(&pending, signum))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}

int main()
{
    std::cout << "pid: " << getpid() << std::endl;
    // 定义&&初始化信号集
    sigset_t block, old_block;
    sigemptyset(&block);
    sigemptyset(&old_block);

    // 向block信号集添加信号
    sigaddset(&block, SIGINT);

    // 屏蔽2号信号
    int n = sigprocmask(SIG_SETMASK, &block, &old_block);
    (void)n;

    // 获得pending信号集并打印
    sigset_t pending;
    while(true)
    {
        sigemptyset(&pending);
        int m = sigpending(&pending);
        (void)m;

        PrintPending(pending);

        sleep(1);
    }

    return 0;
}

结果:

6.2Pending 标志位是在递达前清 0,还是递达后清 0?

新增捕捉2号信号以及探讨pending信号位的转变是在递达之前1->0还是之后0->1,我们在代码中获取并打印pending信号集。

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

void PrintPending(sigset_t &pending)
{
    for(int signum = 31; signum >= 1; signum--)
    {
        if(sigismember(&pending, signum))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}

void handler(int signo)
{
    std::cout << "处理了:" << signo << "号信号" << std::endl;
    std::cout << "#########################" << std::endl;
    sigset_t pending;
    sigemptyset(&pending);
    int m = sigpending(&pending);
    (void)m;

    // 打印
    PrintPending(pending);

    std::cout << "#########################" << std::endl;
}

int main()
{
    // 捕捉2号信号
    signal(2, handler);

    std::cout << "pid: " << getpid() << std::endl;
    // 定义&&初始化信号集
    sigset_t block, old_block;
    sigemptyset(&block);
    sigemptyset(&old_block);

    // 向block信号集添加信号
    sigaddset(&block, SIGINT);

    // 屏蔽2号信号
    int n = sigprocmask(SIG_SETMASK, &block, &old_block);
    (void)n;

    // 获得pending信号集并打印
    int cnt = 0;
    sigset_t pending;
    while(true)
    {
        sigemptyset(&pending);
        int m = sigpending(&pending);
        (void)m;

        // 打印
        PrintPending(pending);

        sleep(1);

        cnt++;
        // 恢复2号信号
        if(cnt == 20)
        {
            std::cout << "2号信号恢复" << std::endl;
            sigprocmask(SIG_SETMASK, &old_block, nullptr);
        }
    }

    return 0;
}

结果:

结论: pending 信号位是在信号递达(调用处理函数)之前由内核从 1 置为 0

6.3验证9号和19号信号不能被屏蔽

如果我们把 1~31 号信号全部添加进 block 集合进行屏蔽,进程是不是就无敌了?

代码:

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

void PrintPending(sigset_t &pending)
{
    for(int signum = 31; signum >= 1; signum--)
    {
        if(sigismember(&pending, signum))
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
    }
    printf("\n");
}

void handler(int signo)
{
    std::cout << "处理了:" << signo << "号信号" << std::endl;
    std::cout << "#########################" << std::endl;
    sigset_t pending;
    sigemptyset(&pending);
    int m = sigpending(&pending);
    (void)m;

    // 打印
    PrintPending(pending);

    std::cout << "#########################" << std::endl;
}

int main()
{
    // 捕捉2号信号
    signal(2, handler);

    std::cout << "pid: " << getpid() << std::endl;

    // 定义&&初始化信号集
    sigset_t block, old_block;
    sigemptyset(&block);
    sigemptyset(&old_block);

    for(int signum = 1; signum <= 31; signum++)
    {
        sigaddset(&block, signum);
    }
    
    // // 向block信号集添加信号
    // sigaddset(&block, SIGINT);

    // 屏蔽2号信号
    int n = sigprocmask(SIG_SETMASK, &block, &old_block);
    (void)n;

    // 获得pending信号集并打印
    int cnt = 0;
    sigset_t pending;
    while(true)
    {
        sigemptyset(&pending);
        int m = sigpending(&pending);
        (void)m;

        // 打印
        PrintPending(pending);

        sleep(1);

        // cnt++;
        // // 恢复2号信号
        // if(cnt == 20)
        // {
        //     std::cout << "2号信号恢复" << std::endl;
        //     sigprocmask(SIG_SETMASK, &old_block, nullptr);
        // }
    }

    return 0;
}

结果:

输入命令

cpp 复制代码
cnt=1; while [ $cnt -le 31 ]; do kill -${cnt} 1133010; let cnt++; sleep 1; done

结论 :大部分信号都能看到位图被置为 1 且不被处理。但当发送 9 号(SIGKILL)时,进程立刻被强杀;发送 19 号(SIGSTOP)时,进程立刻被挂起。9号和19号信号不仅无法被捕捉,也无法被阻塞屏蔽!

结语

从基础的三大核心状态 pendingblockhandler,到终端杀手信号的差异解析;从扒开 task_struct 源码直面底层的 sigset_t 位图机制,再到通过硬核代码亲手将位图可视化,见证信号状态在递达前翻转的瞬间。

至此,关于 Linux 进程信号的基础机制与内核保存方式,我们已经彻底理清了!

信号机制是操作系统与用户进程沟通的脉络,它看似轻巧,底层却涌动着精妙的数据结构设计。掌握了这篇硬核内容,你就不再只是一个只会调 signal API 的"调包侠",而是真正具备底层排错能力的系统开发者。

在下一篇文章中,我们还将继续深挖信号在"内核态"与"用户态"之间究竟是如何流转与处理的,敬请期待!

相关推荐
LLM精进之路1 小时前
IEEE 26 | 参数量不是关键:4B模型VeriGround在匿名化电路生成任务上性能超越GPT-5.4
人工智能·gpt·深度学习·机器学习
weixin_699602441 小时前
Wan Tasks API 集成与使用指南
ai
陈嘿萌1 小时前
学术速递|2026年4月 arXiv 图像融合论文汇总(04.01–04.30)10 篇最新成果
人工智能·机器学习·计算机视觉·图像融合·arxiv
qq_411262421 小时前
四博 AI-S3 双目交互终端开发方案:ESP32-S3 + VB6824 + 双目动画 + 触控/姿态/震动闭环
人工智能·智能音箱
小新同学^O^1 小时前
简单学习 --> 数据标注
人工智能·python·学习·数据标注
2601_949499941 小时前
芯瑞科技推出的400G VR4 OSFP,是专门针对智算中心,为其实现“冷静”算力而精心打造的,属于散热方面的优选产品。
人工智能·科技
STARFALL0011 小时前
MySQL 运维
运维·数据库·mysql
智慧景区与市集主理人1 小时前
5A景区智慧建设|突破转型瓶颈!巨有科技打造高标准智慧文旅标杆
大数据·人工智能·科技
北京领雁科技1 小时前
领雁科技助力某商业银行企业手机银行数智化升级
大数据·人工智能·科技