Linux + arm 内存屏障 - 详解
ARM 硬件层的屏障指令
DMB (Data Memory Barrier):保证在它之前的内存访问(符合给定域/类型)在它之后的内存访问之前对可见性排序。常用域:
ish(Inner Shareable),sy(system-wide,最强)。DSB (Data Synchronization Barrier):比 DMB 更强,等到之前的访问“完成”才继续执行下一条指令;常用于设备寄存器编程后需要确保“已生效”的情景。
ISB (Instruction Synchronization Barrier):刷新取指流水,常用于修改系统寄存器后,或自修改代码等需要让新指令可见的场景。
Acquire/Release 指令(ARMv8):
LDAR(获取/读-获取),STLR(释放/写-释放),提供更精细的有序性而不必全栈DMB。
Linux 内核里的内存屏障原语(SMP 语义)
这些在 SMP 有效,在 UP 可能是 no-op(但保持可读性与可移植性):
smp_mb():全栅栏(读写都排序)。smp_rmb()/smp_wmb():仅读-读/写-写排序(以及与相对方向的最小保障,取决于架构实现)。smp_store_release(p, v)/smp_load_acquire(p):推荐!映射到 ARM 的STLR/LDAR,开销更低,适合无锁队列/环形缓冲区。READ_ONCE(x)/WRITE_ONCE(x, v):防止编译器优化/拆分访问,但不提供跨 CPU 的排序;常与 acquire/release 或smp_*组合用。原子操作:大多自带适当的屏障语义(比如
atomic_xxx_return通常是 full barrier),但具体要看接口文档;不要想当然。
锁类原语自带屏障:
spin_lock/unlock:进入/退出临界区相当于 acquire/release 屏障。mutex_lock/unlock、rcu_read_lock/unlock等也带有已定义的顺序保证。
设备/MMIO 与 I/O 屏障
不要用
*(volatile u32 *)addr = v;直接访问 MMIO;统一使用内核提供的readl/writel(或ioread32/iowrite32)。readl()/writel():大多数架构上带必要的 I/O 可见性屏障(比如在writel()前插入wmb()或在readl()后插入rmb()等),确保 MMIO 与普通内存的顺序。但它们的强度和位置是“架构相关”的。readl_relaxed()/writel_relaxed():省略默认的 I/O 屏障,只做单次 MMIO 访问。需要你显式加mb()/rmb()/wmb()或专用 I/O 屏障来保证顺序。mmiowb():在某些架构/场景下用来约束向不同设备的写入顺序(通常在解锁自旋锁后确保批量 MMIO 写顺序对设备可见)。经验法则:
如果你对设备写 doorbell/控制寄存器,且之前更新了普通内存中的描述符/数据:先
wmb()(或dma_wmb()),再writel()doorbell。如果要读取设备状态后再读普通内存里的缓冲区:先
readl()取状态,再rmb(),再读内存(或使用配套的 acquire 读方案)。
DMA 与缓存一致性(非一致平台尤需注意)
可用 一致性 DMA(coherent)或 非一致性 DMA(streaming)。
非一致性路径:
CPU → 设备(CPU 填好 buffer/描述符):
dma_sync_single_for_device(dev, dma_addr, len, DMA_TO_DEVICE)(或在某些轻量场景用dma_wmb()之后写 doorbell)writel()doorbell/启动寄存器
设备 → CPU(设备写回数据):
中断/轮询得知完成
dma_sync_single_for_cpu(dev, dma_addr, len, DMA_FROM_DEVICE)再读 buffer(必要时配合
dma_rmb()/rmb())
一致性 DMA路径(cache 一致):
常用顺序:更新内存 →
dma_wmb()→writel();读回数据前dma_rmb()。
简化口诀:“写前 wmb,读后 rmb;doorbell 前 wmb;拿状态后 rmb。”
常见并发模式示例
生产者(CPU0)/消费者(CPU1)单向通信(无锁队列一类)
/* 生产者:先写数据,再发布索引 */
buf[idx] = data;
smp_store_release(&tail, idx + 1);
/* 消费者:先拿到已发布的索引,再读数据 */
int t = smp_load_acquire(&tail);
if (head < t) {
item = buf[head];
head++;
}
store_release确保对buf的写在发布tail之前对其他 CPU 可见;load_acquire确保拿到新的tail后,再读buf一定能看到对应数据。
中断上下文与线程上下文
线程设置“已就绪标志”,中断处理里读取:
/* 线程上下文 */
WRITE_ONCE(flag, 1);
smp_wmb(); /* 或者把下一条换成 store_release */
writel(START, dev->doorbell);
/* 中断上下文 */
status = readl(dev->status);
smp_rmb(); /* 或 load_acquire 来读取 flag 等 */
if (READ_ONCE(flag))
handle();
更新 MMIO 描述符 + Doorbell
/* 更新环形队列的描述符(普通内存) */
desc->len = len;
desc->addr = dma_addr;
/* 确保描述符写入先于设备可见 */
dma_wmb();
/* 通知设备 */
writel(DBELL_KICK, dev->db_reg);
内存类型与顺序直觉
Normal(缓存able):ARM 默认弱内存模型,读写可能乱序,需要栅栏/acq-rel。
Device(nGnRnE / nGnRE / GRE):对同一设备 MMIO 访问有先后规则,但CPU 对普通内存与 MMIO 的相对顺序不一定天然符合你的期望;因此 Linux 以
readl/writel+ 栅栏来提供统一模型。不要用
volatile代替屏障;volatile只约束编译器,不约束 CPU 重排序。
该用谁?最简决策表
| 目的 | 用法 |
| 线程间发布数据 | smp_store_release / smp_load_acquire |
| 强制完全排序 | smp_mb() |
| 只约束读序/写序 | smp_rmb() / smp_wmb() |
| 访问 MMIO(设备寄存器) | readl()/writel()(性能敏感才考虑 _relaxed + 显式屏障) |
| Doorbell 前确保内存已可见 | dma_wmb() 或 wmb(),然后 writel() |
| 设备写回后 CPU 读缓冲 | dma_sync_single_for_cpu() 或 dma_rmb()/rmb() 之后再读 |
| 上锁/解锁的顺序保证 | spin_lock=acquire,spin_unlock=release |
常见坑
只用
WRITE_ONCE/READ_ONCE却没有配对的 acquire/release 或smp_mb()——在 ARM 上会出错。用
_relaxed版本的readl/writel却忘记加mb()/wmb()/rmb()。在非一致性 DMA 平台忘了
dma_sync_single_for_*,导致读到脏缓存或设备看不到最新内存。把
dsb sy当成“万金油”滥用,性能大杀器;优先考虑acquire/release语义。

浙公网安备 33010602011771号