计算机体系结构 21:页故障与映射修改

核心问题

上一篇把地址翻译分成三类结果:TLB miss 后成功、权限失败产生 page fault、翻译成功后再进入数据缓存。本篇继续追踪 page fault 之后发生什么。

一个页故障不是“硬件把问题修好”。硬件报告故障信息,软件决定要不要修复映射、如何更新页表、何时让当前 hart 的旧翻译失效、是否通知其他 hart。本文用一个有限状态轨迹回答三个问题:

  • page fault 报告了什么。
  • 软件补映射后,为什么还要本地 SFENCE.VMA
  • 多 hart 场景下,为什么本地 fence 不等于远端 shootdown。

异常处理要区分“报告、修复、重试、广播”。硬件只完成报告和控制流转移;页表修改、失效范围和远端协调是软件协议。

边界

一手依据仍然是 RISC-V Privileged Architecture 的 supervisor 规范。2026-09-22 通过可读官方 supervisor 页面核对到 Supervisor-Level ISA 1.13;固定版本 URL 当前不可直接读取,因此正文引用可读官方 URL,不把固定版本 URL 当作本轮已访问证据。相关点包括 stvecsepcscausestvalsstatus 以及 SFENCE.VMA。规范说明 satp 写入不自动提供页表更新与后续地址翻译之间的排序,也不自动清空地址翻译缓存;SFENCE.VMA 用来让当前 hart 后续隐式页表读取观察到内存中的管理结构更新。其他 hart 需要额外协调。

本文证据来自同一个有限 Python 模型:

sv32_model.py

输出附件:

sv32_trace.jsonsv32_model_stdout.json

从仓库根目录复跑:

1
python3 examples/computer-architecture/translation/src/sv32_model.py

证据等级:功能执行。

这不是实机内核陷入记录。模型没有真实 stvec 跳转、没有保存通用寄存器、没有页分配器、没有调度器、没有 IPI,也没有远端 hart 确认回执。sepc 在本文中只作为规范概念说明;模型没有执行完整指令流,因此输出中不伪造 sepc 数值。

陷入入口与上下文边界

同步 page fault 和异步中断都进入 supervisor trap 机制,但原因不同。page fault 由当前指令的访存或取指触发,软件通常需要让 sepc 指向相关指令,以便修复后重试或报告错误;异步中断来自外部事件或计时器,不代表当前指令本身地址翻译失败。

stvec 决定 trap 入口基址和模式。Direct 模式下所有 trap 进入同一个入口;Vectored 模式下,中断可按 cause 偏移到不同入口,同步异常仍进入基址入口。进入 S 模式 trap 时,sstatus 中的 SPP 记录先前特权级,SPIE/SIE 保存和关闭中断使能状态;返回时由 sret 恢复相关状态并回到 sepc

硬件不会自动保存全部通用寄存器。真正的内核入口需要软件保存将要破坏的 GPR,建立内核栈或 trap frame,再调用具体处理例程。本文模型只记录 scausestval 和教学 reason,不模拟 stvec 分派、GPR 保存、sret 或异步中断。

规范允许 stval 在某些场景为 0;因此本文 JSON 中的故障 VA 只是本模型给出的 informative trap value,不能写成所有平台所有 page fault 都必定提供非零故障地址。

页故障报告的是“这次访问不成立”

模型中的缺页地址是:

1
2
3
4
VA = 0x00c00008
VPN[1] = 3
VPN[0] = 0
offset = 0x8

根页表有 level-1 非叶子 PTE,指向二级页表 PPN 0x220,但二级叶子 PTE 缺失。第一次 load 产生:

1
2
3
4
5
6
7
8
{
"event": "page_fault",
"hart": "hart0",
"scause": "load_page_fault",
"stval": "0xc00008",
"reason": "level0_invalid_or_nonleaf",
"pte_address": "0x220000"
}

这里的信息可以分层读:

字段 含义
scause=load_page_fault 触发访问类型是 load
stval=0x00c00008 本模型提供的故障相关虚拟地址;规范允许某些情况为 0
reason=level0_invalid_or_nonleaf 教学模型给出的内部分类
pte_address=0x220000 本次遍历需要检查的二级 PTE 地址

真实硬件不需要给出本文这种 reason 字符串。它会按规范报告 trap cause 和 trap value;操作系统再结合页表、VMA、权限和进程状态判断后续动作。

page fault 是“软件处理入口”,不是“错误类型的终点”。同样是 page fault,可能是 demand paging、写时复制、栈增长、权限违规,也可能是非法地址。

修复映射后需要让当前 hart 忘掉旧翻译

