2. ARM内存模型基础:内存类型、内存属性与内存屏障指令

各位好,我是老周。今天咱们来啃一块硬骨头——ARM的内存模型。说实话,我见过太多工程师在MMU配置上翻车,十有八九都是因为没搞懂内存类型和属性的底层逻辑。你想想看,CPU和总线之间怎么交互数据?Cache要不要用?多个核看到的数据是不是一致的?这些问题,全得靠内存模型来回答。

这一节我会把内存类型、属性和屏障指令揉碎了讲。嗯,咱们不搞学院派那套,直接上实战视角。

2.1 三种内存类型:Normal / Device / Strongly-Ordered

ARM把内存访问的行为分成了三大类。说白了,就是告诉CPU:你面前这块地址,到底是个什么脾气。

内存类型 典型场景 核心特征
Normal DDR、SRAM、Flash 允许合并、重排、预取;Cache友好
Device MMIO寄存器、FIFO 不允许合并;访问次数、大小、顺序严格
Strongly-Ordered 中断控制器、看门狗 最严格;所有访问按程序顺序执行

2.1.1 Normal Memory

这是最宽松的类型。CPU可以对Normal内存做各种优化:把多次写合并成一次、提前预读、乱序执行。我调试过一个网络驱动,吞吐死活上不去,最后发现是DMA缓冲区被配成了Device类型,CPU没法预取,性能直接腰斩。

关键点:Normal内存是唯一允许Cacheable的类型。如果你想让数据进Cache,必须先把内存类型设为Normal。

2.1.2 Device Memory

Device类型专门伺候外设寄存器。它有几个硬规矩:

  • 每次读写必须严格按照代码写的次数执行——不能合并,不能省略。
  • 访问大小必须匹配——你写32位寄存器,就不能拆成两个16位。
  • 访问顺序必须保持——先写A再写B,硬件上就得先走A再走B。

我踩过一个坑:某款SoC的GPIO寄存器被配成了Normal类型,结果写IO时CPU把两次连续的写操作合并了,外设直接罢工。从那以后,我对外设地址空间的类型检查就格外小心。

2.1.3 Strongly-Ordered Memory

这是最严格的类型。所有访问完全按程序顺序执行,不允许任何重排或合并。说白了,就是让CPU变成"老实人"——你写什么,它就做什么。

不过代价也很大:性能极差。我一般只在中断控制器、看门狗这类对时序极度敏感的场景才用。平时能用Device就别上Strongly-Ordered。

2.2 内存属性:Shareability 与 Cacheability

这两个属性决定了数据在多核系统中的可见性。很多工程师只配了内存类型,忘了配属性,结果多核一跑就出诡异问题。

2.2.1 Cacheability

Cacheability决定了一块内存能不能被Cache缓存。它分三级:

  • Non-Cacheable:每次访问都走总线,直接读写内存。适合DMA缓冲区、外设寄存器。
  • Write-Back (WB):写操作先进Cache,等时机成熟再写回内存。性能最好,但需要软件维护一致性。
  • Write-Through (WT):写操作同时更新Cache和内存。读性能好,写性能差一些。

我的习惯:数据量大且不需要多核共享的,用Write-Back。需要多核共享的,要么用Non-Cacheable,要么用Write-Back加Cache维护。

2.2.2 Shareability

Shareability告诉硬件:这块内存会被几个核访问?

  • Non-Shareable:只有当前核访问。硬件可以不做跨核一致性。
  • Inner Shareable:同一个Cluster内的多个核共享。
  • Outer Shareable:跨Cluster共享。

我遇到过一个问题:两个核通过共享内存通信,A核写数据,B核读数据。结果B核一直读到旧值。查了半天,发现共享内存配成了Non-Shareable,硬件压根没做一致性广播。改成Inner Shareable后问题解决。

注意:Shareable和Cacheable必须搭配使用。Non-Shareable + Write-Back是常见组合,但如果你在多核场景下这么配,必须自己用Cache操作维护一致性。

