Linux进程信号——信号保存

发布于:2025-07-26 ⋅ 阅读:(12) ⋅ 点赞:(0)

目录

1、再次认识信号

1.1、概念

1.2、感性理解

1.3、在内核中的表示

1.4、sigset_t 信号集

2、信号集操作函数

2.1、增删改查

2.2、sigprocmask 函数

2.3、sigpending 函数

1、再次认识信号

在这篇文章中,我们将进一步补充和理解 信号传递 过程中的一些关键概念。

1.1、概念

信号传递过程:信号从产生到执行的过程可以分为三部分:

  1. 信号产生(Produce):信号的发出有四种主要方式:键盘输入、系统调用、软件条件和硬件异常。

  2. 信号未决(Pending):信号从产生到执行的中间状态,指信号已产生,但未被立即处理。

  3. 信号递达(Delivery):信号递达后,进程会根据信号的处理方式来执行相应的动作。

信号阻塞(Block):指信号在任何阶段的传递过程中,信号的处理被暂停。阻塞的信号无法继续处理,直到解除阻塞。

1.2、感性理解

可以通过网购的过程来比喻信号的传递过程:

  • 信号产生:在购物平台上完成订单(信号的产生)。

  • 信号未决:订单在运输途中(信号的未决状态)。

  • 信号递达:快递送达后,用户根据自己的情况进行处理(信号递达后的处理动作)。

  • 信号阻塞:假设快递运输中发生堵车(信号传递被阻塞)。

在购物中,我们下单后,物流信息(未决信息)会记录,但快递可能因堵车导致延迟(信号的阻塞)。而信号的递达过程就类似快递送达后用户进行处理的动作,比如默认拆快递、忽略或退货等。

1.3、在内核中的表示

对于传递中的信号来说,需要存在三种状态表达:

  1. 信号是否阻塞
  2. 信号是否未决
  3. 信号递达时的执行动作

在内核中,每个进程都需要维护这三张与信号状态有关的表:block 表、pending 表、handler 表

所谓的 block 表 和 pending 表 其实就是 位图结构

