内存屏障的作用是强制对内存的访问顺序进行排序,保证多线程或多核处理器下的内存访问的一致性和可见性。通过插入内存屏障,可以防止编译器对代码进行过度优化,也可以解决CPU乱序执行引起的问题,确保程序的执行顺序符合预期。
Linux内核提供了多种内存屏障,包括通用的内存屏障、数据依赖屏障、写屏障、读屏障、释放操作和获取操作等。
Linux内核中的内存屏障源码主要位于include/linux/compiler.h和arch/*/include/asm/barrier.h中。
include/linux/compiler.h中定义了一系列内存屏障相关的宏定义和函数,如:
#define barrier() __asm__ __volatile__("": : :"memory")
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() barrier()
#define smp_store_mb(var, value) do { smp_wmb(); WRITE_ONCE(var, value); } while (0)
#define smp_load_acquire(var) ({ typeof(var) ____p1 = ACCESS_ONCE(var); smp_rmb(); ____p1; })
#define smp_cond_load_acquire(p, c) ({ typeof(*p) ____p1; ____p1 = ACCESS_ONCE(*p); smp_rmb(); unlikely(c) ? NULL : &____p1; })
其中,barrier()是一个内存屏障宏定义,用于实现完整的内存屏障操作;smp_mb()、smp_rmb()、smp_wmb()分别是读屏障、写屏障、读写屏障的宏定义;smp_read_barrier_depends()是一个读屏障函数,用于增加依赖关系,确保之前的读取操作在之后的读取操作之前完成;smp_store_mb()和smp_load_acquire()分别是带屏障的存储和加载函数,用于确保存储和加载操作的顺序和一致性。
smp_mb()
:全局内存屏障,用于确保对共享变量的所有读写操作都已完成,并且按照正确顺序进行。smp_rmb()
:读屏障,用于确保对共享变量的读取不会发生在该读取之前的其他内存访问完成之前。smp_wmb()
:写屏障,用于确保对共享变量的写入不会发生在该写入之后的其他内存访问开始之前。rmb()
:读内存屏障,类似于smp_rmb()
,但更强制。它提供了一个更明确和可靠的方式来防止乱序执行。wmb()
:写内存屏障,类似于smp_wmb()
,但更强制。它提供了一个更明确和可靠的方式来防止乱序执行。read_barrier_depends()
:依赖性读栅栏,用于指示编译器不应重排与此函数相关的代码顺序。这些内存屏障原语主要用于指示编译器和处理器不要对其前后的指令进行优化重排,以确保内存操作按照程序员预期的顺序执行。
读内存屏障(如rmb()
)只针对后续的读取操作有效,它告诉编译器和处理器在读取之前先确保前面的所有写入操作已经完成。类似地,写内存屏障(如wmb()
)只针对前面的写入操作有效,它告诉编译器和处理器在写入之后再开始后续的其他内存访问。
smp_xxx()
系列的内存屏障函数主要用于多核系统中,在竞态条件下保证数据同步、一致性和正确顺序执行。在单核系统中,这些函数没有实际效果。
而read_barrier_depends()
函数是一个特殊情况,在某些架构上使用它可以避免编译器对代码进行过度重排。
在x86系统上,如果支持lfence汇编指令,则rmb()(读内存屏障)的实现可能会使用lfence指令。lfence指令是一种序列化指令,它会阻止该指令之前和之后的加载操作重排,并确保所有之前的加载操作已经完成。
具体而言,在x86架构上,rmb()可能被实现为:
#define rmb() asm volatile("lfence" ::: "memory")
这个宏定义使用了GCC内联汇编语法,将lfence
指令嵌入到代码中,通过volatile
关键字告诉编译器不要对此进行优化,并且使用了"clobber memory"约束来通知编译器该指令可能会影响内存。
如果在x86系统上不支持lfence汇编指令,那么rmb()(读内存屏障)的实现可能会使用其他可用的序列化指令来达到相同的效果。一种常见的替代方案是使用mfence指令,它可以确保所有之前的内存加载操作已经完成。
在这种情况下,rmb()的实现可能如下:
#define rmb() asm volatile("mfence" ::: "memory")
同样,这个宏定义使用了GCC内联汇编语法,将mfence
指令嵌入到代码中,并通过"clobber memory"约束通知编译器该指令可能会影响内存。
内存一致性模型是指多个处理器或多个线程之间对共享内存的读写操作所满足的一组规则和保证。
在并发编程中,由于多个处理器或线程可以同时访问共享内存,存在着数据竞争和可见性问题。为了解决这些问题,需要定义一种内存一致性模型,以规定对共享内存的读写操作应该如何进行、如何保证读写操作的正确顺序和可见性。
常见的内存一致性模型包括:
这些序列关系的正确性和顺序一致性是通过硬件层面的内存屏障指令、缓存一致性协议和处理器乱序执行的机制来实现的。
**顺序一致性(Sequential Consistency)**模型是指内核对于多个线程或进程之间的操作顺序保持与程序代码中指定的顺序一致。简单来说,它要求内核按照程序中编写的顺序执行操作,并且对所有线程和进程都呈现出一个统一的全局视图。
在这个模型下,每个线程或进程看到的操作执行顺序必须符合以下两个条件:
**弱一致性(Weak Consistency)**模型是指在多个线程或进程之间,对于操作执行顺序的保证比顺序一致性模型更为宽松。在这种模型下,程序无法依赖于全局的、确定性的操作执行顺序。
弱一致性模型允许发生以下情况:
**松散一致性模型(Relaxed Consistency Models)**是与多处理器系统相关的概念。它涉及到多个处理器或核心共享内存时的数据一致性问题。
在传统的严格一致性模型下,对于所有处理器/核心来说,读写操作都必须按照全局总序列进行排序。这会带来很大的开销和限制,并可能导致性能下降。
而松散一致性模型则允许部分乱序执行和缓存一致性协议的使用,以提高并行度和性能。它引入了几种松散的一致性模型,包括:
对于读取(Load)和写入(Store)指令,一共有四种组合:
常见的内存屏障使用场景和相应的规则:
在 Linux 内核中,存在一些隐式的内存屏障,它们无需显式地在代码中添加,而是由编译器或硬件自动插入。这些隐式的内存屏障可以确保代码在不同的优化级别下保持正确性和一致性,同时提高代码的性能。
以下是一些常见的隐式内存屏障:
Linux内核有很多锁结构,这些锁结构都隐含一定的屏障操作,以确保内存的一致性和顺序性。
在 Linux 中,"优化屏障"是通过barrier()
宏定义来实现的。这个宏定义确保编译器不会对其前后的代码进行过度的优化,以保证特定的内存访问顺序和可见性。
在 GCC 编译器中,"优化屏障"的定义可以在linux-5.6.18\include\linux\compiler-gcc.h
源码文件中找到。这个头文件通常包含一些与编译器相关的宏定义和内联汇编指令,用于处理一些特殊情况下需要控制代码生成和优化行为的需求。
具体而言,barrier()
宏定义使用了GCC内置函数__asm__ volatile("": : :"memory")
来作为一个空的内联汇编语句,并加上了"memory"约束来告知编译器,在此处需要添加一个内存屏障。
在ARM架构中,有三条内存屏障指令:
参考:Linux内核源码分析教程