尝试总结memory barrier (经典)

本文深入探讨了内核中定义的内存屏障原语及其在不同场景下的应用,详细解析了barrier()、mb()、rmb()和wmb()等宏的作用,并通过实例说明了这些宏如何确保内存访问的一致性和有序性。

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

讨论完了给一个总结,有些话是别人说的,有的还是clf的网友的,为了不使文档显得杂乱,都不具名了。
欢迎批评指正!


内核中定义的内存屏障原语有:

#define barrier() __asm__ __volatile__("": : :"memory")
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)

#ifdef CONFIG_SMP
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) do { (void) xchg(&var, value); } while (0)
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() do { } while(0)
#define set_mb(var, value) do { var = value; barrier(); } while (0)
#endif


1). smp_xxx()和xxx()的区别

为了给其它CPU也提供相关的barrier宏。 例如x86的rmb()是用了lfence指令,但其它CPU不能用这个指令。


2). 关于barrier()宏,jkl大师是这么说的:

CPU越过内存屏障后,将刷新自己对存储器的缓冲状态。这条语句实际上不生成任何代码,但可使gcc在
barrier()之后刷新寄存器对变量的分配。

也就是说,barrier()宏只约束gcc编译器,不约束运行时的CPU行为。 举例:

1 int a = 5, b = 6;
2 barrier();
3 a = b;

在line 3,GCC不会用存放b的寄存器给a赋值,而是invalidate b的Cache line,重新读内存中的b值,赋值给a。


3). mb() vs. rmb() vs. wmb()

rmb()不允许读操作穿过内存屏障;wmb()不允许写操作穿过屏障;而mb()二者都不允许。

看IA32上wmb()的定义:
#ifdef CONFIG_X86_OOSTORE
#define wmb() alternative("lock;addl $0,0(%%esp)", "sfence", X86_FEATURE_XMM);
#else
#define wmb() __asm__ __volatile__ ("": : :"memory");
#endif

Intel和AMD都没有在IA32 CPU中实现乱序写(Out-Of-Order Store),所以wmb()定义为空操作,不约束CPU行为;但
有些IA32 CPU厂商实现了OOO Store,所以就有了使用sfence的那个wmb()实现。


4). 内存屏障的体系结构语义

4.1) 只有一个主体(CPU或DMA控制器)访问内存时,无论如何也不需要barrier;但如果有两个或更多主体访问内存,且
其中有一个在观测另一个,就需要barrier了。

4.2) IA32 CPU调用有lock前缀的指令,或者如xchg这样的指令,会导致其它的CPU也触发一定的动作来同步自己的Cache。
CPU的#lock引脚链接到北桥芯片(North Bridge)的#lock引脚,当带lock前缀的执行执行时,北桥芯片会拉起#lock
电平,从而锁住总线,直到该指令执行完毕再放开。 而总线加锁会自动invalidate所有CPU对 _该指令涉及的内存_
的Cache,因此barrier就能保证所有CPU的Cache一致性。

4.3) 接着解释。
lock前缀(或cpuid、xchg等指令)使得本CPU的Cache写入了内存,该写入动作也会引起别的CPU invalidate其Cache。
IA32在每个CPU内部实现了Snoopying(BUS-Watching)技术,监视着总线上是否发生了写内存操作(由某个CPU或DMA控
制器发出的),只要发生了,就invalidate相关的Cache line。 因此,只要lock前缀导致本CPU写内存,就必将导致
所有CPU去invalidate其相关的Cache line。

两个地方可能除外:
-> 如果采用write-through策略,则根本不存在缓存一致性问题(Linux对全部内存采用write-back策略);
-> TLB也是Cache,但它的一致性(至少在IA32上)不能通过Snoopying技术解决,而是要发送
INVALIDATE_TLB_VECTOR这个IPI给其它的CPU。

4.4) 进一步解释,MESI协议

包括IA32的许多体系结构的CPU,为了保证缓存一致性,实现了MESI协议。

M: Modified,已修改
E: Exclusive,排他
S: Shared,共享
I: Invalid,无效

IA32 的CPU实现了MESI协议来保证Cache coherence。 CPU的总线监测单元,始终监视着总线上所有的内存写操作,
以便随时调整自己的Cache状态。

-> Modified。 本CPU写,则直接写到Cache,不产生总线事物;其它CPU写,则不涉及本CPU的Cache,其它CPU
读,则本CPU需要把Cache line中的数据提供给它,而不是让它去读内存。

-> Exclusive。只有本CPU有该内存的Cache,而且和内存一致。 本CPU的写操作会导致转到Modified状态。

-> Shared。 多个CPU都对该内存有Cache,而且内容一致。任何一个CPU写自己的这个Cache都必须通知其它
的CPU。

-> Invalid。 一旦Cache line进入这个状态,CPU读数据就必须发出总线事物,从内存读。


5) 考虑到DMA

5.1). Wirte through策略。 这种情形比较简单。

