【Linux 内核源码分析】内存屏障

内存屏障

内存屏障的作用是强制对内存的访问顺序进行排序,保证多线程或多核处理器下的内存访问的一致性和可见性。通过插入内存屏障,可以防止编译器对代码进行过度优化,也可以解决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()分别是带屏障的存储和加载函数,用于确保存储和加载操作的顺序和一致性。

内存屏障原语

  1. smp_mb():全局内存屏障,用于确保对共享变量的所有读写操作都已完成,并且按照正确顺序进行。
  2. smp_rmb():读屏障,用于确保对共享变量的读取不会发生在该读取之前的其他内存访问完成之前。
  3. smp_wmb():写屏障,用于确保对共享变量的写入不会发生在该写入之后的其他内存访问开始之前。
  4. rmb():读内存屏障,类似于smp_rmb(),但更强制。它提供了一个更明确和可靠的方式来防止乱序执行。
  5. wmb():写内存屏障,类似于smp_wmb(),但更强制。它提供了一个更明确和可靠的方式来防止乱序执行。
  6. 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"约束通知编译器该指令可能会影响内存。

内存一致性模型

内存一致性模型是指多个处理器或多个线程之间对共享内存的读写操作所满足的一组规则和保证。

在并发编程中,由于多个处理器或线程可以同时访问共享内存,存在着数据竞争和可见性问题。为了解决这些问题,需要定义一种内存一致性模型,以规定对共享内存的读写操作应该如何进行、如何保证读写操作的正确顺序和可见性。

常见的内存一致性模型包括:

  1. 顺序一致性(Sequential Consistency):在顺序一致性模型下,所有的读写操作对其他处理器或线程都是按照程序顺序可见的,即每个操作都会立即对所有处理器或线程可见,并且所有处理器或线程看到的操作顺序是一致的。这种模型简单直观,但在实践中由于需要强制同步和屏障,会导致性能开销较大。
  2. 弱一致性模型(Weak Consistency Models):弱一致性模型允许对共享变量的读写操作可能出现乱序或重排序,但是要求满足一定的一致性条件。常见的弱一致性模型有Release-Acquire模型、Total Store Order(TSO)模型和Partial Store Order(PSO)模型等。这些模型通过使用特定的内存屏障指令或同步原语,对读写操作进行排序和同步,以实现一定程度的一致性保证。
  3. 松散一致性模型(Relaxed Consistency Models):松散一致性模型放宽了对共享变量读写操作顺序的限制,允许更多的重排序和乱序访问。常见的松散一致性模型有Release Consistency模型、Entry Consistency模型和Processor Consistency模型等。这些模型通过定义不同的一致性保证级别和同步原语,允许更灵活的访问和重排序,从而提高系统的并发性能。

这些序列关系的正确性和顺序一致性是通过硬件层面的内存屏障指令、缓存一致性协议和处理器乱序执行的机制来实现的。

**顺序一致性(Sequential Consistency)**模型是指内核对于多个线程或进程之间的操作顺序保持与程序代码中指定的顺序一致。简单来说,它要求内核按照程序中编写的顺序执行操作,并且对所有线程和进程都呈现出一个统一的全局视图。

在这个模型下,每个线程或进程看到的操作执行顺序必须符合以下两个条件:

  1. 串行语义:每个线程或进程内部的操作必须按照原始程序中的指定顺序执行,不会发生重排序。
  2. 全局排序:所有线程和进程之间的操作必须按照某种确定的全局排序进行。

**弱一致性(Weak Consistency)**模型是指在多个线程或进程之间,对于操作执行顺序的保证比顺序一致性模型更为宽松。在这种模型下,程序无法依赖于全局的、确定性的操作执行顺序。

弱一致性模型允许发生以下情况:

  1. 重排序:线程或进程内部的操作可以被重新排序,只要最终结果对外表现一致即可。
  2. 缓存不一致:不同线程或进程之间的缓存数据可能不及时更新,导致读取到过期数据。
  3. 写入写入冲突:当两个线程同时写入相同地址时,写入结果的先后顺序可能会出现变化。

**松散一致性模型(Relaxed Consistency Models)**是与多处理器系统相关的概念。它涉及到多个处理器或核心共享内存时的数据一致性问题。

在传统的严格一致性模型下,对于所有处理器/核心来说,读写操作都必须按照全局总序列进行排序。这会带来很大的开销和限制,并可能导致性能下降。

而松散一致性模型则允许部分乱序执行和缓存一致性协议的使用,以提高并行度和性能。它引入了几种松散的一致性模型,包括:

  1. 总线事务内存:TM允许并发线程之间通过事务进行原子操作,并尽可能避免锁竞争。这样可以提高并行度和响应速度。
  2. 松散记忆顺序:此模型允许处理器乱序执行指令,并通过内存屏障等机制确保特定操作的有序性。
  3. 内存同步原语:Linux内核提供了多种同步原语(如原子操作、自旋锁、信号量等),用于控制共享数据的访问顺序和正确同步。

