目录
1、再次认识信号
在这篇文章中,我们将进一步补充和理解 信号传递 过程中的一些关键概念。
1.1、概念
信号传递过程:信号从产生到执行的过程可以分为三部分:
信号产生(Produce):信号的发出有四种主要方式:键盘输入、系统调用、软件条件和硬件异常。
信号未决(Pending):信号从产生到执行的中间状态,指信号已产生,但未被立即处理。
信号递达(Delivery):信号递达后,进程会根据信号的处理方式来执行相应的动作。
信号阻塞(Block):指信号在任何阶段的传递过程中,信号的处理被暂停。阻塞的信号无法继续处理,直到解除阻塞。
1.2、感性理解
可以通过网购的过程来比喻信号的传递过程:
信号产生:在购物平台上完成订单(信号的产生)。
信号未决:订单在运输途中(信号的未决状态)。
信号递达:快递送达后,用户根据自己的情况进行处理(信号递达后的处理动作)。
信号阻塞:假设快递运输中发生堵车(信号传递被阻塞)。
在购物中,我们下单后,物流信息(未决信息)会记录,但快递可能因堵车导致延迟(信号的阻塞)。而信号的递达过程就类似快递送达后用户进行处理的动作,比如默认拆快递、忽略或退货等。
1.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
函数指针数组表中的 0
、1
下标位置;除此之外,还有一个 错误 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
- 置
1
:XXX._val[3] |= (1 << 31)
- 置
0
:XXX._val[3] &= (~(1 << 31))
- 假设待操作对象为
所以可以仅凭 sigset_t
信号集,对 1024
个比特位进行任意操作,关于 位图 结构的实现后续介绍
2、信号集操作函数
对于信号的产生或阻塞,实质上就是对 block
和 pending
两个表的增、删、改、查操作。
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
函数显示当前进程中哪些信号处于未决状态。
总结:
信号集操作:通过
sigprocmask
、sigpending
等函数,可以方便地对进程的信号屏蔽(阻塞)表和未决信号集进行操作。阻塞和未决状态:信号在阻塞期间无法递达,直到解除阻塞后,信号才会递达并处理。
sigpending
函数:提供了获取未决信号集的功能,帮助开发者查看信号是否已被阻塞并未递达。