目录
- 一、保存信号
-
- [1.1 信号其他相关常见概念](#1.1 信号其他相关常见概念)
- [1.2 在内核中的表示](#1.2 在内核中的表示)
- [1.3 sigset_t](#1.3 sigset_t)
- [1.4 信号集操作函数](#1.4 信号集操作函数)
-
- [sigprocmask 函数](#sigprocmask 函数)
- sigpending函数
- [1.5 相关函数的使用](#1.5 相关函数的使用)
- 二、捕捉信号
-
- [2.1 信号什么时候处理?怎么处理?](#2.1 信号什么时候处理?怎么处理?)

个人主页:矢望
个人专栏:C++、Linux、C语言、数据结构、Coze-AI、MySQL
一、保存信号
为什么要进行信号保存呢? 信号在收到之后不会立即被处理,信号的产生之后和处理之前是由时间窗口的,所以信号必须被保存起来!
1.1 信号其他相关常见概念
- 信号递达 :实际执行信号的处理动作称为信号递达
(Delivery)。 - 信号未决 :信号从产生到递达之间的状态,称为信号未决
(Pending)。 - 阻塞 :进程可以选择阻塞
(Block)某个信号。被阻塞的信号产⽣时将保持在未决状态,直到进程解除对此信号的阻塞,才执行递达的动作。
注意:阻塞和忽略是不同的,只要信号被阻塞就不会递达,而忽略是在递达之后可选的一种处理动作。
1.2 在内核中的表示
在内核中一共会有三张表,阻塞block表、信号未决pending表、信号递达handler表 。

关于pending表

这个pending表的作用,我们上期博客已经接触过了。
关于block表

关于block表和pending表我们将来应该一起来看,当block表的相关位置为1时,代表信号阻塞,此时信号将永远不会进行处理,一直处于未决状态;当block表为0时,此时pending表为0代表没有相关信号,pending位为1代表存在相关信号,并且该信号可以被递达,也就是可以被进程处理。
在以表格的方式呈现:
blocked位 |
pending位 |
含义 | 是否会被处理? |
|---|---|---|---|
1 |
0 |
阻塞,且没有信号到达 | 不会(也没有信号) |
1 |
1 |
阻塞,且有信号在等待 | 暂时不会,直到blocked位为1 |
0 |
0 |
未阻塞,但无信号 | 没有信号可处理 |
0 |
1 |
未阻塞,有信号待处理 | 立即递达 |
关于handler表


如上两图,handler表你可以理解为sighandler_t类型的指针数组,数组的下标就是信号-1。
如上图,所以signal(2, myhandler);底层会做什么呢? 它会将handler表所在的2号信号的位置,写入myhandler方法。到时候进行信号递达时就执行myhandler方法。

所以什么叫做忽略,什么叫做默认? 当要对指定信号进行忽略或者默认时,就是在handler表中找到相关信号的下标,然后将递达方法替换成上图的SIG_DFL或者SIG_IGN方法,这就叫做对信号进行忽略或者默认。
因此,在信号还没有产生的时候,进程就已经可以识别和处理信号了,因为程序员已经内置了信号的管理和处理的方法!
1.3 sigset_t
OS让用户控制信号的本质就是访问和操作上面的三张表,它们属于内核数据结构,所以想要修改它们,OS就必须提供系统调用。其中signal系统调用就是控制修改handler表的,那么pending和block表呢?
OS提供了一种类型叫做sigset_t,本质上是一个整数类型(通常是 unsigned long 或结构体),用来存储64个信号的存在状态。
每个信号只有一个bit位的未决标志,非0即1,阻塞标志也是这样表示的。因此,未决和阻塞标志可以用相同的数据类型sigset_t来存储,sigset_t称为信号集 ,这个类型可以表示每个信号的"有效"或"无效"状态,在阻塞信号集中"有效"和"无效"的含义是该信号是否被阻塞,而在未决信号集中"有效"和"无效"的含义是该信号是否处于未决状态。阻塞信号集也叫做当前进程的信号屏蔽字,这里的"屏蔽"应该理解为阻塞而不是忽略。
因此pending和block表通过操作sigset_t来控制修改。
1.4 信号集操作函数
sigprocmask 函数
sigprocmask 是用于读取和修改进程的阻塞信号集(block表)的系统调用。它是控制信号阻塞的核心接口 。

返回值成功返回0,失败返回-1。
函数的第一个参数常用的有以下取值:

SIG_BLOCK表示新增信号集中的内容,SIG_UNBLOCK表示解除信号集中的内容,SIG_SETMASK表示覆盖式修改信号集。
第二个参数是输入型参数,表示输入的信号集,第三个参数表示输出型参数,表示上一个信号集是什么样子。
sigpending函数
sigpending 函数用于获取当前进程的未决信号集(pending表)。表明哪些信号已经产生,但因为被阻塞而仍在等待中 。

这个函数很简单,只是获取pending表中的内容。
那么就有人问了,那谁来修改pending表呢? 我们之前已经修改过pending表了,我们之前使用的kill命令就是在修改pending表,信号产生的几种方式都是在通过OS来修改pending表!
操作sigset_t的函数 :

| 函数名 | 作用 |
|---|---|
sigemptyset |
将信号集初始化为空集,即不包含任何信号。 |
sigfillset |
将信号集初始化为满集,即包含所有可阻塞的信号。 |
sigaddset |
向信号集中添加一个指定的信号。 |
sigdelset |
从信号集中删除一个指定的信号。 |
sigismember |
测试一个指定的信号是否在信号集中(是成员返回 1,否则返回 0)。 |
注意:不能直接使用位运算(如 set |= (1 << signo))来操作信号集。
1.5 相关函数的使用
接下来,我要对2号信号进行屏蔽,这样,当2号信号到达的时候,pending位确实会变成1,但它不会递达。所以我们就应该看到pending表中2号位是1。
相关代码:
cpp
void PrintPending(sigset_t &pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo)) // 判断 signo 信号是否未决
{
std::cout << "1";
}
else std::cout << "0";
}
std::cout << std::endl;
}
int main()
{
// 屏蔽2号信号
sigset_t block_set, old_set;
// 初始化信号集
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, 2); // 设置2号信号位为1
int n = sigprocmask(SIG_SETMASK, &block_set, &old_set); // 覆盖式屏蔽2号信号
(void)n;
std::cout << "进程正在运行, pid: " << getpid() << std::endl;
while(true)
{
// 获取 pending 表
sigset_t pending;
n = sigpending(&pending);
(void)n;
// 打印 pending 表
PrintPending(pending);
sleep(1);
}
return 0;
}
在运行时,我们预期看到,进程收到2号信号之后,信号没有被递达,而是一直处于未决状态。
编译运行:

如上图,我们看到了预期的现象,2号信号被屏蔽之后,确实再次收到时无法递达了,此时2号位一直是1。
注意:9号信号和19号信号依旧无法屏蔽。
那么接下来,如果我们隔一段时间把2号位的屏蔽取消掉呢?我们应该看到2号位被递达,我们再次修改代码,在while循环逻辑中加上一个if条件即可:
cpp
int cnt = 0;
while(true)
{
// 获取 pending 表
sigset_t pending;
n = sigpending(&pending);
(void)n;
// 打印 pending 表
PrintPending(pending);
if(cnt == 20)
{
std::cout << "解除对2号信号屏蔽!" << std::endl;
n = sigprocmask(SIG_SETMASK, &old_set, nullptr); // 更新为之前的信号集
(void)n;
}
cnt++;
sleep(1);
}
编译运行:

如上图,我们在前20s向进程发送了2号信号,由于它是被屏蔽的,所以信号并没有递达,但是在20s之后我们解除了对2号信号的屏蔽,此时2号信号立即被递达处理,之后进程就立刻终止了。
所以我们一旦解除对某一个信号的屏蔽,该信号就会立即被递达。
还有一个问题,我们知道了解除某一个信号的屏蔽,信号会立即递达,那么pending位就会由1变0,那么这项工作它是在处理信号之前恢复为0,还是,处理信号之后恢复为0的呢?
为了解决这个问题,我们可以将2号信号进行自定义捕获,然后我们可以在自己的handler方法中,再次获取pending表,打印出来就可以解决了。
cpp
void PrintPending(sigset_t &pending)
{
for(int signo = 31; signo > 0; signo--)
{
if(sigismember(&pending, signo)) // 判断 signo 信号是否未决
{
std::cout << "1";
}
else std::cout << "0";
}
std::cout << std::endl;
}
void handler(int signo)
{
std::cout << "--------------------------------------enter handler" << std::endl;
sigset_t pending;
int n = sigpending(&pending);
(void)n;
PrintPending(pending);
std::cout << "处理完成, signo: " << signo << std::endl;
std::cout << "--------------------------------------leave handler" << std::endl;
}
int main()
{
// 捕捉 2 号信号
signal(2, handler);
// 屏蔽2号信号
sigset_t block_set, old_set;
// 初始化信号集
sigemptyset(&block_set);
sigemptyset(&old_set);
sigaddset(&block_set, 2); // 设置2号信号位为1
int n = sigprocmask(SIG_SETMASK, &block_set, &old_set); // 覆盖式屏蔽2号信号
(void)n;
std::cout << "进程正在运行, pid: " << getpid() << std::endl;
int cnt = 0;
while(true)
{
// 获取 pending 表
sigset_t pending;
n = sigpending(&pending);
(void)n;
// 打印 pending 表
PrintPending(pending);
if(cnt == 20)
{
std::cout << "解除对2号信号屏蔽!" << std::endl;
n = sigprocmask(SIG_SETMASK, &old_set, nullptr); // 更新为之前的信号集
(void)n;
}
cnt++;
sleep(1);
}
return 0;
}
编译运行:

如上图,我们发现,在handler方法中,pending位就已经恢复成0了,所以解除屏蔽之后pending位由1变0这项工作它是在处理信号之前执行的。
二、捕捉信号
2.1 信号什么时候处理?怎么处理?
之前我们说信号到来的时候,进程不会立即对信号进行处理,它会在合适的时候对信号进行处理。
那么到底是什么时候呢? 当进程调度的时候,从内核态返回用户态的时候,此时就会进行信号的检测和处理!

用户态 :进程执行代码,访问数据都在访问上图中的[0, 3]GB地址空间时,就是在访问自己的数据,自己的代码,此时就是用户态 。
内核态 :进程执行代码,访问数据在访问[3, 4]GB地址空间时,就是访问OS的过程,此时就是内核态。
内核态的权限级别更高。

如上图所示,横线上面是用户态,横线下面是内核态。
当进程在执行的时候,会发生多次身份的转变,有时候是用户态身份,有时候是内核态身份。进程在执行过程中会由于中断、异常、系统调用等原因而进入内核,此时进程在内核处理完毕准备返回用户态的时候就会进行信号的检测与处理,它会检查pending表,如果信号未决了,并且没有被屏蔽,内核就会处理这个信号,如果信号的处理动作是SIG_IGN或者SIG_DFL,进程就会直接在内核态处理完成信号,但如果信号被自定义捕捉了,进程就必须到用户态进行信号的处理。这里如果非要让内核态进行处理也是可以的,因为内核态的权限更高嘛,但是处理自定义捕捉方法不能让内核态来做,因为如果这个方法中不正当的方法比如execl("rm", "/")时,此时就会危害到OS,所以就必须让用户态身份处理自定义捕捉方法。
信号自定义捕捉的流程,使用以下的一张图就可以搞定:

一共有4次用户态和内核态之间的相互转换。
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~