对于读取(Load)和写入(Store)指令,一共有四种组合:

  1. Load-Load(LL):两个读取指令之间的顺序关系。LL序列表示第一个读取操作必须在第二个读取操作之前完成才能保证顺序一致性。
  2. Load-Store(LS):一个读取指令和一个写入指令之间的顺序关系。LS序列表示读取操作必须在写入操作之前完成才能保证顺序一致性。
  3. Store-Load(SL):一个写入指令和一个读取指令之间的顺序关系。SL序列表示写入操作必须在读取操作之前完成才能保证顺序一致性。
  4. Store-Store(SS):两个写入指令之间的顺序关系。SS序列表示第一个写入操作必须在第二个写入操作之前完成才能保证顺序一致性。

内存屏障的使用规则

常见的内存屏障使用场景和相应的规则:

  1. 在读写共享变量时,应该使用对应的读屏障和写屏障来确保读写操作的顺序和一致性。例如,在写入共享变量后,应该使用写屏障(smp_wmb())来确保写入操作已经完成,然后在读取共享变量前,应该使用读屏障(smp_rmb())来确保读取操作是在之前的写入操作之后进行的。
  2. 在多个 CPU 访问同一个共享变量时,应该使用内存屏障来保证正确性。例如,在使用自旋锁等同步机制时,应该在获取锁和释放锁的操作之前加上读写屏障(smp_mb())来确保操作的顺序和一致性。
  3. 在编写高并发性能代码时,可以使用内存屏障来优化代码的性能。例如,在使用无锁算法时,可以使用适当的内存屏障来减少 CPU 的缓存一致性花费,从而提高性能。
  4. 在使用内存屏障时,应该遵循先使用再优化的原则,不应该过度依赖内存屏障来解决并发问题。例如,在使用锁时,应该尽量减少锁的使用次数和持有时间,以避免竞争和饥饿等问题。
  5. 在选择内存屏障时,应该考虑所在的 CPU 架构和硬件平台,以选择最适合的屏障类型和实现方式。例如,在 x86 架构下,应该使用 lfence、sfence 和 mfence 指令来实现读屏障、写屏障和读写屏障。

内核中隐式的内存屏障

在 Linux 内核中,存在一些隐式的内存屏障,它们无需显式地在代码中添加,而是由编译器或硬件自动插入。这些隐式的内存屏障可以确保代码在不同的优化级别下保持正确性和一致性,同时提高代码的性能。

以下是一些常见的隐式内存屏障:

  1. 编译器层面的内存屏障:编译器会根据代码的语义和优化级别自动插入适当的内存屏障。例如,编译器会在函数调用前后插入内存屏障来确保函数的参数和返回值在内存中的正确性。
  2. 硬件层面的内存屏障:现代处理器中的指令重排和缓存一致性机制会自动插入内存屏障。例如,处理器会根据内存访问模式自动插入读写屏障,以确保读写操作的顺序和一致性。
  3. 内核层面的内存屏障:Linux 内核中的同步原语(如自旋锁、互斥锁)会在关键代码段中插入内存屏障,以确保多个 CPU 访问共享变量的顺序和一致性。

Linux内核有很多锁结构,这些锁结构都隐含一定的屏障操作,以确保内存的一致性和顺序性。

  • spin locks(自旋锁):在LOCK操作中会使用一个类似于原子交换的操作,确保只有一个线程能够获得锁。UNLOCK操作包括适当的内存屏障以保证对共享数据的修改可见。
  • R/W spin locks(读写自旋锁):用于实现读写并发控制。与普通自旋锁类似,在LOCK和UNLOCK操作中都包含适当的内存屏障。
  • mutexes(互斥锁):在LOCK操作中可能会使用比较和交换等原子指令,并且在UNLOCK操作中也会包含适当的屏障来保证同步和顺序性。
  • semaphores(信号量):在LOCK和UNLOCK操作中均包含适当的内存屏障来确保同步和顺序性。
  • R/W semaphores(读写信号量):与普通信号量类似,在LOCK和UNLOCK操作中都会有相应的内存屏障。
  • RCU(Read-Copy Update,读拷贝更新):RCU机制不直接使用传统意义上的锁,但它涉及到对共享数据进行访问、更新或者删除时需要进行同步操作,确保数据的一致性和顺序性。

优化屏障

在 Linux 中,"优化屏障"是通过barrier()宏定义来实现的。这个宏定义确保编译器不会对其前后的代码进行过度的优化,以保证特定的内存访问顺序和可见性。

在 GCC 编译器中,"优化屏障"的定义可以在linux-5.6.18\include\linux\compiler-gcc.h源码文件中找到。这个头文件通常包含一些与编译器相关的宏定义和内联汇编指令,用于处理一些特殊情况下需要控制代码生成和优化行为的需求。

具体而言,barrier()宏定义使用了GCC内置函数__asm__ volatile("": : :"memory")来作为一个空的内联汇编语句,并加上了"memory"约束来告知编译器,在此处需要添加一个内存屏障。

内存屏障指令

在ARM架构中,有三条内存屏障指令:

  1. DMB (Data Memory Barrier):用于确保在指令执行之前的所有数据访问操作都已完成,并且写回到内存中。
  2. DSB (Data Synchronization Barrier):用于确保在指令执行之前的所有数据访问操作都已完成,并且其结果可见。
  3. ISB (Instruction Synchronization Barrier):用于确保在指令执行之前的所有指令已被处理器执行完毕,并清空处理器流水线。

参考:Linux内核源码分析教程

你可能感兴趣的:(Linux,linux)