Linux:信号保存与捕捉

认识信号的3张表,理解信号的捕捉与保存

一、理解信号的几个概念

  1. 实际执行信号的处理动作称为信号递达 (Delivery)
  2. 信号从产生到递达之间的状态,称为信号未决 (Pending(待处理))
  3. 进程可以选择 **阻塞 (Block)**某个信号。
  4. 被阻塞的信号产生时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。

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

二、信号在内核中的表示

信号在内核中的表示示意图(task_struct 关联的三个表):

  • block(阻塞表):标记信号是否被阻塞
  • pending(未决表):标记信号是否已产生但未递达
  • **handler(**处理动作表):标记信号的处理方式(默认 / 忽略 / 自定义函数)

示例说明:

  • **SIGHUP(1):**未阻塞也未产生过,递达时执行默认处理动作。
  • **SIGINT(2):**已产生但被阻塞,暂不能递达;即使处理动作是忽略,未解除阻塞前仍无法执行。
  • SIGQUIT(3) 未产生过,一旦产生会被阻塞,处理动作是自定义函数 sighandler

补充规则:如果在进程解除对某信号的阻塞之前,这种信号产生多次:

  • POSIX.1 允许系统递送该信号一次或多次。
  • Linux 实现:常规信号在递达前产生多次只计一次;实时信号可依次排队(本章不讨论实时信号)。
cpp 复制代码
// 内核结构 2.6.18
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;
};

三、对三张表的操作

1. sigset_t

  • 每个信号只有一个 bit 的未决标志(0/1,不记录产生次数),阻塞标志同理。
  • 未决和阻塞标志可用相同数据类型 sigset_t 存储,该类型称为信号集 ,用每个 bit 表示信号的 "有效 / 无效" 状态:
    • 阻塞信号集(信号屏蔽字):bit 为 1 表示该信号被阻塞
    • 未决信号集:bit 为 1 表示该信号处于未决状态
  • 类比权限中的 umask

2.信号集操作函数

sigset_t 类型的 bit 存储方式依赖系统实现,用户只能通过以下函数操作,不能直接打印解释:

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);
  1. 函数 sigemptyset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 清零,表示该信号集不包含任何有效信号。

  2. 函数 sigfillset 初始化 set 所指向的信号集,使其中所有信号的对应 bit 置位,表示 该信号

集的有效信号包括系统支持的所有信号。

注意,在使用 sigset_t 类型的变量之前,一定要调用 sigemptyset 或 sigfillset 做初始化,使信号集处于确定的状态。初始化 sigset_t 变量之后就可以在调用 sigaddset 和 sigdelset 在该信号集中添加或删除某种有效信号。

这四个函数都是成功返回 0, 出错返回 - 1。

sigismember 是一个布尔函数,用于判断一个信号集的有效信号中是否包含 某种 信号,若包含则返回 1, 不包含则返回 0, 出错返回 - 1。

3. sigprocmask

  • 功能:读取或更改进程的信号屏蔽字(阻塞信号集)
cpp 复制代码
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
  • 返回值:成功返回 0,出错返回 - 1。
  • 参数说明:
    • oset 非空:原信号屏蔽字会通过 oset 传出。
    • set 非空:根据 how 参数更改信号屏蔽字。
    • osetset 都非空:先备份原屏蔽字到 oset,再按 sethow 修改。

表格

how 参数 作用 等价操作
SIG_BLOCK set 中的信号添加到当前屏蔽字 mask |= set
SIG_UNBLOCK 从当前屏蔽字中解除 set 中的信号 mask &= ~set
SIG_SETMASK 将当前屏蔽字直接设为 set 的值 mask = set
  • 补充(细节):若调用 sigprocmask 解除了对未决信号的阻塞,函数返回前至少会递达其中一个信号。

4 sigpending

  • 功能:读取当前进程的未决信号集 ,通过 set 参数传出。
cpp 复制代码
#include <signal.h>
int sigpending(sigset_t *set);

返回值:成功返回 0,出错返回 - 1。

5. 使用上面的函数写一个小demo

阻塞2号信号