-> 本CPU写内存,是write through的,因此无论什么时候DMA读内存,读到的都是正确数据。
-> DMA写内存,如果DMA要写的内存被本CPU缓存了,那么必须Invalidate这个Cache line。下次CPU读它,就
直接从内存读。

5.2). Write back策略。 这种情形相当复杂。

-> DMA读内存。被本CPU总线监视单元发现,而且本地Cache中有Modified数据,本CPU就截获DMA的内存读操作,
把自己Cache Line中的数据返回给它。

-> DMA写内存。而且所写的位置在本CPU的Cache中,这又分两种情况:
a@ Cache Line状态未被CPU修改过(即cache和内存一致),那么invalidate该cache line。
b@ Cache Line状态已经被修改过,又分2种情况:

<1> DMA写操作会替换CPU Cache line所对应的整行内存数据,那么DMA写,CPU则invalidate
自己的Cache Line。
<2> DMA写操作只替换Cache Line对应的内存数据的一部分,那么CPU必须捕获DMA写操作的新
数据(即DMA想把它写入内存的),用来更新Cache Line的相关部分。

 

来源:http://www.linuxforum.net/forum/printthread.php?Cat=&Board=linuxK&main=653778&type=thread

 

<think>我们正在讨论内联汇编中的内存屏障问题,特别是`__asm____volatile__`与"memory"屏障的使用。根据用户提供的引用,我们可以总结如下:1.内存屏障(MemoryBarrier)用于限制编译器和处理器对内存操作的重排序,确保内存操作按照程序顺序执行。2.在GCC的内联汇编中,使用`__asm__`关键字来编写内联汇编语句,`__volatile__`(或`volatile`)用于告诉编译器不要优化这段汇编(例如,不要移动或删除它,因为它可能有副作用)。3.在汇编模板中使用`"memory"`破坏描述(clobber)会告诉编译器:汇编代码可能读取或写入任意内存位置(而不仅仅是输入输出操作数指定的内存)。这会导致编译器在执行汇编代码之前将所有寄存器中缓存的变量值刷新到内存,并在执行后重新加载可能改变的内存变量。用户提供的引用[1]展示了Linux内核中内存屏障的实现:```c#definebarrier()__asm____volatile__("":::"memory")```这个宏没有汇编指令(即空字符串),但是有一个`"memory"`破坏描述。它的作用是告诉编译器:此汇编代码可能会读取或写入内存,因此编译器会确保在屏障之前的所有内存操作都已完成(从寄存器的角度看),并且屏障之后的操作不会重排序到屏障之前。引用[2]详细解释了`"memory"`clobber的作用:它相当于一个编译器级别的读写内存屏障,会强制编译器将寄存器的值写回内存,并重新加载后续需要的内存值。引用[4]则从硬件内存模型的角度说明,不同的处理器有不同的内存排序行为,而使用内存屏障可以保证在多处理器环境下的正确性。现在,我们根据用户的问题“howtouse__asm__and__volatile__withmemorybarrierininlineassemblyfence”来回答。###使用__asm____volatile__实现内存屏障在C/C++中,我们通常使用内联汇编来插入内存屏障指令(如x86的`mfence`、`sfence`、`lfence`)或使用编译器屏障(如上面的`barrier()`宏)。下面分别介绍这两种情况:####1.编译器内存屏障(无硬件指令)```c__asm____volatile__("":::"memory");```-`__asm__`:表示内联汇编。-`__volatile__`:表示不要优化这段内联汇编(即使它没有输出,也要保留)。-`""`:空汇编指令,即不生成任何硬件指令。-`:::"memory"`:这里没有输入、输出操作数,但在破坏描述部分指定了`"memory"`,告诉编译器内存可能被修改,因此编译器会防止重排序内存操作。这种屏障只影响编译器优化,不生成任何处理器指令。它确保在屏障之前的写操作在屏障之后的读操作之前完成(从编译器的角度)。####2.硬件内存屏障(生成处理器指令)以x86架构为例,我们可以使用`mfence`指令(全内存屏障):```c__asm____volatile__("mfence":::"memory");```-`"mfence"`:这是x86的汇编指令,用于全内存屏障。-`:::"memory"`:同样,我们使用`"memory"`破坏描述,确保编译器不会重排内存操作。注意:在x86架构上,由于x86有较强的内存模型,通常只需要编译器屏障(如第一种)就足够防止编译器的重排。但在多处理器环境中,有时需要硬件屏障来确保所有处理器看到一致的内存顺序。####3.通用写法(考虑可移植性)如果你希望代码可移植到不同架构,可以使用编译器内置函数(如GCC的`__sync_synchronize()`),它会根据目标架构生成适当的内存屏障指令。或者,你可以使用条件编译:```c#ifdefined(__x86_64__)||defined(__i386__)__asm____volatile__("mfence":::"memory");#elifdefined(__arm__)__asm____volatile__("dmbish":::"memory");#elifdefined(__aarch64__)__asm____volatile__("dmbish":::"memory");#else#error"Unsupportedarchitecture"#endif```####注意事项-使用`__volatile__`是必要的,因为即使没有输出操作数,编译器也可能尝试优化掉内联汇编语句(如果它认为没有副作用)。`volatile`确保内联汇编语句保留在它该在的位置。-`"memory"`破坏描述告诉编译器内存可能被修改,因此编译器不会假设之前读入寄存器的值在屏障后仍然有效,从而避免使用过时的寄存器缓存值。###示例场景假设我们有一个共享变量`flag`和`data`,我们想要确保在设置`data`之后设置`flag`(以防止乱序执行导致其他线程看到`flag`被设置但`data`还未准备好的情况):```cintdata=0;intflag=0;//线程1:写数据data=42;//插入内存屏障,确保data的写入在flag之前完成__asm____volatile__("":::"memory");flag=1;//线程2:读数据while(flag==0){/*等待*/}//插入内存屏障,确保读取flag后,再读取data__asm____volatile__("":::"memory");intvalue=data;```在上面的例子中,编译器屏障确保编译器不会将`flag=1`重排到`data=42`之前,也不会将`intvalue=data`重排到循环之前。但请注意,这并不能防止处理器级别的乱序执行(在x86上,写操作不会乱序,但读操作可能会乱序),因此有时需要硬件屏障。###总结使用`__asm____volatile__`和`"memory"`破坏描述可以创建内存屏障:-对于编译器屏障,使用空指令和`"memory"`破坏描述。-对于硬件屏障,在汇编模板中写入相应的屏障指令(如x86的`mfence`)并加上`"memory"`破坏描述。在实际应用中,应结合目标硬件架构和同步需求选择适当的屏障类型。§§相关问题§§1.内存屏障在哪些场景下是必需的?2.不同处理器架构(如x86,ARM,PowerPC)的内存屏障指令有什么区别?3.除了内联汇编,还有哪些方法可以实现内存屏障?4.编译器屏障和硬件屏障的主要区别是什么?</think>### 理解和使用 `__asm__ __volatile__` 与内存屏障 `__asm__ __volatile__` 是 GCC 内联汇编的关键语法,结合 `"memory"` 破坏描述符可实现内存屏障功能。其核心作用是**防止编译器和处理器对内存操作进行重排序**,确保内存操作的可见性和顺序性。以下是具体用法和原理: #### 1. 基本语法结构 ```c __asm__ __volatile__ ( "汇编指令模板" : 输出操作数 // 可选 : 输入操作数 // 可选 : "破坏描述符" // 关键:"memory" 表示内存屏障 ); ``` - **`__volatile__`**:禁止编译器优化或移动该汇编块(防止编译器重排序)。 - **`"memory"`** 破坏描述符:通知编译器此汇编块可能修改任意内存(隐式内存读写)。 #### 2. 内存屏障的实现方式 **编译器级内存屏障**(无硬件指令): ```c // Linux 内核中的典型实现 [^1] #define barrier() __asm__ __volatile__("" ::: "memory") ``` - **作用**:强制编译器在屏障前完成所有内存加载/存储操作,并刷新寄存器中的缓存值。 - **原理**: - `""` 表示无实际汇编指令。 - `"memory"` 告知编译器内存可能被修改,需同步寄存器与内存状态。 - 编译期阻止跨屏障的指令重排序。 **硬件级内存屏障**(生成 CPU 指令): ```c // x86 架构的完整内存屏障 __asm__ __volatile__ ("mfence" ::: "memory"); ``` - **作用**:在编译器屏障基础上,额外插入 `mfence` 指令强制 CPU 内存操作顺序。 - **适用场景**:多核环境下的同步原语(如自旋锁、RCU)。 #### 3. 关键机制解析 - **`"memory"` 破坏描述符**: - 强制编译器假设**所有内存可能被修改**。 - 编译器会: 1. 将屏障前缓存在寄存器的变量值写回内存。 2. 屏障后重新从内存加载变量值 [^2]。 - 效果:形成**编译器级的读写屏障**。 - **`__volatile__` 的作用**: - 防止编译器删除“无输出”的汇编块(如空指令屏障)。 - 确保汇编指令在代码中的位置不被移动 [^3]。 #### 4. 实际应用示例 **场景:确保共享变量的写入顺序** ```c int data = 0; int flag = 0; // 线程1:写入数据 data = 42; barrier(); // 编译器屏障,确保 data 写入先于 flag flag = 1; // 线程2:读取数据 while (flag == 0) { /* 等待 */ } barrier(); // 确保读取 flag 后刷新 data int val = data; // 保证读到 data=42 ``` #### 5. 不同架构的注意事项 - **x86**:强内存模型,`mfence` 用于全内存排序 [^4]。 - **ARM/PowerPC**:弱内存模型,需使用 `dmb`/`sync` 等指令。 - **跨平台建议**:优先使用编译器内置屏障(如 `__sync_synchronize()`)。 > **关键总结**: > 1. `__asm__ __volatile__("" ::: "memory")` → 纯编译器屏障。 > 2. `__asm__ __volatile__("mfence" ::: "memory")` → 编译器 + 硬件屏障。 > 3. 使用 `"memory"` 强制内存同步,`__volatile__` 防止优化。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值