2.3 内存屏障指令:DMB / DSB / ISB

内存屏障是ARM架构里最容易被忽视的"隐形杀手"。很多工程师写驱动时觉得加屏障是"玄学",其实不然——屏障是给CPU和编译器下的一道死命令。

指令 全称 作用范围 典型场景
DMB Data Memory Barrier 内存访问顺序 保证DMA描述符写完后才启动DMA
DSB Data Synchronization Barrier 内存访问完成等待 修改页表后等待TLB无效完成
ISB Instruction Synchronization Barrier 指令流水线刷新 修改页表后刷新预取指令

2.3.1 DMB:保证顺序,不保证完成

DMB保证:在它之前的所有内存访问,在它之后的内存访问开始前,必须对其他观察者可见。但它不保证之前的访问已经完成——只保证顺序。

// 典型用法:写描述符 + DMB + 通知DMA
*desc = prepared_data;    // 写DMA描述符
DMB();                    // 保证desc写完后,DMA引擎能看到
*reg = START_BIT;         // 启动DMA

我习惯在每次写外设控制寄存器之前,先检查是否需要DMB。特别是DMA场景,少了这个屏障,描述符可能还没刷到内存,DMA就已经开始搬运旧数据了。

2.3.2 DSB:等所有访问完成

DSB比DMB更狠。它不光保证顺序,还保证之前的所有内存访问全部完成之后,才执行后面的指令。说白了,DSB会让CPU停下来等你。

// 修改页表后必须用DSB等待完成
write_ttbr1(new_page_table);  // 写页表基地址
DSB();                        // 等待页表写完成
ISB();                        // 刷新流水线,让CPU用新页表

性能陷阱:DSB非常昂贵。我见过有人在内核热路径上滥用DSB,导致系统吞吐量下降30%。能用DMB就别用DSB,除非你真的需要等完成。

2.3.3 ISB:刷新指令流水线

ISB的作用是清空CPU的指令流水线,让CPU重新预取指令。它和内存访问无关,只影响指令执行。

什么时候用?

  • 修改了页表之后——让CPU用新映射取指。
  • 修改了异常向量表之后——确保新向量生效。
  • 修改了系统控制寄存器之后——比如关了MMU再开。

我调试过一个自修改代码的bug:代码在运行时修改了自身指令,但CPU流水线里还存着旧指令,结果执行出错。加一条ISB就解决了。

2.4 知识结构总览

下面这张图把内存类型、属性和屏障指令的关系串起来了。我建议你把它存下来,配置MMU时对照着看。

ARM内存模型知识结构图 ARM内存模型核心知识结构 内存类型 Normal Device Strongly-Ordered 内存属性 Cacheability Shareability 内存屏障指令 DMB - 保证顺序 DSB - 等待完成 ISB - 刷新流水线 配置MMU时的决策顺序 先选内存类型 → 再配Cacheability和Shareability → 最后在关键点加屏障指令 常见组合速查 DDR: Normal + Write-Back + Inner Shareable | MMIO: Device + Non-Cacheable + Non-Shareable 中断控制器: Strongly-Ordered + Non-Cacheable | 共享内存: Normal + Non-Cacheable 或 Write-Back + Inner Shareable

2.5 避坑指南

最后,我把自己这些年踩过的坑总结成几条铁律:

  • 外设寄存器永远不要配成Normal。哪怕你觉得"反正就一个寄存器",CPU的合并优化会让你怀疑人生。
  • 多核共享内存必须配Shareable。否则你看到的"共享"只是假象。
  • 修改页表后,DSB + ISB是标准套餐。少一个都可能出问题。
  • DMA描述符写完,加DMB再通知DMA。这个顺序错了,DMA可能读到半成品描述符。
  • 不要迷信Strongly-Ordered。它确实安全,但性能代价太大。能用Device就别上它。