软件决定修复这个缺页时,会写入一个新的叶子 PTE。模型把缺失页补成:

1
2
PPN = 0x320
flags = R|W|V|A|D

随后执行本地 SFENCE.VMA

1
2
3
4
5
6
7
{
"event": "sfence_vma_local",
"hart": "hart0",
"before": 3,
"after": 3,
"va": "0xc00008"
}

beforeafter 都是 3,并不矛盾。这个 VA 之前没有成功翻译项,所以本地 TLB 中没有对应条目可删。SFENCE.VMA 的意义不只是“删掉一个一定存在的条目”,还在于把页表内存更新与后续隐式页表读取排好序。

重试同一 load 后得到:

1
2
3
4
5
6
7
{
"event": "tlb_miss_page_walk",
"va": "0xc00008",
"pte_addresses": ["root + 3 * 4", "0x220000"],
"pa": "0x320008",
"access": "load"
}

这个轨迹的顺序是:

1
2
3
4
5
6
load VA 0x00c00008
-> page fault
-> software writes leaf PTE
-> local SFENCE.VMA(va)
-> retry load
-> page walk succeeds, PA 0x320008

它只证明模型里的软件修复路径可执行,不证明真实操作系统必然为每个 page fault 立即分配物理页。真实系统还要考虑 VMA 是否存在、权限是否允许、是否需要从磁盘读页、是否触发 OOM、是否是写时复制等。

修改已有映射时,本地成功不代表远端成功

页故障修复是“从无到有”。另一种更容易出错的情况是“已有映射被改写”。模型设置两个 hart 先访问同一个 VA:

1
2
3
VA = 0x01000000
old PPN = 0x330
new PPN = 0x331

修改前:

1
2
3
4
5
{
"event": "tlb_miss_page_walk",
"hart": "hart0",
"pa": "0x330000"
}
1
2
3
4
5
{
"event": "tlb_miss_page_walk",
"hart": "hart1",
"pa": "0x330000"
}

两个 hart 都缓存了旧翻译。随后 hart0 修改 PTE,把 PPN 从 0x330 改成 0x331,只执行本地 SFENCE.VMA。重试结果是:

1
2
3
4
{
"hart0_after_local_sfence": "0x331000",
"hart1_without_remote_sfence": "0x330000"
}

hart0 已看到新翻译,hart1 仍命中旧翻译。这个反例说明:本地 fence 的范围是当前 hart。要让其他 hart 也停止使用旧翻译,需要系统软件发起远端失效协议,并等待足够的确认点。

“改了页表”不是全系统事实;只有参与协议的 hart 才会按协议边界观察到新映射。多核正确性要问“谁知道、何时知道、是否确认”。

权限故障不是映射缺失

本文模型里还有两类权限 fault:

1
2
3
4
5
{
"reason": "store_without_write",
"scause": "store_page_fault",
"stval": "0x800004"
}

只读页被 store,修复方式不是简单补一个 PTE。软件必须判断这是不是合法写时复制、是否允许升级权限,或者应该向进程报告错误。

另一个例子是 U 模式访问 supervisor-only 页:

1
2
3
4
{
"reason": "user_to_supervisor_page",
"pte_address": "tlb_cached_pte"
}

这说明 fault 可以发生在 TLB hit 路径上。处理这类 fault 时,不能把它和“二级 PTE 缺失”混在一起。

本篇验收

  • 能解释 scausestval 在 page fault 中的作用,并说明本文没有伪造 sepc 执行流。
  • 能按顺序复述缺页修复轨迹:fault、软件写 PTE、本地 SFENCE.VMA、重试。
  • 能说明 satp 或 PTE 写入不自动完成旧翻译失效。
  • 能用两个 hart 的反例说明“本地 fence 不等于远端 shootdown”。
  • 能把映射缺失、权限 fault、远端 stale translation 分开处理。

练习

练习 1

VA=0x00c00008 第一次 load 时二级 PTE 缺失。软件补上 PPN 0x320 的 R/W 叶子 PTE 后,重试得到的 PA 是多少?

答案:offset 是 0x8,所以 PA 是:

1
0x320 * 4096 + 0x8 = 0x320008

练习 2

hart0 和 hart1 都缓存了 VA=0x01000000 -> PA=0x330000。hart0 把页表改成 PPN 0x331 并只执行本地 SFENCE.VMA。hart1 下一次 load 必然看到新 PA 吗?

答案:不必然。本文模型中 hart1 仍命中旧 TLB 项,得到 0x330000。本地 SFENCE.VMA 只处理当前 hart;远端 hart 需要额外 shootdown 协议。

参考资料