欢迎来到小林的博客!!
️博客主页:✈️林 子
️博客专栏:✈️ Linux
️社区 :✈️ 进步学堂
️欢迎关注:点赞收藏✍️留言
- 实际执行信号处理动作成为递达。
- 信号从产生到递达之间的状态,成为未决。
- 进程可以选择阻塞某个信号。
- 被阻塞的信号产生时会保持在未决状态,直到进程接触对此信号的阻塞,才执行递达动作。
- 阻塞和忽略是不同的,只要信号被阻塞,就不会被递达,而忽略递达的一种处理方式。
递达的三种处理方式
1.默认
一般默认的处理方式就是终止。
2.忽略
不对该信号做处理。
3.自定义
类似handler函数,自己指定函数处理信号。
默认和忽略是什么区别? 默认是一种默认的处理方式,和忽略的处理方式是直接不处理。
信号是由操作系统发送给进程的,而信号不一定会被立即处理,那么这就意味着进程必须有保存信号的能力!由此我们可以推断出,信号一定是以一个数据结构存储在进程控制块(PCB)这个结构体当中!而我们 1-31 个信号可以想象成对应的 1 - 31 个比特位。比特为1 则说明收到该信号,为0则说明没有收到该信号。
而在内核中的表示方式为:
block 是阻塞表,对应的数组下标是 1 - 31 个信号。 pending 是递达表,为1则说明被递达。 为0则说明没有被递达。handler 是一个函数指针数组,每个元素是下标对应的信号处理的函数指针。
信号阻塞是一种什么场景? 简单来说! 被阻塞的信号不会被递达! 递达就是对信号的处理!再通俗一点,阻塞就是阻止对信号的处理,就是暂时先不处理该信号! 等解除阻塞后再处理该信号。
如何阻塞信号?
我们可以利用int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
这个函数对指定信号进行阻塞, 而set是输入型数据,oset是输出型数据。
sigset_t
sigset_t 是一个位图,这个位图不能让用户直接操作,而是通过系统调用接口来修改这个位图。 这个位图 block 和pending都可以使用,每个bit位都是一个未决标志。而信号并不需要记录收到多少次,只需要记录收到或者没收到,对应0和1 。这个位图被称为信号集 , 在阻塞信号集中(block),这个标志位表示是否被阻塞。在未决信号集(pending)中表示是否处于未决。
信号集的操作函数
#include < >
int sigemptyset(sigset_t *set); //该信号集的所有bit位置为0
int sigfillset(sigset_t *set); //初始化set所指向的信号集,使其中所有信号的对应bit置位,表示 该信号集的有效信号包括系统支持的所有信号。
int sigaddset (sigset_t *set, int signo); //往位图添加信号
int sigdelset(sigset_t *set, int signo); //往位图删除信号
int sigismember(const sigset_t *set, int signo); //信号集中是否包含某种信号
这四个函数都是成功返回0,出错返回-1。sigismember 包含则返回1,不包含则返回0,出错返回-1。
**sigprocmask 阻塞函数 **
int sigprocmask(int how, const sigset_t *set, sigset_t *oset);
调用函数sigprocmask可以读取或更改进程的信号屏蔽字(阻塞信号集)。调用成功返回0 ,调用失败返回-1
set是要修改的位图,oset是保存旧位图。
int how参数:
SIG_BLOCK
: set 包含了我们希望添加到信号屏蔽的信号,相当于 mask = mask | set
SIG_UNBLOCK
: set 包含了我们希望解除阻塞的信号,相当于 mask = mask & ~set
SIG_SETMASK
: 设置当前屏蔽字为set指向的值,mask = set
sigpending 读取未决信号集
int sigpending(sigset_t *set);
读取当前进程的未决信号集,调用成功返回0,调用失败返回-1。
了解了以上操作函数之后,我们接下来可以写个代码做个小实验。
#include
#include
#include
#include
#include
void show(sigset_t* set)
{
int i = 0;
for(i =1; i <= 31 ; i++)
{
if(sigismember(set,i))
printf("1");
else printf("0");
}
printf("\n");
}
int main()
{
sigset_t set,p; //定义信号集
sigemptyset(&set); //初始化信号集
sigemptyset(&p);
sigaddset(&set,2); //往set信号集添加一个2号信号
sigprocmask(SIG_SETMASK,&set,NULL) ; //设置屏蔽信号集为set
while(1)
{
sigpending(&p); //获取信号集
show(&p); //打印信号集
sleep(1);
}
return 0;
}
然后我们执行程序后 按 ctrl + c 发送2号信号,看看会发生什么。
我们会发现两个现象。
1.第二个比特位由 0 置 1 ,证明该信号被保存进PCB里的位图结构
2. 发送2号信号后,程序并没有终止。
所以,我们可以知道,2号信号被屏蔽了。 只有解除屏蔽时才会处理2号信号,否则信号将一直处于未决状态。
而操作系统给进程发送信号,本质就是往进程控制块(PCB)内部的位图结构的对应位置由0置1 , 阻塞信号也是相同道理, 而handler 表 存放的是处理信号的函数的地址。
要知道信号处理的全过程,我们要清楚2个概念。用户态和内核态。
用户态就是用户代码和数据被访问或执行的时候,此时所处的状态就是用户态。
OS的代码和数据被执行的时候,计算机所处的状态就叫做内核态。
当我们的进程在执行系统调用时,会转换为内核态,因为用户态不能执行OS的代码和数据!因为用户没有权限,因为操作系统不相信任何人!
而实际上当一个进程执行的时间片到了之后,操作系统会进入内核态。把该进程下掉,然后把新执行的进程放上来,再转换为用户态执行该进程。 而CPU为了分清楚当前是内核态还是用户态,会有一个CR寄存器来保存当前的用户状态。
而我们还要知道,用户态使用的是用户级页表,每个用户级页表都是独立的,因为进程具有独立性!
而操作系统也有一份系统级页表,系统级页表被所有进程所共享!!这就是为什么在不同的进程在使用系统调用时,都能找到同一份操作系统提供的代码和数据!本质就是因为所有进程共享系统级页表!!
有了以上前置知识之后,我们就可以来剖析信号处理的全过程。
首先,进程在CPU调度时会从用户态陷入内核态 -> 陷入内核态先不着急返回,先去看是否收到信号,如果收到信号,再看该信号是否被阻塞,如果没被阻塞,那么执行对应的handler操作,如果是默认,那么直接释放这个进程,如果是忽视,那么直接返回到用户态,如果是自定义处理,那么从内核态返回到用户态,执行进程中的handler处理信号函数 -> 再陷入内核态,执行sys_sigreturn()函数 -> 返回用户态。
我们可以用一个无穷大的符号来总结。
为什么不在内核态处理信号,反而回到用户态处理信号? 原因很简单! 因为操作系统不相信任何人! 如果你在handler函数进行一些非法操作,例如 rm -r * 。那么这样的破坏是非常大的,所以要转换到用户态来处理信号。如果处理方式是忽视,那么直接返回用户态。如果是默认,那么释放掉进程。