What is Context Switch

Execution Flow and Context

我们先看什么是执行流 execution flow,execution flow 是一个抽象的概括,包含两个维度:

  1. CPU 寄存器中的值随着时间而变化
  2. execution flow 所属的栈随着 flow 的变化,不断进行压栈、退栈

那么上下文就是 execution flow 中某一时刻的瞬时快照/切片.记录的是

  • 各个寄存器的值
  • SP 指针当前的值,用于指明栈在当前时刻的状况

反过来,一连串连续的上下文构成了一个执行流.

从 stack 的视角来看,一个基本的上下文切换流程是:

  • 保存当前 execution flow 的寄存器状态
  • 切换栈指针
  • 恢复另一个 execution flow 的寄存器状态

Categorization of Context Switch

所有的上下文切换不尽相同,这种不同主要是由不同执行流类型之间的切换导致的.我们在用户态和内核态下都存在多个线程/进程执行流.所以我们可以大致区分出四类 context switch

  1. 内核态执行流之间的切换
  2. 用户态执行流之间的切换
  3. 用户态进入内核态,内核态退出到用户态
  4. 中断嵌套
主动切换、被动切换

主动切换主要是内核态执行流之间、用户态执行流之间,还有内核态执行流退出到用户态.

用户态进入内核态的过程中,syscall 是主动的,但是中断进入内核是被动的.此外,中断嵌套(比如处理中断的过程中又发生 NMI 中断)也是被动的.

ABI & Context Switch

  • Caller-saved registers
  • Callee-saved registers

在主动切换时,我们只需要保存 callee-saved registers,因为 caller-saved registers 已经处于栈上了,由编译器管理.

Interruption Context Switch

这里指的中断上下文切换,并不完全只是指的中断,还包括其他进入内核的方式,比如 syscall()

注意 这里要注意的点在于:如果是中断的话,我们不能依赖 ABI 和编译器为我们保存的寄存器,而是必须完整地保存所有寄存器.原因在于,从栈的视角看,发生中断时,当前代码可能正好执行到一半,没有发生需要 context switch 的函数调用,那么我们就需要保存下所有 register 的信息并压到栈上.

栈复用

x86 下,有个特殊的处理,如果是同一级别中断,也就是说中断处理程序的特权级和被中断执行流的特权级相同,就不会硬件切换栈.为什么这样做是可以的呢?

  1. kernel 态都是 OS 自己在操控,所以栈是默认可信的;
  2. 从语义的角度说,用户态进入内核态,语义发生的变化(执行 OS 层面操作需要新的环境),但是 kernel 态之间切换的话,语义没有改变,中断只是在当前执行流上再插入一段临时的处理逻辑;
  3. 当然性能也是一个 concern,不切换栈性能也有提升