三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Memory Barrier

Memory Barrier

内存屏障(Memory Barrier)是并发编程中最精密、也最容易用错的底层机制。如果说READ_ONCE是约束编译器的“软件契约”,那么内存屏障就是约束CPU硬件的“物理命令”。它的核心作用是防止CPU和编译器对内存操作进行“乱序执行”,从而保证多核环境下数据共享的正确性。


为什么要用内存屏障?—— 源自硬件的“健忘”

现代CPU为了追求极致性能,会采用流水线(Pipeline)乱序执行(Out-of-Order Execution)技术。这意味着,你写的A=1; B=2在CPU眼里,可能会变成B=2; A=1去执行。在单核下这无所谓(因为最终结果一致),但在多核下就是灾难。

考虑一个经典的“生产者-消费者”场景:

  1. CPU0写数据到data,然后写标志flag = 1(表示数据准备好了)。

  2. CPU1循环检查flag,如果flag == 1则读data

如果没有屏障,CPU1可能先看到flag=1,但data的写入还在CPU0的写缓冲(Store Buffer)里没刷到缓存,于是读到了一个过期的旧值。

内存屏障就是用来强制规定“前面的写操作必须在我后面的写操作之前被全局可见”


内存屏障的分类与职责

Linux内核定义了几种核心屏障,它们的“威力”从弱到强,各有分工:

屏障宏作用(按“前面/后面”操作划分)典型场景
smp_rmb()读屏障:确保屏障之前的所有读操作,都在屏障之后的读操作之前完成。消费者读flag后,保证能读到最新的data
smp_wmb()写屏障:确保屏障之前的所有写操作,都在屏障之后的写操作之前完成。生产者写data后,保证flag=1的写入能看到data的新值。
smp_mb()全屏障:兼具读写双重限制,屏障前后的所有内存操作都不能越过它。复杂同步原语(如信号量、自旋锁)的底层实现。
smp_read_barrier_depends()数据依赖屏障:针对有数据依赖的读操作(如if (p) { p->data }),在Alpha架构上需要,其他架构为空。RCU(Read-Copy-Update)机制的基石。

注意:以上都是smp_前缀的屏障,它们只在SMP(多核)配置下有实际效果,在UP(单核)下会被优化为空操作。如果需要强制对所有CPU生效(包括UP),使用不带_smp的版本,如rmb()wmb()mb()


底层原理:CPU如何执行屏障指令

不同架构的实现方式不同,但万变不离其宗。

x86架构(强一致性模型)

x86的硬件内存模型相对严格,大多数普通内存操作不会被重排(除了某些Store-Load场景)。因此,smp_mb()在x86上通常直接编译为lock addl $0x0, (%rsp)指令。

  • lock前缀:将后续指令变为一条全序的屏障,强制CPU刷新写缓冲(Store Buffer),确保所有之前的写操作都完成,并禁止后续读操作被提前执行。

ARM64架构(弱一致性模型)

ARM对乱序执行非常宽容,因此需要更明确的指令。smp_mb()在ARM64上会编译为dmb ish(Data Memory Barrier, Inner Shareable)。

  • dmb:让所有内存访问指令在屏障前后形成明确的分隔。

  • ish:表示该屏障对整个内部共享域(即所有CPU核心)可见,保证全局生效。


经典组合:万能“保序”模式

在实际编程中,内存屏障极少单独使用,而是与READ_ONCE/WRITE_ONCE组合,形成完整的“保序套餐”。

生产者端(CPU0)

WRITE_ONCE(data, 42); // 1. 写数据(无优化) smp_wmb(); // 2. 写屏障:确保data的写入在flag之前完成 WRITE_ONCE(flag, 1); // 3. 写标志

消费者端(CPU1)

if (READ_ONCE(flag)) { // 1. 读标志 smp_rmb(); // 2. 读屏障:确保flag的读取在data之前完成 value = READ_ONCE(data); // 3. 读数据 }

这个组合的威力在于smp_wmb()强制CPU0的写缓冲全部刷入缓存,smp_rmb()强制CPU1的无效队列(Invalidate Queue)全部清空,从而保证了“写-写”和“读-读”的全局顺序。


容易踩的陷阱:配错对应关系

最常见的错误是“写屏障配读屏障”。例如,生产者只写了data,但消费者却用了rmb()而非wmb()(或相反)。这会导致一侧的限制没有被正确执行。

核心原则

  • 保护写操作顺序→ 用写屏障(smp_wmb)。

  • 保护读操作顺序→ 用读屏障(smp_rmb)。

  • 如果既要读又要写,且两者都关心顺序 → 用全屏障(smp_mb)。


常用模式与场景

场景推荐组合备注
自旋锁/信号量smp_mb()对称操作,需要完全的内存顺序。
生产者-消费者(单向)smp_wmb()+smp_rmb()最经典、最轻量。
RCU保护指针更新smp_store_release()/smp_load_acquire()这是更现代、更安全的“获取-释放”语义,能自动处理屏障,推荐优先使用。

总结:内存屏障的本质

内存屏障不是“禁止乱序”,而是“强制顺序”。它通过特定的CPU指令,让CPU在执行屏障前后的内存操作时,必须按照我们指定的逻辑顺序去同步缓存和内存,从而在多核环境中建立起可预测的、顺序一致的共享内存视图。

在Linux内核中,理解并正确使用内存屏障是写出无锁、高性能代码的必修课。

← 返回列表