第19讲 Xv6 上下文切换
(上下文切换的实现;状态机的封装与恢复)
处理器虚拟化
os是什么,是保存许多进程状态机的状态。然后选一个做调度。
为什么while(1)不会让操作系统卡死?
中断。对xv6来说,强行执行了一条ecall
切换到操作系统代码执行,操作系统决定让哪个进程继续执行。
使得cpu在时间上分割成多个cpu。
这件事究竟是如何发生的?
热身:协程
co_yield()// snapshut and switch
如果编译器会是不是帮你添加co_yield()是不是意味着你在用户态对状态机做调度,实际上是一个用户态的操作系统
每个协程有独自的stack
复习:程序的状态
寄存器
内存
虚拟化:状态机的管理
寄存器组 ($x0…$x31, $pc) 只有一份,物理内存也只有一份
- 寄存器的虚拟化:我们可以把寄存器保存到内存
- 内存的虚拟化:$satp 的数据结构
操作系统代码最重要的 invariant (假设单处理器)
- 操作系统代码开始真正 “处理” 系统调用/中断时,所有进程的状态都被 “封存” 在操作系统中
- 可以通过
struct proc
里的指针访问 (struct trapframe
) - 中断/异常处理的一小段代码需要保证这一点
- 中断返回时,把进程的状态机 “恢复” 到 CPU
- 可以通过
状态的封存:Trivial 的操作系统实现
用最直观的 “封存” 方式
- 直接都保存到内存
- 假设操作系统代码直接 “看到” 所有物理内存 (L1)
|
|
- 保存:把 x1, …, x31 保存到当前的 proc 即可
- 就满足了 “状态机封存” 的 invariant
- 恢复:把 pages 送到 $satp 对应的数据结构里
- 通常我们是把这个数据结构准备好,只要一个赋值就行
x86的中断/异常就比较麻烦了
好处,中断处理程序就简单了,只需要push保存寄存器
再次调试系统调用
ecall 指令的行为
- 关闭中断
- 复制 $pc 到 $sepc
- 设置 $sstatus 为 S-mode,$scause 为 trap 的原因 (ecall, 8)
- 跳转到 $stvec (S-mode trap vector)
究竟是什么原因让某南大计算机老师的slides上出现了抽象的UESTC的厕所照片,这究竟是人性的扭曲还是道德的沦丧。
是操作系统给进程戴了VR眼镜,给进程的地址是虚假的地址空间。
ecall 时额外的系统状态
- $satp 控制了 “虚假” 的地址空间
- 进程访问内存时仿佛戴了 VR
- $sscratch 保存了进程的 trap frame 地址
- 均由操作系统设置
Trampoline 代码完成的工作
把寄存器保存到 trap frame
- 全靠 (struct trapframe *)$sscratch 寄存器
切换到内核线程
-
堆栈切换: $sp ←
tf->kernel_sp
-
设置当前处理: $tp ←
tf->kernel_hartid
-
设置页表: $satp ←
1
tf->kernel_trap
- xv6: 与物理内存一一映射
- 通过 info mem 查看内核线程的地址空间映射
- 低位的内存是 PLIC (0xc000000) 和 UART (0x10000000)
- 物理内存一一映射 (A = Access, D = Dirty, xv6 中不使用)
-
跳转到处理程序
tf->kernel_trap
执行
调用 usertrap() 后的系统状态
所有进程都被 “封存”
- 通过
struct proc
就可以找到寄存器、内存、操作系统对象、…… - 进程对应的 “内核线程” 开始执行
- L2 - Kernel Multithreading
- 从另一个角度,“进程” 就是拥有了地址空间的线程
操作系统代码可以为所欲为
- 修改任何一个状态机
- 例如,执行系统调用
- 执行系统调用时可能发生 I/O 中断
- 将任何另一个状态机调度到处理器上 (userret)
小结:状态机的封存
在执行完 “寄存器现场保存” 之后
- 操作系统处于 “invariant 成立” 的状态
- 每个进程的状态机都被 “封存”
- 能被操作系统内核代码访问
- xv6:
struct proc
- xv6:
- 操作系统可以把任何一个状态机 “加载” 回 CPU
- 恢复寄存器和 $satp,然后 sret (保持 invariant, 包括 $scratch)
因为被封存,我们的处理器可以选择把任何一个状态机恢复
- 机制:允许在中断/异常返回时把任何进程加载回 CPU
- 策略:处理器调度 (下次课)
class: center, middle