3、内存模型基础:什么是内存模型?x86/ARM/AArch64架构的内存排序差异。
内存模型(Memory Model)是处理器架构与软件之间的一份“排序契约”。它定义了在多处理器(或多核)环境下,一个CPU核心对内存的写入操作何时能够被其他核心观察到,以及读写指令在硬件层面可以以何种顺序被重排。理解内存模型是掌握内存屏障与DMA描述符同步的基石。
核心观点: 没有内存模型,多核并发编程将陷入“指令乱飞”的混沌状态。内存模型决定了哪些重排是合法的,哪些是需要通过屏障来禁止的。
3.1 什么是内存模型?
内存模型本质上是一组规则,它描述了在共享内存系统中,内存操作的顺序和可见性。它主要解决两个问题:
- 顺序性(Ordering): 程序中的内存访问指令(Load/Store)在硬件上实际执行的顺序。
- 可见性(Visibility): 一个处理器对内存的修改何时对另一个处理器可见。
现代处理器为了提升性能,普遍采用以下技术,这些技术正是导致内存重排的根源:
- 指令流水线与乱序执行(Out-of-Order Execution): CPU内部动态调整指令执行顺序以填满流水线。
- 写缓冲(Store Buffer)与失效队列(Invalidate Queue): 缓存一致性协议(如MESI)的异步优化,导致写操作延迟可见。
- 编译器优化: 编译器在生成机器码时也可能重排内存访问指令。
根据对重排的容忍程度,内存模型通常分为两类:
| 模型类型 | 特点 | 典型架构 |
|---|---|---|
| 强排序(Strong Ordering / TSO) | 保留大部分程序顺序,仅允许少数特定重排(如Store-Load)。编程模型相对简单,但性能提升有限。 | x86 / x86-64 |
| 弱排序(Weak Ordering / WMO) | 允许大量重排,包括Load-Load、Load-Store、Store-Store、Store-Load。性能潜力巨大,但需要显式使用内存屏障来保证同步。 | ARM / AArch64, PowerPC |
关键区别: x86的强排序意味着大多数情况下你不需要担心重排,但代价是硬件设计复杂、功耗较高。ARM的弱排序将重排控制权交给了软件,通过精细的屏障指令可以在保持高性能的同时实现精确同步。
3.2 x86架构的内存排序
x86(包括x86-64)采用总存储顺序(Total Store Order, TSO)模型。这是最接近程序员直觉的模型之一。其核心规则是:
- Store-Load重排: 允许。这是x86中唯一允许的经典重排。一个后续的Load指令可能先于前面的Store指令完成(如果Store在写缓冲中未刷出)。
- Load-Load, Load-Store, Store-Store重排: 禁止。具有相同地址的读写操作保持程序顺序。
这意味着,对于大多数场景,x86程序员不需要插入昂贵的内存屏障。但在实现无锁数据结构或与DMA描述符交互时,仍然需要mfence(或带lock前缀的指令)来强制刷新写缓冲,确保Store的全局可见性。
x86陷阱: 虽然x86是强排序,但非临时性存储(Non-temporal Store)指令(如
movnti)和字符串操作(如rep movs)具有弱排序语义,必须配合sfence或mfence使用。
3.3 ARM / AArch64架构的内存排序
ARM(32位)和AArch64(64位)采用弱排序(Weakly Ordered)模型。这意味着硬件可以自由地对内存访问进行重排,只要不破坏单线程的数据依赖性。这种设计使得ARM架构在功耗和性能上具有极大优势,但也给并发编程带来了巨大挑战。
ARM架构允许以下所有重排:
- Load-Load重排
- Load-Store重排
- Store-Store重排
- Store-Load重排
此外,ARM还存在一个独特的多拷贝原子性(Multi-copy Atomicity)问题:一个核心的写入可能被部分核心观察到,而其他核心尚未看到。这进一步增加了同步的复杂性。
为了在弱排序模型下保证同步,ARM提供了多种内存屏障指令:
| 指令 | 全称 | 作用 |
|---|---|---|
DMB |
Data Memory Barrier | 保证屏障前的所有内存访问在屏障后的内存访问之前完成(按指定域和类型)。 |
DSB |
Data Synchronization Barrier | 比DMB更强,不仅保证内存访问顺序,还会等待所有缓存、写缓冲、分支预测等操作完成,常用于上下文切换或修改页表。 |
ISB |
Instruction Synchronization Barrier | 清空指令流水线,确保屏障后的指令都是从缓存或内存中重新获取。用于修改代码或系统寄存器后。 |
DMA描述符同步关键: 在ARM上,CPU写入DMA描述符(Store)必须使用
DMB 或 DSB 指令,确保描述符内容在通知DMA控制器(通过MMIO写入)之前,已经全局可见。否则,DMA控制器可能读到过时的描述符。
3.4 x86 vs ARM/AArch64 排序差异总结
下表从多个维度对比了两种主流架构的内存模型差异,这对于编写可移植的内核代码至关重要。
| 对比维度 | x86 / x86-64 (TSO) | ARM / AArch64 (Weakly Ordered) |
|---|---|---|
| 模型强度 | 强排序 | 弱排序 |
| 允许的重排 | 仅 Store-Load | Load-Load, Load-Store, Store-Store, Store-Load |
| 写缓冲可见性 | 写缓冲对当前核心可见,对其他核心不可见(需mfence) | 写缓冲延迟可见,且存在多拷贝原子性问题 |
| 典型屏障指令 | mfence, lfence, sfence |
DMB, DSB, ISB |
| DMA描述符同步 | 通常只需 wmb() (Store-Store屏障,x86上为编译屏障) |
必须使用 wmb() (实际生成 DMB 指令) |
| 性能特点 | 硬件保证顺序,单核性能强,但功耗较高 | 软件控制顺序,多核扩展性好,功耗低 |
移植陷阱: 在x86上看似正确的无锁代码,移植到ARM上几乎必然出错。例如,一个简单的自旋锁实现,在x86上可能只需要
lock cmpxchg,但在ARM上必须配合DMB指令来保证临界区的内存访问顺序。Linux内核通过smp_mb()、smp_wmb()等宏抽象了这些差异。
3.5 对DMA描述符同步的启示
理解上述差异后,我们可以得出关于DMA描述符同步的关键结论:
- 在x86上: 由于Store-Store重排被禁止,CPU按顺序写入描述符的各个字段(如地址、长度、标志),最后写入“拥有权”位(Ownership bit)时,硬件保证之前的写入已经按顺序到达写缓冲。但为了确保DMA控制器(可能位于不同总线域)能看到最新值,通常仍需要执行一个
sfence或mfence,或者使用带lock前缀的指令来触发写缓冲刷新。 - 在ARM/AArch64上: 情况完全不同。CPU可能先写入“拥有权”位,再写入描述符的地址字段。这会导致DMA控制器读取到错误的描述符。因此,在写入描述符之后、通知DMA控制器之前,必须插入一个
DMB或DSB屏障。Linux内核中的wmb()宏在ARM上正是展开为DMB指令。
最佳实践: 编写与DMA描述符交互的驱动程序时,始终使用架构无关的屏障宏(如
wmb(), mb()),而不是直接使用特定架构的指令。这能确保代码在x86、ARM、RISC-V等不同架构上的正确性和可移植性。