一、背景说明
以下内容为 RISC-V 处理器内存管理单元(MMU)设计中的实践经验分享,重点关注相关概念和工程实现问题。
二、SATP 寄存器与 Sv39 地址翻译
2.1 SATP 寄存器结构
SATP(Supervisor Address Translation and Protection)是 RISC-V 中控制地址翻译的核心 CSR 寄存器(64 位 Sv39 模式):
| 字段 | 位宽 | 说明 |
|---|---|---|
| MODE | 高 4 位 | 地址翻译模式,Sv39 下为 8(bare 为 0) |
| ASID | 16 位 | 地址空间标识符,最多支持 65536 个地址空间 |
| PPN | 低 44 位 | 第一级页表的物理页号(基地址) |
一次写入需将整个 64 位 SATP 值写入。PPN 乘上页大小(4KB)即得到第一级页表在物理内存中的起始地址。
2.2 WARL 机制
WARL(Write Any, Read Legal):对某些 CSR 字段,写入任意值都不会报错,但读回时永远返回合法值。对于不支持的写入值,硬件保持原有值不变。学校 Lab 4 中 mask 的概念与此一致——只有经过 mask 的数据才能真正写入寄存器。
之所以设计 WARL,是为了规范 CSR 的权限检查——硬件不必对每个非法写入都报异常,简化了实现。
2.3 MODE 和 ASID
- Sv39 模式下:MODE 字段只有 0(bare,无地址翻译)和 8(Sv39)两种合法值
- 可通过 mask 只允许第 3 位可更改(0 ↔ 8)
- 仅在 S 模式(Supervisor/内核模式)或 U 模式(User/用户模式)下 SATP 激活
- ASID 用完(16 位耗尽):需回收旧 ASID,此时必须执行 SFENCE.VMA 来刷掉旧 ASID 对应的 TLB 条目
三、Sv39 多级页表遍历(PTW)
3.1 为什么需要多级页表
RISC-V Sv39 使用 39 位虚拟地址(27 位 VPN + 12 位页内偏移),44 位物理地址(34 位 PPN + 12 位页内偏移)。
如果直接做 27 位到物理地址的一对一映射,需要一个 $2^{27}$ 条目的大表——约 128MB,远超可接受范围。内存才多大?这表放进去直接就疯了。
解决思路:分级。Sv39 将 27 位 VPN 分成三个 9 位片段(VPN[2]、VPN[1]、VPN[0]),每级 512 个条目($2^9 = 512$),构成三级页表。
3.2 三级页表遍历流程
- 从 SATP.PPN 获得第一级(L2)页表基地址
- 用 VPN[2](高 9 位)在 L2 页表中索引 → 得到 L1 页表的 PPN
- 用 VPN[1] 在 L1 页表中索引 → 得到 L0 页表的 PPN
- 用 VPN[0] 在 L0 页表中索引 → 得到最终物理页框的 PPN
- PPN + 页内偏移 = 最终物理地址
核心思想:用多级索引将 $O(2^{27})$ 的大表问题转化为三级 $O(512)$ 的小表问题——相当于取了一个 log 运算。程序实际使用的地址空间往往稀疏,多级页表只需要维护实际使用到的部分。
3.3 页大小与对齐
Sv39 使用 4KB 页($2^{12}$ 字节),因此虚拟地址低 12 位和物理地址低 12 位完全相同(页内偏移无需翻译)。地址翻译实质上是在虚拟页和物理页之间建立映射。
四、PTW 状态机设计
4.1 状态定义
PTW(Page Table Walker)状态机设计:
| 状态 | 含义 |
|---|---|
| IDLE(空闲) | PTW 空闲,等待翻译请求 |
| WAIT(等待) | 已向总线发出内存请求,等待总线返回 PTE 数据 |
| PROCESS(处理) | 总线返回数据后,分析 PTE 是否合法、是否指向下一级页表、是否需要更新 A/D 位 |
| DATA_RETURN(完成) | 三级页表遍历完成,返回最终翻译结果给流水线 |
| FAULT_RETURN(异常返回) | 翻译过程中出现异常,将虚拟地址、PTE 和异常信息返回流水线 |
4.2 关键设计细节
内存请求重发:总线同时只能处理一个请求。有可能 MMU 发出请求时总线忙(bus full)——此时请求实际上没有发出去,需要重发。因此从 IDLE 到 WAIT 之间需要重发判断:如果总线忙,就一直在 IDLE 等待。
级数计数:需要记录当前访问到第几级页表。如果已经到了最后一级(L0),但 PTE 还指向下一级页表,应报异常(页表遍历终止条件)。
三级页表的遍历顺序:没有 TLB 时,必须依次访问三级页表,共三次内存访问。三次访问可能被中断(总线被占用),此时状态机保留当前状态,等待重发。
关于访问三次内存能否保证不被中断:在简化实现中可以停掉流水线前端的取指,保证连续三次访问。但实际情况中可能被打断——总线满就打回去重发;如果是一般中断(指令集层面),需要从头再来。状态机的存在就是为了记录当前状态以应对这些情况。
4.3 A/D 位更新
- A 位(Accessed):任何页访问时硬件自动置 1
- D 位(Dirty):store 操作时硬件自动置 1
更新 A/D 位时,需要将从内存读到的 PTE 整个修改后直接写回内存(不是只写到 TLB)。因为 TLB 是只读的,不维护反向同步。
一旦页表被更新(A/D 位修改),RISC-V 规范要求执行 SFENCE.VMA 来刷新相关的 TLB 条目。因此硬件不需要在内存和 TLB 之间进行一致性维护——TLB 只管缓存,页表修改后通过 SFENCE.VMA 整体失效即可。
五、TLB 与 SFENCE.VMA
5.1 TLB 基本概念
TLB(Translation Lookaside Buffer):页表遍历的硬件缓存。当首次将虚拟地址翻译为物理地址后,将映射关系缓存到 TLB 中。后续访问同一页时不必重复遍历三级页表。
做了一次 MMU 地址翻译之后把结果存起来,以后同一页的访问直接查 TLB 就行——不需要再多次访问内存。
5.2 TLB 的设计特性
- 只读:TLB 不写回内存。因为页表修改后通过 SFENCE.VMA 主动刷掉,无需反向同步
- 每核私有:TLB 不在多核之间共享,简化了一致性维护
- 覆盖范围:如果物理内存空间较小,TLB 可以完全覆盖所有物理页,此时 MMU 几乎只需访问 TLB
TLB 只读这个设计,一旦要扩展到多核,难度马上暴增。
5.3 SFENCE.VMA 指令
SFENCE.VMA(Supervisor Fence Virtual Memory Address)用于刷新 TLB(使 TLB 条目失效)。
根据 rs1、rs2 的不同取值:
| rs1 | rs2 | 效果 |
|---|---|---|
| X0 | X0 | 失效所有地址空间中的所有虚拟地址映射 |
| X0 | ≠ X0 | 失效 rs2 指定地址空间中的所有映射 |
| ≠ X0 | X0 | 失效所有地址空间中当前虚拟地址的映射 |
| ≠ X0 | ≠ X0 | 失效 rs2 指定地址空间中指定虚拟地址的映射 |
反直觉的情况(rs1 ≠ X0, rs2 = X0):为什么一个虚拟地址会出现在多个地址空间中?一个典型例子是 printf 这样的库函数——内核可能将同一块物理内存映射到所有进程的地址空间中,使所有进程共享同一份代码。此时 PTE 的 G 位(Global)置 1。普通 SFENCE.VMA 不会失效 Global 映射,需要用 rs2 = X0 来刷除。
六、MXR 与 SUM 标志
6.1 MXR
MXR(Make eXecutable Readable):在 sstatus.MXR = 1 时,允许对仅允许执行(Execute-only)的页进行读取。反之(MXR = 0),read=0 但 execute=1 的页只能取值不能读取数据。
6.2 SUM
SUM(permit Supervisor User Memory access):在 sstatus.SUM = 1 时,允许 S 模式(内核)访问 U 模式(用户)的页。
使用场景:操作系统内核有时需要访问用户程序的内存空间(如系统调用参数传递),如果不置 SUM 位,硬件会触发异常。
6.3 PTE 的 U 位
PTE 中的 U 位:= 1 表示只有用户程序可访问此页,S 模式不可访问(除非 SUM = 1)。
七、异常处理机制
7.1 Trap = 异常 + 中断
RISC-V 将异常(Exception,同步事件,如缺页、非法访问)和中断(Interrupt,异步事件,如外设中断)统称为 Trap。
7.2 异常处理的本质
异常处理看起来复杂,实质上就是写寄存器 + 跳转两件事。
- 写寄存器:将异常相关信息写入 CSR(发生了什么、在哪里发生、什么原因)
- 跳转:PC 跳转到异常处理程序的入口地址
7.3 关键 CSR 寄存器
| 寄存器 | 含义 |
|---|---|
| xEPC(Exception PC) | 发生异常的指令的 PC 值。异常处理完成后,通过 MRET/SRET 回到此地址重新执行 |
| xCAUSE | 异常原因编号(如 12 = 指令缺页,13 = Load 缺页,15 = Store/AMO 缺页) |
| xTVAL | 与异常相关的虚拟地址(Load/Store 时为此指令访问的虚拟地址,取值异常时为此指令的虚拟地址) |
| xtvec(Trap Vector Base) | 异常/中断处理程序的入口基地址 |
其中 $x$ 表示特权级别:M 为机器模式(Machine),S 为监管者模式(Supervisor)。
7.4 异常向量表与入口跳转
RISC-V 支持两种 Trap 跳转模式:
- Direct 模式(异常):所有异常直接跳转到 xtvec 指定的基地址(PC = base)
- Vectored 模式(中断):跳转到
base + 4 × cause(根据中断编号自动偏移)
为什么异常和中断分开?异常更紧急,需要统一入口处理;中断更多来自外设,用户可能需要自定义服务程序,因此支持向量化跳转。
7.5 与地址翻译相关的异常类型
| 操作 | Page Fault | Access Fault |
|---|---|---|
| 取指(Instruction) | 12 | 1 |
| Load | 13 | 5 |
| Store/AMO | 15 | 7 |
- Page Fault:PTE 的 V 位 = 0(页不在内存,在磁盘中),由操作系统处理缺页
- Access Fault:PTE 权限检查失败(PMA/PMP 违规等)
若同时满足多个异常条件,按优先级上报。
八、缓存与多核漫谈
8.1 L1/L2 Cache 的角色
L1 Cache 是每个核私有的,L2 是多核共享的。L1 miss 代价还能接受(去 L2 找),但 L2 miss 直接去主存性能下降非常大。所以 L2 宁可提高复杂度(全相联)也要确保命中率。
8.2 多核缓存一致性
MESI 协议:多核缓存一致性最经典的协议。
- L1 私有 → 不同核的 L1 不能直接交换数据
- L2 共享 → 多核通过 L2 实现数据同步
- 内存一致性(Memory Consistency)在共享 L2 层面实现
多核一致性难度极大——一个人写一个人读还好,两个人一起写就得好好设计。后续课程会详细讲解 MESI 协议。
8.3 关于 AI 辅助硬件设计
大模型能辅助写硬件模块(如 ALU、Register File),但需要工程师分解好任务、定义好接口信号。不能让大模型”一句话设计一个 CPU”——必须把框架定好、约束给足、验证规则写清楚。大模型输出是概率性的,错的时候”冷不丁就错”,输出文本却”像模像样”——判断正确性很困难。
课程事务
- 已实现的部分:五级流水线、缓存(含分支预测冲刷时的锁存问题修复)、类 TPU 矩阵乘法协处理器
- Lab 涉及页表遍历(PTW)、TLB、CSR 特权指令、PMP/PMA 检查
- 多核 MESI 的实验设计仍在讨论中(硬件实现挑战大,可能先做软件模拟)