首发于2022-11-11
完善于2022-12-05
这是Sum_sugar 的第002篇原创文章
前言:本系列以BUPT人工智能学院徐梦炜老师的课件为蓝本,以《王道操作系统考研复习指导》为知识补充,旨在系统梳理操作系统课程的知识点,以学生的视角出发解释学习操作系统过程中遇到的种种问题。如有疏漏及错误之处,烦请不吝赐教。
前文回顾补充
计时器中断
计时器中断是操作系统重新夺回CPU控制权的一种方式,可以避免进程完全控制CPU。否则,进程可能会其中无限循环执行。硬件计时器可以在内核模式下被重置,达到中断的效果。
当前的计时器中断后,操作系统可以调度另一个进程运行,该进程可以是之前被中断的进程。
current privilege level(CPL,当前特权级)
x86体系结构中,在cs段寄存器中使用较低的两位作为当前特权级,也就是指示当前为用户态还是内核态。
几个小问题
- 用户级代码总是运行在用户进程下吗?——是的
- 用户级代码总是运行在用户态下吗?——大多数是的,除了eBPF
- 内核级代码总是运行在系统进程下吗?——不是,中断处理程序运行在用户进程下。
- 内核级代码总是运行在内核态下吗?——不是,图形界面,UI界面运行在用户态下。
- 代码/CPU如何判断当前是处于用户态还是内核态?——通过2比特的cs段选择器
用户→内核模式切换
异常(exceptions)
异常,也称内中断,是指来自CPU执行指令内部的事件(同步的)。如程序的非法操作码、地址越界、运算溢出、虚存系统的缺页及专门的陷入指令等引起的事件。包括除数为0,执行特权指令等等。
中断(interrupts)
中断,也称外中断,是指来自CPU执行指令外部的事件(异步的),通常是信息输入输出,如设备发出的IO结束中断,表示设备输入输出处理已经完成;时间中断,表示一个固定的时间片已到,让处理机处理计时、启动定时运行的任务等等。
系统调用(system calls)
系统调用,指用户在程序中调用操作系统所提供的一些子功能,系统中的各种共享资源都由操作系统统一掌管,因此在用户程序中,凡是与资源有关的操作(如存储分配,IO传输与文件管理等),都必须通过系统调用的方式向操作系统提出服务请求,并由操作系统代为完成。
中断向量表(IVT)
中断向量表存储了异常、中断、系统调用等不同处理程序的条目,其中每个条目指向内核中不同处理程序的第一条指令。(在实模式下使用,存在一个固定的位置)
在x86架构中,总共有256个条目,每个条目占4字节。
中断描述符表(interrupt description table, IDT)
中断描述符表告诉CPU 中断服务程序(ISR)被加载到了哪里。(在保护模式下使用)
在中断描述符表中,每一个条目被称为“门”,一共有256个门。
在32位处理器上,每个门的长度为8字节,或在64位处理器上,每个门的长度为16字节。
其位置保存在IDTR,也就是IDT寄存器中(位置不固定),并用LIDT汇编指令加载。
- offset: 占32比特,被分为两部分,表示中断服务程序的地址。
- selector: 段选择器,指向全局描述符表中的有效代码段。
- gate type:4比特的值,定义中断描述符表表示的门类型:任务门,中断门,陷入门,系统调用门等等。
- DPL: 2比特的值,定义允许通过INT访问此中断的CPU特权级。
- P:当前位,必须设置为1才能使描述符有效。
如何从中断发生以后定位到中断处理程序?
寄存器IDTR指向中断描述符表的开头,根据中断号找到中断描述符表里面对应的项,从项内可以提取出offset。对于进程来说,每一段内存有一个基地址。把offset加到基地址上,就可以找到相应的中断处理程序。
中断号从硬件处得知。
中断向量表(IVT) vs 中断描述符表(IDT)
- 好处:都保证了从用户到内核空间的入口数量有限,门的有限。好处在于写内核的时候只需要保证这几个门的安全性就可以了。
- IVT运行在实模式下,IDT运行在保护模式下
- IVT每个条目占4字节,IDT在32位处理器下占8字节,在64位处理器下占16字节
- IVT通常位于0000:0000H,IDT可以位于内存中的任何位置,并通过LIDT指令进行定位
中断屏蔽
中断分为可屏蔽中断与不可屏蔽中断两种特权指令。
可屏蔽中断是指通过INTR线发出的中断请求,通过改变屏蔽字可以实现多重中断,从而使中断处理更加灵活(CPU可以响应也可以不响应)。所有的软件中断、所有的系统调用、以及部分硬件异常是可屏蔽中断。
不可屏蔽中断是指通过NMI线发出的中断请求,通常是紧急的硬件故障、如电源掉电等,CPU必须响应。此外,异常也是不能被屏蔽的。部分的硬件异常是不可屏蔽中断。通常由eflag寄存器指定。
屏蔽指的是中断被延迟而不是被忽略,也就是进入缓冲区buffer。
中断栈
中断栈是内核内存中(内核态)保存着中断程序状态的一种特殊的栈。当没有中断的时候中断栈为空,且运行在用户空间。
为了可靠性和安全性,中断栈没有直接使用用户空间的栈。
内核看到的memory也是虚拟地址,但是内核知道虚拟地址可以map到什么物理地址。
操作系统有几个中断栈?
答案:1×进程(线程)数量的中断栈
triple fault第三次中断:reboot
double fault第二次中断:进入一个新的中断处理程序,重新执行中断处理程序。且可以共享一个中断栈。
每一个进程或者线程都有自己的中断栈。这可以让操作系统更好地处理不同进程的中断。
内核→用户态模式切换
新生成了一个进程
用户发起操作,但是是以系统调用的方式在内核态下完成的。
中断、异常、系统调用恢复到用户态的程序
时间上的中断切换进程
upcalls 上行调用☆
与系统调用相反,是特权指令进入用户模式。由内核态来调用用户态的代码。
- 异步IO通知
- 进程之间的通信
- 用户级异常处理——确保在应用程序关闭前保存文件
- 用户级资源分配——JAVA垃圾回收
x86状态切换实例
背景知识
在x86中内存是分段的,每一个进程都有自己的一个段。在分段的内存里寻址需要两个寄存器。
找一段代码的时候用cs和eip两个寄存器。(寄存器的值没法直接改,但是可以用其他指令来更改)
找栈地址的时候用ss和esp两个寄存器。(可以改)
EFLAGS寄存器保存了处理器运行的状态,比如中断是否被屏蔽等等。
上述寄存器属于特殊目的寄存器,另一类是通用寄存器。
x86切换讲解![请添加图片描述](https://i-blog.csdnimg.cn/blog_migrate/77d4cf642d6754c403384872546015d3.png)
cs eip 寄存器指向当前运行的代码;
ss esp 寄存器指向当前用的栈的位置;
中断、异常、系统调用发生以后,硬件做的事:
- 首先把中断屏蔽掉,如果不屏蔽掉可能发生多级中断。
- 把特殊寄存器的值存放到临时寄存器内。
- 把栈从用户态切换到内核态的中断栈。
- 把上述三个特殊寄存器的值存放到内核中断栈中。
- 选择性地存放一些error code。
- 然后调用内核的中断处理程序。
是否可以去掉第二步?
为什么要先存放到临时寄存器内?而不是直接push?
答:寄存器的值可能会被改变,现场就没有保存下来。
切换栈其实就是改ss esp寄存器的值。
中断、异常、系统调用发生以后,操作系统做的事:
- 保存当前通用寄存器的值。可能保存了当前函数和当前进程执行上下文的信息。
- 执行中断处理程序。
- 恢复原来的进程。把刚刚push进来的值都pop出去。把栈里面的值又放回寄存器。
- 还有几个硬件放进去的值,用ireturn语句将其pop出去。
为什么没有指向堆的寄存器?
因为不需要,指向栈是需要压栈和出栈。
堆是由用户库来维护的。
总结
中断处理程序对于用户来说是不可见的,并且不会改变程序的状态。
中断是安全的。其中有很多软硬协同。