目录
前言:
1、再次认识信号
1.1、概念
1.2、感性理解
1.3、在内核中的表示
1.4、sigset_t 信号集
2、信号集操作函数
2.1、增删改查
2.2、sigprocmask
2.3、sigpending
3.信号的处理机制
3.1处理情况
3.2合适时机
4用户态与内核态
4.1、概念
4.2、重谈进程地址空间
4.3、信号的处理过程
5.信号捕捉
5.1、内核如何实现信号的捕捉?
5.2sigaction
6 小结
前言:
信号从产生到执行,并不会被立即处理,这就意味着需要一种 “方式” 记录信号是否产生,对于 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 这两个函数的参数都是 信号集,前者是 屏蔽信号集,后者是 未决信号集
在对 信号集 进行增删改查前,一定要先初始化
信号在被解除 阻塞状态 后,很快就会 递达 了
关于信号何时递达、以及递达后的处理动作,在下一篇文章中揭晓
以上关于 信号、信号集 的操作都是在进程中进行的,不影响操作系统
3.信号的处理机制
3.1处理情况
普通情况
所谓的普通情况就是指 信号没有被阻塞,直接产生,记录未决信息后,再进行处理
在这种情况下,信号是不会被立即递达的,也就无法立即处理,需要等待合适的时机
特殊情况
当信号被 阻塞 后,信号 产生 时,记录未决信息,此时信号被阻塞了,也不会进行处理
当阻塞解除后,信号会被立即递达,此时信号会被立即处理
特殊情况 很好理解,就好比往气球里吹气,当气球炸了,空气会被立即释放,因为空气是被气球 阻塞 的,当气球炸了之后(阻塞 解除),空气立马往外跑,这不就是 立即递达、立即处理 吗?
3.2合适时机
信号的产生是 异步 的
也就是说,信号可能随时产生,当信号产生时,进程可能在处理更重要的事,此时贸然处理信号显然不够明智
比如进程正在执行一个重要的
IO
,突然一个终止信号发出,IO
立即终止,对进程、磁盘都不好
因此信号在 产生 后,需要等进程将 更重要 的事忙完后(合适的时机),才进行 处理
合适的时机:进程从 内核态 返回 用户态 时,会在操作系统的指导下,对信号进行检测及处理
至于处理动作,分为:默认动作、忽略、用户自定义
搞清楚 “合适” 的时机 后,接下来需要学习 用户态 和 内核态 相关知识
4用户态与内核态
对于 用户态、内核态 的理解及引出的 进程地址空间 和 信号处理过程 相关知识是本文的重难点
4.1、概念
先来看看什么是 用户态和内核态
用户态:执行用户所写的代码时,就属于 用户态
内核态:执行操作系统的代码时,就属于 内核态
自己写的代码被执行很好理解,操作系统的代码是什么?
- 操作系统也是由大量代码构成的
- 在对进程进行调度、执行系统调用、异常、中断、陷阱等,都需要借助操作系统之手
- 此时执行的就是操作系统的代码
也就是说,用户态 与 内核态 是两种不同的状态,必然存在相互转换的情况
用户态 切换为 内核态:
当进程时间片到了之后,进行进程切换动作
调用系统调用接口,比如 open、close、read、write 等
产生异常、中断、陷阱等
内核态 切换为 用户态:
进程切换完毕后,运行相应的进程
系统调用结束后
异常、中断、陷阱等处理完毕
信号的处理时机就是 内核态 切换为 用户态,也就是 当把更重要的事做完后,进程才会在操作系统的指导下,对信号进行检测、处理
下面来结合 进程地址空间 深入理解 操作系统的代码 及 状态切换 的相关内容(拓展知识)
4.2、重谈进程地址空间
首先简单回顾下 进程地址空间 的相关知识:
进程地址空间 是虚拟的,依靠 页表+MMU机制 与真实的地址空间建立映射关系
每个进程都有自己的 进程地址空间,不同 进程地址空间 中地址可能冲突,但实际上地址是独立的
进程地址空间 可以让进程以统一的视角看待自己的代码和数据
不难发现,在 进程地址空间 中,存在 1 GB
的 内核空间,每个进程都有,而这 1 GB
的空间中存储的就是 操作系统 相关 代码 和 数据,并且这块区域采用 内核级页表 与 真实地址空间 进行映射
为什么要区分 用户态 与 内核态 ?
- 内核空间中存储的可是操作系统的代码和数据,权限非常高,绝不允许随便一个进程对其造成影响
- 区域的合理划分也是为了更好的进行管理
所谓的 执行操作系统的代码及系统调用,就是在使用这 1 GB
的内核空间
进程间具有独立性,比如存在用户空间中的代码和数据是不同的,难道多个进程需要存储多份 操作系统的代码和数据 吗?
当然不用,内核空间比较特殊,所有进程最终映射的都是同一块区域,也就是说,进程只是将 操作系统代码和数据 映射入自己的 进程地址空间 而已
而 内核级页表 不同于 用户级页表,专注于对 操作系统代码和数据 进行映射,是很特殊的
当我们执行诸如 open 这类的 系统调用 时,会跑到 内核空间 中调用对应的函数
而 跑到内核空间 就是 用户态 切换为 内核态 了(用户空间切换至内核空间)
这个 跑到 是如何实现的呢?
在 CPU 中,存在一个 CR3 寄存器,这个 寄存器 的作用就是用来表征当前处于 用户态 还是 内核态
当寄存器中的值为 3 时:表示正在执行用户的代码,也就是处于 用户态
当寄存器中的值为 0 时:表示正在执行操作系统的代码,也就是处于 内核态
通过一个 寄存器,表征当前所处的 状态,修改其中的 值,就可以表示不同的 状态,这是很聪明的做法
重谈 进程地址空间 后,得到以下结论
所有进程的用户空间 [0, 3] GB 是不一样的,并且每个进程都要有自己的 用户级页表 进行不同的映射
所有进程的内核空间 [3, 4] GB 是一样的,每个进程都可以看到同一张内核级页表,从而进行统一的映射,看到同一个 操作系统
操作系统运行 的本质其实就是在该进程的 内核空间内运行的(最终映射的都是同一块区域)
系统调用 的本质其实就是在调用库中对应的方法后,通过内核空间中的地址进行跳转调用
那么进程又是如何被调度的呢?
操作系统的本质
- 操作系统也是软件啊,并且是一个死循环式等待指令的软件
- 存在一个硬件:操作系统时钟硬件,每隔一段时间向操作系统发送时钟中断
进程被调度,就意味着它的时间片到了,操作系统会通过时钟中断,检测到是哪一个进程的时间片到了,然后通过系统调用函数 schedule() 保存进程的上下文数据,然后选择合适的进程去运行
4.3、信号的处理过程
当在 内核态 完成某种任务后,需要切回 用户态,此时就可以对信号进行 检测 并 处理 了
情况1:信号被阻塞,信号产生/未产生
信号都被阻塞了,也就不需要处理信号,此时不用管,直接切回 用户态 就行了
下面的情况都是基于 信号未被阻塞 且 信号已产生 的前提
情况2:当前信号的执行动作为 默认
大多数信号的默认执行动作都是 终止 进程,此时只需要把对应的进程干掉,然后切回 用户态 就行了
情况3:当前信号的执行动作为 忽略
当信号执行动作为 忽略 时,不做出任何动作,直接返回 用户态
情况4:当前信号的执行动作为 用户自定义
这种情况就比较麻烦了,用户自定义的动作位于 用户态 中,也就是说,需要先切回 用户态,把动作完成了,重新坠入 内核态,最后才能带着进程的上下文相关数据,返回 用户态
在 内核态 中,也可以直接执行 自定义动作,为什么还要切回 用户态 执行自定义动作?
因为在 内核态 可以访问操作系统的代码和数据,自定义动作 可能干出危害操作系统的事
在 用户态 中可以减少影响,并且可以做到溯源
为什么不在执行完 自定义动作 直接后返回进程?因为 自定义动作 和 待返回的进程 属于不同的堆栈,是无法返回的
并且进程的上下文数据还在内核态中,所以需要先坠入内核态,才能正确返回用户态
注意: 用户自定义的动作,需要先切换至 用户态 中执行,执行结束后,还需要坠入 内核态
通过一张图快速记录信号的 处理 过程
5.信号捕捉
接下来谈谈 信号 是如何被 捕捉 的
5.1、内核如何实现信号的捕捉?
如果信号的执行动作为 用户自定义动作,当信号 递达 时调用 用户自定义动作,这一动作称为 信号捕捉
用户自定义动作 是位于 用户空间 中的
当 内核态 中任务完成,准备返回 用户态 时,检测到信号 递达,并且此时为 用户自定义动作,需要先切入 用户态 ,完成 用户自定义动作 的执行;因为 用户自定义动作 和 待返回的函数 属于不同的 堆栈 空间,它们之间也不存在 调用与被调用 的关系,是两个 独立的执行流,需要先坠入 内核态 (通过 sigreturn() 坠入),再返回 用户态 (通过 sys_sigreturn() 返回)
上述过程可以总结为下图
5.2sigaction
sigaction
也可以 用户自定义动作,比 signal
功能更丰富
#include <signal.h>
int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);
struct sigaction
{
void (*sa_handler)(int); //自定义动作
void (*sa_sigaction)(int, siginfo_t *, void *); //实时信号相关,不用管
sigset_t sa_mask; //待屏蔽的信号集
int sa_flags; //一些选项,一般设为 0
void (*sa_restorer)(void); //实时信号相关,不用管
};
返回值:成功返回 0
,失败返回 -1
并将错误码设置
参数1:待操作的信号
参数2:sigaction
结构体,具体成员如上所示
参数3:保存修改前进程的 sigaction
结构体信息
这个函数的主要看点是 sigaction
结构体
struct sigaction
{
void (*sa_handler)(int); //自定义动作
void (*sa_sigaction)(int, siginfo_t *, void *); //实时信号相关,不用管
sigset_t sa_mask; //待屏蔽的信号集
int sa_flags; //一些选项,一般设为 0
void (*sa_restorer)(void); //实时信号相关,不用管
};
其中部分字段不需要管,因为那些是与 实时信号 相关的,我们这里不讨论
重点可以看看 sa_mask 字段
sa_mask:当信号在执行 用户自定义动作 时,可以将部分信号进行屏蔽,直到 用户自定义动作 执行完成
也就是说,我们可以提前设置一批 待阻塞 的 屏蔽信号集,当执行 signum 中的 用户自定义动作 时,这些 屏蔽信号集 中的 信号 将会被 屏蔽(避免干扰 用户自定义动作 的执行),直到 用户自定义动作 执行完成
可以简单用一下 sigaction
函数
#include <iostream>
#include <cassert>
#include <cstring>
#include <signal.h>
#include <unistd.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;
}
static void handler(int signo)
{
cout << signo << " 号信号确实递达了" << endl;
// 最终不退出进程
int n = 10;
while (n--)
{
// 获取进程的 未决信号集
sigset_t pending;
sigemptyset(&pending);
int ret = sigpending(&pending);
assert(ret == 0);
(void)ret; // 欺骗编译器,避免 release 模式中出错
DisplayPending(pending);
sleep(1);
}
}
int main()
{
cout << "当前进程: " << getpid() << endl;
//使用 sigaction 函数
struct sigaction act, oldact;
//初始化结构体
memset(&act, 0, sizeof(act));
memset(&oldact, 0, sizeof(oldact));
//初始化 自定义动作
act.sa_handler = handler;
//初始化 屏蔽信号集
sigaddset(&act.sa_mask, 3);
sigaddset(&act.sa_mask, 4);
sigaddset(&act.sa_mask, 5);
//给 2号 信号注册自定义动作
sigaction(2, &act, &oldact);
// 死循环
while (true);
return 0;
}
当 2
号信号的循环结束(10
秒),3、4、5
信号的 阻塞 状态解除,立即被 递达,进程就被干掉了
注意: 屏蔽信号集 sa_mask
中已屏蔽的信号,在 用户自定义动作 执行完成后,会自动解除 阻塞 状态
6 小结
截至目前,信号 处理的所有过程已经全部学习完毕了
信号产生阶段:有四种产生方式,包括 键盘键入、系统调用、软件条件、硬件异常
信号保存阶段:内核中存在三张表,blcok 表、pending 表以及 handler 表,信号在产生之后,存储在 pending 表中
信号处理阶段:信号在 内核态 切换回 用户态 时,才会被处理