What is Context Switch
Execution Flow and Context
我们先看什么是执行流 execution flow,execution flow 是一个抽象的概括,包含两个维度:
- CPU 寄存器中的值随着时间而变化
- execution flow 所属的栈随着 flow 的变化,不断进行压栈、退栈
那么上下文就是 execution flow 中某一时刻的瞬时快照/切片.记录的是
- 各个寄存器的值
- SP 指针当前的值,用于指明栈在当前时刻的状况
反过来,一连串连续的上下文构成了一个执行流.
从 stack 的视角来看,一个基本的上下文切换流程是:
- 保存当前 execution flow 的寄存器状态
- 切换栈指针
- 恢复另一个 execution flow 的寄存器状态
Categorization of Context Switch
所有的上下文切换不尽相同,这种不同主要是由不同执行流类型之间的切换导致的.我们在用户态和内核态下都存在多个线程/进程执行流.所以我们可以大致区分出四类 context switch
- 内核态执行流之间的切换
- 用户态执行流之间的切换
- 用户态进入内核态,内核态退出到用户态
- 中断嵌套
主动切换、被动切换
主动切换主要是内核态执行流之间、用户态执行流之间,还有内核态执行流退出到用户态.
用户态进入内核态的过程中,syscall 是主动的,但是中断进入内核是被动的.此外,中断嵌套(比如处理中断的过程中又发生 NMI 中断)也是被动的.
ABI & Context Switch
- Caller-saved registers
- Callee-saved registers
在主动切换时,我们只需要保存 callee-saved registers,因为 caller-saved registers 已经处于栈上了,由编译器管理.
Interruption Context Switch
注意 这里要注意的点在于:如果是中断的话,我们不能依赖 ABI 和编译器为我们保存的寄存器,而是必须完整地保存所有寄存器.原因在于,从栈的视角看,发生中断时,当前代码可能正好执行到一半,没有发生需要 context switch 的函数调用,那么我们就需要保存下所有 register 的信息并压到栈上.这里指的中断上下文切换,并不完全只是指的中断,还包括其他进入内核的方式,比如
syscall()
栈复用
在 x86 下,有个特殊的处理,如果是同一级别中断,也就是说中断处理程序的特权级和被中断执行流的特权级相同,就不会硬件切换栈.为什么这样做是可以的呢?
- kernel 态都是 OS 自己在操控,所以栈是默认可信的;
- 从语义的角度说,用户态进入内核态,语义发生的变化(执行 OS 层面操作需要新的环境),但是 kernel 态之间切换的话,语义没有改变,中断只是在当前执行流上再插入一段临时的处理逻辑;
- 当然性能也是一个 concern,不切换栈性能也有提升