我们知道信号的处理不是即时的,进程在合适的时机才会处理信号,而这个时机就比如从内核态返回用户态。
1. 用户态与内核态
在操作系统中,用户态(User Mode)和内核态(Kernel Mode)是两种不同的CPU运行模式,它们决定了进程可以执行的操作范围和对系统资源的访问权限。
用户态是进程执行普通应用程序代码的状态。在用户态下,进程的权限受到限制,不能直接访问硬件设备或执行特权指令。用户态进程通常只能访问分配给它的内存空间,以及通过操作系统提供的系统调用接口进行受控的资源访问。
内核态是操作系统的核心部分运行的状态,也称为特权态或监管态。在内核态下,操作系统拥有对所有硬件资源的完全访问权限,可以执行任何CPU指令。内核态用于执行需要高权限的操作,如内存管理、进程调度、硬件设备驱动等。
进程地址空间被分为两部分:用户空间与内核空间,每个空间都有自己的页表去映射内存,内核空间使用的页表叫做内核级页表,用户空间使用的页表叫做用户级页表。内核级页表在整个内存中指保留一份。
内核空间中的虚拟地址,指向了内存中的操作系统的代码和数据,操作系统本身也是一个软件,也要有自己的代码和数据,任何用户访问操作系统的本质,其实都是去执行操作系统的代码。用户访问操作系统,其实就是通过地址空间的内核空间区域来访问的!当一个进程想要访问操作系统,就可以通过自己的地址空间的内核部分来访问。
每个进程都有自己的独立的地址空间,那么每个进程都有自己独立的内核空间,因此每个进程都可以访问到操作系统!也就是说,内核空间存在的意义在于,不论当前哪一个进程在调度,都可以随时通过该进程的内核空间来找到操作系统!
可以简单理解为:当进程执行用户空间的代码,此时就处于用户态,当进程执行内核空间的代码,此时就处于内核态。当然,其实此处并不是进程自己去执行内核空间的代码,而是唤醒操作系统去执行。最简单的例子就是系统调用,当进程调用系统调用的时候,此时需要更高级别的权限来访问内核的底层数据。毫无疑问普通的进程是没有这个权限的,当进程进行系统调用,此时就会唤醒操作系统去执行内核空间的代码,此时就完成了用户态到内核态的切换。
2. 处理过程
在从内核态返回用户态之前,操作系统就会去处理信号。
当操作系统因为某些原因陷入内核后,会先处理用户的需求,当处理完需求后,就会检测当前是否有需要处理的信号。也就是上图的C
部分,该过程就是检测是否有信号要处理。
检测的结果有三种:
- 没有要处理的信号,直接返回用户态
- 有要处理的信号,且该信号的处理方式是默认处理函数,那么直接在内核态处理该信号,处理完毕后返回(C->D->A)
- 有要处理的信号,且该信号的处理方式是用户自定义函数,那么要先切换回用户态执行自定义函数(E),执行完函数后再到内核态(F),最后再切换回用户态(A)
如果信号的处理方式是默认处理方式,此时直接在内核态执行代码,主要有两个原因:
- 信号的默认处理方式,是操作系统自己提供的,因此不会有安全性问题,可以直接以内核态的高级权限执行
- 大部分信号的默认处理方式,是直接杀掉当前进程,杀掉进程的行为,需要内核态的权限,因此直接在内核态就可以杀掉这个进程
当信号的处理方式是用户自定义函数,那么要先切换回用户态执行,因为用户自定义的handler函数,其安全性是不确定的,如果贸然给这个函数一个内核态的权限,用户有可能会拿高级权限去做不安全的事情,所以不能给用户自定义的函数内核态权限,而是回到用户态执行这个函数。
那么下一个问题就是,执行完handler函数后,E已经在用户态了,为什么还要回到内核态,在回到用户态?
比如说某个时刻,进行了A -> B的陷入内核过程,那么当B执行完毕后,就要回到A。所以在内核态B中一定会存储一条信息(准确来说叫做上下文),指明之前A执行到那一行代码,从而在B -> A的时候,可以知道跳转回哪里。
当用户态进程陷入内核态时,内核会保存用户态进程的上下文信息,包括:
- 寄存器值:例如程序计数器(PC)、堆栈指针(SP)、通用寄存器等。
- 内存状态:例如内存页表、虚拟地址空间等。
- 其他状态:例如进程状态、信号掩码等。
内核通过保存这些上下文信息,可以记录用户态进程执行到哪个位置,以及该进程的运行状态。当内核处理完用户态进程的请求后,会恢复用户态进程的上下文信息,并将控制权返回给用户态进程。
用户态进程恢复执行后,会从之前中断的位置继续执行,而不会意识到自己曾经陷入内核态。也就是说,陷入内核态之后,只有内核态知道之前的用户态执行到哪里,所以E状态下不能直接跳转回原来执行的地方,必须先回到内核态,去找到原先执行的位置,在返回用户态。
而每一次在从内核态返回用户态之前,操作系统都会处理信号。
如上图中一共发生了两次信号处理,即 C->E
,F->A
,这两个时候都会检测并处理信号。