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;
}

运行结果:

相关推荐
Avan_菜菜15 小时前
FRP 内网穿透完整实战:从 HTTP 映射到 HTTPS 自签代理
运维·nginx·https
SelectDB2 天前
Litefuse 开源并推出单进程轻量模式,25 秒就能跑起来的 Agent 可观测与评估平台
运维·后端·自动化运维
zzzzzz3103 天前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
XIAOHEZIcode3 天前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
用户0328472220704 天前
如何搭建本地yum源(上)
运维
大树887 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠7 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质7 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz7 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工7 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信