ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

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

RISC-V内存架构深度解析:PMA、ePMP、Cache、CMO、MMU与RVWMO实战指南 1. 从一颗芯片的“记忆迷宫”说起为什么内存架构值得深挖搞RISC-V的人迟早会撞上一堵墙指令集本身看着挺简洁RV32I/RV64I那点东西几天就能翻完但一旦你开始碰真实的SoC——尤其是带操作系统、跑多核、接DMA外设的那种——你会发现真正让人头秃的根本不是指令编码而是内存系统。PMA、ePMP、Cache、CMO、MMU、RVWMO这几个词单拎出来每个都能写一篇长文但它们凑在一起才构成一个完整的“内存世界观”。我最初接触这些概念是在调试一个多核RISC-V平台的时候。当时遇到一个诡异现象CPU写了一段数据DMA读出来是旧的加个fence有时候好使有时候不好使开了MMU之后性能反而掉了三成。后来一路刨下去才发现问题分散在PMA属性配置、ePMP权限、Cache一致性、CMO指令使用和RVWMO内存模型理解这几个层面上。这也是我写这篇东西的动机——把RISC-V内存架构这条线从头到尾捋一遍不是照本宣科翻译手册而是把每个模块“为什么存在、解决什么问题、实际怎么配、容易踩什么坑”讲清楚。这篇文章适合谁看如果你已经写过RISC-V的裸机代码或者正在做RISC-V平台的BSP/驱动/固件开发又或者你在学体系结构想找个真实的落地视角那这篇内容应该对你有用。我会尽量用大白话解释原理用实际配置举例把那些手册里一笔带过但实操中要命的地方标出来。全文围绕PMA、ePMP、Cache、CMO、MMU和RVWMO这六个核心点展开每个点都会给出可参考的配置思路和排查方法。2. 内存架构整体设计与思路拆解2.1 为什么RISC-V要把内存相关的东西拆成这么多模块先看一个基本事实RISC-V的特权规范把内存管理相关的内容拆成了好几个相对独立的机制而不是像某些架构那样揉成一个大MMU。这个设计选择背后有很实际的考量。PMAPhysical Memory Attributes描述的是物理地址空间的属性——这段地址是内存还是设备能不能缓存能不能乱序访问支不支持原子操作这些属性是平台固定的跟当前跑什么程序无关。ePMPenhanced Physical Memory Protection则是在PMA基础上做权限控制规定某个hart在某个特权级下能不能访问某段物理地址是读、写还是执行。Cache和CMOCache Management Operations管的是性能和数据可见性。MMU负责虚拟地址到物理地址的翻译。RVWMORISC-V Weak Memory Ordering定义的是多hart之间内存操作的可见顺序规则。拆开的好处是灵活一个简单的MCU可以只实现PMA不要MMU一个应用处理器可以全都要。坏处是开发者得理解每个模块的边界知道什么问题该找谁。我见过不少人把Cache一致性问题当成MMU问题查或者把PMA属性配错导致的异常当成ePMP权限问题方向错了排查效率极低。2.2 从地址出发一次内存访问到底经过了什么要理解这套架构最好的办法是跟着一次load/store走一遍。假设CPU执行一条ld a0, 0(x1)地址是虚拟地址VA第一步MMU如果开启把VA翻译成物理地址PA同时检查页表项里的权限位。如果翻译失败或者权限不够触发page fault。第二步PA进入PMA检查看这段物理地址的属性是否允许当前类型的访问——比如你想对一个标记为“I/O设备”的地址做缓存读PMA就会拦下来。第三步ePMP检查当前hart在当前特权级下是否有权访问这个PA范围。第四步如果地址可缓存访问落到Cache如果不可缓存直接打到总线上。第五步如果是多hart场景RVWMO规则决定这次访问和其他hart的访问之间需不需要额外的fence或CMO来保证顺序和可见性。这个流程里任何一步出问题都会表现为“程序行为不对”但根因完全不同。理解这个链路是高效调试的前提。2.3 方案选型的现实考量不是每个平台都需要全套实际做项目的时候内存架构的复杂度要跟需求匹配。我整理了一个简单的对照帮你在设计阶段做取舍平台类型PMAePMPCacheCMOMMURVWMO关注度简单MCU必须可选通常无无无低实时控制核必须建议有可选可选可选中应用处理器必须必须必须必须必须高多核异构SoC必须必须必须必须视核而定极高这个表不是规范是我自己在几个项目里总结的经验值。比如做电机控制的实时核Cache可以不要但PMA和ePMP一定要配清楚否则一个野指针就能把外设寄存器写乱。而做Linux能跑的应用核MMU和CMO是绕不开的RVWMO的理解深度直接决定你能不能写出正确的无锁代码。3. 核心细节解析与实操要点3.1 PMA物理地址属性的“户口本”PMA本质上是一张表描述物理地址空间的每一段“是什么、能干什么”。典型的属性包括可缓存性Cacheability这段地址能不能进Cache。可缓存又分可缓存、不可缓存、可缓存但需软件维护一致性等。访问顺序Ordering对这段地址的访问是否允许乱序、合并。原子性支持Atomicity支不支持AMO原子内存操作。访问宽度支持哪些位宽的访问比如某些设备寄存器只支持32位访问你非要用8位读就可能出问题。可执行性这段地址能不能取指。配置PMA通常不是在运行时做的而是在平台设计阶段就定好通过硬件描述或者固件里的静态表体现。比如在设备树里你会看到类似reg 0x0 0x80000000 0x0 0x10000000这样的内存节点配套的属性决定了它的行为。注意PMA配错最典型的表现是“访问设备寄存器时数据不对”或者“对内存做非对齐访问触发异常”。排查时先确认目标地址落在哪个PMA区间再看该区间的属性是否匹配你的访问方式。实操中我建议把平台的PMA表整理成一张文档标注每段的起止地址、类型、可缓存性、支持的访问宽度。这个文档在调试DMA、外设驱动、Cache维护代码时能省大量时间。我自己的习惯是用一个简单的表格维护每次改硬件描述就同步更新。3.2 ePMP比PMP更细的物理内存权限控制ePMP是PMP的增强版核心作用是给物理内存访问加权限检查。它把物理地址空间划分成若干区域每个区域可以配置读/写/执行权限并且可以针对不同的特权级M/S/U设置不同规则。ePMP相比传统PMP的增强点主要在于支持更灵活的权限组合、对锁定lock语义的完善、以及和Smepmp扩展配合时能实现更严格的安全隔离。在多核场景下每个hart有自己的ePMP配置这意味着你可以让核A能访问某段共享内存而核B不能。配置ePMP的典型流程确定需要保护的区域比如内核代码段、只读数据段、外设寄存器区、共享内存区。为每个区域选择匹配的pmpcfg和pmpaddr值。pmpaddr存的是地址右移2位后的值因为PMP粒度是4字节。设置权限位R/W/X以及地址匹配模式TOR、NA4、NAPOT。对需要锁定的区域设置L位锁定后直到复位才能修改。// 示例配置一个可读可写不可执行的区域覆盖0x80000000开始的1MB // pmpaddr 0x80000000 2 0x20000000 // 假设使用pmpcfg0的第0个条目 #define PMP_R 0x01 #define PMP_W 0x02 #define PMP_X 0x04 #define PMP_A_NAPOT 0x18 // NAPOT模式 #define PMP_L 0x80 // 1MB区域用NAPOT表示地址低若干位全1 // 1MB 2^20NAPOT编码需要地址低19位为1因为粒度4字节2^20/42^18再加1位 // 实际编码需根据规范计算这里示意提示ePMP的地址匹配模式选择很关键。TOR模式适合连续区域NA4适合单个4字节NAPOT适合2的幂次大小的区域。选错模式会导致保护范围不对要么保护过头要么形同虚设。我踩过的一个坑早期调试时把内核代码段配成可写结果一个指针越界把代码改了系统跑飞。后来把代码段设成RX、数据段设成RW、外设区设成RW且不可缓存问题就定位得很快。ePMP配好之后很多内存越界问题会直接变成异常而不是静默地破坏数据这对调试是巨大的帮助。3.3 Cache性能的发动机也是一致性的麻烦源头RISC-V的Cache设计比较开放规范没有强制规定Cache的结构所以不同实现差异很大。常见的有VIPT虚拟索引物理标签、PIPT物理索引物理标签等。对软件开发者来说需要关心的是Cache的层级、行大小、替换策略、以及是否支持硬件一致性。查看Cache信息在Linux下可以用lscpu或者读/sys/devices/system/cpu/cpu0/cache/下的文件。比如# 查看各级Cache的大小、行大小、关联度 cat /sys/devices/system/cpu/cpu0/cache/index0/size cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size cat /sys/devices/system/cpu/cpu0/cache/index0/ways_of_associativity这些信息在做性能优化和Cache维护时很有用。比如你知道行大小是64字节那在做CMO的时候就要按64字节对齐操作。Cache带来的核心问题是一致性。在单核且没有DMA的场景下Cache基本是透明的。但一旦有多核或者DMA就必须考虑核A写了数据核B能不能看到DMA读内存会不会读到Cache里的旧数据解决一致性有两条路硬件维护一致性比如总线上的snoop机制和软件维护一致性用CMO指令。RISC-V里两者都可能出现取决于具体实现。软件维护一致性的场景下你需要在对共享数据操作前后插入合适的CMO。注意不要假设所有RISC-V平台都有硬件Cache一致性。很多嵌入式RISC-V核为了省面积是不做硬件一致性的这时候软件必须显式维护。判断方法看手册里有没有提到coherent interconnect或者snoop filter没有的话基本就是软件维护。3.4 CMOCache管理指令的正确打开方式CMO是RISC-V里用来管理Cache的指令集合主要包括cbo.clean把Cache行写回内存并保持干净。cbo.flush把Cache行写回内存并无效化。cbo.inval无效化Cache行不写回。cbo.zero把Cache行清零某些实现支持。这些指令的操作对象是Cache行所以使用时要按行大小对齐。典型使用场景场景一DMA发送数据前。CPU准备好数据在Cache里DMA要读内存。这时候需要对数据区域做cbo.clean或cbo.flush确保数据写回内存。场景二DMA接收数据后。DMA把数据写到内存CPU要读。这时候Cache里可能有旧数据需要cbo.inval把对应行无效化强制CPU从内存重新读。场景三指令Cache维护。修改了代码之后要执行新代码需要对指令Cache做fence.i配合CMO。# 示例对地址a0开始的一段区域做clean操作 # 假设Cache行大小64字节 li t0, 64 add t1, a0, a1 # a1是长度t1是结束地址 1: cbo.clean (a0) add a0, a0, t0 blt a0, t1, 1b fence提示CMO指令的执行可能需要特权级支持U模式下通常不能直接执行。另外CMO的性能开销不小能批量做就不要逐行做能少做就不要多做。我见过有人每次DMA传输都对整个Cache做flush性能直接腰斩。一个实操心得把CMO操作封装成函数传入起始地址和长度内部按行大小循环。这样调用方不用关心行大小也方便后续换平台时统一修改。同时在这些函数里加上fence保证CMO的效果对其他hart可见。3.5 MMU虚拟地址翻译的细节与陷阱MMU负责虚拟地址到物理地址的翻译RISC-V的MMU规范叫Sv39/Sv48/Sv57数字表示虚拟地址位数。以Sv39为例虚拟地址39位物理地址56位页表三级。页表项PTE里除了物理页号还有权限位V有效、R读、W写、X执行、U用户态可访问、G全局、A已访问、D已脏。这些位的组合决定了访问是否合法。MMU配置的关键步骤设置satp寄存器写入页表基址和模式比如Sv39。执行sfence.vma刷新TLB。确保页表本身在PMA里是可缓存的否则性能会很差。# 设置satp启用Sv39 # 假设页表基址在0x80000000MODE8表示Sv39 li t0, 0x8000000000000000 # MODE8 60 li t1, 0x80000000 12 # PPN or t0, t0, t1 csrw satp, t0 sfence.vmaMMU的常见陷阱A/D位不更新有些实现不自动更新Access和Dirty位需要软件模拟这会显著影响性能。如果你的平台有这个问题考虑在页表项里预先设置A/D位。TLB一致性修改页表后必须执行sfence.vma否则TLB里可能还是旧映射。多核场景下一个核改了页表其他核的TLB也要刷新这通常需要通过IPI核间中断来触发。大页使用Sv39支持2MB和1GB大页合理使用能减少TLB miss。但大页的权限粒度粗用错了会带来安全问题。注意MMU开启后所有地址都变成虚拟地址包括访问外设。所以外设寄存器区也需要在页表里建立映射通常映射成不可缓存。忘了映射外设会导致访问直接page fault。3.6 RVWMO弱内存序下的编程心智模型RVWMO是RISC-V的弱内存序模型。简单说硬件可以为了性能对内存访问做重排只要不违反规定的顺序约束。这跟x86的强内存序不同写RISC-V多线程代码时不能想当然。RVWMO的核心规则可以概括为几条同一地址的访问保持程序顺序。带有acquire/release语义的指令会建立顺序约束。fence指令可以显式指定顺序要求。原子指令AMO有特定的顺序语义。实际编程中最常用的是fence rw,rw全屏障和fence.i指令屏障。在实现锁、无锁队列、生产者消费者模式时必须正确使用这些屏障。// 示例用RVWMO的release-acquire实现一个简单的标志位同步 // 生产者 data 42; __atomic_store_n(flag, 1, __ATOMIC_RELEASE); // 消费者 while (__atomic_load_n(flag, __ATOMIC_ACQUIRE) 0) {} assert(data 42);这里__ATOMIC_RELEASE和__ATOMIC_ACQUIRE会编译成带acquire/release语义的指令或者配合fence保证data的写入在flag置1之前对其他hart可见。提示RVWMO的形式化模型RVWMO axiomatic model定义了允许和禁止的行为集合。如果你在做形式化验证或者写复杂的无锁算法建议直接参考规范里的形式化定义而不是靠直觉。我见过太多“看起来对”的代码在弱内存序下出问题。一个实用建议在RISC-V上写多线程代码优先使用C11/C11的原子操作和内存序让编译器和运行时库去处理底层屏障。只有在性能极度敏感或者需要精细控制时才手写fence。另外用工具做验证比如用herd工具跑RVWMO模型检查你的代码模式能提前发现很多问题。4. 实操过程与核心环节实现4.1 从零配置一个带MMU和Cache的RISC-V平台假设我们要在一个支持Sv39和CMO的RISC-V平台上从裸机开始配置内存系统目标是能跑一个简单的多任务程序。步骤如下第一步确认硬件能力。读misa寄存器看支持的扩展读设备树或硬件手册确认PMA区间、Cache行大小、是否支持CMO和Sv39。第二步配置PMA。根据硬件手册把地址空间分段。典型的分段地址范围类型可缓存可执行备注0x80000000-0x8FFFFFFFDRAM是是主内存0x10000000-0x1000FFFF外设否否UART等0x0-0x0FFFFFFFBoot ROM是是启动代码第三步配置ePMP。给M模式全权限给S模式配置内核区域权限给U模式配置用户区域权限。锁定关键区域。第四步建立页表。分配页表页建立恒等映射虚拟地址等于物理地址作为初始映射然后逐步切换到更精细的映射。外设区映射为不可缓存。第五步启用MMU。写satp执行sfence.vma。注意启用MMU的那条指令本身需要在恒等映射的区域里否则会取指失败。第六步配置Cache和CMO。确认Cache行大小实现CMO封装函数。在DMA驱动里调用这些函数。第七步验证。写测试程序多核读写共享内存、DMA传输、修改代码后执行确认行为符合预期。4.2 参数计算实例PMP地址编码和页表项构造PMP的pmpaddr寄存器存的是物理地址右移2位。假设要保护从0x80000000开始、大小4KB的区域使用NA4模式地址右移2位0x80000000 2 0x20000000NA4模式下pmpaddr直接写这个值匹配单个4字节不对NA4匹配的是4字节要匹配4KB需要用NAPOT。NAPOT模式下要匹配2的幂次大小且对齐的区域。4KB 2^12地址低12位为0。NAPOT编码要求地址低12-210位全1再往上的位是地址高位。具体计算4KB区域NAPOT编码的pmpaddr (base 2) | ((size/4 - 1) 1)这里需要仔细按规范算。实际上更简单的做法是用TOR模式设置pmpaddr0为起始地址2pmpaddr1为结束地址2配置项选TOR。这样不容易算错。页表项构造Sv39的PTE是64位PPN占位44-53共10位对应物理地址的12-21位不对Sv39的PPN是44位分三段。构造PTE时// 构造一个指向物理地址pa的叶页表项权限RW4KB页 #define PTE_V (1UL 0) #define PTE_R (1UL 1) #define PTE_W (1UL 2) #define PTE_X (1UL 3) #define PTE_U (1UL 4) #define PTE_A (1UL 6) #define PTE_D (1UL 7) uint64_t pte ((pa 12) 10) | PTE_V | PTE_R | PTE_W | PTE_A | PTE_D;这里(pa 12) 10是因为PPN在PTE的位10开始。这个计算容易错建议封装成函数并写单元测试。4.3 多核场景下的CMO和fence配合实录在一个四核RISC-V平台上我实现了一个核间通信的环形缓冲区。生产者核写数据到缓冲区然后更新写指针消费者核读数据更新读指针。缓冲区内存是可缓存的。关键代码模式// 生产者 buffer[widx] data; __atomic_thread_fence(__ATOMIC_RELEASE); // 保证数据写入在指针更新前可见 __atomic_store_n(wptr, widx 1, __ATOMIC_RELAXED); // 消费者 while (__atomic_load_n(wptr, __ATOMIC_ACQUIRE) rptr) {} data buffer[rptr]; __atomic_store_n(rptr, rptr 1, __ATOMIC_RELEASE);这里没有直接用CMO因为缓冲区是可缓存的且平台有硬件一致性。如果平台没有硬件一致性就需要在更新指针前后加CMO// 无硬件一致性时的生产者 buffer[widx] data; cmo_flush(buffer[widx], sizeof(data)); // 写回数据 __atomic_thread_fence(__ATOMIC_RELEASE); __atomic_store_n(wptr, widx 1, __ATOMIC_RELAXED); cmo_flush(wptr, sizeof(wptr)); // 写回指针注意CMO和fence的顺序很重要。先flush数据再fence再更新指针再flush指针。顺序错了可能导致消费者看到新指针但读到旧数据。实测下来有硬件一致性的平台性能明显更好因为省掉了大量CMO。但即使有硬件一致性fence还是不能省因为硬件一致性保证的是Cache行级别的一致性不保证不同地址之间的顺序。5. 常见问题与排查技巧实录5.1 内存架构问题速查表现象可能原因排查方向解决方法访问外设寄存器数据不对PMA属性配错确认地址落在哪个PMA区间修正PMA配置外设区设为不可缓存多核数据不同步Cache一致性或fence缺失检查是否有硬件一致性检查fence使用加CMO或fence或启用硬件一致性DMA读到旧数据Cache未写回检查DMA前是否做了clean/flush在DMA启动前对数据区做cbo.cleanCPU读到DMA旧数据Cache未无效化检查DMA完成后是否做了inval在DMA完成后对数据区做cbo.inval修改代码后执行异常指令Cache未同步检查是否执行了fence.i和CMO修改代码后执行fence.i必要时对I-Cache做invalpage fault频繁页表映射缺失或权限不对检查页表项和satp补全映射修正权限位性能突然下降CMO过度使用或TLB miss高检查CMO调用频率和页表大小减少不必要的CMO使用大页多核TLB不一致页表修改后未刷新其他核TLB检查sfence.vma和IPI修改页表后通过IPI触发其他核sfence.vma5.2 几个我踩过的坑和独家技巧坑一以为所有RISC-V都有硬件Cache一致性。早期做一个双核项目核间共享内存通信代码在模拟器上跑得好好的上板子就出问题。后来查手册发现这个核根本没有硬件一致性必须软件维护。教训上板前一定确认硬件一致性能力别靠模拟器。技巧用fence.i的时机。fence.i只保证当前核的指令流能看到之前的存储操作不保证其他核。多核场景下修改共享代码需要先让其他核停止执行该区域修改后再让它们继续。这个在实现JIT或者动态加载时特别重要。坑二CMO操作没有按行对齐。有一次对一段非对齐地址做cbo.clean结果部分数据没写回。CMO指令要求地址按Cache行对齐不对齐的行为是实现定义的可能只操作包含该地址的行也可能触发异常。后来改成先对齐再操作问题解决。技巧批量CMO的优化。如果要对一大段内存做CMO逐行做开销很大。可以先用cbo.zero或者利用硬件的预取机制。另外如果知道某段内存不会被缓存比如刚分配还没访问过可以跳过CMO。坑三RVWMO下想当然的代码。写了一个无锁队列在x86上测没问题移植到RISC-V就偶发失败。原因是x86的强内存序掩盖了缺少fence的问题。后来用C11原子操作重写并加了__ATOMIC_ACQ_REL问题消失。教训在弱内存序平台上任何跨线程共享数据都要显式处理内存序。技巧用形式化工具验证。对于复杂的无锁算法可以用herd7工具配合RVWMO模型跑一下看看有没有不允许的执行。虽然不能替代测试但能发现一些微妙的顺序问题。5.3 调试工具和方法读CSRcsrr读misa、mstatus、satp、pmpcfg等确认当前配置。用模拟器QEMU支持RISC-V可以开-d int看异常-d mmu看地址翻译。但模拟器的内存模型可能和硬件不同只能参考。逻辑分析仪如果条件允许抓总线信号看实际的内存访问序列对排查PMA和Cache问题很有帮助。性能计数器很多RISC-V核有mhpmcounter可以统计Cache miss、TLB miss等用perf工具读取。提示调试内存问题时先缩小范围。比如先确认单核单线程下行为是否正确再加多核再加DMA再加MMU。每加一层就测一次问题定位会快很多。6. 写在最后一些个人体会这套内存架构的东西刚接触时确实容易晕。我的经验是别想着一次全搞懂而是按需深入。先搞清楚PMA和ePMP保证裸机程序能正确访问内存和外设然后搞Cache和CMO解决DMA和多核的数据可见性最后搞MMU和RVWMO支撑操作系统和复杂并发。每一步都有明确的场景驱动学起来不枯燥。另外手册和规范是必读的但别指望读一遍就懂。我的做法是先读一遍有个印象然后遇到具体问题时回去查对应章节配合实际代码和调试器验证。这样反复几次那些概念就真正变成自己的了。最后分享一个小技巧维护一个自己的“内存问题排查笔记”每次遇到问题就记下现象、原因、解决方法。积累多了你会发现很多问题其实是同一类根因的不同表现排查速度会越来越快。这个习惯我在多个平台上都受益过推荐你也试试。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进