🌇前言
信号从产生到执行,并不会被立即处理,这就意味着需要一种 “方式” 记录信号是否产生,对于 31
个普通信号来说,一个 int
整型就足以表示所有普通信号的产生信息了;信号还有可能被 “阻塞”,对于这种多状态、多结果的事物,操作系统会将其进行描述、组织、管理,这一过程称为 信号保存 阶段
足球场上的计分系统,用于记录球队得分信息
🏙️正文
1、再次认识信号
补充 信号传递 的相关概念
1.1、概念
信号 传递过程:信号产生 -> 信号未决 -> 信号递达
- 信号产生(
Produce
):由四种不同的方式发出信号 - 信号未决(
Pending
):信号从 产生 到 执行 的中间状态 - 信号递达(
Delivery
):进程收到信号后,对信号的处理动作
在这三种过程之前,均有可能出现 信号阻塞 的情况
- 信号阻塞(
Block
):使信号传递 “停滞”,无论是否产生,都无法进行处理
信号递达后的三种处理方式:
SIG_DFL
默认处理动作,大多数信号最终都是终止进程SIG_IGN
忽略动作,即进程收到信号后,不做任何处理动作handler
用户自定义的信号执行动作
注意:
- 信号阻塞 是一种手段,可以发生在 信号处理 前的任意时段
- 信号阻塞 与 忽略动作 不一样,虽然二者的效果差不多:什么都不干,但前者是 干不了,后者则是 不干了,需要注意区分
1.2、感性理解
将 信号传递 的过程比作 网上购物
可以抽象出以下概念:
- 信号产生:在某某购物平台上下达了订单
- 信号未决:订单下达后,快递的运输过程
- 信号递达:快递到达驿站后,你对于快递的处理动作
- 信号阻塞:快递运输过程中堵车了
只要你下单了,你的手机上肯定会有 物流信息(未决信息已记录),当 快递送达后(信号递达),物流记录 不再更新
而 堵车 是一件不可预料的事情,也就是说:在下单后,快递可能一会儿送达(没有阻塞),可能五天送达(阻塞 -> 解除阻塞),有可能永不送达,因为快递可能永远堵车(阻塞)
堵车也有可能在你下单前发生(信号产生前阻塞)
至于 信号递达后的处理动作 如何理解呢?
- 快递送达后,正常拆快递(默认动作)
- 快递送达后,啥也不干,就是玩(忽略)
- 快递送达后,直接把快递退回去(用户自定义)
当然,用户自定义的情况可以有很多种,也有可能是直接把快递扔了
综上,网购的整个过程可以看作 信号传递过程,本文探讨的是 信号保存阶段,即 物流信息
1.3、在内核中的表示
对于传递中的信号来说,需要存在三种状态表达:
- 信号是否阻塞
- 信号是否未决
- 信号递达时的执行动作
在内核中,每个进程都需要维护这三张与信号状态有关的表:block
表、pending
表、handler
表
所谓的 block
表 和 pending
表 其实就是 位图结构
一个 整型 int
就可以表示 31
个普通信号(实时信号这里不讨论)
- 比如
1
号信号就是位图中的0
位置处,0
表示 未被阻塞/未产生未决,1
则表示 阻塞/未决 - 对于信号的状态修改,其实就是修改 位图 中对应位置的值(
0/1
) - 对于多次产生的信号,只会记录一次信息(实时信号则会将冗余的信号通过队列组织)
如何记录信号已产生 -> 未决表中对应比特位置置为
1
?
- 假设已经获取到了信号的
pending
表- 只需要进行位运算即可:
pending |= (1 << (signo - 1))
- 其中的
signo
表示信号编号,-1
是因为信号编号从1
开始,需要进行偏移如果想要取消 未决 状态也很简单:
pending &= (~(1 << (signo - 1)))
至于 阻塞
block
表,与pending
表 一模一样
对于上图的解读:
SIGHUP
信号未被阻塞,未产生,一旦产生了该信号,pending
表对应的位置置为1
,当信号递达后,执行动作为默认SIGINT
信号被阻塞,已产生,pending
表中有记录,此时信号处于阻塞状态,无法递达,一旦解除阻塞状态,信号递达后,执行动作为忽略该信号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
函数指针数组表中的 0
、1
下标位置;除此之外,还有一个 错误 SIG_ERR
表示执行动作为 出错
简单对这三张表作一个总结,
task_struct
中存在:
block
表(位图结构)比特位的位置,表示哪一个信号;比特位的内容代表 是否 对应信号被阻塞pending
表(位图结构)比特位的位置,表示哪一个信号;比特位的内容代表 是否 收到该信号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
- 置
1
:XXX._val[3] |= (1 << 31)
- 置
0
:XXX._val[3] &= (~(1 << 31))
所以可以仅凭 sigset_t
信号集,对 1024
个比特位进行任意操作,关于 位图 结构的实现后续介绍
2、信号集操作函数
对于 信号 的 产生或阻塞 其实就是对 block
和 pending
两张表的 增删改查
2.1、增删改查
对于 位图 的 增删改查 是这样操作的:
- 增:
|
操作,将比特位置为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
至于参数,非常简单,无非就是 待操作的信号集变量、待操作的比特位
注意:在创建 信号集 sigset_t
类型后,需要使用 sigemptyset
或 sigfillset
函数进行初始化,确保 信号集 是合法可用的
2.2、sigprocmask
sigprocmask
函数可用用来对 block
表 进行操作
#include <signal.h> int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
返回值:成功返回 0
,失败返回 -1
并将错误码设置
参数1:对 屏蔽信号集 的操作
SIG_BLOCK
希望添加至当前进程block
表 中阻塞信号,从set
信号集中获取,相当于mask |= set
SIG_UNBLOCK
解除阻塞状态,也是从set
信号集中获取,相当于mask &= (~set)
SIG_SETMASK
设置当前进程的block
表为set
信号集中的block
表,相当于mask = set
参数2:就是一个信号集,主要从此信号集中获取屏蔽信号信息
参数3:也是一个信号集,保存进程中原来的 block
表(相当于给你操作后,反悔的机会)
这个函数就是 参数 1 比较有讲究,主打的就是一个 从 set
信号集 中获取阻塞信号相关信息,然后对进程中的 block
表进行操作,并且有三种不同的操作方式
演示程序1:将 2
号信号阻塞,尝试通过 键盘键入 发出 2
信号
#include <iostream> #include <unistd.h> #include <signal.h> using namespace std; int main() { //创建信号集 sigset_t set, oset; //初始化信号集 sigemptyset(&set); sigemptyset(&oset); //阻塞2号信号 sigaddset(&set, 2); //2 号信号被记录 //设置当前进程的 block 表 sigprocmask(SIG_BLOCK, &set, &oset); //死循环 while(true) { cout << "我是一个进程,我正在运行" << endl; sleep(1); } return 0; }
显然,当 2
号信号被阻塞后,是 无法被递达 的,进程也就无法终止了
演示程序2:在程序运行五秒后,解除阻塞状态
#include <iostream> #include <unistd.h> #include <signal.h> using namespace std; int main() { // 创建信号集 sigset_t set, oset; // 初始化信号集 sigemptyset(&set); sigemptyset(&oset); // 阻塞2号信号 sigaddset(&set, 2); //2 号信号被记录 // 设置当前进程的 屏蔽信号集 sigprocmask(SIG_BLOCK, &set, &oset); // 死循环 int n = 0; while (true) { if (n == 5) { // 采用 SIG_SETMASK 的方式,覆盖进程的 block 表 sigprocmask(SIG_SETMASK, &oset, nullptr); // 不接收进程的 block 表 } cout << "我是一个进程,我正在运行" << endl; n++; sleep(1); } return 0; }
现象:在 2
号信号发出、程序运行五秒解除阻塞后,信号才被递达,进程被终止
如何证明信号已递达?
- 当
n == 5
时,解除阻塞状态,程序立马结束 - 并只打印了 五条 语句,证明在第六秒时,程序就被终止了
- 至于如何进一步证明,需要借助 未决信号表
2.3、sigpending
这个函数很简单,获取当前进程中的 未决信号集
#include <signal.h> int sigpending(sigset_t *set);
返回值:成功返回 0
,失败返回 -1
并将错误码设置
参数:待获取的 未决信号集
如何根据 未决信号集 打印
pending
表
- 使用函数
sigismember
判断当前信号集中是否存在该信号,如果存在,输出1
,否则输出0
- 如此重复,将
31
个信号全部判断打印输出即可
所以可以将上面的 演示程序 修改下:
#include <iostream> #include <cassert> #include <unistd.h> #include <signal.h> using namespace std; static void DisplayPending(const sigset_t pending) { //打印 pending 表 cout << "当前进程的 pending 表为: "; int i = 1; while(i < 32) { if(sigismember(&pending, i)) cout << "1"; else cout << "0"; i++; } cout << endl; } int main() { // 创建信号集 sigset_t set, oset; // 初始化信号集 sigemptyset(&set); sigemptyset(&oset); // 阻塞2号信号 sigaddset(&set, 2); //记录 2 号信号 // 设置当前进程的 屏蔽信号集 sigprocmask(SIG_BLOCK, &set, &oset); // 死循环 int n = 0; while (true) { if (n == 5) { // 采用 SIG_SETMASK 的方式,覆盖进程的 block 表 sigprocmask(SIG_SETMASK, &oset, nullptr); // 不接收进程的 block 表 } //获取进程的 未决信号集 sigset_t pending; sigemptyset(&pending); int ret = sigpending(&pending); assert(ret == 0); (void)ret; //欺骗编译器,避免 release 模式中出错 DisplayPending(pending); n++; sleep(1); } return 0; }
结果:当 2
号信号发出后,当前进程的 pending
表中的 2
号信号位被置为 1
,表示该信号属于 未决 状态,并且在五秒之后,阻塞结束,信号递达,进程终止
疑问:当阻塞解除后,信号递达,应该看见 pending
表中对应位置的值由 1
变为 0
,但为什么没有看到?
- 很简单,因为当前
2
号信号的执行动作为终止进程,进程都终止了,当然看不到 - 解决方法:给
2
号信号先注册一个自定义动作(别急着退出进程)
所以改进后的代码如下:
#include <iostream> #include <cassert> #include <unistd.h> #include <signal.h> using namespace std; static void handler(int signo) { cout << signo << " 号信号确实递达了" << endl; //最终不退出进程 } static void DisplayPending(const sigset_t pending) { // 打印 pending 表 cout << "当前进程的 pending 表为: "; int i = 1; while (i < 32) { if (sigismember(&pending, i)) cout << "1"; else cout << "0"; i++; } cout << endl; } int main() { // 更改 2 号信号的执行动作 signal(2, handler); // 创建信号集 sigset_t set, oset; // 初始化信号集 sigemptyset(&set); sigemptyset(&oset); // 阻塞2号信号 sigaddset(&set, 2); //记录 2 号信号 // 设置当前进程的 屏蔽信号集 sigprocmask(SIG_BLOCK, &set, &oset); // 死循环 int n = 0; while (true) { if (n == 5) { // 采用 SIG_SETMASK 的方式,覆盖进程的 block 表 sigprocmask(SIG_SETMASK, &oset, nullptr); // 不接收进程的 block 表 } // 获取进程的 未决信号集 sigset_t pending; sigemptyset(&pending); int ret = sigpending(&pending); assert(ret == 0); (void)ret; // 欺骗编译器,避免 release 模式中出错 DisplayPending(pending); n++; sleep(1); } return 0; }
显然,这就是我们想要的最终结果
先将信号 阻塞,信号发出后,无法 递达,始终属于 未决 状态,当阻塞解除后,信号可以 递达,信号处理之后,未决 表中不再保存信号相关信息,因为已经处理了
综上,信号在发出后,在处理前,都是保存在 未决表 中的
注意:
- 针对信号的 增删改查 都需要通过 系统调用 来完成,不能擅自使用位运算
sigprocmask
、sigpending
这两个函数的参数都是 信号集,前者是 屏蔽信号集,后者是 未决信号集- 在对 信号集 进行增删改查前,一定要先初始化
- 信号在被解除 阻塞状态 后,很快就会 递达 了
- 关于信号何时递达、以及递达后的处理动作,在下一篇文章中揭晓
以上关于 信号、信号集 的操作都是在进程中进行的,不影响操作系统
🌆总结
以上就是本次关于 Linux进程信号【信号保存】的全部内容了,在本文中,我们首先再一次对信号有了较深的理解,知道了在内核中存在三张表记录信号的处理流程,然后我们学习了信号集的操作函数,模拟实现了 阻塞信号 - 产生信号 - 未决信号 - 解除阻塞 - 递达信号 的全过程,最终证明 信号在产生之后是保存在 未决表 中的
相关文章推荐
Linux进程信号 ===== :>
Linux进程间通信 ===== :>
【消息队列、信号量】、【共享内存】、【命名管道】、【匿名管道】
Linux基础IO ===== :>
【软硬链接与动静态库】、【深入理解文件系统】、【模拟实现C语言文件流】、【重定向及缓冲区理解】、【文件理解与操作】
Linux进程控制 ===== :>
Linux进程学习 ===== :>
Linux基础 ===== :>
【gdb】、【git】、【gcc/g++】、【vim】、Linux 权限理解和学习、听说Linux基础指令很多?这里都帮你总结好了