一个 整型 int 就可以表示 31 个普通信号(实时信号这里不讨论)

  • 比如 1 号信号就是位图中的 0 位置处,0 表示 未被阻塞/未产生未决,1 则表示 阻塞/未决
  • 对于信号的状态修改,其实就是修改 位图 中对应位置的值(0/1
  • 对于多次产生的信号,只会记录一次信息(实时信号则会将冗余的信号通过队列组织)

如何记录信号已产生 -> 未决表中对应比特位置置为 1 ?

1.假设已经获取到了信号的 pending 表
2.只需要进行位运算即可:pending |= (1 << (signo - 1))
3.其中的 signo 表示信号编号,-1 是因为信号编号从 1 开始,需要进行偏移


如果想要取消 未决 状态也很简单:pending &= (~(1 << (signo - 1)))
至于 阻塞 block 表,与 pending 表 一模一样

 对于上图的解读:

1.SIGHUP 信号未被阻塞,未产生,一旦产生了该信号,pending 表对应的位置置为 1,当信号递达后,执行动作为默认
2.SIGINT 信号被阻塞,已产生,pending 表中有记录,此时信号处于阻塞状态,无法递达,一旦解除阻塞状态,信号递达后,执行动作为忽略该信号
3.SIGQUIT 信号被阻塞,未产生,即使产生了,也无法递达,除非解除阻塞状态,执行动作为自定义
阻塞 block 与 未决 pending 之间并没很强的关联性,阻塞不过是信号未决的延缓剂

  • 信号在 产生 之前,可以将其 阻塞,信号在 产生 之后(未决),依然可以将其 阻塞

于 handler 表是一个 函数指针表,格式为:返回值为空,参数为 int 的函数

可以看看 默认动作 SIG_DEL 和 忽略动作 SIG_IGN 的定义

/* Type of a signal handler.  */
typedef void (*__sighandler_t) (int);

/* Fake signal functions.  */
#define SIG_ERR	((__sighandler_t) -1)		/* Error return.  */
#define SIG_DFL	((__sighandler_t) 0)		/* Default action.  */
#define SIG_IGN	((__sighandler_t) 1)		/* Ignore signal.  */

 默认动作就是将 0 强转为函数指针类型,忽略动作则是将 1 强转为函数指针类型,分别对应 handler 函数指针数组表中的 01 下标位置;除此之外,还有一个 错误 SIG_ERR 表示执行动作为 出错。

简单对这三张表作一个总结,task_struct 中存在:

1.block 表(位图结构)比特位的位置,表示哪一个信号;比特位的内容代表 是否 对应信号被阻塞
2.pending 表(位图结构)比特位的位置,表示哪一个信号;比特位的内容代表 是否 收到该信号
3.handler 表(函数指针数组)该数组的下标,表示信号编号;数组的特定下标的内容,表示该信号递达后的执行动作

1.4、sigset_t 信号集

无论是 block 表 还是 pending 表,都是一个位图结构,依靠 除、余 完成操作,为了确保不同平台中位图操作的兼容性,将信号操作所需要的 位图 结构封装成了一个结构体类型,其中是一个 无符号长整型数组

/* A `sigset_t' has a bit for each signal.  */

# define _SIGSET_NWORDS	(1024 / (8 * sizeof (unsigned long int)))
typedef struct
  {
    unsigned long int __val[_SIGSET_NWORDS];
  } __sigset_t;

#endif

 注:_SIGSET_NWORDS 大小为 32,所以这是一个可以包含 32 个 无符号长整型 的数组,而每个 无符号长整型 大小为 4 字节,即 32 比特,至多可以使用 1024 个比特位

sigset_t 是信号集,其中既可以表示 block 表信息,也可以表示 pending 表信息,可以通过信号集操作函数进行获取对应的信号集信息;信号集 的主要功能是表示每个信号的 “有效” 或 “无效” 状态 

block 表 通过信号集称为 阻塞信号集或信号屏蔽字(屏蔽表示阻塞),pending 表 通过信号集中称为 未决信号集 

如何根据 sigset_t 位图结构进行比特位的操作? 

  • 假设现在要获取第 127 个比特位
  • 首先定位数组下标(对哪个数组操作)127 / (8 * sizeof (unsigned long int)) = 3
  • 求余获取比特位(对哪个比特位操作):127 % (8 * sizeof (unsigned long int)) = 31 
  • 对比特位进行操作即可
    • 假设待操作对象为 XXX
    • 置 1XXX._val[3] |= (1 << 31)
    • 置 0XXX._val[3] &= (~(1 << 31))

所以可以仅凭 sigset_t 信号集,对 1024 个比特位进行任意操作,关于 位图 结构的实现后续介绍

2、信号集操作函数

对于信号的产生或阻塞,实质上就是对 blockpending 两个表的增、删、改、查操作。

2.1、增删改查

对于位图(bit map)的增删改查操作如下:

  • | 操作,将比特位置为 1。

  • & 操作,将比特位置为 0。

  • :灵活使用 |& 操作。

  • :判断指定比特位是否为 1。

直接操作位图并不推荐,操作系统提供了一些系统接口来对信号集进行操作。

信号集操作函数

#include <signal.h>

int sigemptyset(sigset_t *set);	// 初始化信号集
int sigfillset(sigset_t *set);	// 填充信号集
int sigaddset(sigset_t *set, int signum);	// 增加信号
int sigdelset(sigset_t *set, int signum);	// 删除信号
int sigismember(const sigset_t *set, int signum);	// 查找信号是否在信号集中

这些函数返回值为 0 表示成功,-1 表示失败,并设置错误码。

  • sigemptyset:初始化信号集,使信号集为空。

  • sigfillset:将信号集填满,即将所有信号都添加到集里。

  • sigaddset:将指定的信号加入信号集。

  • sigdelset:将指定的信号从信号集中删除。

  • sigismember:检查指定的信号是否在信号集内。

2.2、sigprocmask 函数

sigprocmask 函数用于对 block 表 进行操作,即控制信号的屏蔽状态。

sigprocmask 函数原型

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
  • how:表示操作的类型,支持三种操作:

    • SIG_BLOCK:将 set 信号集中的信号添加到当前进程的阻塞表(屏蔽信号)。

    • SIG_UNBLOCK:解除 set 信号集中的信号的阻塞状态。

    • SIG_SETMASK:将当前进程的阻塞信号表设置为 set 信号集。

  • set:待操作的信号集,包含待操作的信号。

  • oldset:保存当前进程的屏蔽信号集,操作完成后,可以用来恢复之前的状态。

示例程序 1:将 SIGINT(2号信号)信号阻塞,尝试通过键盘输入 Ctrl+C 发出信号。

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

int main() {
    sigset_t set, oset;

    sigemptyset(&set);
    sigemptyset(&oset);

    sigaddset(&set, SIGINT);	// 阻塞 SIGINT 信号

    sigprocmask(SIG_BLOCK, &set, &oset);  // 设置当前进程的屏蔽信号集

    while (true) {
        cout << "我是一个进程,我正在运行" << endl;
        sleep(1);
    }

    return 0;
}

现象

  • SIGINT 信号被阻塞后,进程无法接收到 Ctrl + C 产生的信号,进程将继续运行。

示例程序 2:在程序运行五秒后,解除阻塞状态。

#include <iostream>
#include <unistd.h>
#include <signal.h>
using namespace std;

int main() {
    sigset_t set, oset;

    sigemptyset(&set);
    sigemptyset(&oset);

    sigaddset(&set, SIGINT);	// 阻塞 SIGINT 信号

    sigprocmask(SIG_BLOCK, &set, &oset);  // 设置当前进程的屏蔽信号集

    int n = 0;
    while (true) {
        if (n == 5) {
            // 解除阻塞,恢复原来的屏蔽信号集
            sigprocmask(SIG_SETMASK, &oset, nullptr);
        }

        cout << "我是一个进程,我正在运行" << endl;
        n++;
        sleep(1);
    }

    return 0;
}

现象

  • SIGINT 信号被阻塞五秒钟后,解除阻塞,信号递达,程序终止。

2.3、sigpending 函数

sigpending 函数用于获取当前进程的 未决信号集,即当前被产生但尚未递达的信号。

sigpending 函数原型

int sigpending(sigset_t *set);
  • 返回值:成功返回 0,失败返回 -1 并设置错误码。

示例程序:展示进程的未决信号集。

#include <iostream>
#include <unistd.h>
#include <signal.h>
#include <cassert>
using namespace std;

static void DisplayPending(const sigset_t pending) {
    cout << "当前进程的 pending 表为: ";
    for (int i = 1; i < 32; i++) {
        if (sigismember(&pending, i))
            cout << "1";
        else
            cout << "0";
    }
    cout << endl;
}

int main() {
    sigset_t set, oset;

    sigemptyset(&set);
    sigemptyset(&oset);

    sigaddset(&set, SIGINT);	// 阻塞 SIGINT 信号

    sigprocmask(SIG_BLOCK, &set, &oset);  // 设置当前进程的屏蔽信号集

    int n = 0;
    while (true) {
        if (n == 5) {
            sigprocmask(SIG_SETMASK, &oset, nullptr);  // 恢复原来的屏蔽信号集
        }

        sigset_t pending;
        sigemptyset(&pending);

        int ret = sigpending(&pending);
        assert(ret == 0);
        
        DisplayPending(pending);

        n++;
        sleep(1);
    }

    return 0;
}

现象

  • 当信号发出并被阻塞时,信号会进入 未决状态。在解除阻塞后,信号会递达并处理。

  • sigpending 函数显示当前进程中哪些信号处于未决状态。

总结: 

  • 信号集操作:通过 sigprocmasksigpending 等函数,可以方便地对进程的信号屏蔽(阻塞)表和未决信号集进行操作。

  • 阻塞和未决状态:信号在阻塞期间无法递达,直到解除阻塞后,信号才会递达并处理。

  • sigpending 函数:提供了获取未决信号集的功能,帮助开发者查看信号是否已被阻塞并未递达。


网站公告

今日签到

点亮在社区的每一天
去签到