目录标题
- 信号的一些概念
- 信号的保存
- pending表
- block表
- handler表
- 信号的捕捉
- 内核态和用户态
- 信号的捕捉
信号的一些概念
1.进程会收到各种各样的信号,那么程序对该信号进行实际处理的动作叫做信号的递达。
2.我们之前说过当进程收到信号的时候可能并不会立即处理这个信号,而是先将信号进行保存,那么我们把信号从产生到抵达之间的状态称为信号未决。
3.进程可以阻塞某个信号,这样即使发送信号给对应的进程进程也不会做出任何的处理。
4.被阻塞的信号会一直保持在未决的状态,只有当进程解除了对此信号的阻塞时才会执行该信号的动作。
5.阻塞和忽略是两个不同的东西,一个信号要是被阻塞说明即使收到了该信号也不会执行该信号的动作,而忽略是收到该信号之后根据该信号执行的一个动作,只不过该动作是忽略也就是什么都不做而已。
信号的保存
之前的学习中我们知道操作系统是要给每个进程保存收到的信号的,而保存的位置就是task_struct中的一个位图,这是我们当时说的保存方法,但是信号存在三种状态比如说是否收到了信号,是否阻塞了信号,以及信号与之对应的动作以及动作是否被自定义修改了,所以内核使用一张图来保存信号肯定是不够用的,所以在实际的应用中使用了三张表一起来保存了信号,这三个表分别为:pending表,block表和handler表,那么接下来我们来一一介绍这三个表的功能。
pending表
pinding表本质上就是位图,我们之前说进程在运行的任何时候都会收到信号,这个信号并不会被立即处理,所以进程得保存这个信号,保存信号的方式是位图,那么这个位图就是pending表,位图的比特位位置表示哪个信号,比特位的值表示是否收到了该信号,如果收到了信号就将对应位置的值修改为1,如果没有收到信号对应位置的值就是0.
block表
block表也是一个位图,位图的位置表示信号编号,位图的值表示是否阻塞了对应的信号,被阻塞的信号不会被递达,只有当阻塞被解除了信号才会被递达,即使没有收到对应的信号我们也是可以阻塞一些信号的,这里的逻辑可以用下面的伪代码来表示:
if(1<<(signo)&pcb->block))
{
//信号是被阻塞的,不递达
}
else
{
if((1<<(signo-1))&pcb->pending)
{
//递达该信号
}
}
那这里就可以这么来理解,操作系统会扫描PCB中的位图来判断是否有信号需要被处理,如果信号对应在block上的值为1就直接跳过,如果对应在block上的值为0就接续查看对应在panding位图上的值,如果值为1的活就执行的对应的方法,那么这就是block表的功能。
handler表
handler表是一个函数指针数组,每个元素都是一个函数指针,数组的位置(下标)表示信号的编号,数组下标对应的内容就表示对应信号的处理方法,signal函数可以修改信号对应的方法,那么这个本质上就是修改handler表对应元素的指向,当信号被递达时就可以根据信号的值来找到handler表中处理信号的方法并执行该方法,那么这里的图片就是下面这样:
所以看到这里我们可以得出三个结论:如果一个信号没有产生的话,并不妨碍他可以先被阻塞,因为我们只需修改block表中的值跟记录是否收到信号的pending表没有关系,如果阻塞的过程中收到了信号,那么我们就修改pending位图上对应的值并不会接着去找handler表中的方法 2.进程为什么能够识别信号,是因为内核中存在三个表这三个表能让进程认识信号并处理对应的信号。 3.如果SIGQUIT信号未产生过,一旦产生SIGQUIT信号将被阻塞,它的处理动作是用户自定义函数sighandler。如果在进程解除对某信号的阻塞之前这种信号产生过多次,将如何处理?POSIX.1允许系统递送该信号一次或多次。Linux是这样实现的:常规信号在递达之前产生多次只计一次,而实时信号在递达之前产生多次可以依次放在一个队列里。
信号的捕捉
信号产生的时候不会立即被处理,而是在合适的时候被处理,那这个合适的时间是什么时候呢?并且我们上面说到操作系统会在某些时刻检查block表和pending表,那这个时刻又是指的什么时候呢?那么要想解决这些问题我们就得谈谈什么是内核太什么是用户态。
内核态和用户态
我们自己写的代码经过编译和运行之后都是运行在用户态上的,但是在我们在编写代码的时候难免会访问到操作系统的资源(getpid,waitpid)和硬件资源(printf,read,write),当我们访问操作系统的资源或者硬件资源的时候,我们都得直接或者间接的访问系统提供的借口,通过这些接口来访问操作系统的资源,我们把这些接口称为系统调用
调用系统调用的时候,普通用户不能以自己的身份来调用系统调用,必须将自己的身份变为内核态,
这就好比当我们是一个公司的基层时副总裁的办公室我们几乎不能直接进出,但是当我们的身份变成了公司的CEO时,副总裁的办公室我们就可以随意的进出,所以当我们的身份变成了内核态时我们才能够有资格使用或者访问内核态提供的代码,实际执行系统调用的人是进程,但是身份是内核态,因为用户态调用内核态的时候要发生身份的转换,所以系统调用往往比较费时间一些,所以要尽量避免频繁的调用系统调用
但是这里存在一个问题,你说进程只是那一个进程,但是却存在两个身份,那该操作系统是如何来实现这一点的呢?那么这里我们就得提到寄存器这个东西
cpu中存在大量的寄存器,寄存器分为两种:1.可见寄存器,2.不可见寄存器,虽然寄存器分为两种但是只要和当前进程强相关的寄存器我们都将其称为上下文数据,比如说cpu中存在一个寄存器专门用来指向当前进程的pcb这样就可以快速的知道当前运行的进程是哪个,再比如说有个寄存器中存储的是当前页表的起始地址,这样就可以快速的找到当前进程的页表,
cpu中还存在一个名为CR3的寄存器,这个寄存器中的内容表征当前进程的运行级别,0表示内核态,3表示用户态,所以想要判断当前的进程是用户态还是内核态就只需要查看CR3里面的内容就行,这里存在一个问题:我是一个进程,进程是怎么跑到内核中执行对应的方法呢?之前我们提到的进程地址空间往往指的是用户空间,我们写的代码存放或者申请空间都是通过用户级地址空间+页表来进行映射,这个空间的大小为3GB
这里的页表是用户级页表该页表每个进程都有一份,但是地址空间的总大小是4GB,用户级空间只占了3GB那剩下的1GB空间用来干什么呢?我们把这个1GB的空间称为内核级空间,这一块空间也对应了一个页表,这个页表就叫内核级页表,这个页表的作用就是将操作系统级别的代码从内核虚拟地址空间映射到内存上面,
因为操作系统的代码只有一份,在机器启动的时候会将这份代码加载到内存上,并且每个进程都要使用操作系统代码,所以内核级页表只有一份,每个进程都通过这个页表来映射找到内存上的操作系统的代码,每一个进程都有自己的地址空间(用户空间独占),内核空间(被映射到了每个进程的地址空间的3-4G),所以进程要想访问操作系统的接口,其实只需要在自己的地址空间上进行跳转即可,由用户区跳转到内核区然后再由用户级页表跳转访问内存上的内核代码即可,每一个进程的进程地址空间都有3-4GB的内容,都会共享一个内核级页表,所以无论进程如何切换都不会修改进程地址空间中3-4GB的内容。那用户凭什么能够执行并访问内核的接口或者数据呢?原因是当操作系统发现你要访问内核的数据和代码时,会帮进程修改CR3寄存器的内容,这样就从用户态变成了内核态那么这时就有权利访问内核的数据,那操作系统为什么能知道我们要访问内核的数据和代码呢?答案是当我们要访问操作系统的接口时最开始还是用户态执行的,当以用户态执行系统调用接口时,系统调用接口做的最多的事情还是讲寄存器中的3号状态修改成为0号状态也就是将用户态改成内核态,然后才跳转区域访问内核的数据和代码,那么这就是用户态和内核态的概念
信号的捕捉
我们说操作系统会在合适的时候处理信号,那么这个合适的时候就是从内核态返回用户态的时候对信号进行处理,那么这就说明我们之前一定先进入了内核态,
当执行系统调用和进程切换(一个进程没有执行完,那一定是放到运行队列或者等待队列,那放进队列的时候一定是以操作系统的身份把我放了进去,所以得先变成内核态 就会变成内核态)
当操作系统进入内核态并且马上要返回普通状态的时候还会做一件事就是检查内核中的三张表,
如果block中的信号为0,pending中的信号也为0就会接着检查下一个信号,当block中的值为1就算pending中的值为1的话也不会执行该信号,当block的值为0并且penging的值为1的话就会执行该信号,然后就去handler中查找对应的方法,处理的方法有默认,忽略,自定义,默认的方法中有70%是终止进程,因为当前的身份是内核态所以可以很轻松的终止当前进程,忽略就是要处理该信号,但是处理的动作是什么都不做,所以直接将pending对应位置的信号设置为0就行,自定义的方法的实现在用户态,那这里就存在一个问题:我们能不能以内核态的身份来执行用户的代码呢?从技术的角度上我们可以以内核态的身份访问用户的代码,但是从设计的角度上来看是不能的,因为操作系统不相信任何人,handler方法中可能会有对系统造成危险的操作,而这些操作操作系统是无法识别是安全还是有害的,所以当我们以内核态的身份执行用户的代码的话,这部分代码可能会被恶意分子利用,然后进行一部分越权的非法动作,所以即使从技术上的角度能这么干,操作系统也不让你这么干,所以当要执行自定义代码的时候操作系统得通过特定的调用,将自己的身份从内核态转换称为用户态,然后再执行自定义代码,那么这个时候自定方法出现了问题就是用户自己的问题了,这个时候想干什么坏事情就会收到操作系统的管制了
执行完自定义方法之后也不能直接返回之前调用方法的地方,因为我们无法知道当初是从哪里进行跳转的,不能从用户态的一个地方跳转到另外一个地方,这里必须得有操作系统的支持,因为在从用户态跳转到内核态的时候操作系统保存了你的上下文信息,所以执行完自定义方法后得从用户态再跳转回内核,再通过特定的系统调用由内核态回到用户态上次的地方再继续向后执行
那么这就是信号捕捉的全部流程希望大家能够理解。