第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)

      struct page { int prot; void *va, *pa; }
      struct proc {
      uint64_t x1, x2, ... x31;
      struct page pages[MAXPAGES];
      };
      
  • 保存:把 x1, …, x31 保存到当前的 proc 即可

    • 就满足了 “状态机封存” 的 invariant
  • 恢复:把 pages 送到 \$satp 对应的数据结构里

    • 通常我们是把这个数据结构准备好,只要一个赋值就行

x86的中断/异常就比较麻烦了

好处,中断处理程序就简单了,只需要push保存寄存器

再次调试系统调用

ecall 指令的行为

  • 关闭中断
  • 复制 \$pc 到 \$sepc
  • 设置 \$sstatus 为 S-mode,\$scause 为 trap 的原因 (ecall, 8)
  • 跳转到 \$stvec (S-mode trap vector)

img

究竟是什么原因让某南大计算机老师的slides上出现了抽象的UESTC的厕所照片,这究竟是人性的扭曲还是道德的沦丧。

是操作系统给进程戴了VR眼镜,给进程的地址是虚假的地址空间。

ecall 时额外的系统状态

  • \$satp 控制了 “虚假” 的地址空间
    • 进程访问内存时仿佛戴了 VR
  • \$sscratch 保存了进程的 trap frame 地址
    • 均由操作系统设置

Trampoline 代码完成的工作

把寄存器保存到 trap frame

  • 全靠 (struct trapframe *)\$sscratch 寄存器

切换到内核线程

  • 堆栈切换: \$sp ← tf->kernel_sp

  • 设置当前处理: \$tp ← tf->kernel_hartid

  • 设置页表: \$satp ←

    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
  • 操作系统可以把任何一个状态机 “加载” 回 CPU
    • 恢复寄存器和 \$satp,然后 sret (保持 invariant, 包括 \$scratch)

因为被封存,我们的处理器可以选择把任何一个状态机恢复

  • 机制:允许在中断/异常返回时把任何进程加载回 CPU
  • 策略:处理器调度 (下次课)

class: center, middle