先把2号信号(ctrl + c)阻塞了,然后发送2号信号,将pending表打印出来,观察pending表的中的信号未决

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

void PrintPending(const 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;
}

int main()
{
    // 1.屏蔽2号信号
    sigset_t block_set, odd_set;
    sigemptyset(&block_set);
    sigemptyset(&odd_set);

    sigaddset(&block_set, SIGINT);
    int n = sigprocmask(SIG_SETMASK, &block_set, &odd_set);
    (void)n;

    while(true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 2.获取pending表
        int n = sigpending(&pending);
        (void)n;
        // 3. 打印pending表
        PrintPending(pending);    

        sleep(1);
    }
    return 0;
}

现象:

可以看到,在pending表中,2号信号是未决状态(待处理状态)

把所有信号都阻塞了

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

void PrintPending(const 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;
}

int main()
{
    // 1.屏蔽2号信号
    sigset_t block_set, odd_set;
    sigemptyset(&block_set);
    sigemptyset(&odd_set);
    for(int i = 0; i <= 31; i++)
        sigaddset(&block_set, i);

    int n = sigprocmask(SIG_SETMASK, &block_set, &odd_set);
    (void)n;

    while(true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 2.获取pending表
        int n = sigpending(&pending);
        (void)n;
        // 3. 打印pending表
        PrintPending(pending);    

        sleep(1);
    }
    return 0;
}

可以看到,9号信号,和19号信号,是无法被阻塞的。

四、信号捕捉

1.当block表的阻塞信号被取消后,pending表是在handler调用前恢复成0,还是在handler调用后恢复成0?

简单验证:

可以在handler方法内部,继续获取pending表,打印观察,如果pending表的对应信号位还是1,就说明是在调用后恢复成0,反之,在调用前恢复成0.

代码:

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

void PrintPending(const 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 << "------------------entery handler----------" << std::endl;
    sigset_t pending;
    sigemptyset(&pending);
    // 2.获取pending表
    int n = sigpending(&pending);
    (void)n;
    std::cout << "处理完成" << signo << std::endl;
    PrintPending(pending);
    std::cout << "------------------leave handler----------" << std::endl;
}
int main()
{
    std::cout << "我的pid:" << getpid() << std::endl;
    // 0. 捕捉2号信号
    signal(2, handler);
    // 1.屏蔽2号信号
    sigset_t block_set, odd_set;
    sigemptyset(&block_set);
    sigemptyset(&odd_set);
    for (int i = 0; i <= 31; i++)
        sigaddset(&block_set, i);

    int n = sigprocmask(SIG_SETMASK, &block_set, &odd_set);
    (void)n;
    int cnt = 0;
    while (true)
    {
        sigset_t pending;
        sigemptyset(&pending);
        // 2.获取pending表
        int n = sigpending(&pending);
        (void)n;
        // 3. 打印pending表
        PrintPending(pending);
        if (cnt == 20)
        {
            int n = sigprocmask(SIG_SETMASK, &odd_set, nullptr);
            (void)n;
        }
        cnt++;
        sleep(1);
    }
    return 0;
}

运行结果:

可以看到,是在调用handler前就恢复成0了

2.信号捕捉的流程:

如果信号的处理动作是用户自定义函数,在信号递达时就调用这个函数,称为捕捉信号。

由于信号处理函数的代码是在用户空间的,处理过程比较复杂,举例如下:

  • 用户程序定义了 SIGQUIT 信号的处理函数 sighandler
  • 当前正在执行 main 函数,这时发生中断或异常切换到内核态
  • 在中断处理完毕后要返回用户态的 main 函数之前检查到有信号 SIGQUIT 递达
  • 内核决定返回用户态后不是恢复 main 函数的上下文继续执行,而是执行 sighandler 函数,sighandlermain 函数使用不同的堆栈空间,它们之间不存在调用和被调用的关系,是两个独立的控制流程。
  • sighandler 函数返回后自动执行特殊的系统调用 sigreturn 再次进入内核态
  • 如果没有新的信号要递达 ,这次再返回用户态就是恢复 main 函数的上下文继续执行了。

