资讯详情

资讯详情

RISC-V内存架构深度解析:PMA、ePMP、Cache与RVWMO实战指南

1. 从一条访存指令说起为什么内存架构值得单独开一篇很多人学 RISC-V 的时候把注意力全放在指令集编码、流水线、中断控制器上等到真正去跑一个带操作系统的镜像或者去调一块带缓存的 SoC才发现真正让人头大的是内存这一摊子事。一条最简单的ld指令从 CPU 发出到数据回来中间要经过地址翻译、权限检查、属性判定、缓存查找、一致性维护这一整条链路任何一环配置错了表现可能是随机死机、DMA 数据错乱、或者性能莫名其妙掉一半。这篇是 RISC-V 专栏的第 06b 篇专门把内存架构这块拆开讲。核心围绕五个东西PMA物理内存属性、ePMP增强型物理内存保护、Cache 与 CMO缓存管理操作、MMU 细节、以及RVWMORISC-V 弱内存序模型的形式化。这五个概念不是并列关系而是从底层物理属性到上层内存序模型的一条完整栈PMA 决定一段物理地址“是什么”ePMP 决定“谁能访问”MMU 决定“虚拟地址怎么映射到物理地址并带上权限”Cache/CMO 决定“访问怎么被加速和同步”RVWMO 则规定“多核之间访存顺序的可见性规则”。适合谁看如果你已经能看懂 RISC-V 特权手册里 Sv39/Sv48 的页表格式能跑通一个裸机或 Linux 环境但对“为什么我的驱动要加 fence”“为什么 CMO 指令在某些核上是可选扩展”“PMA 和 PMP 到底谁先谁后”这些问题还模棱两可那这篇就是写给你的。我会尽量把每个机制的“为什么这么设计”讲清楚而不是只罗列寄存器字段。先给一个全局的访存链路图景后面每一节都挂在这条链上CPU 发出虚拟地址 - MMU 查页表Sv39/48/57得到物理地址 权限 属性 - PMA 检查物理地址属性可缓存/可执行/原子操作支持等 - ePMP/PMP 检查物理地址权限读/写/执行 - Cache 查找若属性为可缓存 - 访问实际内存或外设 - 多核场景下由 RVWMO fence/CMO 保证顺序与一致性这条链路里MMU 和 PMA 的先后、PMA 和 PMP 的先后是面试和实际调试里最容易被问、也最容易踩坑的点。下面逐层展开。2. PMA 与 ePMP物理地址的“属性”和“门禁”是两回事2.1 PMA 到底管什么物理内存属性判定PMAPhysical Memory Attributes直译就是物理内存属性。它描述的是一段物理地址区间本身固有的硬件特性跟当前运行什么软件、什么特权级没关系。你可以把它理解成“这块地皮的地质报告”是能盖楼的可缓存内存还是只能当临时堆场的外设寄存器还是根本不能碰的保留区域。PMA 通常由 SoC 设计者在硬件层面固定或者通过少量可配置寄存器比如某些平台提供的 PMA 配置 CSR来设定。它涵盖的属性维度大致有这些可缓存性Cacheability这段地址能不能进 Cache。主存一般可缓存外设寄存器一般不可缓存。可执行性Executability能不能从这里取指执行。原子操作支持AMO Support支不支持amoadd、amoswap这类原子指令。很多外设区域不支持原子操作硬发过去会触发异常。访问宽度与对齐要求某些设备寄存器只支持 32 位对齐访问你发一个 8 位读可能就出错。幂等性/副作用读操作是否有副作用比如读一个 FIFO 会弹出数据这直接影响能不能预取、能不能乱序。读写权限的硬件层面限制有些区域硬件上就是只读的如 ROM。为什么 PMA 要独立于 PMP 存在因为属性判定和权限检查是两个正交的维度。一段地址可以是“可缓存的主存”属性同时“只有 S 模式能写”权限。如果把它们混在一起硬件设计会非常别扭。PMA 由地址解码逻辑在很靠前的位置完成PMP 检查则通常在 MMU 之后、Cache 之前。注意PMA 违规和 PMP 违规触发的异常类型不同。PMA 违规通常表现为访问直接失败或触发 access fault而 PMP 违规触发的是明确的 PMP 异常。调试时先分清是哪一类能省很多时间。2.2 ePMP在 PMP 基础上补上“执行权限”和“锁定”语义PMPPhysical Memory Protection是 RISC-V 特权规范里定义的物理内存保护机制最多 16 个或 64 个配置项每项描述一段物理地址的读/写/执行权限作用于 S 模式和 U 模式M 模式默认不受限除非锁定。ePMPenhanced PMP是在原 PMP 基础上做增强的版本主要补了几个关键能力更细的执行权限控制原 PMP 的 execute 权限在某些实现里语义模糊ePMP 明确了 X 权限的判定规则尤其是和 M 模式执行相关的边界。锁定语义增强ePMP 允许对配置项加锁锁定后连 M 模式都不能修改这对安全启动链很关键。对 M 模式访问的约束原 PMP 对 M 模式基本不设限ePMP 允许对 M 模式的取指和数据访问做更严格约束。与 Smepmp 扩展配合ePMP 通常和 Smepmp 扩展一起出现提供更规范的权限模型。实际配置 ePMP 时最容易出错的是地址匹配模式。PMP/ePMP 支持几种匹配方式匹配模式含义典型用途OFF不匹配关闭该项TOR从上一个项的地址到本项地址连续区间划分NA44 字节精确匹配单个寄存器保护NAPOT2 的幂次对齐区间大块内存保护NAPOT 是最常用的因为它能用较少的配置项覆盖大块内存。比如你要保护 4KB 的一段区域NAPOT 编码里低若干位全 1 表示区间大小。这里有个坑NAPOT 的区间大小必须是 2 的幂且起始地址必须按区间大小对齐。我曾经见过有人想保护0x8000_1000到0x8000_2FFF这种非对齐区间结果 NAPOT 编码算错保护范围偏移导致该保护的没保护、不该拦的被拦。2.3 PMA 与 ePMP 的先后顺序一个高频面试点访存时PMA 判定在前ePMP 检查在后但两者都在 MMU 翻译之后。为什么是这个顺序因为 PMA 是地址解码的一部分硬件上它和物理地址的产生几乎同时完成属于“这段地址是什么”的判定而 ePMP 是“当前特权级能不能访问这段地址”的判定需要知道当前特权级和访问类型逻辑上更靠后。这个顺序带来的一个实际影响是如果一段地址的 PMA 属性是“不可缓存”那么即使 ePMP 允许访问它也不会进 Cache。反过来如果 PMA 说可缓存但 ePMP 拒绝那访问直接失败根本走不到 Cache。理解这个顺序对排查“为什么我的外设访问被缓存了”这类问题至关重要——大概率是 PMA 配置错了而不是 ePMP。实操心得调试内存访问异常时先确认物理地址落在哪个 PMA 区间再看 ePMP 配置。很多“权限明明开了却还是 fault”的问题根源在 PMA 把这段地址标成了不可访问或属性不匹配。3. Cache 与 CMO性能加速器和它的“同步代价”3.1 Cache 在 RISC-V 里的定位可选但几乎必备RISC-V 规范本身不强制要求 CacheCache 属于实现定义的部分。但现实中除了极低端的 MCU几乎所有能跑 Linux 的核都带 Cache。Cache 的存在带来两个直接问题一致性Coherency和可见性Visibility。一致性多个核或核与 DMA 之间看到的内存数据是否一致。可见性一个核写的值什么时候能被另一个核看到。RISC-V 把 Cache 管理操作抽象成CMOCache Management Operations扩展主要包括CBO.CLEAN把脏 Cache line 写回内存。CBO.FLUSH写回并无效化。CBO.INVAL无效化丢弃。CBO.ZERO把 Cache line 清零某些实现提供。这些指令的粒度通常是 Cache line操作对象是虚拟地址或物理地址取决于实现和扩展版本。3.2 为什么 CMO 是“可选扩展”而不是必选这是个设计哲学问题。RISC-V 追求的是可组合性一个面向微控制器的核可能根本没有 Cache那 CMO 就没意义一个带硬件一致性的多核系统很多 CMO 操作由硬件自动完成软件不需要显式调用。所以 CMO 被设计成可选扩展由具体实现决定支持哪些。但这就带来一个现实问题同一份驱动代码在不同核上行为可能不同。有的核需要你手动cbo.flush才能让 DMA 看到数据有的核硬件一致性已经帮你搞定你手动 flush 反而可能引入性能损失甚至错误。所以写可移植驱动时通常要通过设备树或平台抽象层来判断当前平台需要哪种同步方式。3.3 CMO 与 fence 的关系别把它们混为一谈很多人把fence和 CMO 混着用其实它们解决的是不同问题fence解决内存序问题保证访存指令的可见顺序不涉及 Cache line 的写回或无效化。CMO解决Cache 状态问题把数据真正推到内存或从内存丢弃。一个典型场景CPU 写了一段数据然后启动 DMA 把数据搬走。如果 Cache 是 write-back 的数据可能还在 Cache 里没到内存DMA 读到的就是旧数据。这时候你需要用fence保证写操作在 CMO 之前完成顺序保证。用cbo.clean或cbo.flush把数据写回内存。再启动 DMA。只做 fence 不做 CMO数据可能还在 Cache只做 CMO 不做 fenceCMO 可能和前面的写乱序。两者配合才对。注意CMO 指令本身也可能需要 fence 来保证其效果对其他 hart 可见。具体要不要加、加哪种 fence取决于实现的 memory model 和 CMO 的完成语义建议查具体核的手册。3.4 Cache 一致性协议硬件帮你到什么程度多核 RISC-V 系统里Cache 一致性通常由硬件协议如基于目录或 snooping 的协议保证但这属于实现层面不在基础规范里强制。带硬件一致性的系统核间共享数据不需要软件显式 CMO但核与 DMA 之间往往仍需要软件干预因为 DMA 通常不参与 Cache 一致性协议。这就引出一个经典的分层认知场景是否需要软件 CMO核与核共享可缓存数据通常不需要硬件一致性核与 DMA 共享数据通常需要DMA 不参与一致性访问不可缓存外设不需要本来就不进 Cache自修改代码需要取指和数据 Cache 可能不一致自修改代码这个场景特别容易被忽略。你写了一段指令到内存然后跳过去执行如果数据 Cache 和指令 Cache 不一致可能执行到旧指令。正确做法是写完指令后fencecbo.flush数据侧再fencecbo.inval指令侧最后fence.i。这一套下来才算稳妥。4. MMU 细节Sv39/Sv48/Sv57 的页表与 TLB 那些事4.1 地址翻译的基本流程RISC-V 的 MMU 支持多种分页方案常见的是 Sv39、Sv48、Sv57数字代表虚拟地址有效位数。以 Sv39 为例虚拟地址 39 位页大小 4KB页表三级。翻译过程从satpCSR 取出根页表物理地址。用虚拟地址的 VPN[2] 索引根页表得到下一级页表地址。用 VPN[1] 索引二级页表得到三级页表地址。用 VPN[0] 索引三级页表得到最终 PTE。从 PTE 取出物理页号 PPN拼接页内偏移得到物理地址。每一级 PTE 都有有效位、权限位、属性位。任何一级无效或权限不符都会触发 page fault。4.2 PTE 里的权限位和属性位别只看 R/W/XPTE 的权限位不只是 R/W/X还有几个关键位VValid有效位为 0 表示该 PTE 无效。R/W/X读/写/执行权限。U是否允许 U 模式访问。GGlobal全局映射TLB 刷新时不受影响。AAccessed是否被访问过硬件可能自动置位。DDirty是否被写过硬件可能自动置位。RSW留给软件用的两位。PBMT页级内存类型可覆盖 PMA 的部分属性如果支持 Svpbmt 扩展。NAPOT/其他某些扩展引入的位。这里有个非常容易踩的坑W 位为 1 但 R 位为 0 是非法组合。RISC-V 规定如果 W1 则 R 必须为 1否则该 PTE 视为无效。我见过有人为了做“只写”映射把 R 设 0、W 设 1结果直接 page fault查了半天才发现是规范不允许。另一个坑是A/D 位的更新。有些实现硬件自动更新 A/D 位有些需要软件模拟。如果硬件不自动更新而软件又没处理可能导致页被频繁换出或统计错误。做内核移植时要确认这一点。4.3 TLB 刷新sfence.vma 的正确用法TLB 是 MMU 的缓存缓存虚拟地址到物理地址的映射。修改页表后必须刷新 TLB否则可能用到旧映射。RISC-V 提供sfence.vma指令可以带参数sfence.vma刷新所有 TLB 项。sfence.vma rs1刷新与 rs1 指定的虚拟地址相关的项。sfence.vma rs1, rs2刷新指定地址空间ASID下的相关项。用错参数的后果是性能损失或正确性问题。比如你只改了一个页的映射却用了全量刷新性能会掉反过来你改了映射却只刷新了部分可能用到旧映射导致数据错乱。实操心得sfence.vma之后通常还需要配合fence或fence.i具体取决于你改的是数据映射还是指令映射。改指令映射后sfence.vmafence.i是标配。4.4 大页与页表遍历优化Sv39/Sv48 支持大页2MB、1GB 等通过让中间级 PTE 直接指向物理页而非下一级页表来实现。大页能显著减少 TLB miss 和页表遍历开销但代价是内部碎片。内核里通常用大页映射内核线性区用户态按需使用。页表遍历page table walk本身也有优化空间。有些实现带页表遍历缓存PWC缓存中间级页表项减少内存访问次数。这部分属于微架构软件层面感知不到但会影响性能调优的判断。5. RVWMO 形式化弱内存序到底“弱”在哪5.1 为什么 RISC-V 选择弱内存序强内存序如 x86 的 TSO对程序员友好但限制硬件优化空间。RISC-V 选择RVWMORISC-V Weak Memory Ordering允许硬件对访存指令做更多重排以换取性能和能效。代价是软件必须显式使用 fence 来建立顺序。RVWMO 的核心是一套形式化的公理系统用“程序顺序”“全局内存顺序”“读-写关系”等概念定义哪些重排是允许的。这套形式化模型的好处是硬件实现有明确的合规边界软件也有明确的推理依据。5.2 RVWMO 的几个关键概念理解 RVWMO抓住几个概念就够了程序顺序Program Order单个 hart 内指令的原始顺序。全局内存顺序Global Memory Order所有 hart 观察到的访存总顺序。读-读、读-写、写-读、写-写四类访存对的重排规则。地址依赖、数据依赖、控制依赖依赖关系会影响重排可能性。fence 的语义fence 建立跨 hart 的顺序约束具体约束范围由 fence 的前驱集和后继集决定。RVWMO 允许的行为举例一个 hart 先写 A 再写 B另一个 hart 可能先看到 B 再看到 A除非中间有 fence。这就是为什么多核编程里共享数据的发布-订阅模式必须配 fence。5.3 fence 的参数pred 和 succ 怎么理解fence指令带两个参数前驱集predecessor和后继集successor。语义是前驱集里的访存在后继集里的访存之前对其他 hart 可见。常见组合fence 形式含义典型场景fence rw, rw全序最强通用同步fence w, w写-写顺序发布数据fence r, r读-读顺序获取数据fence rw, w读写在前写在后释放语义fence r, rw读在前读写在后获取语义用fence rw, rw最保险但性能最差。实际优化时根据同步原语的语义选择更弱的 fence能提升性能。比如实现一个 release 操作fence rw, w就够了。5.4 RVWMO 与 CMO、MMU 的交互RVWMO 管的是访存顺序CMO 管的是 Cache 状态MMU 管的是地址翻译。三者交互时顺序很重要改页表 -sfence.vma- 后续访存用新映射。写数据 -fence-cbo.flush- DMA 可见。多核共享数据 -fence建立发布-订阅顺序。任何一步顺序错了都可能出问题。形式化模型的价值在于它让你能精确推理“我到底需要哪种 fence、放在哪里”。注意RVWMO 的形式化模型有多个版本如 RVWMO 1.0不同版本对某些边界情况的定义可能有差异。做严格验证时要确认你参考的是哪个版本。6. 实操从零配置一套内存保护与缓存同步方案6.1 场景设定与目标假设我们在一个虚拟的 RISC-V 多核平台上要跑一个带 DMA 的驱动需求是主存区域可缓存、可执行。外设寄存器区域不可缓存、不可执行、不支持原子操作。驱动缓冲区需要和 DMA 共享必须保证数据可见性。多核之间通过共享内存通信需要正确的 fence。下面按步骤配置。6.2 第一步划分 PMA 区间先根据平台手册把物理地址空间划分成几个 PMA 区间。假设区间地址范围属性主存0x8000_0000 - 0x8FFF_FFFF可缓存、可执行、支持 AMO外设0x1000_0000 - 0x1000_FFFF不可缓存、不可执行、不支持 AMOROM0x0000_0000 - 0x0000_FFFF可缓存、可执行、只读PMA 配置通常通过平台特定的 CSR 或硬件固定。如果是可配置的按区间写入对应属性寄存器。6.3 第二步配置 ePMP 保护用 ePMP 给不同特权级划分权限。假设M 模式可访问所有区域。S 模式可访问主存可读写外设不可执行外设。U 模式只能访问主存的一部分。ePMP 配置项示例NAPOT 模式项 0: 主存区间, NAPOT, R/W/X, 允许 S/U 项 1: 外设区间, NAPOT, R/W, 允许 S, 不允许 X 项 2: ROM 区间, NAPOT, R/X, 允许 S/U配置时注意 NAPOT 编码区间大小必须是 2 的幂起始地址按大小对齐。比如 256MB 主存NAPOT 编码里低 28 位全 1。6.4 第三步建立页表映射用 Sv39 建立页表。主存用大页映射减少 TLB 压力外设用 4KB 页精确映射。关键 PTE 配置主存V1, R1, W1, X1, U0内核态, A1, D1。外设V1, R1, W1, X0, U0, PBMT不可缓存如果支持。用户区V1, R1, W1, X1, U1。建完页表后执行sfence.vma刷新 TLB。6.5 第四步DMA 缓冲区的缓存同步驱动流程CPU 写数据到缓冲区。fence rw, w保证写在 CMO 之前完成。cbo.clean或cbo.flush把数据写回内存。启动 DMA。DMA 完成后如果需要读 DMA 写的数据先cbo.inval无效化对应 Cache line再fence然后读。这里有个细节cbo.inval要小心如果 Cache line 里有未写回的数据直接 inval 会丢数据。所以 inval 前通常先 clean或者确认该 line 是干净的。6.6 第五步多核同步的 fence 配置共享内存通信场景用发布-订阅模式// 生产者 data 42; fence rw, w; // 保证 data 写在 flag 之前可见 flag 1; // 消费者 while (flag 0) ; fence r, rw; // 保证读到 flag 后再读 data read data;fence 的选择依据是同步语义发布用fence rw, w获取用fence r, rw。如果拿不准用fence rw, rw最保险。7. 常见问题与排查技巧实录7.1 问题速查表现象可能原因排查方向访问外设触发 access faultPMA 属性不匹配或 ePMP 拒绝查 PMA 区间和 ePMP 配置DMA 读到旧数据Cache 未写回检查 CMO 和 fence 顺序多核数据不同步fence 缺失或位置错误检查发布-订阅的 fence页表修改后仍用旧映射TLB 未刷新执行 sfence.vma自修改代码执行旧指令指令 Cache 未同步fence cbo.flush cbo.inval fence.iW1 但 R0 的 PTE 无效规范禁止该组合修正 PTE 权限位NAPOT 保护范围偏移区间非 2 的幂或未对齐重新计算 NAPOT 编码7.2 独家避坑技巧技巧一先确认异常类型再动手。PMA 违规、ePMP 违规、page fault 触发的异常原因不同先看mcause/scause的具体编码能快速缩小范围。很多人一上来就改页表结果问题在 PMA。技巧二CMO 和 fence 成对出现。单独用 CMO 或单独用 fence 都可能出问题。记住“先 fence 后 CMOCMO 后再 fence”这个基本模式能覆盖大多数场景。技巧三NAPOT 编码用工具算。手算 NAPOT 容易错写个小脚本或查表确认。区间大小、起始地址、编码值三者要对上。技巧四多核调试先单核验证。多核问题往往复杂先在单核上把 PMA/ePMP/MMU 配通再加第二个核能快速定位是配置问题还是一致性问题。技巧五RVWMO 推理用形式化工具。如果对某个 fence 配置没把握可以用 RVWMO 的形式化模型工具如 herd 工具链做模拟验证比拍脑袋靠谱。7.3 一个真实的排查案例虚构代称某开发者在虚拟平台上调一个 DMA 驱动现象是 DMA 偶尔读到旧数据。排查过程先确认 PMA缓冲区在主存区间属性可缓存没问题。查 ePMP权限开了没问题。查 CMO驱动里有cbo.clean但位置在启动 DMA 之后。修正把fencecbo.clean移到启动 DMA 之前。问题解决。这个案例的教训是CMO 的位置比有没有更重要。写在 DMA 启动之后等于没写。8. 写在最后的一点个人体会内存架构这块初学的时候觉得概念多、寄存器多、容易乱但真正理解之后会发现它其实是一条非常清晰的链路PMA 定属性ePMP 定权限MMU 定映射Cache/CMO 定加速和同步RVWMO 定顺序。每一层解决一个明确的问题层与层之间的顺序也有硬性道理。我个人在实际操作中的体会是遇到内存相关的问题不要急着改代码先画一遍访存链路确认问题出在哪一层。大部分“玄学”问题追到根上都是某一层的配置或顺序错了。另外形式化模型虽然看起来学术但它是排查多核顺序问题的利器值得花时间啃一啃。最后分享一个小技巧把常用的 fence 组合和 CMO 序列整理成一个速查卡片贴在工位上写驱动的时候对照着用能省很多反复查手册的时间。这个内容后续还可以扩展到 Svpbmt、Svnapot 等扩展以及不同核的 CMO 实现差异有机会再单独开一篇聊。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →