Linux——信号(2)信号保存与捕捉
一、信号的保存
上次我们说到,捕捉一个信号后有三种处理方式:默认、忽略、自定义,其中自定义我们用signal系统调用完成,至于忽略信号,也需要signal实现,比如我现在想忽略2号信号,则:
::signal(2,SIG_IGN); (::表示系统调用)
忽略本身就是信号捕捉的一种方法,动作是忽略。
恢复某种信号的默认行为 : ::signal(num,SIG_DFL);
二、信号相关的概念
1.信号递达:实际执行信号的处理动作
2.信号未决:信号从产生到递达之间的状态
3.进程可以选择阻塞某个信号,若该信号产生,一定会把信号进行未决状态,永远不递达,除非我们解除阻塞,阻塞和忽略是不同的,忽略是信号已经递达的一种方式。
三、PCB中信号的表
上次我们提到,PCB中有一个记录信号的表(位图),每一个比特位记录着对应的信号,这个表叫pending,除此之外还有两个表handler和block,handler就是记录每一个信号的递达方法,是一个函数指针数组,每一个下标代表着每一个信号的处理方式。
至于block表,也是一个位图,比特位的位置代表着信号编号,但内容表示当前信号是否被阻塞/屏蔽。
四、sigset_t
从图中来看,每个信号只有一个bit位的未决标志,阻塞标志也是如此,不记录该信号产生多少次,因此,未决和阻塞标志可以用相同数据类型sigset_t存储,称为信号集,阻塞信号集也称当前进程的信号屏蔽字。
有了这个数据类型,我们就可以用相关接口实现对信号表的增删查改
五、信号的修改函数
1.sigprocmask
这个函数是用来修改block表的
第一个参数是用来传宏的:
SIG_BLOCK:表示在block表中添加对某信号的屏蔽
SIG_UNBLOCK:表示在block表中解除对某信号的屏蔽
SIG_SETMASK:覆盖掉之前所标记(屏蔽)的信号,并标记新的信号状态。
第二个参数就是传指定进程的block集
第三个参数是输出型参数,用于信号状态的恢复。
2.sigpending
用于获取当前进程的pending表,因此也是输出型参数
六、信号的捕捉过程
上图就是大致的信号自定义捕捉的过程,其中,上半部分是用户态,下半部分是内核态,那什么是用户态和内核态?
七、用户态与内核态
1.补充内容
(1)硬件中断
当我们从外部访问操作系统时,其实并不能让操作系统定期轮询去访问这些外设的状态,所以真实的情况是这样的:
就以键盘为例,当我们的按键按下了,键盘的外设就会发起中断,实际上我们的外设并没有直接连接CPU,而是连接到了一个叫中断控制器的东西,当收到中断时,它就会知道是哪个设备发出并知道中断号,然后控制器通知CPU发生中断,此时CPU就可以获得对应的中断号。而为了处理这些设备,操作系统在设计的时候有一个东西叫中断向量表,我们就当成函数指针数组,这些下标就是中断号。所以中断号对应的中断方法都是设计好的。但此时CPU可能在执行某些进程,因收到中断,它就会把进程的相关数据放在相关寄存器内形成中断的上下文,此过程我们称CPU现场保护。然后CPU就有空间来处理这个中断:根据中断号来查表并实行对应方法——中断处理例程。
所以,操作系统就不用轮询检测外设,只要外设有需求直接随时把中断交给控制器即可。
(2)时钟中断
我们发现,任何进程都在操作系统的指挥下调度、执行,但是,谁来控制的操作系统?
实际上,我们的外设部分还有一个东西——时间源,它就像闹钟一样,每隔一段时间会像外设一样发送中断(只不过间隔的时间非常非常短),而且它也有自己的中断号和对应的方法,它的对应方法就是进程调度!但调度不意味着切换进程,还记得我们讲过的时间片,每个进程都有自己的时间片,到了就切换程序,现在看来,这个时间片更像是一个计数器,然后时间源每发送一次中断计数器则--,每减一次相当于一次调度,当减到0时才进行进程切换。
所以,操作系统就可以在硬件的推动下自己调度!因此实质上,操作系统可以进行躺平,只要想实现什么功能直接加到中断向量表中即可,我们也可以把操作系统理解为一个死循环。
(3)软中断
上面的中断的触发,都是靠硬件设备才能发生。那有没有靠软件发生的中断呢?有!
为了让OS支持进行系统调用,CPU设计了对应的汇编指令(int或syscall),可以让CPU内部触发中断。这便是软中断。
在中断向量表中我们也发现了有对应软中断的方法,其中在其处理的内部有一个系统调用表,里面记录着各种系统调用,而我们实际调用他们本质是通过数组下标!但是我们用户是如何把系统调用号给操作系统的?放在寄存器,然后通过int 0x80,syscall(触发软中断)陷入内核,然后CPU就会自动执行系统调用的处理方法,并用系统调用号依据系统调用表来找到具体的系统调用。(决定系统调用号的就是我们传的参数,每个调用传什么参数以及约定好了)
所以,系统调用接口,并不是C语言的函数,而是系统调用号和约定的参数+内部的表和陷入内核触发的中断以及返回值的寄存器共同组成的!
但我们用的系统调用往往都有返回值,这些返回值是通过寄存器或用户传入的缓冲区地址来返回给我们的。
既然系统调用是靠中断完成,那么一定是靠汇编语言,但我们实际用的调用貌似没有那么麻烦,这是因为glibc把这些系统调用进行了封装,也就是我们一直使用的都是C语言封装的系统调用(但其本身并和C无关)。
我们之前提到过的缺页中断、内存碎片处理、除0、野指针等错误,全都会转化成软中断,然后传递中断号,执行对应的解决方法。包括我们的虚拟地址空间也会触发中断,解决方法就是申请对应的物理内存。
因此,操作系统就是躺在中断处理例程上的代码块! CPU内部的软中断,比如int0x80或者syscall,我们叫做陷阱 。CPU内部的软中断,比如除零/野指针等,我们叫做异常。
2.回归正题
这是我们的虚拟地址空间,分为了用户页表和内核页表,其中用户页表每个进程都有一份,但内核页表整个操作系统只有一份,所以无论如何切换进程,都可以找到同一个操作系统。所以OS系统调用的执行,就是在进程的地址空间中执行的。
简单来讲就是,用户态就是指我们在访问自己的代码时所处的状态,内核态就是CPU经过系统调用访问操作系统时的状态。
八、信号的捕捉操作
1.sigaction
这个接口和我们的signal类似以下是演示代码
void handler(int signo)
{std::cout<<"新的处理方法"<<std::endl;exit(1);
}int main()
{struct sigaction act,oact;act.sa_handler= handler;::sigaction(2,&act,&oact);
}
在处理信号的期间,是有可能进行陷入内核的,所以在此期间如果我们再次传该信号时就会形成嵌套递归,而OS是不允许信号处理方法嵌套的,因此当进行处理信号时会把对应信号的block表置为1,等到信号处理完会自动解除。