一、内存类型:
ARMv8架构将系统中所有的内存,按照它们的特性,划分成两种,即普通内存和设备内存。并且它们是互斥的,也就是说系统中的某段内存要么是普通内存,要么是设备内存,不能都是。
1)普通内存(Normal Memory)
普通内存的特性是,在没有别的写入的情况下,每次读取出来的值都是一样的。针对普通内存,Arm处理器会采用比较激进的优化方式,从而导致指令重排序的问题。
普通内存可以被指定为支持缓存(Cached)或不支持缓存(Non-Cached)。如果两个模块之间不支持数据一致性协议,那么它们之间的共享内存一定是不支持缓存的。
2)设备内存(Device Memory)
设备内存一般是对外部设备的一段内存映射,在没有写入的情况下,可能每次读取出来的值都不一样。也有可能写入这段内存会产生别的边际效应,如触发一个中断。
二、共享域
为了支持数据一致性协议,需要增加硬件很多开销,会降低系统的性能,同时也会增加系统的功耗。但是,很多时候并不需要系统中的所有模块之间都保持数据一致性,而只需要在系统中的某些模块之间保证数据一致性就行了。因此,需要对系统中的所有模块,根据数据一致性的要求,做出更细粒度的划分。
ARMv8架构将这种划分称作为域(Domain),并且一共划分成了四类:
1)非共享(Non-shareable)域
处于这个域中的内存只由当前CPU核访问,既然只能自己访问,那当然不用考虑跟系统中的其它模块,如其它CPU核或其它设备之间的数据同步问题。所以,如果一个内存区域是非共享的,系统中没有任何硬件会保证其缓存一致性。如果一不小心共享出去了,别的CPU核可以访问了,那必须由软件自己来保证其一致性。
2)内部共享(Inner Shareable)域
处于这个域中的内存可以由系统中的多个模块同时访问,并且系统硬件保证对于这段内存,对于处于同一个内部共享域中的所有模块,保证缓存一致性。
一个系统中可以同时存在多个内部共享域,对一个内部共享域做出的操作不会影响另外一个内部共享域。
3)外部共享(Outer Shareable)域
处于这个域中的内存也可以由系统中的多个模块同时访问,并且系统硬件保证对于这段内存,对于处于同一个外部共享域中的所有模块,保证缓存一致性。外部共享域可以包含一个或多个内部共享域,但是一个内部共享域只能属于一个外部共享域,不能被多个外部共享域共享。
对一个外部共享域做出的操作会影响到其包含的所有的内部共享域。
4)全系统共享(Full System)域
这个很好理解,表示对内存的修改可以被系统中的所有模块都感知到。
在一个具体的系统中,不同域的划分是由硬件平台设计者决定的,不由软件控制。并且,Arm的文档中也没有提及具体要怎么划分。但有一些指导原则,一般在一个操作系统中可以看到的所有CPU核要分配在一个内部域里面,如下图所示:
这些域的划分只是为了更细粒度的管理内存的缓存一致性,理论上所有内存都放到全系统共享域中,从功能上说也可以,但会影响性能。
可缓存性和共享性一定是对普通内存才有的概念。设备内存一定是不支持缓存的,且是外部共享的。
三、屏障
不同处理器提供的内存屏障指令不同, ARM64 处理器提供了 3 种内存屏障。
(1)指令同步屏障( Instruction Synchronization Barrier, ISB),指令是 isb。
(2)数据内存屏障( Data Memory Barrier, DMB),指令是 dmb。
(3)数据同步屏障( Data Synchronization Barrier, DSB),指令是 dsb。
指令同步屏障指令冲刷流水线,在屏障指令执行完毕后重新取程序中屏障指令后面的所有指令,以便使用最新的内存管理单元配置检查权限和访问。屏障指令确保以前执行的改变上下文的操作(包括缓存维护指令、页表缓存维护指令或修改系统控制寄存器)在屏障指令执行完的时候已经完成。
数据内存屏障保证屏障前面的内存访问和屏障后面的内存访问的相对顺序,屏障前面的内存访问必须在屏障后面的内存访问之前被观察到,但是不保证屏障前面的内存访问完成。
数据同步屏障保证屏障前面的内存访问、缓存维护指令和页表缓存维护指令在屏障完成之前已经完成,屏障后面的任何指令在屏障完成之后才能开始执行,是比数据内存屏障更强的屏障。
DMB和DSB指令都需要带一个参数,这个参数指明了数据屏障指令的作用范围和针对的共享域。共享域前面说过了,一共有四种。作用范围表示数据屏障指令具体对哪些存储器访问操作起作用,ARMv8共定义了三种,分别是:
1. Load - Load, Load - Store:表示内存屏障保证其之前的所有加载操作一定在其之前完成,其之后的所有加载和存储操作一定在其之后才开始,但是其之前的存储操作有可能会在其之后才执行。
2. Store - Store:表示内存屏障保证其之前的所有存储操作一定在其之前完成,而其之后的存储操作一定在其之后才能开始,但是对于加载操作没有任何限制。
3. Any - Any:表示内存屏障保证其之前的所有加载和存储操作一定在其之前完成,而其后的所有加载和存储操作一定在其之后才能开始。
注意,ARMv8不提供所谓的Store-Load型的顺序保证,如果真的需要这种保证,只能使用Any-Any型的。关于DMB和DSB指令的参数,可以总结为如下表格:
参数 | 作用范围 | 共享域 |
OSHLD | Load - Load, Load - Store | 外部共享域 |
OSHST | Store - Store | 外部共享域 |
OSH | Any - Any | 外部共享域 |
NSHLD | Load - Load, Load - Store | 非共享域 |
NSHST | Store - Store | 非共享域 |
NSH | Any - Any | 非共享域 |
ISHLD | Load - Load, Load - Store | 内部共享域 |
ISHST | Store - Store | 内部共享域 |
ISH | Any - Any | 内部共享域 |
LD | Load - Load, Load - Store | 全系统共享域 |
ST | Store - Store | 全系统共享域 |
SY | Any - Any | 全系统共享域 |
ARM64 架构定义的内存屏障宏如下。
(1) #define mb() asm volatile("dsb sy” : : : "memory")
使用数据同步屏障指令,保证屏障前面的读写操作在屏障完成之前已经完成,使整个系统看见。
(2) #define wmb() asm volatile("dsb st” : : : "memory")
使用数据同步屏障指令,保证屏障前面的写操作在屏障完成之前已经完成,使整个系统看见。
(3) #define rmb() asm volatile("dsb ld” : : : "memory")
使用数据同步屏障指令,保证屏障前面的读操作在屏障完成之前已经完成,使整个系统看见。
(4) #define read_barrier_depends() do { } while (0)
(5) #define smp_mb() asm volatile("dmb ish” : : : "memory")
使用数据内存屏障指令, 保证屏障前面的读写操作和屏障后面的读写操作的相对顺序,使内部共享域(包括所有处理器)看见。
(6) #define smp_wmb() asm volatile("dmb ishst” : : : "memory")
使用数据内存屏障指令,保证屏障前面的写操作和屏障后面的写操作的相对顺序,使内部共享域看见。
(7) #define smp_rmb() asm volatile("dmb ishld” : : : "memory")
使用数据内存屏障指令,保证屏障前面的读操作和屏障后面的读写操作的相对顺序,使内部共享域看见。
(8) #define smp_read_barrier_depends() do { } while (0)
(9) #define dma_wmb() asm volatile("dmb oshst” : : : "memory")
使用数据内存屏障指令,保证屏障前面的写操作和屏障后面的写操作的相对顺序,使外部共享域(包括所有处理器和外围设备)看见。
(10) #define dma_rmb() asm volatile("dmb oshld” : : : "memory")
使用数据内存屏障指令,保证屏障前面的读操作和屏障后面的读写操作的相对顺序,使外部共享域看见。
ARM64 还提供了带有隐含单向屏障的加载和存储指令。
(1)加载获取指令 ldar。
加载获取指令后面并且匹配目标地址的共享域的所有加载存储操作必须在加载获取指令之后被观察到。所以,可以看出来,这条指令是个单向屏障,只挡住了后面出现的所有内存操作指令,但是没有挡住这条指令之前的所有内存操作指令。
(2)存储释放指令 stlr。
存储释放指令前面并且匹配目标地址的共享域的所有加载存储操作必须在存储释放指令之前被观察到,并且存储释放指令生成的存储操作在该指令执行完之后可以被观察到。所以,这条指令也是一个单向屏障,只挡住了前面出现的所有内存操作指令,但是没有挡住这条指令之后的所有内存操作指令。
单向屏障的作用范围可以总结为下面这张图:
ARM64 还提供了上面两条指令的独占版本: ldaxr 和 stlxr。
ARM64 架构的内核使用加载获取指令实现了加载获取函数 smp_load_acquire(p)和 smp_cond_load_acquire(ptr, cond_expr),使用存储释放指令实现了存储释放函数 smp_store_release(p, v)。