内存管理
RISC-V 提供了灵活的内存管理机制,支持虚拟内存和物理内存的管理。本章介绍 RISC-V 的内存管理单元(MMU)和页表机制。
内存管理概述
地址空间
RISC-V 支持两种地址空间:
- 物理地址空间:实际的硬件内存地址
- 虚拟地址空间:程序看到的逻辑地址
地址翻译
内存管理单元(MMU)负责将虚拟地址翻译为物理地址:
虚拟地址 -> MMU -> 物理地址
地址翻译模式
RISC-V 支持多种地址翻译模式,通过 satp 寄存器的 MODE 字段选择:
| MODE | 名称 | 虚拟地址位数 | 物理地址位数 |
|---|---|---|---|
| 0 | Bare | 无翻译 | - |
| 8 | Sv39 | 39 | 56 |
| 9 | Sv48 | 48 | 56 |
| 10 | Sv57 | 57 | 56 |
裸机模式(Bare Mode)
MODE = 0 时,不进行地址翻译,虚拟地址等于物理地址:
# 设置裸机模式
csrw satp, zero
Sv39 模式
Sv39 使用 39 位虚拟地址,支持 512 GB 的虚拟地址空间:
虚拟地址格式(39位):
[38:30] VPN[2] - 二级页表索引(9位)
[29:21] VPN[1] - 一级页表索引(9位)
[20:12] VPN[0] - 零级页表索引(9位)
[11:0] Offset - 页内偏移(12位)
Sv48 模式
Sv48 使用 48 位虚拟地址,支持 256 TB 的虚拟地址空间:
虚拟地址格式(48位):
[47:39] VPN[3] - 三级页表索引(9位)
[38:30] VPN[2] - 二级页表索引(9位)
[29:21] VPN[1] - 一级页表索引(9位)
[20:12] VPN[0] - 零级页表索引(9位)
[11:0] Offset - 页内偏移(12位)
页表结构
页表项(PTE)
每个页表项为 64 位,包含物理页号和权限信息:
页表项格式(64位):
[63:54] 保留
[53:10] PPN - 物理页号(44位)
[9:8] RSW - 保留给软件使用
[7] D - 脏位(Dirty)
[6] A - 访问位(Accessed)
[5] G - 全局映射
[4] U - 用户模式可访问
[3] X - 可执行
[2] W - 可写
[1] R - 可读
[0] V - 有效位
权限位说明
| 位 | 名称 | 描述 |
|---|---|---|
| V | Valid | 页表项有效 |
| R | Read | 可读权限 |
| W | Write | 可写权限 |
| X | Execute | 可执行权限 |
| U | User | 用户模式可访问 |
| G | Global | 全局映射(ASID 无关) |
| A | Accessed | 页面被访问过 |
| D | Dirty | 页面被写过 |
权限组合
| R | W | X | 描述 |
|---|---|---|---|
| 0 | 0 | 0 | 指向下一级页表 |
| 0 | 0 | 1 | 只执行 |
| 0 | 1 | 0 | 只读(保留) |
| 0 | 1 | 1 | 读执行 |
| 1 | 0 | 0 | 只读 |
| 1 | 0 | 1 | 读执行 |
| 1 | 1 | 0 | 读写 |
| 1 | 1 | 1 | 读写执行 |
多级页表
Sv39 页表结构
Sv39 使用三级页表:
虚拟地址 -> VPN[2] -> 页表2 -> VPN[1] -> 页表1 -> VPN[0] -> 页表0 -> 物理页
地址翻译流程:
- 从 satp 获取根页表地址
- 使用 VPN[2] 索引二级页表
- 获取一级页表地址
- 使用 VPN[1] 索引一级页表
- 获取零级页表地址
- 使用 VPN[0] 索引零级页表
- 获取物理页号
- 组合页内偏移得到物理地址
页表遍历示例
# Sv39 地址翻译示例
# 虚拟地址:0x0000000012345678
# 提取 VPN
# VPN[2] = (va >> 30) & 0x1FF = 0
# VPN[1] = (va >> 21) & 0x1FF = 0x91
# VPN[0] = (va >> 12) & 0x1FF = 0x345
# Offset = va & 0xFFF = 0x678
# 翻译过程
# 1. 读取 satp 获取根页表物理地址
csrr t0, satp
slli t0, t0, 12 # PPN << 12 = 物理地址
# 2. 索引二级页表
li t1, 0 # VPN[2] = 0
slli t1, t1, 3 # * 8(每个 PTE 8 字节)
add t0, t0, t1
ld t2, 0(t0) # 读取 PTE
# 3. 检查有效位
andi t3, t2, 1
beqz t3, page_fault
# 4. 获取一级页表地址
srli t0, t2, 10
slli t0, t0, 12
# 5. 索引一级页表
li t1, 0x91 # VPN[1]
slli t1, t1, 3
add t0, t0, t1
ld t2, 0(t0)
# 6. 获取零级页表地址
srli t0, t2, 10
slli t0, t0, 12
# 7. 索引零级页表
li t1, 0x345 # VPN[0]
slli t1, t1, 3
add t0, t0, t1
ld t2, 0(t0)
# 8. 获取物理页号
srli t0, t2, 10
slli t0, t0, 12
# 9. 组合偏移
ori t0, t0, 0x678 # 物理地址
TLB(Translation Lookaside Buffer)
TLB 是页表的硬件缓存,加速地址翻译。
TLB 结构
TLB 缓存虚拟页号到物理页号的映射:
+----------------+----------------+-------+
| 虚拟页号 (VPN) | 物理页号 (PPN) | 权限 |
+----------------+----------------+-------+
TLB 刷新
修改页表后需要刷新 TLB:
# 刷新所有 TLB 条目
sfence.vma zero, zero
# 刷新特定虚拟地址的 TLB 条目
sfence.vma a0, zero # a0 = 虚拟地址
# 刷新特定 ASID 的 TLB 条目
sfence.vma zero, a0 # a0 = ASID
# 刷新特定虚拟地址和 ASID 的 TLB 条目
sfence.vma a0, a1 # a0 = 虚拟地址, a1 = ASID
地址空间标识符(ASID)
ASID 用于区分不同进程的地址空间,避免切换进程时刷新整个 TLB。
ASID 配置
satp 寄存器包含 ASID 字段:
satp 寄存器:
[63:60] MODE - 地址翻译模式
[59:44] ASID - 地址空间标识符(16位)
[43:0] PPN - 根页表物理页号
设置 ASID
# 设置 ASID 和页表
li t0, (8 << 60) # Sv39 模式
ori t0, t0, (1 << 44) # ASID = 1
ori t0, t0, page_table_ppn
csrw satp, t0
内存属性
物理内存属性(PMA)
PMA 定义物理内存区域的属性:
- 是否可缓存
- 是否支持原子操作
- 访问宽度限制
物理内存保护(PMP)
PMP 提供更细粒度的内存访问控制:
PMP 寄存器
- pmpcfg0-pmpcfg15:配置寄存器
- pmpaddr0-pmpaddr63:地址寄存器
PMP 配置
pmpcfg 格式:
[7] L - 锁定位
[5:4] A - 地址匹配模式
00 = OFF(禁用)
01 = TOR(上界)
10 = NA4(4字节对齐)
11 = NAPOT(幂等对齐区域)
[2] X - 可执行
[1] W - 可写
[0] R - 可读
PMP 配置示例
# 配置 PMP 区域 0:允许所有访问
li t0, 0x1F # L=0, A=NA4, R=W=X=1
csrw pmpcfg0, t0
li t0, 0xFFFFFFFF
csrw pmpaddr0, t0
# 配置 PMP 区域 1:只读区域
li t0, (1 << 7) | (2 << 4) | 1 # L=1, A=NAPOT, R=1
csrw pmpcfg0, t0
页错误处理
页错误类型
| 异常码 | 名称 | 描述 |
|---|---|---|
| 12 | Instruction page fault | 指令页错误 |
| 13 | Load page fault | 加载页错误 |
| 15 | Store/AMO page fault | 存储页错误 |
页错误信息
发生页错误时:
- mepc(或 sepc):触发错误的指令地址
- mcause(或 scause):异常原因
- mtval(或 stval):触发错误的虚拟地址
页错误处理
page_fault_handler:
# 读取错误地址
csrr a0, mtval
# 读取错误原因
csrr a1, mcause
# 分配新页面
call allocate_page
# 更新页表
call update_page_table
# 刷新 TLB
sfence.vma a0, zero
# 返回
j trap_exit
内存管理示例
初始化页表
// C 代码
#define PAGE_SIZE 4096
#define PTE_V (1 << 0)
#define PTE_R (1 << 1)
#define PTE_W (1 << 2)
#define PTE_X (1 << 3)
#define PTE_U (1 << 4)
typedef unsigned long pte_t;
// 创建简单的恒等映射
void setup_page_table(pte_t* page_table) {
// 映射前 1MB 物理内存
for (int i = 0; i < 256; i++) {
pte_t pte = (i << 10) | PTE_V | PTE_R | PTE_W | PTE_X;
page_table[i] = pte;
}
}
启用虚拟内存
# 启用 Sv39 虚拟内存
enable_vm:
# 设置页表
la t0, page_table
srli t0, t0, 12 # 获取 PPN
li t1, (8 << 60) # Sv39 模式
or t0, t0, t1
csrw satp, t0
# 刷新 TLB
sfence.vma zero, zero
ret
小结
本章介绍了 RISC-V 内存管理:
- 地址翻译模式:Bare、Sv39、Sv48、Sv57
- 页表结构:页表项格式和权限位
- 多级页表:Sv39 三级页表翻译流程
- TLB:地址翻译缓存和刷新
- ASID:地址空间标识符
- 内存保护:PMP 物理内存保护
- 页错误处理:缺页异常处理
内存管理是操作系统内核的核心功能,理解这些机制对于开发操作系统和系统软件至关重要。