1. 引言
我们都知道,带有优化的编译器,会尝试重新排序汇编指令,以提高程序的执行速度。但是,当在处理同步问题的时候,重新排序的指令应该被避免。因为重新排序可能会打乱我们之前想要的同步效果。其实,所有的同步原语都可以充当优化和内存屏障。
优化屏障保证屏障原语前后的C语言转换成汇编语言之后,指令序列不会发生变化。比如说,对于Linux内核的barrier()
宏,展开后就是asm volatile("":::"memory")
,就是一个优化屏障。asm
告知编译器插入一条汇编指令,volatile
关键字禁止编译器用程序的其它指令重新洗牌asm
指令。memory
关键字强迫编译器假设RAM中所有的位置都被汇编指令更改了;因此,编译器不会使用CPU寄存器中的值优化asm
指令之前的代码。我们需要注意的是优化屏障不能保证汇编指令的执行不会乱序,这是由内存屏障保障的。
内存屏障确保屏障原语前的指令完成后,才会启动原语之后的指令操作。
2. 架构相关的内存屏障实现
X86系统中,下面这些汇编指令都是串行的,可以充当内存屏障:
- 所有操作I/O端口的指令;
- 前缀
lock
的指令; - 所有写控制寄存器,系统寄存器或debug寄存器的指令(比如,
cli
和sti
指令,可以改变eflags
寄存器的IF标志); lfence
、sfence
和mfence
汇编指令,分别用来实现读内存屏障、写内存屏障和读/写内存屏障;- 特殊的汇编指令,比如
iret
指令,可以终止中断或异常处理程序。
ARM系统中,使用
ldrex
和strex
汇编指令实现内存屏障。
3. Linux内核使用的内存屏障原语
Linux内核中使用的内存屏障原语如下,如表5-6所示。当然了,这些原语完全可以作为优化屏障,阻止编译器优化该屏障前后的汇编指令。读内存屏障只对内存的读操作指令有效;写内存屏障只对内存的写操作指令有效。smp_xxx()
之类的内存屏障只对发生在多核系统里的竞态条件有效,单核系统中,什么也没有做。其它的内存屏障对多核系统和单核系统都有效。
表5-6 Linux内存屏障
macro | 描述 |
mb() | MP和UP的内存屏障 |
rmb() | MP和UP的读内存屏障 |
wmb() | MP和UP的写内存屏障 |
smp_mb() | MP内存屏障 |
smp_rmb() | MP读内存屏障 |
smp_wmb() | MP写内存屏障 |
内存屏障的实现跟系统架构息息相关。在X86系统上,如果支持lfence
汇编指令,则rmb()
实现为:
asm volatile("lfence":::"memory")
如不支持lfence
汇编指令,则rmb()
实现为:
asm volatile("lock;addl $0,0(%%esp)":::"memory")
asm volatile
的作用之前的文章已经介绍过,不再赘述。lock;addl $0,0(%%esp)":::"memory"
的意思是,对栈顶保存的内存地址内的内容加上0,所以这条命令本身没有意义,主要还是lock
前缀,对数据总线加锁,从而使该条指令对CPU而言,称为内存屏障。
而wmb()
的实现事实上非常简单,就是barrier()
的宏声明。这是因为,现有的Intel处理器不会对写内存访问进行重新排序,所以无法插入特定的内存屏障指令。但是,该宏还是会禁止编译器打乱指令。
值得注意的是多核处理器中,所有的原子操作指令都会前缀lock
,所以都可以充当内存屏障。
4. 总结
内存屏障主要解决的还是硬件数据总线上对于指令的读取可能会发生乱序问题。所以,内存屏障的使用场合就是对系统进行设置或者配置时,因为这些设置关系到后面的程序能否正确工作,所以需要内存屏障,保证程序运行之前,系统的配置已经生效。