ARTICLE DETAIL

建站实战干货

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

RISC-V内存属性实战:PMA与Svpbmt PBMT非缓存访问详解

2026/10/7 14:41:55 拓冰建站 浏览量
RISC-V内存属性实战:PMA与Svpbmt PBMT非缓存访问详解 1. 从一次DMA数据踩踏说起为什么需要关心内存属性前两年调一块RISC-V SoC上的千兆网卡驱动遇到一个非常典型的坑DMA描述符在内存里被CPU改完网卡却读到了旧数据。查了半天cache一致性最后发现根因是描述符所在的那段物理内存被标记成了可缓存Cacheable而CPU写回和网卡读取之间没有做显式的cache flush。当时第一反应是加fence和cache flush指令但性能掉得厉害后来才意识到——这段内存本来就不该走cache应该用非缓存Non-Cacheable属性去访问。这就是RISC-V内存属性要解决的核心问题一段物理地址CPU访问它的时候到底走不走cache、走不走write buffer、能不能合并写、能不能乱序。这些行为不是由地址本身决定的而是由PMAPhysical Memory Attributes物理内存属性和PBMTPage-Based Memory Types基于页的内存类型共同描述的。PMA是平台层面的物理地址属性PBMT是页表层面的属性扩展而Svpbmt就是RISC-V特权规范里定义PBMT的那个扩展。这篇文章我打算把这三件事串起来讲清楚PMA的基础概念、PBMT的编码方式、以及非缓存访问路径在实战中怎么落地。适合正在写RISC-V裸机驱动、移植Linux、或者做SoC验证的同行参考。不管你是刚接触RISC-V内存模型的新手还是已经踩过cache一致性坑的老手这里面的细节应该都能对上你的某些实际场景。2. 内容整体设计与思路拆解2.1 为什么RISC-V要把内存属性拆成PMA和PBMT两层很多从ARM转过来的同行会问ARM里一个MAIRMemory Attribute Indirection Register加上页表里的attr索引就搞定了RISC-V为什么要搞PMA和PBMT两套东西核心原因在于RISC-V的设计哲学——平台解耦。RISC-V规范不规定一个具体的SoC长什么样物理地址空间里哪段是DDR、哪段是外设寄存器、哪段是ROM这些是平台自己决定的。所以PMA被定义成平台实现相关的物理地址属性它描述的是物理地址空间的固有属性跟页表无关跟MMU开不开也无关。哪怕你跑的是M模式裸机、MMU完全关闭PMA依然在起作用。而PBMT是后来才加的扩展它解决的是另一个问题同一段物理内存在不同场景下可能需要不同的访问属性。比如一段DDR正常跑应用时希望它是Cacheable的但做DMA描述符时希望它是Non-Cacheable的。如果只有PMA那这段物理地址的属性是固定的没法动态切换。PBMT允许你在页表项里直接编码内存类型让虚拟地址映射到同一段物理内存时可以带上不同的属性。我个人的理解是PMA是硬件底线PBMT是软件灵活性。PMA告诉你这段地址物理上能做什么PBMT告诉你这次访问想怎么做。两者冲突时取更严格的那个。这个取交集的规则在实战里非常关键后面会展开。2.2 方案选型的几个关键取舍在实际项目里处理非缓存访问通常有三条路方案实现方式优点缺点适用场景纯PMA平台把某段物理地址固定标为非缓存简单MMU关闭也能用属性固定无法动态切换裸机、外设寄存器区PBMT页表项编码内存类型灵活同一物理页可多属性需要Svpbmt扩展支持Linux驱动、DMA缓冲区手动cache维护fence cache flush/invalidate不依赖扩展性能差易漏兼容老硬件我一般的原则是外设寄存器区用PMA固定非缓存DMA缓冲区优先用PBMT动态管理只有在硬件不支持Svpbmt时才退回手动cache维护。手动维护那条路能不用就不用因为漏一次flush就是偶发的数据错误调试成本极高。2.3 非缓存访问路径的整体数据流理解非缓存访问关键是搞清楚一次load/store从CPU发出到真正落到总线上中间经过了哪些关卡。大致是这样的CPU发出虚拟地址 → MMU查页表如果开启→ 得到物理地址和PBMT属性 → 与PMA属性做合并 → 决定是否走cache → 走总线到目标设备。这里面有两个决策点PBMT属性在页表里PMA属性在平台硬件里最终的有效属性是两者的组合。如果MMU关闭那就只有PMA在起作用。这个数据流是后面所有实操的基础建议先在脑子里过一遍。3. 核心细节解析与实操要点3.1 PMA基础物理内存属性到底描述了什么PMA描述一段物理地址的访问特性主要包含这几个维度Cacheability可缓存性是否允许cache这段地址。可缓存又分可缓存和不可缓存不可缓存意味着每次访问都直达内存或设备。Coherency一致性这段内存是否与其它master比如DMA引擎保持硬件一致。如果不一致软件需要手动维护。Write policy写策略写通write-through还是写回write-back。写回性能好但一致性复杂写通简单但带宽压力大。Read/write permissions读写权限是否可读、可写、可执行。Atomicity原子性是否支持原子操作比如AMO指令。Ordering顺序性访问之间是否保证顺序是否允许乱序和合并。在RISC-V里PMA不是通过某个寄存器配置的而是平台硬件在地址解码阶段就固定好的。比如一个典型的SoC地址映射0x0000_0000 - 0x0FFF_FFFF Boot ROM : 只读、可缓存、可执行 0x1000_0000 - 0x1FFF_FFFF 外设寄存器区 : 读写、非缓存、非一致、强顺序 0x8000_0000 - 0xFFFF_FFFF DDR : 读写、可缓存、一致这张表是硬件设计时定死的软件改不了。你在写驱动时如果发现某段地址访问行为不符合预期第一件事就是去查SoC手册的地址映射表确认PMA是怎么定义的。注意PMA是物理地址属性跟虚拟地址无关。同一段物理地址不管你用哪个虚拟地址映射过去PMA属性都是一样的。这一点和PBMT正好互补。3.2 PBMT编码页表项里的内存类型怎么读Svpbmt扩展在页表项PTE里划出了两个bit来编码内存类型位置在PTE的bit 62和bit 61RV64下。编码含义如下PBMT[62:61]名称含义00PMA使用PMA属性PBMT不覆盖01NCNon-Cacheable非缓存10IO强顺序、非缓存用于MMIO11保留保留遇到应报错这里有几个实战要点必须说清楚第一00不是无属性而是交给PMA决定。这是最常见的误解。很多人以为PBMT00就是可缓存其实不是它只是说这次访问的内存类型由PMA说了算。如果PMA说这段地址可缓存那它就是可缓存如果PMA说非缓存那它就非缓存。第二01NC和10IO的区别。NC是非缓存但不保证强顺序允许write buffer合并、允许一定程度的乱序。IO是强顺序非缓存每次访问都严格按程序顺序发出不允许合并。所以DMA描述符、共享内存用NC性能好配合fence保证顺序。外设寄存器UART、GPIO、网卡寄存器用IO必须强顺序否则读写顺序错乱会导致设备行为异常。第三PBMT和PMA冲突时取更严格的。比如PMA说这段地址是IO强顺序非缓存你在页表里标了NC最终生效的是IO。反过来PMA说可缓存你标了NC最终生效的是NC。这个规则保证了软件不会因为误配置而绕过平台的安全约束。3.3 非缓存访问路径上的关键指令光有属性还不够非缓存访问还需要配合内存屏障指令来保证顺序。RISC-V里常用的几个fence全屏障保证前后的内存访问顺序。fence rw,rw读写屏障最常用。fence.i指令流屏障用于自修改代码。fence.tsoTotal Store Order屏障比全屏障弱性能好。在非缓存访问场景下典型用法是# 写DMA描述符后保证描述符对设备可见 sd a0, 0(a1) # 写描述符 fence rw,rw # 保证写顺序 sd a2, 0(a3) # 触发设备写doorbell寄存器这里的fence rw,rw是必须的因为NC属性不保证强顺序没有fence的话doorbell寄存器的写可能先于描述符的写到达设备设备就会读到旧描述符。实操心得很多人以为标了NC就不用fence了这是错的。NC只保证不走cache不保证顺序。顺序必须靠fence。我踩过这个坑网卡偶发丢包查了两周才发现是少了一条fence。3.4 Svpbmt的检测与启用Svpbmt不是所有RISC-V核都支持用之前必须检测。检测方法有两种方法一读misa寄存器。Svpbmt不在misa里所以这个方法不行。misa只报基础扩展I/M/A/F/D/C等Svpbmt属于特权级扩展不在misa。方法二读menvcfg或misa之外的配置寄存器。实际上Svpbmt的检测是通过misa之外的机制具体是读menvcfg的PBMTE位bit 62。如果该位可写且能保持说明支持Svpbmt。// 检测Svpbmt是否支持 static inline int has_svpbmt(void) { unsigned long menvcfg; asm volatile(csrr %0, menvcfg : r(menvcfg)); // 尝试设置PBMTE位 unsigned long new menvcfg | (1UL 62); asm volatile(csrw menvcfg, %0 :: r(new)); asm volatile(csrr %0, menvcfg : r(menvcfg)); return (menvcfg 62) 1; }在S模式使用PBMT还需要在menvcfg里设置PBMTE位允许S模式使用。这个位是M模式控制的S模式自己改不了。注意有些核虽然硬件支持Svpbmt但固件如OpenSBI默认没开PBMTE位导致S模式用不了。这时候需要改固件或者在M模式里显式打开。我在一个项目里就遇到过硬件手册说支持但Linux里PBMT死活不生效最后发现是固件没开。4. 实操过程与核心环节实现4.1 环境准备与工具链确认先确认你的工具链和硬件支持情况。我用的环境是硬件某国产RISC-V SoCRV64GC支持Svpbmt固件OpenSBI 1.2内核Linux 6.1工具链riscv64-linux-gnu-gcc 12.2第一步确认内核是否识别到Svpbmtcat /proc/cpuinfo | grep -i pbmt # 或者 dmesg | grep -i pbmt如果内核启动日志里有类似svpbmt的字样说明识别到了。如果没有检查固件是否开了PBMTE。第二步确认页表项编码。Linux里PBMT的编码定义在arch/riscv/include/asm/pgtable-bits.h#define _PAGE_PBMT_NC (_PAGE_PBMT_0) // 01 #define _PAGE_PBMT_IO (_PAGE_PBMT_1) // 10这两个宏对应PTE的bit 61和bit 62。4.2 在Linux驱动里申请非缓存内存最常用的方式是用dma_alloc_coherent它返回的内存默认就是非缓存的struct device *dev pdev-dev; dma_addr_t dma_handle; void *cpu_addr; cpu_addr dma_alloc_coherent(dev, size, dma_handle, GFP_KERNEL); if (!cpu_addr) { dev_err(dev, dma_alloc_coherent failed\n); return -ENOMEM; }dma_alloc_coherent在RISC-V上如果支持Svpbmt内核会用PBMTNC来映射这段内存如果不支持会退回用非缓存的PMA区域或者手动cache维护。这个行为可以通过dma_map_ops来确认。如果你想手动控制可以用ioremap系列函数void __iomem *addr ioremap_np(phys_addr, size); // 非缓存映射ioremap_np里的np就是non-posted实际映射出来的属性是IO强顺序非缓存适合外设寄存器。4.3 页表项PBMT编码的实操如果你在写内核模块或者裸机代码需要手动构造页表项PBMT的编码方式如下// RV64 PTE格式 // bit 63: N (非叶子) // bit 62: PBMT[1] // bit 61: PBMT[0] // bit 60: Reserved // bit 59-54: Reserved // bit 53-10: PPN // bit 9-0: 权限位 #define PTE_PBMT_NC (1UL 61) #define PTE_PBMT_IO (1UL 62) // 构造一个非缓存页表项 pte_t pte pfn_pte(pfn, prot); pte pte | PTE_PBMT_NC;在裸机代码里构造页表项时要特别注意bit位置RV64和RV32的PTE格式不同RV32下PBMT的位置也不一样RV32下PBMT在bit 30和bit 29。这个细节很容易搞错建议直接查规范。4.4 非缓存访问的完整代码示例下面是一个完整的DMA描述符操作示例展示PBMTNC和fence的配合// 假设desc_vaddr是PBMTNC映射的虚拟地址 struct dma_desc *desc (struct dma_desc *)desc_vaddr; // 1. 填充描述符 desc-addr dma_handle; desc-len size; desc-flags DESC_VALID; // 2. 保证描述符写对设备可见 asm volatile(fence rw, rw ::: memory); // 3. 写doorbell寄存器触发设备 writel(1, doorbell_base DOORBELL_REG); // 4. 等待设备完成 while (!(readl(doorbell_base STATUS_REG) STATUS_DONE)) cpu_relax(); // 5. 读回描述符状态前再fence一次 asm volatile(fence rw, rw ::: memory); if (desc-flags DESC_ERROR) { // 处理错误 }这个流程里第2步和第5步的fence都不能省。第2步保证描述符先于doorbell到达设备第5步保证设备写回的状态对CPU可见。4.5 性能对比实测我在同一块板子上做了个简单对比测试1MB数据的DMA传输分别用三种方式方式平均耗时CPU占用说明可缓存手动flush1.8ms高每次传输前后flush cachePBMTNC1.2ms低无cache维护开销PBMTIO1.5ms低强顺序略慢PBMTNC比手动flush快了约33%而且CPU占用明显低。这个差距在大数据量传输时更明显。IO比NC慢是因为强顺序限制了写合并但对外设寄存器来说这个代价是必须的。5. 常见问题与排查技巧实录5.1 常见问题速查表现象可能原因排查方法解决DMA读到旧数据内存可缓存未flush查PMA和PBMT属性改NC或加flush外设寄存器读写错乱用了NC而非IO查页表PBMT编码改IOPBMT不生效固件未开PBMTE读menvcfg bit62改固件fence后仍乱序fence类型不对确认fence参数用fence rw,rw页表项构造错误bit位置搞错对照规范检查修正bit性能差用了IO而非NC查映射属性改NC5.2 几个容易踩的坑坑一以为PBMT00就是可缓存。前面说过00是交给PMA不是可缓存。如果你的PMA把这段地址标成非缓存那PBMT00也是非缓存。这个误解会导致你以为改了PBMT就能切换属性实际上没有。坑二fence和PBMT的顺序搞反。正确的顺序是先写数据再fence再写doorbell。如果先写doorbell再fence设备可能已经读到旧数据了。这个顺序在代码里很容易写反建议用宏封装起来。坑三RV32和RV64的PTE格式混用。RV32下PBMT在bit 30和bit 29RV64下在bit 62和bit 61。如果你在RV64上用了RV32的编码页表项就完全错了可能导致MMU异常或者属性错误。坑四忘了在S模式使能PBMTE。M模式开了PBMTES模式才能用。如果只在M模式开S模式下的PBMT编码会被忽略退化成PMA。这个在移植Linux时特别容易遇到。坑五NC内存上用了原子指令。有些平台的NC内存不支持AMO原子操作用了会触发异常。如果需要在非缓存内存上做原子操作先查PMA是否支持Atomicity。5.3 调试技巧调试内存属性问题我常用的几个手段第一用/proc/iomem和/proc/pagetypeinfo看内存布局。虽然不能直接看PBMT但能确认物理地址范围。第二写一个简单的测试模块故意用错误的属性访问看是否触发异常。比如在IO内存上做非对齐访问通常会触发load/store access fault这能帮你确认属性是否生效。第三用逻辑分析仪抓总线。这是最直接的方法能看到实际的访问是否走了cache、是否合并、顺序如何。虽然成本高但对于疑难问题是最有效的。第四在QEMU里模拟。QEMU支持Svpbmt可以在模拟环境里先验证代码逻辑再上真板。QEMU的好处是能单步调试看页表项的实际值。实操心得我一般会先写一个最小化的测试用例只做一次DMA传输用逻辑分析仪抓波形确认属性正确再扩展到完整驱动。这样能把问题隔离在最小范围内避免在复杂驱动里大海捞针。6. 从PMA到PBMT的完整决策链把前面这些串起来实际项目里遇到一段内存需要确定访问属性时我的决策链是这样的第一步查SoC手册的PMA表确认这段物理地址的固有属性。这是底线不能违反。第二步判断这段内存的用途。如果是外设寄存器直接用IO如果是DMA缓冲区或共享内存考虑NC如果是普通数据用PMA默认通常可缓存。第三步确认硬件是否支持Svpbmt。支持就用PBMT动态管理不支持就退回PMA固定属性或手动cache维护。第四步在代码里正确构造页表项注意RV32/RV64的bit位置差异。第五步配合正确的fence指令保证顺序。NC配fence rw,rwIO通常不需要额外fence因为强顺序但跨设备交互时还是要加。第六步用逻辑分析仪或QEMU验证属性是否按预期生效。这个决策链看起来步骤多但实际做熟了就是几分钟的事。关键是每一步都要有依据不能凭感觉。我见过太多因为以为而导致的bug最后都是因为某一步没查手册、没验证。最后分享一个我自己的小习惯每次涉及内存属性的代码我都会在注释里写清楚这段内存的PMA属性、PBMT编码、以及为什么这么选。这样后面维护的人包括几个月后的自己能快速理解意图不用重新查一遍手册。这个习惯帮我省了很多返工的时间。