内存屏障¶
编译器为了提高性能,在对指令优化时会对指令的顺序重新排列,这就是所谓的指令乱序。编译时的乱序访问在C语言中可以通过barrier()
函数来规避:
在多处理器系统中,一个 CPU 核心的指令乱序可能会对其他 CPU 核心产生影响,进而造成不可预期的行为,特别是多核并发访问共享资源时。由于现代处理器普遍采用诸如高速缓存、超标量技术、乱序执行、多级流水线技术等,这些技术都会对指令的执行顺序进行重新排序,因此,为了确保程序在多处理器系统上的正确性,我们就需要合理使用内存屏障指令来确保程序按照我们预期的方式执行。
ARM64 处理器的屏障指令包括:
- DMB(数据内存屏障)指令:保证在
DMB
指令之前的所有内存访问操作都已完成,才提交DMB
指令之后的内存访问操作。 - DSB(数据同步屏障)指令:位于此指令之前的所有内存访问、高速缓存、分支预测和 TLB 维护指令全部完成。
- ISB(指令同步屏障)指令:flush 流水线,使得在
ISB
指令之后的指令重新从指令缓存或内存中重新预取。
Linux 内核中的自旋锁、互斥体等逻辑,都使用了内存屏障指令来确保线程安全。
共享属性域¶
DMB
和DSB
指令后面必须要带参数,用于指定共享属性域(share ability domain)。共享属性域用来描述内存屏障指令的作用域,ARMv8 体系结构定义了4种域:
- 全系统共享域:影响系统中的所有观察者
- 外部共享域:由一个或多个内部共享域组成
- 内部共享域:由多个处理器共享的可共享域,比如一个四核 Cortex-A57 集群
- 不指定共享域:只能由单个处理器访问
内存屏障指令的参数如下表所示:
参数 | 共享属性 |
---|---|
SY | 全系统共享域 |
OSH | 外部共享域 |
ISH | 内部共享域 |
NSH | 不指定共享域 |
还可以对内存屏障指令限定读写方向:仅读——LD 后缀,仅写——ST 后缀。比如ISHLD
就是仅限制内部共享域的内存读指令。
DMB指令¶
DMB
指令只能保证前后的内存访问指令按照顺序执行,内存访问指令包括普通的加载和存储指令,也包括数据高速缓存指令。
DMB
指令后面必须带参数,用来指定共享属性域。
在深入了解DMB
指令之前,有必要了解下什么是数据依赖和地址依赖。数据依赖指的是相邻的读写操作有数据上的依赖关系,比如从 Xn 地址读取内容到 Xm,再把 Xm 地址中的值写入到 Xy 地址中,那么这些读写指令就存在依赖关系。伪代码如下所示:
地址依赖指的是相邻的读写操作有地址上的依赖关系,比如从 Xn 地址读取内容到 Xm 地址中,再把另外一个值写入到 Xm 地址中,那么这些读写指令就存在地址依赖关系。伪代码如下所示:
如果两条指令既没有数据依赖,也没有地址依赖,那么 CPU 就可以进行指令重排以最优化性能。比如有两条指令如下:
由于没有依赖关系,从 CPU 的角度看,先执行ldr
指令还是str
指令,从最终结果来看没有区别。如果想要确保 CPU一定按照写的顺序来执行代码,就可以在这两条指令中间加上DMB
指令:
再来看一个例子:
尽管在ldr
指令和add
指令中间有一条DMB
指令,但是仍然不能保证指令执行的顺序,因为DMB
指令只能保证内存访问指令的执行次序,而ADD
不是内存访问指令,它是有可能在ldr
指令之前执行的。要解决这个问题,就得把DMB
指令换成DSB
指令。
DSB指令¶
DSB
指令要比DMB
指令严格得多,DSB
后面的指令必须满足下面两个条件才能开始执行:
DSB
指令前面的所有内存访问指令必须执行完。DSB
指令前面的高速缓存、分支预测、TLB 等维护指令也必须执行完。
在多核系统中,高速缓存和 TLB 维护指令会广播到其他 CPU 核心,执行本地相关的维护操作。DSB
指令等待广播并收到其他 CPU 核心发送的应答信号才算执行完。
当使用了DSB
指令后,ADD
指令不能重排到LDR
指令前面。
ISB指令¶
ISB
指令会冲刷流水线,然后从指令高速缓存后者内存中重新预取指令。
ARMv8 体系结构中有一个术语——更改上下文操作(context-switching operation),包括高速缓存、TLB、分支预测以及改变系统控制寄存器等操作。更改上下文操作的效果仅仅在上下文同步事件之后能看到。上下文同步事件分为三种:
- 发生一个异常
- 从一个异常返回
- 执行了
ISB
指令
ISB
指令刷新流水线,并从缓存或内存中重新获取指令,确保ISB
指令之前的上下文更改操作对ISB
指令之后的任何指令都可见。它还确保ISB
指令之后的任何上下文更改操作对ISB
指令之前的指令不可见。
将 cpacr_el1的 Bit[21:20] 修改为 0x3,可以打开浮点运算功能。紧接着的FADD
指令如果没有ISB
指令的保护,则可能会提前执行,即浮点运算功能还未启用时就执行,这会引发错误。
案例分析请参考ARM64体系结构编程与实践P300页 ~ P311页。