关于memory barrier/memory ordering 的笔记和总结(2)

本文详细探讨了X86和ARM体系结构下的内存排序规则,指出X86具有强顺序,而ARM则为弱顺序。文章介绍了X86的内存屏障指令lfence、sfence和mfence以及ARM中的DMB指令,强调了它们在保证内存操作顺序中的作用。同时,文章通过实例分析了地址依赖、控制依赖和数据依赖对内存操作顺序的影响,强调了依赖关系与内存屏障的不同之处。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >



             Memory ordering

Memory ordering用来描述系统中的processor对内存的操作如何对其它processor可见(可见的定义见前面的描述)。同时需要说明的是,大多数文献都采用reorder这个表达方式,是从执行等价的角度来描述的:比如P1上执行两个写操作WRITE(A)WRITE(B),如果对于观察者P2来说P1|WRITE(B)先于P2|WRITE(A)可见,那么就可以认为P1的写操作发生了reorder。对读操作也是类似的。

影响memoryordering的因素很多,包括:

  1. 体系结构,X86ARMmemory ordering就截然不同;

  2. 内存的类型,体系结构一般都会定义若干种内存类型,不同的内存类型有不同的memoryordering,比如X86分为Strong Uncacheable (UC)Uncacheable (UC-)Write Combining (WC)Write Through (WT)Write Back (WB)Write Protected (WP),而ARM的内存类型用memory typememory attribute来描述,不同的内存类型通常对应了不同的用途(具体情况见对应的厂商文档)。本文描述的是一般情况,也就是不做特殊处理,直接通过内存分配接口分配到的内存,也就是X86WB类型,ARMShareable Normal memory

  3. 具体的指令,比如INTELREP MOVSBREP STOSBmemory ordering就和一般的mov指令不一样。除非特别说明,而本文描述的是普通的内存访问指令,通常是C语言的赋值语句对应的汇编指令。

和前面描述SC的时候一样,XYZ表示变量,初始值都为0P1 P2表示processorR(X)=1表示从X中读到了值1W(X)=1表示向X中写入了值1P1|R(X)=1表示P1执行R(X)=1。符号A->B表示动作A先于动作B发生。

         X86memory ordering

X86memory ordering属于strong order,其与SC的要求接近,在大多数典型场景,没有必要使用memory barrier指令。即使是和外设的DMA操作共享的内存,X86也能通过bus snoop完成强顺序保证(查看linux内核分配一致性内存的接口dma_alloc_coherent,你会发现尼玛就是分配内存咯,并没有设置页表的PWT/PCD标识,也没有设置Memory type range registers(MTRRs))

                       普通内存操作

X86实现的memory ordering比较接近SC的要求,其违反SC的场景是:读操作可能和按照program order中在前面的对不同地址的写操作发生reorder,也就是读操作先于program order在其前面的对其他地址的写操作对外生效。注意:这里仅限于对不同地址的读和写。

也就是以下执行在X86上是可以发生的,但是并不满足SC的要求:

P1: W(X)=1R(Y)=0

P2: W(Y)=1R(X)=0

本例子明显不符合SC的要求,因为如果P1|R(Y)=0,那么必然有P1|R(Y)=0 –> P2|W(Y)=1

结合SC按照program order生效的要求,很容易得到P1|W(X)=1 -> P2|R(X)=0的悖论。

X86允许读操作先于按照program order在其前面的写操作对外生效,P1|W(X)=1 -> P1|R(Y)=0不一定成立,使得上面的序列在X86上变得合法。

 

以上执行序列可以看成是X86write buffer对程序员的体现,write bufferCPU上的一个部件,当一个写操作由于种种原因不能立即放到cache/内存中的时候,CPU可以先把它放到本CPUwrite buffer中,后续再刷新到内存中,这个write buffer只对本CPU可见。这就造成对一个地址的写操作在本 CPU看来已经完成,但是对其它CPU还不可见,而CPU继续执行后续的读操作,在其它CPU看起来,就造成了读和先前的写发生了reorder

                       Memory barrier指令

X86memory barrier指令包括lfence sfence mfence,这些指令通常在使用内存模型(比如Write Combining的操作),特殊的指令(REP MOVSB REP STOSB)才需要关注。

  1. lfence

    lfence确保program order在其前面的读不会和program order在其后面的读和写发生reorder,也就是lfence前面的读操作总是比lfence后面的读操作和写操作先生效。

  2. sfence

    sfence确保program order在其前面的写不会和program order在其后面的写发生reorder,也就是sfence前面的写操作总是比sfence后面的写操作先生效

  3. mfence

    mfence确保program order在其前面的写和写不会和program order在其后面的读和写发生reorder,也就是sfence前面的读操作和写操作总是比sfence后面的读操作和写操作先生效

                       隐含的memor barrier

