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
语义。