3.sigaction函数

cpp 复制代码
#include <signal.h>
int sigaction(int signo, const struct sigaction *act, struct sigaction *oact);
cpp 复制代码
       The sigaction structure is defined as something like:

           struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
           };

**sigaction 函数可以读取和修改与指定信号相关联的处理动作。调用成功则返回 0, 出错则返回 - 1。**signo 是指定信号的编号。若 act 指针非空,则根据 act 修改该信号的处理动作。若 oact 指针非空,则通过 oact 传出该信号原来的处理动作。

act 和 oact 指向 sigaction 结构体 :将 sa_handler 赋值为常数 SIG_IGN 传给 sigaction 表示忽略信号,赋值为常数 SIG_DFL 表示执行系统默认动作,赋值为一个函数指针表示用自定义函数捕捉信号,或者说向内核注册了一个信号处理函数,该函数返回值为 void, 可以带一个 int 参数(相当于signal),通过参数可以得知当前信号的编号,这样就可以用同一个函数处理多种信号。显然,这也是一个回调函数,不是被 main 函数调用,而是被系统所调用。

当某个信号的处理函数被调用时,内核自动将当前信号加入进程的信号屏蔽字(block表中,该信号0->1) ,当信号处理函数返回时自动恢复原来的信号屏蔽字,这样就保证了在处理某个信号时,如果这种信号再次产生,那么它会被阻塞到当前处理结束为止。 如果在调用信号处理函数时,除了当前信号被自动屏蔽之外,还希望自动屏蔽另外一些信号,**则用 sa_mask 字段说明这些需要额外屏蔽的信号,**当信号处理函数返回时自动恢复原来的信号屏蔽字。 sa_flags 字段包含一些选项,本章的代码都把 sa_flags 设为 0,sa_sigaction 是实时信号的处理函数。

代码样例:

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


void handler(int signo)
{
    std::cout << "捕捉一个信号: " << signo << std::endl;

    sigset_t pending;
    while(true)
    {
        sigpending(&pending);
        for(int i = 31; i >= 1; i--)
        {
            if(sigismember(&pending, i))
            {
                std::cout << "1";
            }
            else
                std::cout << "0";
        }
        sleep(1);
        std::cout << std::endl;
    }
}
int main()
{
    struct sigaction act, oact;
    act.sa_handler = handler;
    sigemptyset(&(act.sa_mask));
    sigaddset(&(act.sa_mask), 3);
    sigaddset(&(act.sa_mask), 4);
    sigaddset(&(act.sa_mask), 5);

    act.sa_flags = 0;
    act.sa_restorer = nullptr;

    sigaction(SIGINT, &act, &oact);
    while(true)
    {
        std::cout << "进程在运行" << getpid() << std::endl;
        sleep(1);
    }
    return 0;
}

运行结果:

相关推荐
龙侠九重天2 小时前
可视化自动化工具实现
运维·自动化·openclaw
水冗水孚2 小时前
以Vultr供应商的VPS为例、十分钟自建一个自己的VPN(图文并茂)
运维·服务器
巨大八爪鱼2 小时前
【方法】Tomcat网站添加用户名密码弹窗认证
运维·服务器·tomcat·jsp·mod_jk
unDl IONA2 小时前
Linux安装RabbitMQ
linux·运维·rabbitmq
米高梅狮子2 小时前
Ubuntu和Containerd
linux·运维·ubuntu
片酷2 小时前
【IsaacLab报错】C++ 标准库版本过低
linux·运维·服务器
以太浮标2 小时前
华为eNSP模拟器综合实验之- 华为设备 LLDP(Link Layer Discovery Protocol)解析
运维·服务器·网络·网络协议·华为·信息与通信·信号处理
草莓熊Lotso2 小时前
手搓工业级 C++ 线程安全日志系统:基于策略模式解耦,兼容 glog 使用风格
linux·运维·服务器·数据库·c++·安全·策略模式
2601_949815332 小时前
Node.js HTTP模块详解:创建服务器、响应请求与客户端请求
服务器·http·node.js