前言
进程在收到信号之后,可以立即处理,也可以在合适的时间再处理(1-31
号普通信号可以不被立即处理)
信号不是被立即处理,信号就要被保存下来,让进程在合适的时间再去处理。
相关概念
在了解进程是如何保存信号之前,先了解一下一些概念:
- 信号递达:进程实际执行信号的处理动作
- 信号未决:信号从产生到递达的状态
- 阻塞:进程可以阻塞某一个信号;(被阻塞的信号不会被递达,直到解除对信号的阻塞,该信号才会被递达)
- 忽略:是进程对于已递达的信号的一种处理方式。
信号保存
进程要将信号保存下来,在合适的时候处理;保存信号不仅要保存是否存在信号,也要保存进程是否阻塞信号以及进程对于信号的处理方式。
在Linux
内核中,对于信号存在三张表分别是:block
、pending
、handler
;它们分别存储了进程对于信号的阻塞信号,收到信号以及进程对于信号的处理方式。
每一个信号都有阻塞、未决两个标识位以及处理动作,这分别和block
、pending
和handler
表一一对应。
block
表,表示进程对于信号的阻塞情况;pending
表,表示处于未决的信号情况;handler
表,函数指针表,存储着进程对于信号的处理方式。
对于block
和pending
表,可以简单理解为位图,对于1-31
号信号,每一个信号对应一个bit
位;
block
中bit
位为1
表示进程阻塞该信号,pending
中bit
位为1
表示该信号处于未决状态(进程收到该信号)。
对于handler
表,其存储的是进程对于信号的处理方式。
了解了信号的保存方式
block
、pending
、handler
表;那发送信号的本质就是将进程的
task_struct
中的pending
表对应的bit
位置1
(修改内核数据结构对象);进程处理信号本质就是检查进程
task_struct
中block
表是否阻塞信号、pending
表是否存在信号,然后进行调用handler
表中进程对于信号的处理方法。
信号集sigset_t
信号保存的block
和pending
表,都是以位图的方式来保存信号阻塞和未决的状态;0
或1
。
对于阻塞和未决标识都可以使用sigset_t
(信号集)这一数据类型来存储。
信号集
sigset_t
,可以表示信号有效和无效的状态。在阻塞信号集中,有效和无效表示信号是否被阻塞。
在未决信号集中,有效和无效表示信号是否处于未决状态。
信号集操作函数
对于sigset_t
信号集,我们不清楚里面有什么;不能直接对信号集进程操作,需要用到特定的函数接口。
相关信号集操作函数,例如初始化
sigemptyset
(设置为0
)、sigfillset
(设置为1
)、sigaddset
(新增信号)、sigdelset
(删除信号)等等。
初始化信号集
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
对于初始化信号集,分为两种:一是所有bit
位设置为0
、二是所有bit
位设置为1
。
sigemptyset
,初始化信号集,所有bit
位设置为0
sigfillset
,初始化信号集,所有bit
位设置为1
新增信号
int sigaddset(sigset_t* set, int signum);
在信号集中,新增一个信号signum
;
删除信号
int sigdelsets(sigset_t* set, int signum);
在信号集set
这,删除信号signum
;
对于上述的
sigemptyset
、sigfillet
、sigaddset
和sigdelset
,这些函数返回值:如果函数调用成功,返回
0
;函数调用失败,返回
-1
。
判断信号是否存在
int sigismember(sigset_t* set, int signum);
sigismember
函数用来判断信号集sig
中是否存在信号signum
;
如果存在就返回1
;如果不存在就返回0
;如果函数调用失败就返回-1
。
系统调用
上述信号集操作函数,是修改当前已有信号集sigset_t
;
而在Linux
内核中,存在block
表和pending
表来记录进程对于信号的屏蔽和未决状态;那我们能否修改内核中的block
表呢?能否获取内核中的block
和pending
表呢?
当然是可以的,我们可以通过系统调用sigprocmask
和sigpending
来对内核中进程的阻塞信号集
和未决信号集
进行相关操作。
sigprocmask
调用该函数,可以修改或者读取信号屏蔽字
int sigprocmask(int how, const sigset_t *_Nullable restrict set, sigset_t *_Nullable restrict oldset);
对于sigprocmask
函数,一共存在三个参数:
第一个参数表示要对内核中的
block
(阻塞信号集)进行什么操作;how
的取值如下图:
第二个参数
set
指向当前信号集,我们需要自己创建信号集;第三个参数
oldset
是一个输出型参数,当调用sigprocmask
修改内核阻塞信号集时,会将内核阻塞信号集输出到oldset
指向的信号集中。
测试:这里简单调用setprocmask
为进程添加屏蔽2
号信号
#include <stdio.h>
#include <signal.h>
#include <iostream>
int main()
{
sigset_t set;
sigemptyset(&set);//初始化信号集
sigaddset(&set, 2);//在信号集中添加2号信号
sigset_t oldset;
sigprocmask(SIG_BLOCK, &set, &oldset);
while (true)
{
}
return 0;
}
sigpending
int sigpending(sigset_t *set);
调用sigpending
函数可以获取进程的pending
表(未决信号集)。
在内核中pending
表是记录进程2未决信号的状态的,给进程发信号的本质就是修改内核中进程的pending
表。
在获取了进程的未决信号集之后,我们可以使用信号集相关操作函数来判断该信号集中是否存在未决的信号。
这里简单验证:阻塞的信号是不会被递达的;
void print_pending()
{
// 获取pending
sigset_t set;
sigpending(&set);
//输出位图
for (int i = 31; i >= 1; i--)
{
if (sigismember(&set, i))
std::cout << '1';
else
std::cout << '0';
}
std::cout << std::endl;
}
void handler(int sig)
{
std::cout << "receive sig : " << sig << std::endl;
}
int main()
{
//自定义捕捉2号信号
signal(2, handler);
//屏蔽2号信号
sigset_t set, oldset;
sigemptyset(&set);
sigaddset(&set,2);
sigprocmask(SIG_BLOCK, &set, &oldset);
int cnt = 10;
while(cnt--)
{
//输出pending表
print_pending();
if(cnt == 0)
{
//解除对2号信号的屏蔽
sigemptyset(&set);
sigprocmask(SIG_SETMASK, &oldset,nullptr);
}
sleep(1);
}
return 0;
}
总结
简单总结上述内容:
信号保存:
block
信号屏蔽表、pending
信号未决表、handler
信号处理方式(函数指针表)信号集
sigset_t
,相关操作:初始化、新增、删除、判断是否存在等等系统调用:
sigprocmask
:获取或设置内核blcok
表、sigpending
获取内核pending
表。