内存屏障
内存屏障( memory barrier)是一种保证内存访问顺序的方法,用来解决下面这些内存访问乱序问题。
(1)编译器编译代码时可能重新排列汇编指令,使编译出来的程序在处理器上运行更快,但是有时候优化的结果可能不符合程序员的意图。
(2)现代的处理器采用超标量体系结构和乱序执行技术,能够在一个时钟周期并行执行多条指令。处理器按照程序顺序取出一批指令,分析找出没有依赖关系的指令,发给多个独立的执行单元并行执行,最后按照程序顺序提交执行结果。用一句话总结就是“顺序取指令,乱序执行,顺序提交执行结果”。有些情况不允许乱序执行,必须严格按照顺序,可是处理器不能识别出依赖关系。常见的情况是处理器访问外围设备控制器的寄存器,例如查询有些外围设备的状态值,需要先向控制寄存器写入数值,然后从状态寄存器读取状态值。
(3)在多处理器系统中,硬件工程师使用存储缓冲区、使无效队列协助缓存和缓存一致性协议实现高性能,引入了处理器之间的内存访问乱序问题。一个处理器修改数据,可能不会把数据立即同步到自己的缓存或者其他处理器的缓存,导致其他处理器不能立即看到最新的数据。注意:处理器的乱序执行不会造成处理器之间的内存访问乱序问题,因为执行结果是按照程序顺序提交的。
内核支持 2 种内存屏障。
(1)编译器屏障。
(2)处理器内存屏障。
编译器屏障
为了提高程序的执行速度,编译器优化代码,对于不存在数据依赖或控制依赖的汇编指令,重新排列它们的顺序,但是有时候优化产生的指令顺序不符合程序员的真实意图,程序员需要使用编译器屏障指导编译器。
编译器屏障是:
barrier();
它阻止编译器把屏障一侧的指令移动到另一侧,既不能把屏障前面的指令移动到屏障后面,也不能把屏障后面的指令移动到屏障前面。编译器屏障也称为编译器优化屏障。
内核定义了宏 READ_ONCE()、 WRITE_ONCE()和 ACCESS_ONCE(),它们可以看作barrier()的弱化形式,只阻止编译器对单个变量优化。 C 语言的关键字“ volatile”(易变的)也可以阻止编译器对单个变量优化。
例如想使用禁止内核抢占的方法保护临界区:
preempt_disable();
//临界区
preempt_enable();
编译器发现临界区和前后的代码不存在数据依赖关系,就会优化代码,可能把代码重新排列如下:
//临界区
preempt_disable();
preempt_enable();
也可能把代码重新排列如下:
preempt_disable();
preempt_enable();
//临界区
这两种排列都是错误的,导致临界区不受保护。为了阻止编译器错误地重排指令,在禁止内核抢占和开启内核抢占的宏里面添加了编译器优化屏障:
include/linux/preempt.h
#define preempt_disable() \
do { \
preempt_count_inc(); \
barrier(); \
} while (0)
#define preempt_enable() \
do { \
barrier(); \
if (unlikely(preempt_count_dec_and_test())) \
__preempt_schedule(); \
} while (0)
GCC 编译器定义的宏“ barrier()”如下:
include/linux/compiler-gcc.h
#define barrier() __asm__ __volatile__("": : :"memory")
关键字“ __volatile__”告诉编译器:禁止优化代码,不要改变 barrier()前面的代码块、barrier()和后面的代码块这 3 个代码块的顺序。
嵌入式汇编代码中的破坏列表( clobber list)“ memory”告诉编译器:内存中变量的值可能变化,不要继续使用加载到寄存器中的值,应该重新从内存中加载变量的值。