<think>嗯,用户这次问的是内存屏障的作用,看来是在深入理解计算机底层机制。之前连续讨论了AUTOSAR内存泄漏,现在转向系统级编程的并发控制,说明用户可能是嵌入式开发者或系统程序员,正在排查多线程环境下的同步问题。 内存屏障这个概念确实容易让人困惑,需要从硬件编译器两个层面解释清楚。用户可能实际遇到过因内存乱序导致的诡异bug,比如数据明明更新了却读到旧值。我得用汽车装配线的例子类比CPU流水线,这样更直观。 硬件优化带来的指令重排是问题的核心。现代CPU为了效率会乱序执行,比如Store Buffer的存在可能让写操作延迟,导致其他核看到操作的顺序代码顺序不一致。内存屏障就是强行给这些“调皮”的优化踩刹车,告诉CPU“到此为止之前的操作必须完成”。 编译器优化也是隐患。比如用volatile只能阻止编译器重排,但管不了CPU层面的乱序。这点在无锁编程中特别致命,用户可能正为此头疼。需要强调屏障是软硬双管齐下的解决方案。 用户应该还想知道具体使用场景,所以示例代码里要展示经典的双检锁模式。这个模式如果漏掉屏障,单例可能被初始化两次——这种bug极难复现但危害巨大。另外内核态的barrier()用户态的__sync_synchronize()区别也得提一下。 最后的提问设计要层层递进:从原理(为什么需要)到实践(怎么用),再到特定领域(Linux内核)替代方案。第五个问题特意问编译器屏障,因为很多人会混淆它与硬件屏障的作用范围。</think>内存屏障(Memory Barrier)的主要作用是**强制保证内存操作的顺序性可见性**,解决多核/多线程环境下因**指令重排序****缓存一致性**引发的数据一致性问题。其核心作用如下: --- ### 核心作用 1. **禁止指令重排序** - 编译器CPU可能为优化性能而乱序执行指令(如写操作延迟、读操作提前)。 - 内存屏障会阻止屏障前后的指令跨越屏障执行,确保程序逻辑顺序。 2. **保证内存可见性** - 强制将缓存数据刷新到主存(写屏障),或从主存加载最新数据(读屏障)。 - 确保一个线程的修改对其他线程立即可见。 3. **同步多核数据一致性** - 在弱一致性内存模型中(如ARM、PowerPC),屏障触发缓存失效/更新,维持多核缓存一致。 --- ### 类型与场景 | 屏障类型 | 作用 | 典型场景 | |-------------------|----------------------------------------------------------------------|--------------------------| | **写屏障(Store Barrier)** | 确保屏障前的所有写操作完成,数据刷入主存后才执行后续操作 | 修改共享数据后释放锁 | | **读屏障(Load Barrier)** | 确保后续读操作前,先加载主存最新数据(清空本地缓存) | 获取锁后读取共享数据 | | **全屏障(Full Barrier)** | 同时具备读写屏障功能(如 `mfence`),确保屏障前后指令无重排且全局可见 | 无锁数据结构、内核同步原语 | --- ### 代码示例(无锁编程中的屏障) ```c // 共享数据 int data = 0; bool ready = false; // 线程A:写入数据 void thread_A() { data = 42; // 写操作 // 写屏障:确保data写入完成后再更新ready __sync_synchronize(); // GCC内置全屏障 ready = true; // 标志位更新 } // 线程B:读取数据 void thread_B() { while (!ready); // 等待ready为true // 读屏障:确保读取data前获取最新值 __sync_synchronize(); printf("%d", data); // 必须输出42而非0或随机值 } ``` > **关键点**:若省略屏障,线程B可能因重排序先看到 `ready=true` 后看到 `data=0`(脏读)。 --- ### 典型应用场景 - **多线程同步**:锁实现(如自旋锁)、条件变量。 - **无锁数据结构**:CAS(Compare-And-Swap)操作前后插入屏障。 - **设备驱动**:确保硬件寄存器按顺序写入。 - **内核同步原语**:如Linux的 `smp_mb()`、`rmb()`、`wmb()`。 ---
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值