?引言
?阻塞信号
?信号保存
?信号捕捉
?操作信号集
1.信号集操作函数
2.其它操作函数
?总结:
在观看本博客之前,建议大家先看一文搞懂Linux信号【上】。由于上一篇博客篇幅太长,为了更好的阅读体验,我拆成了两篇博客。那么接下来,在上一篇的基础上,我们继续学习Linux信号部分。本篇我们主要谈论信号保存和信号处理。
?信号的其他几个相关的概念
首先,先向大家抛出信号中的几个概念
张三在上小学时,非常讨厌数学老师,但是数学老师又很凶。有一次上课时,老师说:“拿起本子记一下作业”。尽管很不喜欢这个老师,但又很害怕这几老师,张三无奈的记下了作业,想着:我现在先不写,假如老师真的发现我没写作业的话,我再写。而相比于懦弱的张三,头铁的李四则选择压根不写,忽略这次信号。
在这里,信号就像是作业。张三选择先记下作业,这就像是阻塞信号,等到什么时候被发现了,才写,写作业的过程,就是信号递达的过程。而李四的行为就是兑现好做出了处理,这个处理就是忽略。
阻塞和忽略是两个不同的概念:
阻塞信号是指在信号还未到来之前,先对某个信号阻塞,等到阻塞解除,才对信号做出处理动作。
忽略:忽略本身就信号的处理动作,只不过这个处理动作是忽略。
?pending位图
我们再一文搞懂Linux信号【上】中说过:信号在内核中是以unsigned int类型的位图来保存的,从低位到高位,比特位的位置代表信号的编号,比特位的内容代表是否收到对应的信号,0代表没有收到,1代表收到了对应的信号。这个位图就叫做pending位图。
所以:发送信号的本质就是修改pending位图。与其说发送信号,不如说是写信号。由于pending位图在task_struct结构体中,属于内核数据结构,所以修改位图的结构只能是操作系统。
?block位图
在操作系统,还有一个位图结构,叫作lock位图。
在block位图中,比特位的位置代表对应的信号的编号。对应的比特位为0,代表该信号没有被阻塞,可以递达;对应的比特位为1,代表该信号被阻塞,无法递达,除非解除阻塞。
所以,一个信号要想递达,①要将pending位图中对应的比特位置为1,②要将block位图中对应的比特位置为0。
?hander数组
如图:
针对如上的三个结构,需要说明的有:
从刚一开始接触信号时,我们就说:信号在产生的时候,不会被立即处理,而是要等到合适的时候再进行处理。什么是合适的时候呢?在进程从内核态返回用户态的时候,也就代表着曾经我一定进入过内核态。为了方便讲解,我们先补充一些预备知识。
?用户态和内核态
如图所示
代码的运行状态分为两种:用户态和内核态,用户态是最基本的运行状态,自己所写的代码全部都是用户态的代码,内核态则比较高级。
当代码中出现①使用操作系统的自身资源(getpid,waitpid.......)②涉及访问硬件资源(printf,scanf.......)时。用户为了访问这些资源,必须直接或者间接的使用操作系统提供的系统调用接口。但是普通用户无法直接调用系统调用接口,必须让自己的身份从用户态变为内核态。实际执行系统调用的进程,但是身份其实是内核。这里,还要说明一点:因为从用户态访问内核资源还要发生身份的变化,成本较高,所以往往系统调用比较浪费时间,所以尽量不要频繁的调用系统调用接口。
?cpu和寄存器
其中,有一个名为CR3的寄存器,这个寄存器表征当前进程是处于用户态还是内核态。寄存器内的数字为0表示处于内核态,数字为3表示处于用户态。
?深挖虚拟内存空间
我们之前在将虚拟内存时,知道虚拟内存一共有4G的空间,其中3G的空间是用户空间,该块空间通过页表和物理内存映射,进而读取用户代码和数据。但是还存在1G的内核空间呢?这是什么鬼?干什么用的?
这块空间同样通过页表和物理内存形成映射,只不过想映射的物理内存中存储的不再是用户的代码和数据,而是操作系统和系统调用的相关代码数据和方法。
用户空间和内核空间的页表等等有什么不同呢?
- 用户空间属于该进程的空间,具有私密性,同时每个进程都有相对应的用户空间页表结构,且不同进程的用户级页表不同。
- 在操作系统启动时,操作系统的相关的代码和数据加载到对应的物理内存,由于操作系统只有一个,所以所有的进程共享一个内核级页表,不具有私密性。
所以,如果进程想要访问操作系统的资源,该如何做?
所以,我们知道从用户态和内核态之间的跳转是非常浪费资源的。当代码执行到需要访问操作系统资源的时候,尽管浪费资源和时间,但是进程还要从用户态变为内核态,然后执行相关的系统调用接口。但是,站在进程的角度,它认为跳转一次太慢了,必须把所有只能在内核态中才能进行的操作完成。进程从用户态切换成内核态常见的原因有:系统调用,进程切换。
因为处理信号也需要在内核态中进行。所以进程就开始检查信号对应的block位图和pending位图。
检查顺序为先查block位图,然后再查pending位图。我展开说一下:
- 首先,查block位图。如果比特位为1,表示被阻塞,然后接着下一位比特位;如果比特位为0,再看pending中该信号对应的比特位,如果为0,接着查block位图的下一位比特位;如果比特位为1,说明该信号目前处于未决状态,应立即处理。然后查对应的处理方法hander表。
但是如果这个信号对应的处理方法是自定义行为呢?自定义函数属于自己编写的代码,在用户态中,操作系统允许进程在内核态中运行用户态的代码吗?
不行。理论上可以,但是操作系统为了安全,不敢这么干。因为它并不知道这个方法要干什么,万一要是恶意者恶搞系统咋办,所以,操作系统能力让进程在内核态中执行用户态的代码,但是不敢这么做。如果进程处于用户态然后执行这个方法,操作系统就没必要担心了,出了事也是这个进程被终止,和操作系统没关系。,
所以,为了执行信号的自定义方法,进程必须从内核态中返回用户态
当执行完方法后,如果有需要,进程还要返回内核态中,继续运行程序。
总结一下:
我们看到,其实整个过程看起来就像是个躺着的8。我把整个过程分为4个小过程,逐一说明
①代码在执行过程中遇到了系统调用或者时间片已到要进行程序替换。进程从用户态变为内核态来执行该过程。
②执行完毕。由于进程状态切换太浪费资源,进程就像一次性把要在内核态中干的所有事情全部搞完,再返回内核态。所以就检测是否收到了信号,如果收到了信号,并且处理方法是自定义方法,在用户态对应的物理内存。
③进程为了执行信号的处理方法,返回用户态执行。执行完毕后,返回内核态继续干其他工作。
④当进程把所有只能在内核态中运行的操作,全部完成后,返回用户态执行。
我们的信号位图又称信号集,分为pending信号集和block信号集。block信号集又称信号屏蔽字。
#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所指向的信号集,使其中所有信号的对应bit清零,表示该信号集不包含 任何有效信号。
- 函数sigfillset初始化set所指向的信号集,使其中所有信号的对应bit置1,表示该信号集的有效信号包括系统支持的所有信号。
- 在使用sigset_ t类型的变量之前,一定要调用sigemptyset 或sigfillset 做初始化,使信号集处于确定的状态。初始化sigset_t变量之后就可以在调用sigaddset和sigdelset在该信号集中添加或删除某种有效信号。
- 这四个函数都是成功返回0,出错返回-1。
- sigismember是一个布尔函数,用于判断一个信号集的有效信号中是否包含某种信号,若包含则返回1,不包含则返回0,出错返回-1。
调用函数sigprocmask
可以读取或更改进程的信号屏蔽字(block)。
#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
返回值:若成功则为0,若出错则为-1
如果oset是非空指针,则读取进程的当前信号屏蔽字通过oset参数传出。如果set是非空指针,则 更改进程的信号屏蔽字,参数how指示如何更改。如果oset和set都是非空指针,则先将原来的信号屏蔽字备份到oset里,然后根据set和how参数更改信号屏蔽字。
SIG_BLOCK:将set指向信号集中的信号,添加到进程阻塞信号集;
SIG_UNBLOCK:将set指向信号集中的信号,从进程阻塞信号集删除;
SIG_SETMASK:将set指向信号集中的信号,设置成进程阻塞信号集;
调用函数sigpending
可以读取当前进程的未决信号集,
#include <signal.h>
int sigpending(sigset_t *set);
现在我们用上述函数来测试一下信号递达的过程:首先是对SIGINT信号进行阻塞,然后通过ctrl+c 发送SIGINT 信号,发现SIGINT信号在pending位图中别标记为1,但是信号未决,直到解除对SIGINT信号的屏蔽,SIGINT信号递达,后续再发送SIGINT信号,会被直接递达,因为ISGINT并没有被阻塞。
#include<iostream>
#include<unistd.h>
#include<signal.h>
#include<cassert>
#include<vector>
#define NUM 32
using namespace std;
vector<int>sigarr={2};
// 打印信号集
static void show_pending(const sigset_t &s)
{
for(int signo=32;signo>=1;signo--)
{
if(sigismember(&s,signo))
{
cout<<"1";
}
else
{
cout<<"0";
}
}
cout<<" "<<endl;
}
// 自定义信号处理方法
void hander(int signo)
{
cout<<"收到一个信号:"<<signo<<endl;
}
int main()
{
//自定义行为
for(auto signo:sigarr)
{
signal(signo,hander);
}
// 初始化信号集
sigset_t block,oblock,pending;
sigemptyset(&block);
sigemptyset(&oblock);
sigemptyset(&pending);
for(auto signo:sigarr)
{
sigaddset(&block,signo);
}
// 写入信号屏蔽字中
sigprocmask(SIG_SETMASK,&block,&oblock);
int cnt=5;
while(1)
{
// 读取pending信号集
sigpending(&pending);
show_pending(pending);
sleep(1);
if(cnt--==0)
{
cout<<"信号屏蔽字已更改"<<endl;
sigprocmask(SIG_SETMASK,&oblock,&block);
}
cout<<"----------------------------------------------"<<endl;
}
}
代码运行如下:
本文到这里,就结束了,谢谢大家的观看。我们下一篇博客再见。