本文基于AArch64执行环境, 介绍现代操作系统中上下文切换的相关内容.
何为上下文
我们正在看一本书的时候如果被其他的事情打断, 返回时为了能够从上次被打断的位置继续读, 就要在被打断的时候记下来当前是读到了哪个第几页的第几行.
操作系统对待线程也是如此, 需要保存的用于恢复线程执行的信息就称为线程的上下文.
那么对于线程来说需要记下的内容有什么呢? 寄存器和栈即可. 拿 AArch64 架构来距离, 线程的上下文就是:
- 通用寄存器
x0-x29
: 函数调用的参数, 某些计算过程的中间值, 都要用到这些寄存器. 线程的执行流可能在任何时候被打断, 当然这些内容也不能丢. - 通用寄存器
lr(x30)
:lr
保存着返回地址, 即当前函数结束之后该返回到哪执行. - 栈顶指针
sp
: 栈的重要性无需多言. - 程序计数器
pc
: 被打断的线程如果再次执行, 从哪里执行呢? 显然是被打断指令的下一条(或者重新执行当前). 这个指令的地址当然也需要被保存好. - PSTATE: 想一下, 有了以上的内容就能够保证线程完整的恢复之前的环境吗? 其他的例如中断是开还是关, 有哪些标志位(NZCV)被设置了. 这些信息在 AArch64 中是保存在 PSTATE 的各个字段中.
- ttbr0:保存着进程的页表
上下文保存和恢复
TODO
协程的上下文
协程是用户级别的线程,
- 协程之间的切换不进入内核
- 切换协程只能是某个协程主动放弃控制权
我们在这里讨论一下协程切换时需要保存的上下文是否与线程有所不同.
首先, PC 一定属于, 这个毋庸置疑. 其次是栈顶指针 sp, 每个协程都有单独的栈, 如果不保存栈的位置, 那么协程内部定义局部变量就没法访问了(局部变量的访问指令都是以 sp 为 base 的偏移来做的).
另外, 关于通用寄存器, 由于协程的切换需要主动调用某个函数(通常叫做yield()
), 在函数的最后将 PC 设置为新协程的上下文 PC. 保存当前协程上下文的操作也在这个函数中, 而其参数我们并不关心, 即x0
-x7
没必要保存. 同样的, caller-saved 寄存器也是没必要保存的, 因为这些寄存器作为函数调用使用的临时变量, 当再次返回该协程时, PC=yield()返回地址, caller 如果关心这些寄存器应当自己执行保存和恢复. 但是callee-saved 寄存器必须要保存到上下文中, 因为在 yield()中, 我们如果修改了 callee-saved 寄存器, 就需要在返回时(也就是再次调度到该协程时) 恢复, 这是 callee 该做的, 也就是上下文中应该有的唯一通用寄存器组.