👦个人主页:Weraphael
✍🏻作者简介:目前正在学习c++和算法
✈️专栏:Linux
🐋 希望大家多多支持,咱一起进步!😁
如果文章有啥瑕疵,希望大佬指点一二
如果文章对你有帮助的话
欢迎 评论💬 点赞👍🏻 收藏 📂 加关注😍
目录
一、再次认识信号
1.1 为什么要进行信号保存
在入门篇提到过:进程收到信号之后,可能不会立即被处理,因为进程可能正在做重要的事,需要等到合适的时间再处理。就比方说:你收到外卖小哥的外卖到了的提醒消息(进程收到信号),但你正在打LOL
(更重要的事),晚点下去拿(保存信号),当打完之后再下去(处理信号)。因此,从信号产生到信号处理这段期间,需要将信号保存起来,那如何保存呢?位图!(具体看1.2
)
- 对于普通信号,它用的是位图,只要收到就会先保存,但是如果这个信号还没处理,又来个信号,那么就只记得最近一次的信号。
- 而对于实时信号。只要发送了,就要立即处理,哪怕此时进程在忙。它的用的是队列。
- 关于进程信号,我们重点关心普通信号即可。
1.2 信号如何被保存
对应的1~31
号信号我们称为普通信号,是不是一个 int
整型(32bit
) 就足以表示所有普通信号的产生信息了。
对于普通的信号处理而言,进程主要关心自己是否有信号以及收到了哪个具体的信号。并且,这个信号是由操作系统发送给进程的进程控制块(task_struct
)。所以结构体task_struct
内一定维护类似于int signal
字段。
如果给进程发的是一号信号,那么则将bit
位的第一位给置为1
(注意这里有第0
位,表示没有收到信号),后面以此类推。所以描述一个信号,用比特位的位置来表示,即 普通信号是用位图来管理信号。
总结:
- 比特位的内容是
0
还是1
,表明是否收到信号。- 比特位的位置(第几个),表示信号的编号。
- 所谓的“发信号”,本质就是操作系统(管理者)去修改
task_struct
的信号位图对应的比特位。也就是写信号。
说明:在后面我们会说,信号是被保存在pending
表中的。
1.3 信号其他相关常见概念
- 信号产生(
Produce
):由四种不同的方式发出信号。(详情见) - 信号未决(
Pending
):信号从产生到处理的中间状态。(信号保存) - 信号递达(
Delivery
):进程收到信号后,对信号的处理动作。(信号处理)
除此之外,进程还允许阻塞(屏蔽)某些信号,我们称之为 信号阻塞。这意味着进程暂时不接收阻塞的信号,使其保持在未决信号集合中,只有在解除阻塞后,信号才会被递送到信号处理程序中。注意:信号阻塞是一种手段,可以发生在 信号处理 前的任意时段。
注意区分阻塞和忽略(信号处理动作):忽略是真的什么都不做,相当于“已读不回”;而阻塞是信号还没到处理阶段,它可能会被处理,可能不会被处理,相当于“未读”,是一种状态。
二、在内核中的表示
在操作系统中,有三张表分别是block
表、pending
表、handler
表。共同构成了操作系统内核管理信号的机制。
block
表:也称信号阻塞表(位图)。它主要用于记录信号有没有被阻塞。如果某信号(比特位的位置)被设置成1
,表示信号被阻塞;如果某信号被设置成0
,表示信号没有被阻塞。pending
表:也称未决信号表(位图)。它主要用于记录已经向进程发送但尚未被处理的信号(信号保存)。如果信号(比特位的位置)对应的比特位是1
,则表示该信号是pending
的,即信号产生但还未被处理。当进程的信号处理函数还没有准备好处理信号时,信号会保持在pending
表中。一旦信号的处理函数准备好,内核会从pending
表中选择一个信号交付给进程。注意:如果这个信号还没处理,又来个信号,那么就只记得最近一次的信号。handler
表:也称信号处理程序表(函数指针数组)。该表存储着信号[1,31]
的系统默认处理动作的函数指针(函数地址);如果用户自定设定了方法(如singal
函数自定义处理方式),则会将该方法的地址填入到信号处理程序表中。当进程接收到一个信号时,内核会查找该信号对应的处理函数,并执行该处理函数来响应信号事件。- 信号它的一切操作都是围绕这三张表!!!
处理信号有三种方式:忽略SIG_IGN
、系统默认动作SIG_DEL
、用户自定义。用户自定义在【信号产生】已经用很多次了,这次来见见忽略和系统默认动作。
Linux
操作系统对于忽略和默认动作的定义
默认动作就是将0
强转为函数指针类型,忽略动作则是将1
强转为函数指针类型,分别对应handler
表中的0
、1
下标位置。
我们可以使用代码来看看效果
- 忽略动作
SIG_IGN
【程序结果】
- 系统默认动作
SIG_DEL
【程序结果】
三、操作系统中位图的数据类型
3.1 sigset_t — 信号集类型
无论是block
表还是pending
表,它们都是位图结构,同时也是内核的数据结构。由于操作系统不相信任何用户,它不允许用户直接修改这两张表。所以要修改这些表,操作系统一定提供了一系列的系统调用接口。
在内核中,操作系统将信号操作所需要的位图结构封装成了一个结构体类型__sigset_t
,在用户层,我们可以直接使用sigset_t
类型。
sigset_t
称为信号集类型,这个类型可以表示每个信号的有
或无
状态。
- 在阻塞信号集
block
表中有
和无
的含义是该信号是否被阻塞。 - 而在未决信号集
pending
表中有
和无
的含义是该信号是否处于未决状态。
至于这个类型内部如何存储这些比特位则依赖于系统实现,从使用者的角度是不必关心的,使用者只能调用以下函数来操作sigset_ t
变量,而不应该对它的内部数据做任何解释,比如直接打印sigset_t
变量是没有意义的!
3.2 信号集操作函数
#include <signal.h>
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset (sigset_t *set, int signo);
int sigdelset(sigset_t *set, int signo);
int sigismember(const sigset_t *set, int signo);
sigemptyset
函数:初始化set
所指向的信号集,将所有信号的对应比特位清零,表示该信号集不包含任何有效信号。sigfillset
函数:初始化set
所指向的信号集,将所有信号的对应bit
设置成1
,表示该信号集的有效信号包括系统支持的所有信号。sigaddset
函数:向指定的信号集中添加特定的信号,设置1
。sigdelset
函数,向指定的信号集中去掉特定的信号,设置0
。- 以上四个函数都是成功返回
0
,出错返回-1
。 sigismember
函数:判断一个信号集的有效信号中是否包含某种信号。若包含则返回1
,不包含则返回0
。
注意:在使用sigset_ t
类型的变量之前,一定要调用sigemptyset
或sigfillset
做初始化,使信号集处于确定的状态。初始化sigset_t
变量之后就可以在调用sigaddset
和sigdelset
在该信号集中添加或删除某种有效信号。
四、系统调用接口 — sigprocmask
sigprocmask
函数用来对block
表进行操作(阻塞信号)。函数原型如下:
#include <signal>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
参数解释:
-
how
参数:指定操作的类型,可以是以下值之一:SIG_BLOCK
:将set
中的信号添加到当前进程的block
表中(mask|=set
)。SIG_UNBLOCK
:从当前进程的block
表中移除set
中的信号(mask&=~set
)。SIG_SETMASK
:设置当前进程的block
表为set
中的值(mask==set
)。
-
set
参数:就是一个信号集,主要从此信号集中获取屏蔽信号信息 -
oldset
参数:指向sigset_t
类型的指针,用于存储之前的block
表。如果不需要获取旧的block
表,可以将oldset
设为nullptr
。 -
返回值:若成功则为
0
,若出错则为-1
。
五、系统调用接口 — sigpending
sigpending
函数用来获取当前进程中的未决信号集pengding
表,为了做检查。函数原型如下:
#include <signal.h>
int sigpending(sigset_t *set);
参数说明:
- 参数:待获取的未决信号集
- 返回值:成功返回
0
,失败返回-1
并将错误码设置
如何根据打印
pending
表
- 使用函数
sigismember
判断当前信号集中是否存在该信号,如果存在,输出1
,否则输出0
- 如此重复,将
31
个信号全部判断打印输出即可
【代码】
代码逻辑:循环打印pending
表,阻塞2
号信号。若进程没有收到2
号信号,那么位图一定是全0
;当进程收到2
号信号时,位图的低2
位比特位由0
变1
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
{
// 在用户层定义sigset_t变量
sigset_t sset;
// 初始化信号集,将所有信号的对应比特位清零
sigemptyset(&sset);
// 将信号集sset添加特定的信号,设置1
sigaddset(&sset, 2);
// sigprocmask函数用来对block表进行操作(阻塞信号)
sigset_t oldset;
sigemptyset(&oldset);
sigprocmask(SIG_SETMASK, &sset, &oldset);
// 重复打印当前进程的pending表。
// 虽然我们阻塞了2号信号,但是只有没产生信号,pending表一定是全0
sigset_t pending_t; // 获取pending表
while (true)
{
// sigpending函数用来获取当前进程中的未决信号集pending表
int n = sigpending(&pending_t);
if (n < 0) continue;
// 打印
// 判断当前信号集中是否存在该信号,如果存在,输出1,否则输出0
for (int i = 31; i >= 1; i--)
{
if (sigismember(&pending_t, i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl << endl;
sleep(1);
}
return 0;
}
【程序结果】
我们再看下面的代码,这段代码在上面的基础上加了解除2
号信号阻塞,那么当收到2
号信号后,由于解除了阻塞,对应的比特位由1
变为0
。
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
{
// 在用户层定义sigset_t变量
sigset_t sset;
// 初始化信号集,将所有信号的对应比特位清零
sigemptyset(&sset);
// 将信号集sset添加特定的信号,设置1
sigaddset(&sset, 2);
// sigprocmask函数用来对block表进行操作(阻塞信号)
sigset_t oldset;
sigemptyset(&oldset);
sigprocmask(SIG_SETMASK, &sset, &oldset);
// 重复打印当前进程的pending表。
// 虽然我们阻塞了2号信号,但是只有没产生信号,pending表一定是全0
sigset_t pending_t; // 获取pending表
int cnt = 0;
while (true)
{
// sigpending函数用来获取当前进程中的未决信号集pending表
int n = sigpending(&pending_t);
if (n < 0) continue;
// 打印
// 判断当前信号集中是否存在该信号,如果存在,输出1,否则输出0
for (int i = 31; i >= 1; i--)
{
if (sigismember(&pending_t, i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl << endl;
sleep(1);
// 解除阻塞
cnt++;
if (cnt == 6)
{
cout << "已解除2号信号阻塞" << endl;
sigprocmask(SIG_SETMASK, &oldset, nullptr);
}
}
return 0;
}
【程序结果】
通过以上结果:当 2
号信号产生后,当前进程的 pending
表中的 2
号信号位被置为 1
,表示该信号属于未决状态,并且在六秒之后,阻塞结束,信号递达,进程终止。
哎?奇怪?当阻塞解除后,信号递达,应该看见 pending
表中对应位置的值由 1
变为 0
,但为什么没有看到?
这是因为当阻塞解除后信号递达,而2
号信号的默认执行动作为终止进程,进程都终止了,当然看不到。
解决方法:自定义2
号信号的处理动作动作(别急着退出进程)
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
void handler(int signum)
{
cout << "解除" << signum << "号信号的阻塞 " << endl;
// 最终不退出进程 没加exit
}
int main()
{
// 捕捉2号信号
signal(2, handler);
// 在用户层定义sigset_t变量
sigset_t sset;
// 初始化信号集,将所有信号的对应比特位清零
sigemptyset(&sset);
// 将信号集sset添加特定的信号,设置1
sigaddset(&sset, 2);
// sigprocmask函数用来对block表进行操作(阻塞信号)
sigset_t oldset;
sigemptyset(&oldset);
sigprocmask(SIG_SETMASK, &sset, &oldset);
// 重复打印当前进程的pending表。
// 虽然我们阻塞了2号信号,但是只有没产生信号,pending表一定是全0
sigset_t pending_t; // 获取pending表
int cnt = 0;
while (true)
{
// sigpending函数用来获取当前进程中的未决信号集pending表
int n = sigpending(&pending_t);
if (n < 0)
continue;
// 打印
// 判断当前信号集中是否存在该信号,如果存在,输出1,否则输出0
for (int i = 31; i >= 1; i--)
{
if (sigismember(&pending_t, i))
{
cout << "1";
}
else
{
cout << "0";
}
}
cout << endl
<< endl;
sleep(1);
// 解除阻塞
cnt++;
if (cnt == 4)
{
sigprocmask(SIG_SETMASK, &oldset, nullptr);
}
}
return 0;
}
【程序结果】
那如果将所有的信号全部屏蔽掉,那是不是信号就不会被递达(处理)了?
我们能想到的,操作系统的设计者肯定考虑到了,肯定有一些信号是无法被屏蔽的。
9
号和19
号不可被屏蔽,也不可被捕捉。我们可以来验证
#include <iostream>
#include <signal.h>
#include <unistd.h>
using namespace std;
int main()
{
cout << "my pid is " << getpid() << endl;
sleep(3);
sigset_t sset, oldset;
sigemptyset(&sset);
sigemptyset(&oldset);
for (int i = 1; i <= 31; i++)
{
sigaddset(&sset, i);
}
// 将所有的信号屏蔽
sigprocmask(SIG_SETMASK, &sset, &oldset);
sigset_t pending_t;
while (true)
{
int n = sigpending(&pending_t);
if (n < 0)
continue;
for (int signo = 31; signo >= 1; signo--)
{
cout << sigismember(&pending_t, signo);
}
cout << endl
<< endl;
sleep(1);
}
return 0;
}
代码如上,大家可以通过kill -num <pid>
命令自行去试。
六、总结
七、相关代码
本篇博客的相关代码:点击跳转