软/硬的分界: 虚拟

如果你啃过 OSTEP 或者什么国内的教材,那么你一定知道 Paging 这些虚拟内存相关的策略,那下面我们整理一下知识点:

  1. 操作系统 采用了 虚拟内存 的策略,这个策略实际上很类似 CPU 的 cache
  2. CPU 使用的是虚拟地址,经 页表 转化为物理地址。这种虚拟内存的方案优势是:
    1. 支持更大的虚拟地址空间,我们可能只有16G的 物理内存,但我们有一个 32位/64位的地址空间,同时,我们也能够适当的完成 swap
    2. 支持隔离,保护一个 进程 免受其他 进程 的影响
  3. 虚拟地址被分为虚拟页号(可能有数级)和页内偏移,经 转化 之后称为一份
  4. 页表 存储在 内存 中,对一个地址空间而言,每一个位置维护一个 page table entry 的内存开销过高,所以会采取多级页表的形式,减少 内存访问 的开销。实际上,很多时候页表的结构类似 radix tree。
  5. 因为 内存 的访问有巨大的 gap, 所以需要 TLB 作为一种 cache
    1. 对于多个进程而言,每个进程会有一套自己独立的地址空间,上下文切换的时候,TLB 需要 Flush 对应的表项,或者给 entry 一个 标示进程 的字段
  6. 具体访问的时候,某个 Page 可能在访问位被标记上读/写 flag,甚至会有 保护页 的存在

然后,我们还知道:

  • TLB 没有对应的 Page entry,会发生 TLB 失效。这个时候,Page 可能在内存中,需要访问加载,可能不在内存中,需要操作系统 来处理缺页异常(page fault).
    • 这需要额外的机制来中断正在运行中的活跃进程,将控制权转移到 操作系统,然后再恢复过来。

好吧,我最早考 OS 的时候觉得上面的观点都是很自然的,但是你会发现有问题就是:

  • 哪些部分是软件(OS)做的,哪些部分是硬件做的?
  • 虚拟地址是对谁而言的?
  • 进程切换 Flush TLB 这些是谁做的?

这个时候你会感觉突然不正常了起来,我们需要知道的是:

  1. OS 支持了什么
  2. 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/硬件 进行额外的支持。实际上,正常的用户态程序运行的时候,我们使用的地址、汇编中的地址都是 虚拟地址

966DE38A-A499-4BDC-8BDF-46F344EFC8CC

Supervisor 模式提供了一种简单基于页面的虚拟内存,这种方式如下所示:

43A18D7F-8D2B-4D62-8AA4-7BD8571D10CB

  1. 硬件/ISA 可以分清楚页表项,并且交给硬件进行访问
  2. 软件(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 寄存器的写操作。

D31816F2-EF17-4CFF-85B5-AD88A1CA02BB

satp 寄存器:

当在 satp 寄存器中启用了分页时,S 模式和 U 模式中的虚拟地址会以从根部遍历页表 的方式转换为物理地址。图 10.14 描述了这个过程:

  1. satp.PPN 给出了一级页表的基址,VA[31:22]给出了一级页号,因此处理器会读取 位于地址(satp. PPN × 4096 + VA[31: 22] × 4)的页表项。
  2. 该 PTE 包含二级页表的基址,VA[21:12]给出了二级页号,因此处理器读取位于地 址(PTE. PPN × 4096 + VA[21: 12] × 4)的叶节点页表项。
  3. 叶节点页表项的 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
.globl trampoline
trampoline:
.align 4
.globl uservec
uservec:
#
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# sscratch points to where the process's p->tf is
# mapped into user space, at TRAPFRAME.
#

# swap a0 and sscratch
# so that a0 is TRAPFRAME
csrrw a0, sscratch, a0

# save the user registers in TRAPFRAME
sd ra, 40(a0)
sd sp, 48(a0)
sd gp, 56(a0)
sd tp, 64(a0)
sd t0, 72(a0)
sd t1, 80(a0)
sd t2, 88(a0)
sd s0, 96(a0)
sd s1, 104(a0)
sd a1, 120(a0)
sd a2, 128(a0)
sd a3, 136(a0)
sd a4, 144(a0)
sd a5, 152(a0)
sd a6, 160(a0)
sd a7, 168(a0)
sd s2, 176(a0)
sd s3, 184(a0)
sd s4, 192(a0)
sd s5, 200(a0)
sd s6, 208(a0)
sd s7, 216(a0)
sd s8, 224(a0)
sd s9, 232(a0)
sd s10, 240(a0)
sd s11, 248(a0)
sd t3, 256(a0)
sd t4, 264(a0)
sd t5, 272(a0)
sd t6, 280(a0)

# save the user a0 in p->tf->a0
csrr t0, sscratch
sd t0, 112(a0)

# restore kernel stack pointer from p->tf->kernel_sp
ld sp, 8(a0)

# make tp hold the current hartid, from p->tf->kernel_hartid
ld tp, 32(a0)

# load the address of usertrap(), p->tf->kernel_trap
ld t0, 16(a0)

# restore kernel page table from p->tf->kernel_satp
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero

# a0 is no longer valid, since the kernel page
# table does not specially map p->tf.

# jump to usertrap(), which does not return
jr t0
# 中间被我省略了
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.

# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero

.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.

# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero

# put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
ld t0, 112(a0)
csrw sscratch, t0

好吧,我承认 reader 手册说的不是很明白,我在 trap 和 ret 中间都省略了一些代码,关于这个 sfence.vma 使用,可以参考:https://utk.instructure.com/courses/66647/files/3182543/download?verifier=JLzdkcM2uy4hNshzq9Tus0Kll1vJys8bsoFbgcl0&wrap=1

38E18604-E8F2-428B-8D5F-FDFEB55163B2

所以这里相当于切换了 satp 从内核到用户态/从内内核态到用户态之后,fence 整个地址空间。

2: 特权

RISC-V 提供了两种额外的模式:

运行最 可信的代码的机器模式(machine mode),以及为 Linux,FreeBSD 和 Windows 等操作系统 提供支持的监管者模式(supervisor mode)

41D68117-8467-417E-AD42-78503FA7754C

硬件对指令有约束,不能在 User 模式下执行特权指令。这提供了某种情况的保护,而操作系统又有这种保护的权限,而关于 ms 模式,就简要贴一下:

默认情况下,发生所有异常(不论在什么权限模式下)的时候,控制权都会被移交到 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 中:

  1. 翻译是由硬件处理的
  2. 出现缺页异常,由操作系统这样的软件来处理。

总结

有一张绝世好图:

7D933BA9-C8E0-4893-ABDD-37527C82C8B9