ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

RISC-V内存架构深度解析:PMA、ePMP、Cache、MMU与RVWMO

2026/10/7 14:41:55 拓冰建站 浏览量
RISC-V内存架构深度解析:PMA、ePMP、Cache、MMU与RVWMO 1. 从一条内存访问指令说起为什么RISC-V的内存架构值得深挖很多人学RISC-V学着学着就卡在内存这块了。指令集本身看着挺清爽RV32I/RV64I那点东西几天就能过一遍但一旦碰到PMA、ePMP、Cache、CMO、MMU、RVWMO这一串缩写文档翻来翻去还是云里雾里。我自己最开始看特权架构手册的时候光是PMP和PMA的关系就绕了好几天更别说后面MMU的页表遍历和RVWMO那套形式化模型了。这篇东西就是把我自己啃这块内容的路径完整梳理一遍。核心围绕RISC-V的内存架构展开具体涉及PMAPhysical Memory Attributes物理内存属性、ePMPenhanced Physical Memory Protection增强型物理内存保护、Cache与CMOCache Management Operations缓存管理操作、MMUMemory Management Unit内存管理单元细节以及RVWMORISC-V Weak Memory OrderingRISC-V弱内存序形式化模型。这些概念不是孤立的它们从物理地址属性一路贯穿到虚拟地址翻译再到多核之间的内存可见性构成了一条完整的链路。这篇文章适合谁看如果你已经写过一些RISC-V的裸机代码或者移植过RTOS、跑过Linux但对内存子系统的细节还停留在“能用就行”的阶段那这篇就是给你准备的。如果你正在做SoC设计、验证或者需要写跟Cache、MMU打交道的底层驱动这里面的内容应该能帮你少踩几个坑。我会尽量把每个概念的“为什么”讲清楚而不是只丢一堆寄存器定义给你。先给一个全局的视角RISC-V的内存架构设计哲学是分层解耦。PMA管的是物理地址空间里每一段内存“能干什么”ePMP管的是“谁可以访问”MMU管的是“虚拟地址怎么映射到物理地址”Cache管的是“怎么加速访问”CMO管的是“怎么维护缓存一致性”RVWMO管的是“多核之间看到的访问顺序是什么”。这六层各司其职但又在实际运行中紧密配合。理解了这个分层逻辑后面看具体细节就不会乱。2. 物理内存属性PMA地址空间的“性格标签”2.1 PMA到底解决什么问题PMA这个概念说白了就是给物理地址空间里的每一段区域贴上标签告诉硬件这段内存有什么特性。你可以把它想象成给不同地块发“土地用途证”——这块地是盖住宅的那块是建工厂的还有一块是公园不能动土。CPU发出一个物理地址访问请求时硬件需要知道这个地址落在哪个区域这个区域支持什么操作然后才能决定怎么处理这个请求。为什么需要PMA因为一个SoC里的物理地址空间不是铁板一块。有的区域是DDR内存可读可写可缓存有的区域是ROM只读且可以缓存有的区域是外设寄存器可读可写但绝对不能缓存而且访问顺序必须严格保持还有的区域可能根本就不存在访问了应该报错。如果没有PMA机制CPU就没办法区分这些情况可能会对外设寄存器做缓存或者对不存在的地址默默返回零这些都是灾难性的。PMA的核心属性包括几个维度。可缓存性Cacheability决定这段内存能不能被Cache缓存以及用什么策略缓存。可执行性Executability决定这段内存能不能取指令。读写权限Read/Write/Execute permissions决定允许什么类型的访问。访问粒度Access granularity决定最小访问单位是多少。原子性Atomicity决定是否支持原子操作。顺序性Ordering决定访问之间是否需要保持顺序。一致性Coherence决定这段内存是否被多个hart一致地看到。2.2 PMA的实现方式与检查时机PMA在硬件里的实现方式通常有两种。一种是硬连线的在SoC设计阶段就把地址空间的属性固定下来用地址译码逻辑实现。另一种是可配置的通过CSR或者内存映射寄存器来设置。大多数RISC-V核用的是硬连线方式因为PMA属性在系统运行期间一般不会变硬连线更简单也更可靠。PMA检查发生的时机很关键。它是在物理地址生成之后、实际内存访问之前进行的。对于支持MMU的系统虚拟地址先经过MMU翻译成物理地址然后物理地址再经过PMA检查。对于不支持MMU的裸机系统CPU直接发出物理地址PMA检查同样生效。这个顺序很重要因为PMA管的是物理地址的属性跟虚拟地址无关。注意PMA检查是在物理地址层面做的这意味着即使MMU把两个不同的虚拟地址映射到了同一个物理地址PMA属性也是一样的。不要试图通过MMU来绕过PMA限制。2.3 PMA与PMP/ePMP的关系这里容易混淆的是PMA和PMPPhysical Memory Protection的关系。PMA描述的是内存的固有属性比如“这段DDR是可缓存的”PMP/ePMP描述的是访问控制策略比如“只有M模式才能访问这段内存”。PMA是“这段内存是什么”PMP是“谁可以访问这段内存”。举个例子一段DDR内存PMA说它是可缓存、可读写、可执行的PMP可能配置为只有S模式和M模式可以访问U模式访问会触发异常。两者是正交的一个管属性一个管权限。实际硬件在处理一个访问请求时两个检查都会做任何一个不通过都会导致访问失败。ePMP是在PMP基础上的增强版本主要增加了对Smepmp扩展的支持。传统PMP只有16个配置项每个项可以设置地址范围和权限位。ePMP增加了更多的配置灵活性比如支持更细粒度的权限控制、支持对M模式本身的限制等。ePMP的一个关键特性是引入了MSECCFG寄存器可以配置M模式是否受PMP限制以及是否允许对PMP配置本身进行锁定。3. ePMP深度拆解从PMP到增强型保护3.1 传统PMP的局限性传统PMP的设计相对简单最多16个PMP配置项每个项包含一个地址寄存器pmpaddr和一个配置寄存器pmpcfg。地址匹配模式支持OFF、TORTop of Range、NA4Natural Aligned 4-byte、NAPOTNatural Aligned Power of Two。权限位包括R、W、X以及一个LLock位。这套机制在大多数场景下够用但有几个明显的局限。第一M模式默认不受PMP限制这意味着如果M模式代码有bug可以随意访问任何物理地址没有硬件层面的保护。第二PMP配置项数量有限16个项在复杂系统中可能不够用。第三缺少对PMP配置本身的保护恶意代码可能修改PMP配置来提权。第四不支持对M模式的细粒度控制比如只想限制M模式访问某些外设但允许访问其他区域。3.2 ePMP的关键增强ePMP针对这些局限做了增强。最核心的变化是引入了mseccfg寄存器它有三个关键位MMLMachine Mode Lockdown、MMWPMachine Mode Whitelist Policy、RLBRule Locking Bypass。MML位的作用是让M模式也受PMP限制。当MML1时M模式访问内存时也会检查PMP配置只有明确允许的访问才能通过。这解决了M模式“裸奔”的问题。MMWP位控制默认策略当MMWP1时没有匹配任何PMP项的访问默认被拒绝当MMWP0时没有匹配的访问默认允许。RLB位允许在特定条件下绕过PMP锁定用于调试或固件更新场景。ePMP还引入了Smepmp扩展增加了对PMP配置项的共享和权限委托机制。比如可以配置某个PMP项对S模式可见但不可修改或者允许S模式在M模式设定的范围内自行配置PMP。这些特性在虚拟化场景和可信执行环境中非常有用。3.3 ePMP配置实操与注意事项配置ePMP的基本流程是这样的首先确定需要保护的物理地址区域然后为每个区域分配一个PMP项设置地址匹配模式和权限位最后配置mseccfg寄存器启用增强功能。# 示例配置PMP项0保护0x80000000-0x8000FFFF区域M模式可读写执行 # pmpaddr0 0x80000000 2 0x20000000 li t0, 0x20000000 csrw pmpaddr0, t0 # pmpcfg0: ANAPOT(3), X1, W1, R1 0x1F li t0, 0x1F csrw pmpcfg0, t0 # 启用MML和MMWP li t0, 0x3 # MML1, MMWP1 csrw mseccfg, t0注意mseccfg寄存器一旦设置MML或MMWP位通常需要复位才能清除。在调试阶段建议先用RLB位保留绕过能力否则一旦配置错误可能导致系统无法启动。实际配置时有几个坑要注意。第一pmpaddr的编码方式NAPOT模式下地址的低位用来编码区域大小不是简单的地址右移。比如要保护4KB区域pmpaddr的低10位应该编码为0x3FF。第二TOR模式需要两个PMP项配合前一个项的地址作为起始后一个项的地址作为结束配置时要注意顺序。第三锁定位L一旦设置不可逆设置前务必确认配置正确。4. Cache与CMO性能与一致性的博弈4.1 RISC-V Cache架构概览RISC-V的Cache设计比较灵活规范没有强制要求特定的Cache结构而是定义了Cache管理操作CMO的接口。实际实现中常见的配置包括L1指令Cache、L1数据Cache、L2统一Cache等多级结构。Cache的替换策略、写策略写回/写直达、行大小等参数由具体实现决定。从软件视角看Cache的存在带来两个问题一致性Coherence和一致性维护Consistency。一致性指的是多个hart看到的同一内存位置的值是否相同一致性维护指的是当内存内容改变时如何确保Cache里的副本也更新。RISC-V通过CMO指令来让软件显式管理Cache而不是完全依赖硬件自动维护。4.2 CMO指令集详解CMO扩展定义了一组指令用于管理Cache和内存之间的数据同步。核心指令包括cbo.clean将Cache行写回内存但保留Cache中的副本。适用于DMA读取前确保内存数据最新。cbo.flush将Cache行写回内存并无效化。适用于DMA写入后确保CPU读到新数据。cbo.inval无效化Cache行不写回。适用于确认Cache中数据不需要保留的场景。cbo.zero将Cache行清零。用于快速初始化大块内存。prefetch.i / prefetch.r / prefetch.w预取指令提前将数据加载到Cache。这些指令的操作对象是Cache块Cache Block通常以Cache行大小为粒度。CMO指令的地址参数需要对齐到Cache行边界否则行为可能是实现定义的。# 示例DMA读取前确保数据已写回内存 # 假设数据地址在a0Cache行大小64字节 cbo.clean (a0) # 可能需要 fence 确保操作完成 fence4.3 CMO使用场景与实操心得CMO最典型的使用场景是DMA传输。当CPU准备好数据要交给DMA发送时需要确保数据已经从Cache写回内存否则DMA可能读到旧数据。当DMA接收数据后CPU需要确保Cache中的旧副本被无效化否则CPU可能读到旧数据。另一个场景是指令流修改。当程序动态生成代码或者修改指令时需要先清理数据Cache再无效化指令Cache最后执行fence.i确保取指流水线看到新指令。# 动态代码修改后的同步流程 # 1. 写回数据Cache cbo.flush (code_addr) # 2. 无效化指令Cache cbo.inval (code_addr) # 3. 同步指令流 fence.i实操心得CMO指令的性能开销不小尤其是cbo.flush和cbo.inval。在频繁DMA的场景下建议用批量操作代替逐行操作或者考虑使用非缓存内存来避免CMO开销。另外不同实现的CMO粒度可能不同写驱动前一定要查清楚具体SoC的Cache行大小。还有一个容易忽略的点CMO指令本身也需要同步。在多核系统中一个hart执行CMO后其他hart不一定立即看到效果需要配合fence或者IPI处理器间中断来同步。这个在写多核通信代码时特别容易踩坑。5. MMU细节从虚拟地址到物理地址的完整旅程5.1 RISC-V MMU的两种模式RISC-V的MMU支持两种分页模式Sv39、Sv48和Sv57分别对应39位、48位和57位虚拟地址。Sv39是RV64的基本要求Sv48和Sv57是可选的。Sv32是RV32的分页模式只支持32位虚拟地址。以Sv39为例虚拟地址39位物理地址56位。页表是三级结构每级页表有512个条目每个条目8字节所以每级页表占4KB正好一个页的大小。虚拟地址被划分为VPN[2]、VPN[1]、VPN[0]和页内偏移四部分分别用于三级页表的索引。5.2 页表遍历过程详解页表遍历从satp寄存器开始。satp包含MODE字段选择分页模式、ASID字段地址空间标识符和PPN字段根页表的物理页号。遍历过程如下从satp.PPN得到根页表基地址加上VPN[2]乘以8的偏移读取第一级页表项PTE。检查PTE的V位如果为0则触发页错误。检查权限位是否允许当前访问类型。如果PTE是叶子节点R1或X1则完成遍历得到物理地址。否则从PTE的PPN字段得到下一级页表基地址继续遍历。重复上述过程直到找到叶子PTE或触发异常。// 简化的Sv39页表遍历伪代码 uint64_t traverse_page_table(uint64_t satp, uint64_t va) { uint64_t ppn satp 0xFFFFFFFFFFF; // 提取根页表PPN for (int level 2; level 0; level--) { uint64_t vpn (va (12 level * 9)) 0x1FF; uint64_t pte_addr (ppn 12) vpn * 8; uint64_t pte *(uint64_t *)pte_addr; if (!(pte PTE_V)) return PAGE_FAULT; if (pte (PTE_R | PTE_X)) { // 叶子节点检查权限 if (!check_permission(pte, access_type)) return PAGE_FAULT; return (pte 10 12) | (va 0xFFF); } ppn pte 10; } return PAGE_FAULT; }5.3 TLB与ASID的作用每次访问都遍历页表太慢了所以MMU里通常有TLBTranslation Lookaside Buffer来缓存最近的翻译结果。TLB的条目包含虚拟页号、物理页号、权限位和ASID。ASIDAddress Space Identifier的作用是区分不同进程的地址空间。当切换进程时如果新进程的ASID不同TLB中旧进程的条目可以保留而不需要刷新因为ASID不同不会误命中。这大大减少了进程切换时的TLB刷新开销。注意ASID的位数由实现决定通常是8位或16位。ASID用完后需要回绕回绕时需要刷新TLB。写OS的TLB管理代码时要特别注意这一点。5.4 MMU配置实操与常见问题配置MMU的基本步骤分配页表内存必须页对齐填充页表项设置satp寄存器执行sfence.vma刷新TLB。# 设置satp启用Sv39根页表PPN为0x80000 li t0, 0x8000000000080000 # MODE8(Sv39), PPN0x80000 csrw satp, t0 sfence.vma zero, zero # 刷新所有TLB条目常见问题包括页表项权限位设置错误导致页错误satp设置后忘记sfence.vma导致TLB使用旧翻译页表内存没有页对齐导致硬件读取错误多级页表中中间级PTE的权限位设置不当。还有一个容易忽略的点A位和D位。A位表示页面被访问过D位表示页面被写过。有些实现会自动更新这两位有些需要软件模拟。如果硬件不自动更新软件需要在页错误处理中手动设置否则会导致无限页错误。6. RVWMO形式化多核内存序的数学描述6.1 为什么需要内存序模型多核系统中每个核都有自己的Cache和流水线内存访问的实际执行顺序可能和程序代码的顺序不一致。比如核A先写X再写Y核B可能先看到Y的新值再看到X的新值。这种乱序如果处理不当会导致程序逻辑错误。内存序模型就是定义“什么顺序是允许的什么顺序是不允许的”。RISC-V采用的是弱内存序Weak Memory Ordering即默认情况下不保证访问顺序需要显式使用fence指令来建立顺序。RVWMO就是这个弱内存序模型的形式化描述。6.2 RVWMO的核心概念RVWMO模型基于几个核心关系程序顺序Program Order、全局内存顺序Global Memory Order、读-写关系、同步关系。形式化定义使用了一阶逻辑和集合论来描述这些关系。简单来说RVWMO定义了哪些操作之间必须保持顺序哪些可以重排。比如同一地址的访问必须保持顺序读后读、读后写、写后读、写后写。带有acquire语义的操作之后的访问不能重排到它之前。带有release语义的操作之前的访问不能重排到它之后。fence指令可以建立更强的顺序关系。6.3 fence指令的语义与使用fence指令的完整格式是fence pred, succ其中pred和succ是位掩码分别表示前驱和后继操作类型。位包括i指令取指、o设备输出、r内存读、w内存写。# 完整的全屏障 fence iorw, iorw # 只保证写-读顺序 fence w, r # 只保证读-读顺序 fence r, rfence的语义是在fence之前的所有pred类型的操作必须在fence之后的所有succ类型的操作之前完成对其他hart可见。这个“完成”的定义涉及全局内存顺序是RVWMO形式化的核心。实操心得fence的粒度越细性能越好但容易出错。我一般先用fence iorw, iorw保证正确性性能优化时再逐步细化。另外fence只影响内存访问顺序不影响寄存器依赖不要混淆。6.4 RVWMO形式化验证实践RVWMO的形式化模型可以用工具进行验证比如Herd7、rmem等。这些工具可以模拟多核执行检查特定代码模式是否满足预期的内存序。一个典型的验证场景是消息传递模式核A写数据然后写标志位核B读标志位然后读数据。在弱内存序下核B可能看到标志位新值但数据旧值。正确的写法是核A在写标志位前加release fence核B在读数据前加acquire fence。# 核A写数据release fence写标志 sd data, (a0) fence w, w sd flag, (a1) # 核B读标志acquire fence读数据 ld t0, (a1) fence r, r ld t1, (a0)形式化验证可以帮助发现这类问题。我自己的经验是先用工具验证代码模式确认无误后再移植到实际硬件。实际硬件可能有额外的约束或优化但形式化模型提供了正确性的基线。7. 常见问题与排查技巧实录7.1 PMA/PMP相关故障排查问题访问外设寄存器时数据不对读到的值总是零或者旧值。排查思路首先检查PMA属性是否把外设区域标记为可缓存。如果外设区域被错误地标记为可缓存CPU可能从Cache读取旧值而不是实际寄存器值。解决方法是在PMA配置中将外设区域标记为非缓存Non-cacheable和强顺序Strongly Ordered。问题M模式代码访问某地址触发异常但PMP配置看起来没问题。排查思路检查ePMP的MMWP位是否启用。如果MMWP1且没有PMP项匹配该地址访问会被拒绝。另外检查MML位如果MML1M模式也受PMP限制。解决方法是添加匹配的PMP项或调整mseccfg配置。7.2 Cache/CMO相关故障排查问题DMA传输后CPU读到旧数据。排查思路确认DMA写入后是否执行了cbo.inval或cbo.flush。注意cbo.inval只无效化不写回如果Cache中有脏数据会丢失。正确做法是DMA写入前先cbo.flush如果有脏数据DMA写入后cbo.inval。问题动态修改代码后执行旧指令。排查思路检查是否执行了完整的同步流程cbo.flush数据Cache→ cbo.inval指令Cache→ fence.i。缺少任何一步都可能导致取指流水线看到旧指令。7.3 MMU相关故障排查问题启用MMU后系统立即页错误。排查思路检查satp的MODE字段是否正确PPN是否指向有效的页表内存。检查根页表的第一级PTE是否有效。检查页表内存是否页对齐。检查PTE的权限位是否允许当前访问类型。问题进程切换后TLB命中错误地址。排查思路检查ASID是否正确设置。如果两个进程使用相同ASID但不同页表TLB会误命中。解决方法是确保每个进程有唯一ASID或者在切换时执行sfence.vma刷新TLB。7.4 RVWMO相关故障排查问题多核通信偶发数据不一致。排查思路检查fence指令是否覆盖了所有需要同步的访问类型。检查fence的位置是否正确——release fence要在写操作之后、标志写之前acquire fence要在标志读之后、数据读之前。使用形式化工具验证代码模式。问题现象可能原因排查方法解决方案外设读值异常PMA标记为可缓存检查PMA配置标记为非缓存M模式访问异常MMWP/MML启用检查mseccfg调整PMP项或mseccfgDMA后读旧数据缺少CMO检查CMO指令添加cbo.flush/inval动态代码不更新同步不完整检查同步流程补全flushinvalfence.iMMU启用即页错误页表配置错误检查satp和PTE修正页表配置TLB误命中ASID冲突检查ASID分配唯一ASID或刷新TLB多核数据不一致fence不完整检查fence语义补全fence或形式化验证8. 从裸机到Linux内存架构的实战映射8.1 裸机环境下的内存配置裸机环境下PMA通常是硬连线的不需要软件配置。PMP/ePMP需要软件初始化一般在启动代码中完成。Cache可能默认开启CMO在DMA场景下手动调用。MMU通常不启用直接使用物理地址。裸机启动代码的典型内存初始化流程设置栈指针指向有效的可读写内存区域配置PMP保护关键区域如果需要DMA则配置相关CMO最后跳转到主程序。8.2 Linux内核中的RISC-V内存管理Linux内核在RISC-V上的内存管理涉及几个关键部分。页表管理使用Sv39/Sv48模式内核空间和用户空间有各自的页表。TLB管理通过sfence.vma指令实现ASID由内核分配。Cache管理在DMA API中通过CMO指令实现一致性。内存序通过Linux的屏障API如smp_mb、smp_rmb、smp_wmb映射到fence指令。Linux启动时内核会解析设备树中的内存节点建立物理内存映射。PMA属性通常由设备树中的“memory”节点和“mmio”节点隐含定义。PMP/ePMP在M模式固件如OpenSBI中配置Linux运行在S模式受PMP限制。8.3 性能优化建议从性能角度看内存子系统的优化有几个方向。减少CMO开销尽量使用非缓存内存做DMA缓冲区避免频繁的Cache维护操作。优化页表遍历使用大页2MB或1GB减少页表级数提高TLB命中率。合理使用fencefence粒度尽量细避免不必要的全屏障。ASID管理合理分配ASID减少TLB刷新次数。个人体会内存子系统的调试往往是最耗时的因为问题现象可能很随机难以复现。我的经验是先保证正确性再优化性能用形式化工具验证内存序用压力测试验证CMO用性能计数器验证优化效果。不要凭直觉猜测问题原因要用工具和数据说话。最后分享一个排查内存问题的通用技巧二分法定位。如果怀疑是PMA/PMP问题先禁用所有保护看是否正常如果怀疑是Cache问题先关闭Cache看是否正常如果怀疑是MMU问题先用恒等映射看是否正常。逐步缩小范围比盲目猜测高效得多。