Linux同步互斥3(基于Linux6.6)---memory barrier
一、概述
1. 为什么需要内存屏障
现代 CPU 通常会通过乱序执行和指令重排来提高性能。这种优化可能导致在多核系统中不同 CPU 之间的内存访问顺序不一致,从而引发竞态条件或不一致的状态。在多线程或多核环境中,某些操作的顺序必须严格执行,内存屏障就是为了确保内存操作的顺序性。
2. 内存屏障的基本原理
内存屏障通过强制 CPU 按照特定的顺序执行内存读写操作,来避免乱序执行和优化带来的不一致问题。内存屏障本质上是一个不产生其他副作用的 CPU 指令,但它会影响 CPU 的指令调度。
内存屏障分为以下几种类型:
- Store Barrier (写屏障):确保屏障之前的所有写操作在屏障之后的写操作之前完成。
- Load Barrier (读屏障):确保屏障之前的所有读操作在屏障之后的读操作之前完成。
- Full Barrier (全屏障):确保屏障之前的所有读写操作都在屏障之后的读写操作之前完成。
二、Memory Barrier API
下面是 Linux 内核中与内存屏障(Memory Barrier)相关的 API 列表,以表格形式展示:
| API | 功能描述 | 示例 |
|---|---|---|
barrier() | 编译器屏障,防止编译器重排指令,通常用于强制插入空操作。 | barrier(); |
smp_mb() | 全内存屏障,确保屏障前后的所有内存操作有序。 | smp_mb(); |
smp_rmb() | 读内存屏障,确保屏障前的读操作在屏障后的读操作之前完成。 | smp_rmb(); |
smp_wmb() | 写内存屏障,确保屏障前的写操作在屏障后的写操作之前完成。 | smp_wmb(); |
smp_store_mb() | 存储内存屏障,确保存储操作在其他 CPU 可见时有序。 | smp_store_mb(var, value); |
smp_load_acquire() | 加载操作,并插入读内存屏障,确保加载操作顺序性。 | smp_load_acquire(&var); |
smp_store_release() | 存储操作,并插入写内存屏障,确保存储操作顺序性。 | smp_store_release(&var, value); |
read_barrier_depends() | 读取操作屏障,确保对读取操作的依赖性顺序。 | read_barrier_depends(); |
write_barrier_depends() | 写入操作屏障,确保对写操作的依赖性顺序。 | write_barrier_depends(); |
说明:
barrier():这个函数用于插入一个空操作,告诉编译器不要重排前后的指令。它不会影响 CPU 的执行,但会影响编译器生成的指令顺序。smp_mb():全内存屏障,确保在它前后的所有内存操作按顺序执行,避免 CPU 对指令的乱序执行。smp_rmb():读内存屏障,保证在其前面的读操作完成后,才会执行后续的读操作。smp_wmb():写内存屏障,确保在其前面的写操作完成后,才会执行后续的写操作。smp_store_mb():这是一种特殊的写屏障,确保写操作对其他 CPU 的可见性。smp_load_acquire():加载操作屏障,确保该操作之前的所有依赖操作都已经完成,适用于需要保证加载值正确的场景。smp_store_release():存储操作屏障,确保存储操作前的所有操作已经完成,适用于发布共享变量的场景。read_barrier_depends()和write_barrier_depends():确保对特定内存操作的依赖顺序。这两个 API 常用于复杂的同步场景中。
barrier()这个接口和编译器有关,对于gcc而言,其代码如下:
tools/include/linux/compiler.h
/* Optimization barrier */
/* The "volatile" is due to gcc bugs */
#define barrier() __asm__ __volatile__("": : :"memory")
各个部分的含义:
__asm__:这是 GCC 用来插入汇编代码的关键字,表示将随后的字符串视为内联汇编代码。__volatile__:关键字告诉编译器不要优化该汇编语句,即使它似乎没有任何作用。这对于保证屏障的效果是必要的,因为如果没有volatile,编译器可能会将其优化掉。"":这里的双引号表示空的汇编语句。实际并不会生成任何指令。: ::这部分表示输入和输出操作数为空,意味着没有输入或输出的寄存器。"memory":这是一个特殊的约束,告诉编译器,该汇编语句涉及内存操作。它意味着,在这个点上,编译器应该确保前后所有的内存访问操作保持顺序,不会重排。这是确保内存屏障的关键部分。
功能:
该宏的目的是通过内联汇编告诉编译器,不要对涉及内存的操作进行重排。也就是说,它插入一个“内存屏障”,确保前后的指令不会因为编译器的优化而乱序执行。
为什么使用 volatile:
在某些版本的 GCC 编译器中,使用普通的内联汇编可能会被优化掉,因此需要加上 volatile 来确保汇编代码始终被执行。
用法:
此宏通常用于多线程和多核处理器系统中,在需要确保内存操作的顺序时使用。例如,它可以防止编译器将内存访问指令重排到屏障之前或之后,从而确保程序行为的正确性。
示例:
int x = 0;
int y = 0;
void func() {
x = 1;
barrier(); // 防止重排
y = 1;
}
在这个例子中,barrier() 确保 x = 1 的赋值操作不会被重排到 y = 1 之后,这对于确保正确的内存访问顺序非常重要,特别是在多线程或多处理器环境中。
优化屏障是和编译器相关的,而内存屏障是和CPU architecture相关的,以ARM为例来描述内存屏障。
三、内存屏障在ARM中的实现
编译器有时会对代码做一些优化,例如尝试在保证程序执行正确的前提下修改指令顺序或优化ldr/str指令,让程序执行地更快。但是编译器毕竟不能完全猜透人的心思,有时候它做的优化会导致程序运行不符预期。
因此,内核中提供了一些额外的函数,可以插在某段代码里,告诉编译器不要在这里做指令优化。这些函数分为两种:
内存屏障:rmb(), wmb(), mb(),可以防止硬件上的指令重排。除了编译器,有的CPU也支持对指令进行重排来优化程序执行效率,这几个函数就是去防止CPU去做这些事情。rmb()是读访问内存屏障,它保证在屏障(调用rmb()的位置处)之后的任何读操作在执行之前,屏障之前的所有读操作都已经完成。wmb()对应写操作,意思同上。mb()就同时包含读和写操作,意思同上。
优化屏障:barrier(),防止编译器对内存访问的优化,类似volatile关键字对于访问变量的作用。它告诉编译器,在插入barrier()的位置处,内存中的内容都被更新了,你想读变量、映射到内存的寄存器等内容都需要真正到内存里去读,这样就能保证barrier之后的读指令不会被优化掉。
上面第1种屏障是和硬件即CPU特性相关的,那么,如果你的CPU没有指令重排的能力,也就没有必要防止指令重排了。例如,一款CPU不支持写指令重排,那么系统中的wmb()就直接被定义成了barrier()。
还有SMP系统中使用的smp_rmb(), smp_wmb(), smp_mb(),它们只用于SMP系统。在单处理器上它们被定义成barrier()。
当然,防止优化后,受影响的代码执行效率会降低,但为了保证正确性,牺牲一点性能是值得的。
优化屏障的一个特定应用是内核的抢占机制。我们看到preempt_disable()/preempt_enable()并不是简单的修改抢占计数:
include/linux/preempt.h
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while (0)
#define preempt_enable_no_resched() \
do { \
barrier(); \
dec_preempt_count(); \
} while (0)
#define preempt_enable() \
do { \
preempt_enable_no_resched(); \
barrier(); \
preempt_check_resched(); \
} while (0)
正常的用法我们都很熟悉:
preempt_disable()
//临界区,不能被抢占。
preempt_enable()
但如果不加屏障,谁也不知道编译器是否会优化成这样:
//临界区,不能被抢占。
preempt_disable()
preempt_enable()
或
preempt_disable()
preempt_enable()
//临界区,不能被抢占。
这样临界区代码就没有受到保护。因此,需要在关抢占时增加preempt_count之后增加一个屏障,告诉编译器在这之前要完成写请求,接着再执行临界区代码,在开抢占时递减preempt_count之前增加一个屏障,告诉编译器要真正去内存里获取这个值,不能偷懒。
volatile:
上面提到了volatile,我也简单说一下,volatile的作用是防止编译器对访存指令做优化,例如,在一个线程的一段代码里要定期读一个变量a,根据读到的不同值做不同事情,但这个a的修改是在另一个线程里做的,那编译器可能就认为a没有被改过从而不是去每次从内存里去读新的a(把a放在一个临时寄存器里,每次读寄存器)。
用volatile关键字修饰a的作用就是让使用a的代码每次都真正从内存里去读
在 ARM 架构中,内存屏障(Memory Barrier)用于强制执行内存访问的顺序,以确保对内存的读写操作按预期顺序执行,避免编译器或硬件重排内存操作。这对于多核处理器系统特别重要,确保不同核心之间的内存访问是同步的。
ARM 中的内存屏障类型
ARM 架构提供了几种类型的内存屏障指令,它们有不同的应用场景:
-
DMB(Data Memory Barrier):-
DMB指令用于确保所有的数据访问在它之前的所有操作完成后才会执行它之后的操作。它会在内存访问指令之间插入一个屏障,确保前面的指令的所有数据访问都完成,然后再开始后续的数据访问。 -
语法:
DMB <option>option可以是SY(全系统的屏障)、LD(仅对加载操作的屏障)、ST(仅对存储操作的屏障)等。
-
示例:
DMB SY表示在执行DMB后,之前所有的内存访问操作必须完成,之后才允许继续进行。
-
-
DSB(Data Synchronization Barrier):-
DSB用于强制同步所有之前的内存访问指令,直到它们都完成。与DMB不同的是,DSB直到所有先前的内存访问都完成后才会继续执行后续操作。通常在进行 DMA 操作或修改控制寄存器时使用。 -
语法:
DSB <option> -
示例:
DSB SY会阻塞直到所有之前的内存访问操作完成,然后才会继续执行后续的指令。
-
-
ISB(Instruction Synchronization Barrier):-
ISB用于确保所有指令流已经从指令缓存中刷新并同步。这是指令级的同步屏障,在修改控制寄存器或修改内存映射时非常重要。 -
语法:
ISB <option> -
示例:
ISB SY会确保修改的控制寄存器值立即生效,并刷新指令缓存。
-
内存屏障的作用
内存屏障的核心作用是在多核处理器系统中确保内存操作的顺序。由于现代处理器(包括 ARM)通常会进行指令重排、缓存优化和延迟执行等优化,因此在某些情况下,不加屏障的内存访问可能会导致不可预见的结果。内存屏障可以帮助开发者控制这些行为,避免数据竞争或同步问题。
示例:在 ARM 中实现内存屏障
在 ARM 中,你可以通过内联汇编来插入内存屏障指令。假设你需要在内存写入操作和后续读取操作之间插入屏障,防止编译器或硬件进行优化重排。
示例:使用 DMB 指令
#define barrier() __asm__ __volatile__ ("DMB SY" : : : "memory")
在这个宏中:
__asm__和__volatile__用于告诉编译器插入内联汇编。DMB SY是 ARM 内存屏障指令,它确保在DMB SY执行之前的所有数据操作(存储和加载)都必须完成,之后的内存操作才能开始。
示例:使用 DSB 和 ISB
#define dsb() __asm__ __volatile__ ("DSB SY" : : : "memory")
#define isb() __asm__ __volatile__ ("ISB SY" : : : "memory")
DSB SY会确保在它之前的所有内存访问操作完成后,才会继续执行后续的操作。ISB SY用于刷新指令缓存,确保对控制寄存器的修改立即生效。
内存屏障的使用场景
-
多核处理器同步: 在多核处理器系统中,内存屏障确保一个核心对内存的写操作对其他核心是可见的,防止读取到过时的数据。
-
DMA(Direct Memory Access)同步: 在使用 DMA 或其他硬件访问内存时,必须使用内存屏障确保硬件操作完成后再执行后续操作。否则,可能会发生数据不一致的问题。
-
修改控制寄存器: 修改系统控制寄存器后,可能需要使用
ISB来同步指令流,确保新的设置立即生效。 -
避免编译器优化重排: 在某些情况下,编译器可能会重排内存访问操作,导致意外的行为,特别是在多线程和并发编程中,内存屏障用于防止这种优化。
2662

被折叠的 条评论
为什么被折叠?



