本篇定位 :\[06-内存 PMP-MMU-RVWMO] 的深度展开。06 是"知道有什么、怎么配",本篇是"为什么、形式化、边界、硬件细节 "。补充PMA、 PMP 增强(ePMP/mseccfg)、Cache 架构与 CMO 扩展、MMU 的 A/D 位/TLB/sfence.vma/大页、RVWMO 三公理与 fence 位详解、原子操作 reservation set 与 aq/rl。
读完你能:调 PMA 相关的诡异 fault、用 ePMP/mseccfg 做安全隔离、配 CMO 做 cache 维护、看懂页表 A/D 位管理策略、说清 RVWMO 三公理、写正确的 LR/SC 自旋锁。
目录
- 一、内存访问的完整路径(先建全景)
- [二、PMA(Physical Memory Attributes)](#二、PMA(Physical Memory Attributes))
-
- [2.1 PMA 是什么](#2.1 PMA 是什么)
- [2.2 PMA 检查的属性清单](#2.2 PMA 检查的属性清单)
- [2.3 Memory type 三类(最核心)](#2.3 Memory type 三类(最核心))
-
- [2.3.1 视角一:按"缓存行为"分(cache / no-cache / device)](#2.3.1 视角一:按"缓存行为"分(cache / no-cache / device))
- [2.3.2 视角二:按"地址映射完整性"分(Main Memory / I/O / Empty)](#2.3.2 视角二:按"地址映射完整性"分(Main Memory / I/O / Empty))
-
- [(1) Main Memory(主存)](#(1) Main Memory(主存))
- [(2) I/O Memory(I/O 内存)](#(2) I/O Memory(I/O 内存))
- [(3) Empty(空区)](#(3) Empty(空区))
- [2.4 PMA 触发的 fault](#2.4 PMA 触发的 fault)
- [2.5 PMA 怎么查 / 怎么知道](#2.5 PMA 怎么查 / 怎么知道)
- [2.6 PMA vs PMP(易混,必须分清)](#2.6 PMA vs PMP(易混,必须分清))
- [2.7 PMA 对你 M-mode 工作的实际影响](#2.7 PMA 对你 M-mode 工作的实际影响)
- [三、PMP 增强:ePMP / mseccfg](#三、PMP 增强:ePMP / mseccfg)
-
- [3.1 基础 PMP 的局限(回顾 + 暴露问题)](#3.1 基础 PMP 的局限(回顾 + 暴露问题))
- [3.2 mseccfg(Machine Security Configuration)CSR](#3.2 mseccfg(Machine Security Configuration)CSR)
- [3.3 MML:让 PMP 约束 M-mode](#3.3 MML:让 PMP 约束 M-mode)
- [3.4 MMWP:M-mode 白名单](#3.4 MMWP:M-mode 白名单)
- [3.5 ePMP 的 E 位(Entry)](#3.5 ePMP 的 E 位(Entry))
- [3.6 安全场景应用(AEGIX 方向)](#3.6 安全场景应用(AEGIX 方向))
-
- [3.7 ePMP 配置示例(安全启动后)](#3.7 ePMP 配置示例(安全启动后))
- [四、Cache 架构与 CMO 扩展](#四、Cache 架构与 CMO 扩展)
-
- [4.1 Cache 层次](#4.1 Cache 层次)
- [4.2 Cache 问题(06 的概念深化)](#4.2 Cache 问题(06 的概念深化))
- [4.3 Cache 维护操作(术语要分清)](#4.3 Cache 维护操作(术语要分清))
-
- [4.4 CMO 扩展(Zicbom / Zicboz / Zicbop)⭐](#4.4 CMO 扩展(Zicbom / Zicboz / Zicbop)⭐)
- [4.5 DMA 一致性三种模式(深化 06)](#4.5 DMA 一致性三种模式(深化 06))
- [4.6 自修改代码与 fence.i](#4.6 自修改代码与 fence.i)
- [五、MMU 深入(超越 06 的 Sv39 概念)](#五、MMU 深入(超越 06 的 Sv39 概念))
-
- [5.1 页表模式全表](#5.1 页表模式全表)
- [5.2 页表项位段(Sv39,深化 06)](#5.2 页表项位段(Sv39,深化 06))
- [5.3 A/D 位管理(06 没讲的关键)⭐](#5.3 A/D 位管理(06 没讲的关键)⭐)
-
- [(1) 硬件管理](#(1) 硬件管理)
- [(2) 软件管理](#(2) 软件管理)
- [5.4 TLB 与 sfence.vma](#5.4 TLB 与 sfence.vma)
- [5.5 ASID(Address Space ID)](#5.5 ASID(Address Space ID))
- [5.6 大页(Superpages)](#5.6 大页(Superpages))
- [5.7 satp 切换与模式切换](#5.7 satp 切换与模式切换)
- [六、RVWMO 形式化](#六、RVWMO 形式化)
-
- [6.1 RVWMO 的三层模型](#6.1 RVWMO 的三层模型)
- [6.2 PPO(保序规则)](#6.2 PPO(保序规则))
- [6.3 三公理](#6.3 三公理)
-
- [(1) Load Value Axiom(取值公理)](#(1) Load Value Axiom(取值公理))
- [(2) Atomicity Axiom(原子性公理)](#(2) Atomicity Axiom(原子性公理))
- [(3) Progress Axiom(前进公理)](#(3) Progress Axiom(前进公理))
- [6.4 fence 指令位详解(06 跳过的细节)](#6.4 fence 指令位详解(06 跳过的细节))
- [6.5 fence 的经典用法(配对模式)](#6.5 fence 的经典用法(配对模式))
- [6.6 I/O 内存 vs Main Memory 的 ordering](#6.6 I/O 内存 vs Main Memory 的 ordering)
- 七、原子操作深化
-
- [7.1 LR/SC 的 reservation set](#7.1 LR/SC 的 reservation set)
- [7.2 LR/SC 的使用约束(规范要求)](#7.2 LR/SC 的使用约束(规范要求))
-
- [7.3 标准自旋锁(带约束)](#7.3 标准自旋锁(带约束))
- [7.4 amo 的 aq/rl 位](#7.4 amo 的 aq/rl 位)
- [7.5 原子操作 vs fence 的选择](#7.5 原子操作 vs fence 的选择)
- [八、内存 ordering 决策总表(深化 06)](#八、内存 ordering 决策总表(深化 06))
- [九、M-mode 程序员的内存清单](#九、M-mode 程序员的内存清单)
- 十、本篇小结
- 速查表
一、内存访问的完整路径(先建全景)
理解 PMA,先看一次 load/store 从 CPU 发出到内存,经过的全部层次:
CPU 执行 load/store/amo 指令
│ 生成虚拟地址 VA(S/U 模式)或物理地址 PA(M 模式)
▼
┌─────────────────────────────────────────┐
│ ① 地址翻译(MMU,仅 S/U + satp≠Bare) │ VA → PA
│ 查 TLB → 未命中查页表 → 页 fault? │ (mcause 12-15)
└─────────────────────────────────────────┘
│ 物理地址 PA
▼
┌─────────────────────────────────────────┐
│ ② PMA 检查(硬件,所有模式含 M) │ ★最底层
│ 内存类型?权限?原子性?缓存性? │ (mcause 4/5/6/7)
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ ③ PMP 检查(S/U 模式;M 模式看 L/mseccfg) │ 软件配的围栏
│ 该区允许 R/W/X? │ (mcause 1/3/5/7)
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ ④ Cache 查找(D-Cache,若 cacheable) │ 命中则返回
│ miss → 总线访问 │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ ⑤ 总线访问(AXI/AHB) │
│ 到达 Main Memory 或 I/O 设备 │
└─────────────────────────────────────────┘
三个关键认知:
- PMA 在 PMP 之前 :PMP 检查的是"特权允不允许",PMA 检查的是"物理上能不能"。PMP 不能授予 PMA 没有的权限。
- M-mode 也受 PMA 约束 :M-mode 绕得过 PMP(默认),但绕不过 PMA------往 Empty 区发 load 照样 access fault,对不支持 amo 的 I/O 区发 amoadd 照样挂。PMA 是比 PMP 更底层的"物理事实"。
- PMA 硬件固定不可改(通常):不像 PMP 有 CSR 可配,PMA 由 SoC 设计连死,你只能查手册知道它,不能改它(少数核有 PMA 配置寄存器,软件可以配置)。
嵌入式视角:M-mode "全权"的边界
你在 06 篇读到"M-mode 全权访问物理内存"------这话要加限定:全权 = 不受 PMP 限制 ,但 PMA 这层你照样过 。你往一个没接外设的保留地址发读,硬件直接 access fault,M-mode 也救不了。调地址错类的死机,先怀疑 PMA(地址写错/区类型错),再怀疑 PMP。
二、PMA(Physical Memory Attributes)
PMA 是 RISC-V 内存模型最底层的一层,定义每个物理地址区域的属性。这是 RISC-V 规范 Privileged ISA 第 3.1 节的核心内容,也是你点名要补的。
2.1 PMA 是什么
- PMA = 物理地址空间的属性表:把整个 PA 空间划分成若干区,每区有固定属性
- 谁定义:SoC 设计者(芯片流片时连死),不是 CPU 核自己
- 谁检查:PMA checker 模块(在总线和核之间),每次访存自动检查
- 可改吗:通常不可;少数核(SiFive 等)有 PMA 配置寄存器可运行时改
- 为什么需要:不同地址区物理性质不同------DDR 内存能缓存能推测访问,设备寄存器有副作用不能缓存不能推测,空区根本没东西。CPU 需要知道这些,才能用对的访问策略
厂商扩展:部分实现支持软件配置
但部分厂商在标准基础上做了扩展,确实允许软件配置 PMA:
芯来科技 :内核架构提供了 mattrii_base 和 mattrii_mask 寄存器,允许软件修改 PMA 属性,但有优先级规则------只能从低优先级属性修改为高优先级属性,不能反向放宽。
Andes V5:通过 CSR 寄存器(pmacfg + pmaaddr)支持软件配置 PMA,地址匹配模式类似 PMP 的 NAPOT 模式,Zephyr RTOS 中甚至提供了 CONFIG_SOC_ANDES_V5_PMA 配置选项来启用这一功能。
2.2 PMA 检查的属性清单
RISC-V 规范把 PMA 分"必须检测 "和"可选检测"两类:
| 属性 | 含义 | 必须? | 你的关注点 |
|---|---|---|---|
| Memory type | Main Memory / I/O / Empty | ✅ 必须 | 决定能不能缓存、能不能推测 |
| R/W/X 权限 | 该区物理上可读/写/执行 | ✅ 必须 | PMA 级权限,与 PMP 不同 |
| Misaligned 支持 | 该区支持非对齐访问? | ✅ 必须 | 不支持则 misaligned fault |
| Atomicity (AMO) | 支持 LR/SC?支持哪些 amo? | 部分 | 多核同步、无锁队列关键 |
| AMO 粒度 | 支持的最小 amo 访问大小 | 可选 | 4/8 字节? |
| Cacheability | 可缓存? | 可选 | I/O 通常不可缓存 |
| Coherence | 在一致性域内? | 可选 | 多核 DMA 一致性 |
| Access size | 支持的访问宽度 | 可选 | 8/16/32/64 位? |
| Memory model | 该区遵循 RVWMO 还是强序 | 可选 | I/O 强序,Main 遵循 RVWMO |
2.3 Memory type 三类(最核心)
2.3.1 视角一:按"缓存行为"分(cache / no-cache / device)
| 类型 | 缓存 | 幂等 | 排序 | 典型用途 |
|---|---|---|---|---|
| Cacheable(可缓存内存) | ✅ | ✅ 幂等 | 弱序(RVWMO) | DDR / SRAM 主存 |
| Non-cacheable(不可缓存内存) | ❌ | ✅ 幂等 | 弱序(RVWMO) | DMA 缓冲、帧缓冲、写合并区 |
| Device(设备内存) | ❌ | ❌ 非幂等 | 强序 | UART/SPI/I2C 等外设寄存器 |
2.3.2 视角二:按"地址映射完整性"分(Main Memory / I/O / Empty)
PMA 把每个区分成三种内存类型:可缓存的主存区(遵循 RVWMO)、不可缓存的 MMIO 设备区(强排序、非幂等)、以及未映射的空洞区(访问触发 Access Fault)。
(1) Main Memory(主存)
- 真 RAM(DDR/SRAM),无访问副作用
- 遵循 RVWMO:可推测访问、可乱序、可缓存
- 可缓存(cacheable),可预取
- 典型:你的 SRAM 区
0x20000000、DDR 区
(2) I/O Memory(I/O 内存)
- 设备寄存器区(MMIO),有访问副作用
- 不遵循 RVWMO 的推测:不能推测读、不能合并写、不能重排(强序)
- 不可缓存(non-cacheable)
- 典型:UART/SPI/I2C 寄存器区
0x40000000 - 访问大小受限(有的设备只支持 32 位访问,你读 8 位它挂)
(3) Empty(空区)
- 没接任何东西的地址区
- 任何访问 → access fault(mcause 5 load / 7 store)
- 典型:memory map 里的 reserved 区
Memory type 决定一切下游行为
Memory type 是 PMA 的总开关:
- Main Memory → 可缓存 + RVWMO + 可推测 + 可对齐优化
- I/O → 不可缓存 + 强序 + 不推测 + volatile 语义
- Empty → 直接 fault
你写
volatile uint32_t *reg = 0x40000000;读 UART,编译器不敢优化掉,但硬件层面 保证它不推测不合并,靠的就是 PMA 标这区是 I/O。PMA 是 volatile 语义的硬件后盾。
2.4 PMA 触发的 fault
PMA 检查失败 → trap,具体 mcause:
| 情况 | mcause | 含义 |
|---|---|---|
| 访问 Empty 区 | 5(load)/ 7(store) | load/store access fault |
| 该区 PMA 不允许该操作(如 I/O 区不支持 amo) | 5/7 | access fault |
| 非对齐访问且该区不支持 misaligned | 4(load)/ 6(store) | misaligned access fault |
| 访问大小超该区支持(如 64 位访问 32 位设备) | 5/7 | access fault |
2.5 PMA 怎么查 / 怎么知道
PMA 没有统一 CSR 可读全貌(这是 RISC-V 的一个痛点),获取方式:
- 查 SoC 手册的 memory map:手册会标每个地址区的类型/权限/缓存性。这是主渠道。
- 少数核有 PMA 配置寄存器:如 SiFive 的 PMA 配置 CSR,可读可改。看核手册。
- 探测法(危险):小心地访问试探,看是否 fault。生产代码别用。
- 厂商 BSP 提供:厂商通常给个 memory map 头文件/链接脚本,标好区属性。
2.6 PMA vs PMP(易混,必须分清)
| PMA | PMP | |
|---|---|---|
| 谁定 | SoC 硬件(流片连死) | M-mode 软件(CSR 配) |
| 可改 | 通常不可(少数核可) | 可,随时改 |
| 层级 | 更底层(物理事实) | 更上层(特权围栏) |
| 检查顺序 | 先 | 后 |
| 对 M-mode | 生效(M 也绕不过) | 默认不限(L 位/mseccfg 除外) |
| 关系 | 决定"物理上能不能" | 决定"特权上允不允许" |
核心规则 :PMP 不能授予 PMA 没有的权限。
- PMA 说某区不可写(如 ROM),你 PMP 配可写 → 写还是 fault(PMA 挡)
- PMA 说某区不支持 amo,你 PMP 配可执行 amo → amo 还是 fault
- 反过来:PMA 允许的,PMP 可以进一步限制(PMP 是收紧,不是放宽)
一句话记住 PMA/PMP
PMA 是物理事实(硬件定,M 也受限),PMP 是特权围栏(软件定,M 默认不受限)。PMP 只能在 PMA 允许的范围内收紧,不能放宽。
2.7 PMA 对你 M-mode 工作的实际影响
- 写裸机代码别乱访问地址:你以为 M-mode 全权,往保留区发读直接 fault。先确认 memory map。
- MMIO 访问要 volatile + 对齐 + 正确大小:PMA 的 I/O type 不让你推测/合并/乱序,volatile 保证编译器层面;对齐和大小要看设备 PMA 支持。
- 设备寄存器别用 amo:除非该 I/O 区 PMA 标支持 amo(少见),否则 amoadd 寄存器会 fault。设备寄存器用普通 read-modify-write。
- 调地址类 fault,先查 PMA:mtval 给地址 → 查 memory map 该地址 type/权限 → 确认你的操作是否被 PMA 允许 → 再看 PMP。
三、PMP 增强:ePMP / mseccfg
06 篇讲了基础 PMP(16 区 / TOR/NA4/NAPOT / L 位锁定)。这里讲 ePMP(enhanced PMP) 扩展,这是安全场景(功能安全/安全启动)的关键。
3.1 基础 PMP 的局限(回顾 + 暴露问题)
基础 PMP 的问题:
- M-mode 默认不受限:PMP 只管 S/U,M-mode 全权。安全场景下,M-mode 代码跑飞能写任何区,PMP 拦不住。
- L 位锁定后不可逆:一旦设 L,该区配置锁死,调试也不方便。
- 默认允许:没配 PMP 的区,所有人(含 S/U)默认可访问(白名单难做)。
3.2 mseccfg(Machine Security Configuration)CSR
ePMP 新增 mseccfg CSR,三个关键位:
| 位 | 名 | 作用 |
|---|---|---|
| bit0 | RLB(Rule Locking Bypass) | 允许修改已 L 锁定的 PMP 区(调试用,生产慎开) |
| bit1 | MMWP(Machine Mode Whitelist Policy) | M-mode 默认拒绝,只允许 PMP 显式允许的区(白名单) |
| bit2 | MML(Machine Mode Lock) | 改变 PMP 语义,PMP 对 M-mode 也生效 |
3.3 MML:让 PMP 约束 M-mode
设 mseccfg.MML = 1 后,PMP 语义改变:
- PMP 对 M-mode 也生效(不再默认全权)
- pmpcfg 的 R/W/X 位语义变化:在 MML 模式下,R/W/X 组合表示"该区对 M-mode 的权限"
- 典型用法:安全启动后设 MML,锁定 M-mode 代码区不可写,防 M-mode 被攻破后改自身代码
3.4 MMWP:M-mode 白名单
设 mseccfg.MMWP = 1 后:
- M-mode 默认拒绝所有访问
- 只能访问 PMP 显式配置允许的区
- 这是"最小权限"原则的硬件实现,功能安全(ISO 26262)隔离用
3.5 ePMP 的 E 位(Entry)
pmpcfg 新增 E 位(原 A 位旁):
- E=0:该区不参与匹配(禁用,但保留配置)
- E=1:该区参与匹配
- 配合 MML/MMWP,实现精细的 M-mode 权限控制
3.6 安全场景应用(AEGIX 方向)
嵌入式视角: 功能安全的内存隔离
要过 ISO 26262,功能安全要求"安全区"和"非安全区"内存隔离。ePMP 给你硬件基础:
- 启动早期配 PMP 划分:安全代码区、安全数据区、非安全区、外设区
- 设
mseccfg.MML=1:让 PMP 约束 M-mode 自身(防 M-mode 跑飞写安全区)- 设
mseccfg.MMWP=1:M-mode 默认拒绝,白名单访问(最小权限)- 锁定关键区 L=1:防运行时被改
这是基础 PMP 做不到的(基础 PMP 管不了 M-mode),ePMP 是功能安全芯片的必备。
3.7 ePMP 配置示例(安全启动后)
c
// 假设:安全启动完成,要锁定 M-mode 只能跑安全代码
// 区0:安全代码区 0x0-0x40000,RX for M-mode,禁止写
pmpaddr0 = 0x40000 >> 2;
pmpcfg0 = (1<<3) /*L*/ | (2<<3) /*A=TOR,...*/ ; // 详看核手册 MML 下的位语义
// 区1:安全数据区 0x40000-0x80000,RW for M-mode
pmpaddr1 = 0x80000 >> 2;
pmpcfg1 = ...;
// 开启 MML + MMWP,让 PMP 约束 M-mode 并白名单
csrs mseccfg, (1<<2) /*MML*/ | (1<<1) /*MMWP*/;
⚠️ MML/MMWP 一旦开,PMP 语义复杂,配错会把自己锁死(连代码都跑不了)。生产代码前要在仿真/JTAG 反复验证。
四、Cache 架构与 CMO 扩展
06 篇讲了 DMA 一致性的"flush/invalidate"概念,这里讲 cache 架构本身和标准 CMO 指令。
4.1 Cache 层次
┌─────────────┐
Hart 0 │ L1 I-Cache │ 私有,小(8-64KB),快(1-3 cycle)
│ L1 D-Cache │
└──────┬───────┘
│
┌──────▼───────┐
│ L2 Unified │ 共享(多核),大(128KB-1MB)
└──────┬───────┘
│
┌──────▼───────┐
│ L3 / LLC │ 多核共享,大(MB级)
└──────┬───────┘
│
Main Memory
- MCU 可能无 cache:直接 SRAM,访问确定性好(实时性优)
- 有 cache 的 MCU:性能好,但引入一致性问题
- Write-through vs Write-back:写策略,影响一致性维护方式
4.2 Cache 问题(06 的概念深化)
| 问题 | 描述 | 后果 |
|---|---|---|
| 脏数据未下刷 | CPU 写了 cache 没 write-back 到内存 | DMA 读到旧数据 |
| 陈旧数据未失效 | DMA 写了内存,CPU cache 还是旧值 | CPU 读到旧数据 |
| 指令 cache 陈旧 | 改了内存里的代码,I-Cache 没更新 | CPU 执行旧指令 |
4.3 Cache 维护操作(术语要分清)
| 操作 | 英文 | 干什么 | 用途 |
|---|---|---|---|
| clean | clean | 脏行下刷到内存(写回) | DMA 发送前,保证数据到内存 |
| invalidate | invalidate | 丢弃 cache 行(不回写) | DMA 接收后,强制 CPU 从内存读 |
| flush | flush | clean + invalidate | 既要下刷又要失效 |
| zero | zero | 直接清零一 cache 行(绕过读) | 分配大 buffer 时省读 |
| prefetch | prefetch | 预取到 cache | 优化性能 |
clean vs invalidate 别用反
- DMA 发送前 (CPU 写完 buffer,DMA 要读):clean(下刷脏数据,让 DMA 看到新数据)
- DMA 接收后 (DMA 写完 buffer,CPU 要读):invalidate (失效 cache,让 CPU 从内存读新数据)
用反了:clean 当 invalidate 用,DMA 数据被旧 cache 覆盖;invalidate 当 clean 用,脏数据丢失。这是 DMA bug 高发点。
4.4 CMO 扩展(Zicbom / Zicboz / Zicbop)⭐
RISC-V 标准 Cache Management Operations 扩展(较新,看核是否支持):
| 扩展 | 指令 | 作用 |
|---|---|---|
| Zicbom | cbo.clean / cbo.flush / cbo.inval |
cache 行维护 |
| Zicboz | cbo.zero |
cache 行清零(绕过读) |
| Zicbop | cbo.prefetch.i / .r / .w |
预取 |
c
// Zicbom 用法(cache block 通常 64 字节,要对齐)
// DMA 发送前
cbo_clean(buf); // 或 asm: cbo.clean 0(%0) :: "r"(buf)
// DMA 接收后
cbo_inval(buf); // 或 asm: cbo.inval 0(%0) :: "r"(buf)
- CMO 操作单位是 cache block(Cacheline,通常 64 字节),地址要 block 对齐
- 操作多个 block 要循环
- 老芯片没 CMO :用厂商提供的 cache 维护函数(如
__flush_dcache_range),每家不同
4.5 DMA 一致性三种模式(深化 06)
| 模式 | 机制 | 软件负担 | 典型 |
|---|---|---|---|
| Coherent interconnect | 总线监听(snoop),CPU/DMA 自动一致 | 无,软件不管 | 高性能多核 |
| Non-coherent + CMO | 软件用 cbo.clean/inval 维护 | 中,要正确 flush/invalidate | 多数 MCU |
| Non-coherent + non-cacheable buffer | DMA buffer 映射成 non-cacheable(PMA 标) | 低,但性能差(绕 cache) | 简单 MCU |
4.6 自修改代码与 fence.i
c
// 写新代码到内存
memcpy(func_addr, new_code, size);
fence.i; // 同步 I-Cache 和 D-Cache!必须
func_addr(); // 执行新代码
fence.i(Zifencei 扩展)保证:之前的 store 对后续指令 fetch 可见- 没它:I-Cache 还是旧代码,CPU 执行旧指令
- JIT/动态代码/在线升级固件必用
- 多核下还要 IPI 让其他核 fence.i(复杂)
五、MMU 深入(超越 06 的 Sv39 概念)
06 讲了 Sv39 三级页表的概念,这里讲细节:A/D 位管理、TLB、sfence.vma、大页。
5.1 页表模式全表
| 模式 | VA 位数 | 页表级数 | 最小页 | 典型 |
|---|---|---|---|---|
| Sv32 | 32 | 2 | 4KB | rv32 Linux |
| Sv39 | 39 | 3 | 4KB | rv64 Linux(最常用) |
| Sv48 | 48 | 4 | 4KB | 大地址空间 Linux |
| Sv57 | 57 | 5 | 4KB | 新,超大地址空间 |
satp.MODE 选哪个:0=Bare(无翻译)、8=Sv39、9=Sv48、10=Sv57。
5.2 页表项位段(Sv39,深化 06)
PTE(64位):
bit0: V(valid) 该 PTE 有效
bit1: R(readable) 可读
bit2: W(writable) 可写
bit3: X(executable) 可执行
bit4: U(user) U 模式可访问(S 模式看 SUM 位)
bit5: G(global) 全局(所有地址空间有效,TLB 不按 ASID 刷)
bit6: A(accessed) 被访问过
bit7: D(dirty) 被写过
bit8-9: RSW 软件可用位
bit10-53: PPN 物理页号
bit54-63: reserved
- R/W/X = 000 且 V=1:这是"指针 PTE",指向下一级页表(不是叶子)
- R/W/X ≠ 000 且 V=1:这是"叶子 PTE",直接给物理页(可在某级终止成大页)
5.3 A/D 位管理(06 没讲的关键)⭐
A 位(访问)和 D 位(脏)的更新有两种策略,选错会踩坑:
(1) 硬件管理
- CPU 访问页时自动置 A ,写时自动置 D
- 优点:软件简单
- 缺点:需要硬件支持(看核手册)
- 检查:
menvcfg/satp相关位,或核手册
(2) 软件管理
- CPU 访问时若 A=0,触发 page fault(mcause 13 load page fault / 15 store page fault)
- OS 在 fault handler 里:置 A 位、更新 PTE、返回重试
- 写时若 D=0,触发 store page fault,OS 置 D
- 优点:硬件简单
- 缺点:软件要处理 fault,不处理就死循环(fault → 重试 → fault)
A/D 位是移植 OS 的高频坑
你若给 RISC-V 移植 RTOS/OS,页表 A/D 位策略不对会死循环:
- 硬件不支持自动 A/D,你 OS 又没装 page fault handler → 每次访问触发 fault,handler 不处理 → 重试 → fault → 死循环
- 或 OS 假设硬件自动 A/D,但该核是软件管理 → 同样死循环
移植前查核手册的 A/D 策略,OS 代码配套。
5.4 TLB 与 sfence.vma
- TLB(Translation Lookaside Buffer):页表翻译的 cache,核内
- 改了页表,TLB 可能还是旧的 → 要刷 TLB
sfence.vma(S-mode 指令):刷 TLB
asm
sfence.vma # 刷所有 TLB(所有地址、所有 ASID)
sfence.vma vaddr # 刷特定虚拟地址的 TLB
sfence.vma vaddr, asid # 刷特定 ASID 的特定地址
sfence.vma zero, asid # 刷特定 ASID 的所有地址
- 改一个 PTE →
sfence.vma vaddr(精细,快) - 切进程(改 satp)→ 通常
sfence.vma(全刷)或用 ASID 避免全刷 - M-mode 也能执行
sfence.vma(虽然是 S 级指令,M 全权)
5.5 ASID(Address Space ID)
- satp 里有 ASID 字段(16 位)
- TLB 项标记 ASID,不同进程的 TLB 项靠 ASID 区分
- 切进程时若 ASID 不同,TLB 项不冲突,不用全刷(只刷旧 ASID 的)
- 优化:减少切进程的 TLB 刷新开销
5.6 大页(Superpages)
Sv39 页表三级,可在任一级终止成大页:
| 终止级 | 页大小 | 用途 |
|---|---|---|
| 第3级(最底) | 4KB | 普通页 |
| 第2级 | 2MB | 大页(减少页表项) |
| 第1级 | 1GB | 巨页(极少用) |
- 大页 PTE:在该级 R/W/X ≠ 000(叶子),直接给物理页
- 优点:减少页表层级、省 TLB 项、减少翻译开销
- 用途:大 buffer(DMA buffer、帧缓冲)用大页,提升 TLB 命中率
5.7 satp 切换与模式切换
- M → S 切换:M-mode bootloader 配好页表,设 satp,mret 到 S-mode
- satp 切换 = 地址空间切换:进程切换时改 satp
- M-mode 不用 satp:M-mode 访问物理地址,M-mode 下 satp 被忽略
六、RVWMO 形式化
06 讲了"什么场景要 fence",这里讲 RVWMO 的形式化模型------三公理、PPO、fence 位详解。这是能看 spec 原文的深度。
6.1 RVWMO 的三层模型
RVWMO(RISC-V Weak Memory Ordering)用三个概念定义:
- Preserved Program Order (PPO):单个 hart 内,哪些内存操作对其他 hart 必须"保序"(其他可重排)
- Global Memory Order (GMO):所有 hart 的所有内存操作的一个全局全序(理论存在)
- Load Value Axiom:load 能读到哪些值
6.2 PPO(保序规则)
单个 hart 内,以下内存操作对按程序序保序(其他可重排):
| 规则 | 操作对 | 说明 |
|---|---|---|
| Overlapping-address | load→load / store→store / load→store(同地址) | 同地址必须保序 |
| Explicit sync | fence 前后 | fence 强制保序 |
| Syntactic dependency | op1 → op2(op2 依赖 op1 的地址/数据/控制) | 数据依赖保序 |
| Pipeline dependency | op1 → op2(执行依赖) | 流水线依赖 |
| ... | (规范列出十余条) |
核心:没有这些关系的操作,硬件可重排。这就是为什么多核共享数据要 fence------你的 store data 和 store flag 没有 PPO 关系,硬件可能先 store flag。
6.3 三公理
RVWMO 由三条公理定义(规范原文):
(1) Load Value Axiom(取值公理)
每个 load 读到的值 = GMO 中最新的以下之一:
- 该 hart 自己 prior store 的值(按程序序)
- 其他 hart 的 store 的值(按 GMO)
即:load 不会凭空读到未写的值,但可能读到"非最新"(因为重排)。
(2) Atomicity Axiom(原子性公理)
对同一地址的 LR/SC 对:若 SC 成功,则 GMO 中该 SC 的 store 与对应 LR 之间,没有其他 hart 对该地址的 store。这保证 LR/SC 的原子性。
(3) Progress Axiom(前进公理)
系统不会无限挂起(没有 livelock)。这保证多核代码最终能前进。
6.4 fence 指令位详解(06 跳过的细节)
fence <predecessor>, <successor>
predecessor / successor 可选(组合):
r: memory read (普通内存读)
w: memory write (普通内存写)
i: device input (设备读,即 MMIO load)
o: device output (设备写,即 MMIO store)
⚠ 注意:fence 的 i 是 "input"(设备读),不是 "instruction"!
指令同步用单独的 fence.i
常用组合:
| fence | 含义 |
|---|---|
fence rw, rw |
内存读写全屏障(最常用) |
fence r, r |
读读屏障(配对读 flag + 读 data) |
fence w, w |
写写屏障 |
fence rw, ow |
内存读写 → 设备写(配 DMA 启动) |
fence iorw, iorw |
最强,含设备 IO 全屏障 |
fence.i |
指令同步(I-Cache 与 D-Cache 同步),单独指令 |
fence predecessor, successor 语义:predecessor 之前的内存操作,对 successor 之后的内存操作可见。
6.5 fence 的经典用法(配对模式)
多核发布-等待模式:
c
// Hart 0(生产者)
data = 42;
fence rw, rw; // 保证 data 写入先于 flag
flag = 1;
// Hart 1(消费者)
while (flag != 1) ;
fence r, r; // 保证先读 flag 再读 data
x = data; // 一定能读到 42
没 fence:Hart 0 可能先写 flag 再写 data(重排),Hart 1 看到 flag=1 时 data 还没写。
6.6 I/O 内存 vs Main Memory 的 ordering
- Main Memory:遵循 RVWMO,可重排、可推测、可合并(受 fence 约束)
- I/O Memory :PMA 标记为 I/O,强序(不重排、不推测、不合并),fence 通常不需要额外加(I/O 访问本身强序)
- 但 I/O 和 Main 混合操作时,可能仍需 fence(如先写 Main buffer,再写 I/O 寄存器启动 DMA)
七、原子操作深化
06 讲了 LR/SC 自旋锁的基本用法,这里讲 reservation set、SC 失败条件、amo 的 aq/rl 位。
7.1 LR/SC 的 reservation set
- LR(Load-Reserved):加载并标记一个"保留集"
- 保留集的范围由实现定(通常一个 cache 行,64 字节)
- SC(Store-Conditional):若保留集未被打破,存储成功(返回 0);否则失败(返回非 0)
- SC 失败条件 :
- 另一个 hart 写了保留集内任意地址
- 另一个 hart 的 LR 到同地址(部分实现)
- 该 hart 执行了另一条 LR
- 保留集"过期"(实现相关,如时间或指令数)
7.2 LR/SC 的使用约束(规范要求)
RISC-V 规范对 LR/SC 序列有约束(否则可能 livelock):
- LR/SC 之间指令数有限(规范给上界,如 16 条)
- LR/SC 之间不能有另一 LR
- LR/SC 之间不能有对同地址的 store
- LR/SC 之间最好别有分支(或分支要小心)
违反约束 → SC 可能永远失败(livelock)。
7.3 标准自旋锁(带约束)
c
void spin_lock(int *lock) {
while (1) {
// 等待锁释放(普通读,避免太多 LR)
while (*(volatile int*)lock != 0) ;
// 尝试获取
int got;
asm volatile(
"lr.w.amo.aq %0, (%1)\n" // LR with acquire
: "=r"(got) : "r"(lock));
if (got == 0) {
// 拿到了,但还要 SC 确认
asm volatile(
"sc.w.amo.rl %0, %2, (%1)\n" // SC with release
: "=r"(got) : "r"(lock), "r"(1));
if (got == 0) return; // SC 成功
}
// SC 失败,重试
}
}
7.4 amo 的 aq/rl 位
amo 指令有两个内存排序位:
- .aq(acquire):该 amo 之后的内存操作不能重排到它之前
- .rl(release):该 amo 之前的内存操作不能重排到它之后
- .aqrl:acquire + release(最强)
asm
amoadd.w.aqrl rd, rs2, (rs1) # 原子加,acquire+release
- 用 aq/rl 实现锁,不用额外 fence
- 不带 aq/rl 的 amo 也有原子性,但不保序(多核要小心)
7.5 原子操作 vs fence 的选择
| 场景 | 用什么 |
|---|---|
| 单条原子操作(计数器) | amo(可带 aq/rl) |
| 临界区进入/退出 | LR/SC + aq/rl(锁) |
| 非原子的发布-等待 | fence rw,rw |
| 设备 IO | fence rw,ow(或 I/O 自带强序) |
八、内存 ordering 决策总表(深化 06)
覆盖所有场景的决策矩阵:
| 场景 | 要不要屏障 | 用什么 | 原因 |
|---|---|---|---|
| 单核普通代码 | 不要 | --- | 对自己程序序执行 |
| 单核自修改代码 | 要 | fence.i |
I-Cache 同步 |
| 多核共享数据发布 | 要 | fence rw,rw 或 amo.aqrl |
保 data→flag 序 |
| 多核共享数据读取 | 要 | fence r,r |
保 flag→data 序 |
| 多核临界区 | 要 | LR/SC.aqrl | 锁的 acquire/release |
| 多核原子计数 | 可选 | amoadd.aqrl | 保序 + 原子 |
| DMA 发送前 | 要 | cbo.clean + fence rw,ow |
数据到内存 + 对 DMA 可见 |
| DMA 接收后 | 要 | cbo.inval |
CPU 从内存读新数据 |
| 设备寄存器写后读 | 看 PMA | I/O 强序通常不用 | PMA I/O type 强序 |
| 改页表后 | 要 | sfence.vma |
刷 TLB |
| 切进程(改 satp) | 要 | sfence.vma(或 ASID) |
刷旧地址空间 TLB |
| 中断返回 | 不要 | mret 隐含 |
mret 有顺序保证 |
九、M-mode 程序员的内存清单
把本篇收束成你能用的清单:
| 层 | 你能改? | 你必须懂 | 你的活 |
|---|---|---|---|
| PMA | 通常不能(少数核可) | ✅ 必须 | 查手册知道区属性,别乱访问 |
| PMP | ✅ 能(CSR) | ✅ 必须 | 划权限围栏给 S/U |
| ePMP/mseccfg | ✅ 能 | 安全场景必须 | AEGIX 功能安全隔离 |
| Cache | 策略 PMA 定,操作 CMO | ✅ 必须 | DMA 一致性维护 |
| MMU/页表 | ✅ 能(若跑 OS) | 概念必须 | bootloader 配 satp 切 S |
| TLB | 间接(sfence.vma) | 跑 OS 要懂 | 改页表后刷 |
| fence/fence.i | ✅ 用指令 | ✅ 必须 | 多核/自修改代码保序 |
| amo/aq/rl | ✅ 用指令 | 多核要懂 | 同步原语 |
一句话总结本篇
PMA 是物理事实(硬件定,M 也受限),PMP/ePMP 是特权围栏(软件定,可约束 M),Cache/MMU 是性能/隔离机制(fence/sfence 是你操控它们的接口)。你的内存能力 = 懂 PMA 边界 + 配 PMP + 用 fence/CMO 维护一致性 + 必要时配 MMU。
十、本篇小结
- PMA 是最底层:物理地址属性(Memory type/RWX/原子性/缓存性),硬件固定,M-mode 也受限;PMP 不能授予 PMA 没有的权限
- PMA 三类:Main Memory(RVWMO/可缓存)、I/O(强序/不缓存/有副作用)、Empty(fault)
- ePMP/mseccfg:MML(约束 M-mode)、MMWP(白名单)、RLB(解锁),功能安全隔离用
- Cache 维护:clean(下刷)/invalidate(失效)/flush(两者)/zero(清零),CMO 扩展(Zicbom)标准化
- DMA 一致性三模式:coherent interconnect / non-coherent + CMO / non-cacheable buffer
- MMU 细节:Sv39 三级页表,A/D 位有硬件/软件两种管理(移植坑),TLB 用 sfence.vma 刷,大页省 TLB
- RVWMO 三公理:Load Value / Atomicity / Progress;PPO 定义单 hart 保序规则
- fence 位 :r/w/i(device input)/o(device output),
fence pred, succ;fence.i同步 I-Cache - 原子深化:LR/SC reservation set + 失败条件 + 使用约束;amo 的 aq(acquire)/rl(release)位替代 fence
- 决策表:单核不用、多核用 fence/amo.aqrl、DMA 用 CMO+fence、改页表用 sfence.vma
速查表
| 想干啥 | 怎么做 |
|---|---|
| 查某地址物理属性 | 查 SoC 手册 memory map(PMA) |
| 某地址访问 fault | 查 mtval → memory map → PMA 是否允许该操作 |
| M-mode 也被拦? | 是,PMA 对 M 生效;PMP 默认不拦 M |
| 约束 M-mode 自己 | 设 mseccfg.MML=1 |
| M-mode 白名单 | 设 mseccfg.MMWP=1 |
| DMA 发送前 | cbo.clean(buf) + fence rw,ow |
| DMA 接收后 | cbo.inval(buf) |
| 改代码后执行 | fence.i |
| 多核发布-等待 | fence rw,rw(写侧) + fence r,r(读侧) |
| 多核临界区 | LR/SC.aqrl 自旋锁 |
| 多核计数器 | amoadd.w.aqrl |
| 改页表后 | sfence.vma |
| 切进程 | sfence.vma(或用 ASID 避免全刷) |
| 页表 A/D 位 | 查核手册是硬件还是软件管理,配套 OS |
| I/O 寄存器访问 | volatile + 对齐 + 正确大小 + 别用 amo |
💡技术之路漫漫,分享是为了更好地交流。如果本文的内容对你有启发,希望能得到你的 点赞 👍 和 收藏 ⭐。
如果你在调试过程中遇到了其他问题,欢迎在 评论区 💬 留言,我们一起探讨。也欢迎 关注 👀 我,一起交流底层开发的那些事儿。