软/硬的分界: 虚拟
如果你啃过 OSTEP 或者什么国内的教材,那么你一定知道 Paging 这些虚拟内存相关的策略,那下面我们整理一下知识点:
- 操作系统 采用了 虚拟内存 的策略,这个策略实际上很类似 CPU 的 cache
- CPU 使用的是虚拟地址,经 页表 转化为物理地址。这种虚拟内存的方案优势是:
- 支持更大的虚拟地址空间,我们可能只有16G的 物理内存,但我们有一个 32位/64位的地址空间,同时,我们也能够适当的完成 swap
- 支持隔离,保护一个 进程 免受其他 进程 的影响
- 虚拟地址被分为虚拟页号(可能有数级)和页内偏移,经 转化 之后称为一份
- 页表 存储在 内存 中,对一个地址空间而言,每一个位置维护一个 page table entry 的内存开销过高,所以会采取多级页表的形式,减少 内存访问 的开销。实际上,很多时候页表的结构类似 radix tree。
- 因为 内存 的访问有巨大的 gap, 所以需要 TLB 作为一种 cache
- 对于多个进程而言,每个进程会有一套自己独立的地址空间,上下文切换的时候,TLB 需要 Flush 对应的表项,或者给 entry 一个 标示进程 的字段
- 具体访问的时候,某个 Page 可能在访问位被标记上读/写 flag,甚至会有 保护页 的存在
然后,我们还知道:
- TLB 没有对应的 Page entry,会发生 TLB 失效。这个时候,Page 可能在内存中,需要访问加载,可能不在内存中,需要操作系统 来处理缺页异常(page fault).
- 这需要额外的机制来中断正在运行中的活跃进程,将控制权转移到 操作系统,然后再恢复过来。
好吧,我最早考 OS 的时候觉得上面的观点都是很自然的,但是你会发现有问题就是:
- 哪些部分是软件(OS)做的,哪些部分是硬件做的?
- 虚拟地址是对谁而言的?
- 进程切换 Flush TLB 这些是谁做的?
这个时候你会感觉突然不正常了起来,我们需要知道的是:
- OS 支持了什么
- ISA 支持了什么,硬件要做什么
然后,OS 以 xv6 的 RISC-V 版本为例,ISA 以 RISC-V 为例,去看看问题到底是什么样子的。
最后,在具体讨论之前,需要注意的是:操作系统 和 ISA 的分界并不是平坦的,很多东西即可以软件做,又可以硬件做(但是很多时候在 ISA 上提供切口大概会方便很多)。
Review
我们在这个 Part 其实很早就聊过 OS 应该做什么: https://zhuanlan.zhihu.com/p/150571417
memory translation
- 每个运行的进程需要从 virtual 的进程转成物理内存
- program 实际处理的是虚拟内存,需要硬件支持
protection and privilege
- 需要提供 user 和 supervisor 的模式,在 RISC-V 中有 machine 和 supervisor 模式
更低的层次不能修改 memory mapping
- supervisor 可以
- supervisor 有独立于用户程序的 virtual 到 physical 映射
提供 traps 和 interrupt,在命令层面提供了到 supervisor 的方法
也就是说:
1: 地址
指令中给的有可能是 虚拟地址,也有可能是物理地址,需要 ISA/硬件 进行额外的支持。实际上,正常的用户态程序运行的时候,我们使用的地址、汇编中的地址都是 虚拟地址
Supervisor 模式提供了一种简单基于页面的虚拟内存,这种方式如下所示:
- 硬件/ISA 可以分清楚页表项,并且交给硬件进行访问
- 软件(OS)也可以进行对应的设置,在内存中操作,处理缺页的异常。
而是否 enable 虚拟内存这种机制,取决于 satp
寄存器:
一个叫 satp(Supervisor Address Translation and Protection,监管者地址转换和保护) 的 S 模式控制状态寄存器控制了分页系统。如图 10.12 所示,satp 有三个域。MODE 域可 以开启分页并选择页表级数,图 10.13 展示了它的编码。ASID(Address Space Identifier, 地址空间标识符)域是可选的,它可以用来降低上下文切换的开销。最后,PPN 字段保存 了根页表的物理地址,它以 4 KiB 的页面大小为单位。通常 M 模式的程序在第一次进入 S 模式之前会把零写入 satp 以禁用分页,然后 S 模式的程序在初始化页表以后会再次进行 satp 寄存器的写操作。
而 satp
寄存器:
当在 satp 寄存器中启用了分页时,S 模式和 U 模式中的虚拟地址会以从根部遍历页表 的方式转换为物理地址。图 10.14 描述了这个过程:
- satp.PPN 给出了一级页表的基址,VA[31:22]给出了一级页号,因此处理器会读取 位于地址(satp. PPN × 4096 + VA[31: 22] × 4)的页表项。
- 该 PTE 包含二级页表的基址,VA[21:12]给出了二级页号,因此处理器读取位于地 址(PTE. PPN × 4096 + VA[21: 12] × 4)的叶节点页表项。
- 叶节点页表项的 PPN 字段和页内偏移(原始虚址的最低 12 个有效位)组成了最终结果: 物理地址就是(LeafPTE. PPN × 4096 + VA[11: 0])
此外,关于之前的 TLB 的问题,指令提供了对应的支持:
这意味着如果操 作系统修改了页表,那么这个缓存会变得陈旧而不可用。S 模式添加了另一条指令来解决 这个问题。这条 sfence.vma 会通知处理器,软件可能已经修改了页表,于是处理器可以 相应地刷新转换缓存。它需要两个可选的参数,这样可以缩小缓存刷新的范围。一个位于 rs1,它指示了页表哪个虚址对应的转换被修改了;另一个位于 rs2,它给出了被修改页表 的进程的地址空间标识符(ASID)。如果两者都是 x0,便会刷新整个转换缓存。
和下面这段
sfence.vma 仅影响执行当前指令的 hart 的地址转换硬件。当 hart 更改了另一个 hart 正在使 用的页表时,前一个 hart 必须用处理器间中断来通知后一个 hart,他应该执行 sfence.vma 指令。这个过程通常被称为 TLB 击落。
我们可以在 xv6 看到相关的代码:
1 | .globl trampoline |
好吧,我承认 reader 手册说的不是很明白,我在 trap 和 ret 中间都省略了一些代码,关于这个 sfence.vma 使用,可以参考:https://utk.instructure.com/courses/66647/files/3182543/download?verifier=JLzdkcM2uy4hNshzq9Tus0Kll1vJys8bsoFbgcl0&wrap=1
所以这里相当于切换了 satp
从内核到用户态/从内内核态到用户态之后,fence 整个地址空间。
2: 特权
RISC-V 提供了两种额外的模式:
运行最 可信的代码的机器模式(machine mode),以及为 Linux,FreeBSD 和 Windows 等操作系统 提供支持的监管者模式(supervisor mode)
硬件对指令有约束,不能在 User 模式下执行特权指令。这提供了某种情况的保护,而操作系统又有这种保护的权限,而关于 m
和 s
模式,就简要贴一下:
默认情况下,发生所有异常(不论在什么权限模式下)的时候,控制权都会被移交到 M 模式的异常处理程序。但是 Unix 系统中的大多数例外都应该进行 S 模式下的系统调 用。M 模式的异常处理程序可以将异常重新导向 S 模式,但这些额外的操作会减慢大多数 异常的处理速度。因此,RISC-V 提供了一种异常委托机制。通过该机制可以选择性地将中 断和同步异常交给 S 模式处理,而完全绕过 M 模式。
mideleg(Machine Interrupt Delegation,机器中断委托)CSR 控制将哪些中断委托给 S 模式。
所以实际上,这种模式是由硬件来保证的。
RISC-V 提供了一组 CSR 寄存器,如 https://zhuanlan.zhihu.com/p/150571417 所示,帮助标示和完成异常、恢复上下文。
实际上,触发 trap 的时候,硬件需要把现在运行的指令记录到 mepc
, 然后 stop 整个流水线,完成之前所说的异常处理。
实际上,回到之前页表,我们可以看到,xv6 中:
- 翻译是由硬件处理的
- 出现缺页异常,由操作系统这样的软件来处理。
总结
有一张绝世好图: