目录
信号的处理时机
引入
- 之前我们说过,信号被os发送到进程,实际上是修改了pending信号集
- 然后进程会在合适的时候处理
那究竟什么时候才能算合适的时候呢?
思考 -- 什么时候才能算合适的时候呢?
处理信号必然不是我们做的,而是os,因为信号是内核数据
- 那么处理信号时,就得处于内核的范畴,不然怎么访问os的数据 -- 也就是处于内核态
- 那么肯定也会存在用户态,和内核态对应
- 也就存在内核态和用户态的切换
- 那么内核态就拥有三种阶段 -- 刚进入内核态,在内核态执行任务时,准备转为用户态时
是哪个阶段开始处理信号的呢?
- 一般是在执行完任务后,内核态准备转为用户态时,会进行信号的处理
- (因为任务肯定第一位嘛,信号的处理又不急于这一时)
用户态转为内核态
引入
虽然我们是这样说的 -- 由内核态转为用户态时,就会处理信号
- 但是这个操作我们看不到,怎么知道到底有没有转换呢?
- 说到底,究竟是怎么转换的呢?未免有点太抽象了
内核地址空间
引入
其实并不抽象,所有看起来复杂的行为都是将[底层一些简单的操作]封装而成的
- 比如,可以首先来思考这个问题 -- 进程为什么能切换成内核态呢?
思考 -- 进程为什么能切换成内核态呢?
内核态,也就是可以使用os内部的数据/接口
那为什么一个进程,它可以有这样的行为呢?
- 肯定是它此时的权限修改成了os的权限
如何使用os内部的数据/接口呢?
- 要使用这些,必然是和使用普通数据/接口一样,要访问实际的内存的
- 毕竟他们本质没有区别,只是权限不一样
- 而访问内存必须通过虚拟地址空间+页表+MMU实现
- 但我们无法直接操作物理内存,既然可以访问内核数据,那内核肯定也有自己的进程空间+页表
那到底有没有呢?
- 有的
虚拟地址空间
之前学习的地址空间中本身就有1G的空间,是存放内核空间的:
- 也就是说,我们切换为内核态,依赖的是 -- 进程地址空间有内核空间
注意点
既然有内核空间,自然也有内核级的页表用来映射
但是要注意!!!
- 每个进程的用户空间,对应的是独立的内存块
难道每个进程拥有的内核空间,它对应的内存也是独立的吗?
- 不会的!
- 内核资源是所有进程共用的,不会让每个进程去瓜分一部分
- [进程的独立性]要求用户空间对应的内存都是独立的,但每个进程可以使用相同的os资源
- 所以,内核级的页表只需要一份(对应的物理内存也是同一块),它可以被所有进程看到
所以,无论进程如何切换,我们看到的都是同一份os资源
原理 (总结一下)
进程可以切换为内核态的原因就在于,每个进程中都有内核地址空间
- 只要切换访问位置和访问权限,自然可以通过内核地址空间访问内核数据噜
为什么+如何进入内核态
引入
知道了进程是可以切换的
- 那么究竟为什么要切换成内核态,底层又是如何切换的呢?
介绍
当我们需要访问内核数据时,就需要进入内核态
- 我们大多都是通过系统调用接口,来使用内核数据的
- 所以,调用系统调用,自动就可以进入内核态(因为之前在使用的时候,并没有什么特殊行为)
并且,我们不用把系统调用想的那么神秘,他也只是一个函数而已(只不过非常接近底层)
- 系统调用和使用库函数本质上没有区别,都是在进程自己的地址空间内进行跳转的
底层原理(int 80)
在每个系统调用函数开始前,都需要先切换成内核态
- 它通过int(interrupt 的缩写) 80h 这个汇编指令,来切换成内核态
- 而汇编的底层是通过CPU的寄存器和权限相关字段实现的
CPU的寄存器
虽然存在两种状态(内核态,用户态),但执行代码时使用的是同一套寄存器
- 当CPU在用户态执行程序时,寄存器用于存储用户程序的状态和数据
- 当切换到内核态时,寄存器则用于内核代码的执行
切换过程中,内核会保存用户态的寄存器状态(也就是保存上下文数据),以便在返回到用户态时恢复,并恢复内核态的上下文;反过来也一样
- 因此,虽然使用的是相同的寄存器,但内核态和用户态的操作和数据是相互独立的
状态切换
int 80h这个指令涉及多个字段和寄存器的修改,比如:
- CR0-CR3寄存器,段寄存器,RPL、CPL、DPL,段选择子等等
如何知道进入了内核态
硬件上
通过读取寄存器的状态(寄存器是硬件的一部分)
软件上
其实我们已经在上面介绍了,只要检测执行的位置是否指向内核空间即可
- 指向内核空间,就说明此时正在执行系统级别的代码,此时也就处于内核态
用户态和内核态的来回切换
为什么要来回切换
总不能从用户态切到内核态后,我们就不切回来了吧
我们肯定还有很多没有执行的用户级代码呢
如何切换
其实原理我们已经都介绍过了
- 只要我们可以拿到进程执行相关的数据,我们也就可以按照上面的原理 -- 跳转地址空间,修改寄存器和相关字段数值,来切换状态
- (当然这里只是一个大概,实际上非常复杂的)
- 刚好cpu的寄存器里面,就存放着很多进程执行相关的数据结构的指针(也就是进程的上下文),可以用来恢复进程,从上次执行的位置继续向下执行
信号的处理流程
知道了原理后,我们就可以明白,os究竟是如何处理信号的
介绍
详细图解
描述
首先我们要先陷入内核态(可能是系统调用,也可能是异常/中断导致的)
- 然后在内核态下,os完成一系列的任务
- 在即将返回用户态,继续向下执行代码前,我们正好处于内核态下,有能力,可以顺手处理收到的信号
所以,os开始处理信号
- os先去检查信号在内核中的两种信号集 -- pending,block
- 如果该信号被阻塞,os可以直接返回到用户态
- 但如果满足条件,我们拿到对应信号的处理函数后 ...
这里就要分情况讨论了:
默认操作
- 如果需要终止进程,os可以直接进行终止进程的调度
- 如果该进程中有需要刷新数据/dump到外设,os在内核态下可以直接操作,操作完再退出进程
- 如果该进程不退出,也是一样的:
- eg:有个信号用来暂停进程,也可以在内核态下直接实现,直接修改进程状态,然后调度下一个进程
忽略操作
- 如果是忽略,os只需要将pending信号集由1改为0,即可返回到用户态,执行下面的代码噜
自定义操作
- 如果是自定义的操作,就有点麻烦了
- 因为默认和忽略都可以在内核态下直接完成,因为os的权限是最大的,什么资源都可以拿到
- 但是在执行自定义函数时,需要转换到用户态才行
为什么要转换到用户态呢?内核态下不能执行吗?
- 内核态当然可以执行进程的代码,它可以拿到所有的资源
- 但是,万一该函数中有违规操作(用户态下不可以执行的操作)
- 却没有被内核态下的进程识别到,就可能会导致不好的后果
- 为了避免这些情况,普通代码还是让用户态进程执行的好
在用户态下执行完成后,也就该返回内核态了
- 不仅是因为,处理完信号后,需要修改pending位图
- 也在于,进程需要返回到陷入内核态的执行位置,然后继续向下执行
- 而这些操作都需要访问os资源
抽象图解
将信号处理过程 -- 从信号需要从用户态陷入到内核态 -> 处理完信号,返回原先的执行位置 抽象出来,得到该图:
交叉点处
就是os开始处理信号的时候
方向
图中的箭头,就是os执行流的变化方向
横线
- 如果用一条横线贯穿该图,那么横线上方就是用户态,下方就是内核态
- (可以看到信号处理的时候是在内核态中)
- 横线与图的四个交点就可以代表,进程的状态变化次数
- 而交点所在的方向,就代表了状态的切换方向