一、 产生原因
1. 编译时。编译器优化导致的内存乱序访问。 编译器在翻译成汇编指令时对其进行优化,如内存访问指令的重新排序可以提高指令并行效率,然而这些优化可能会与程序员的原始代码逻辑不符。
2. *执行时。多个CPU的交互引起内存乱序访问 *。执行时,由于现代处理器普遍采用了超标量架构、乱序发射以及乱序执行等技术来提高指令级并行效率,因此指令的执行序列在处理器流水线中可能被打乱,与程序代码序列不一致,这就产生了程序员错觉——处理器访问内存的次序与代码的次序不同。
在一个单处理器系统里面,不管CPU怎么乱序执行,它最终的结果都是程序想要的结果,也就是类似于顺序执行的模型。 在多核处理器系统中,一个CPU内核中内存的访问乱序执行可能会对系统中其他观察者(如其他CPU核)产生影响,即它们可能观察到的内存执行次序与实际执行次序有很大的不同,特别是多核并发访问共享数据的情况下。这里引申出一个存储一致性问题。
注:内存屏障要解决的是存储一致性问题 , CPU Cache的 MESI协议仅仅是用来解决cache一致性问题(cache与内存的一致问题),而存储一致性问题是多处理器对多个不同内存地址访问次序引发的问题,两者层次不同,无论使能与未使能Cache都存在存储一致性问题。
二、 解决方法
- 1、编译时的乱序访问可通过 barrier()宏函数来规避。barrier函数告诉编译器,不要为了性能优化而将代码重排。
#define barrier() asm volatile("" ::: "memory")
- 2、执行时的乱序可通过内存屏障原语规避。
三、 内存一致性模型
-
- 顺序一致性(SC)内存性模型 保证每一条加载/存储指令与后续的加载存储指令严格按照程序的次序来执行,即保证了“读->读",“读->写",“写->写"以及“写->读"4种情况的次序。
-
- 处理器一致性(PC)内存模型,是SC模型的理一步弱化,放宽了较早的写操作与后续的读操作的次序要求,即放宽了“写->读“次序要求。允许一条加载指令从存储缓冲区(store buffer)中读取一条还没有执行的存储指令的值,而且这个值还没有被写入高速缓存中。X86_X64处理器实现的倒序写(Total Store Ordering, TSO)模型就属于处理器一致性内存模型的一种。
-
- 弱一致性内存模型,是对处理器一致性内存模型进一步弱化,即放宽了“读->读",“读->写",“写->写"以及“写->读"4种情况的执行次序要求,此模型并不意味着程序不能得到正确的预期结果。在这种情况下,程序需要添加适当的同步操作。例如,若一个处理器的存储访问想在另外一个处理器的存储访问之后发生,我们需要使用同步来实现,即内存屏幕指令。
弱一致性内存模型实质上是把一致性问题留给了程序员来解决,程序员必须正确地向处理器表达哪些读写操作是需要同步的。
ARM64处理器实现了弱一致性内存模型,ARM64内存屏障指令原则:
-
- 在内存屏障指令后面的所有数据访问必须等待内存屏障指令(如ARM64的DMB)执行完。
-
- 多条内存屏障指令是按顺序执行的。
四、 ARM64处理器的内存模型
1、普通内存(DDR) —— 使用的是弱一致性内存模型。
2、设备内存(如外设寄存器) —— 使用的是强一致性内存模型。
五、 ARM64中的内存屏障指令
1. 使用内存屏障的场景
单核处理器系统不需要考虑内存屏障,虽然CPU内存支持乱序执行及预测执行,但总体来说,CPU会保证最终执行结果符合程序员的要求。
多核并发下,以下一些典型场景需要考虑使用内存屏障指令:
-
- 在多个不同CPU内核之间共享数据。
-
- 执行和外设相关的操作,如DMA操作。启动DMA操作的流程通常是这样:第一步,把数据写入DMA缓冲区,第二步,设置 DMA相关寄存器来启动DMA。如果这中间没有内存屏障指令,第二步相关操作有可能在第一步前面执行,这样DMA就传错了数据。
-
- 修改内存管理策略,如上下文切换、请求缺页以及修改页表等。
-
- 修改存储指令的内存区域,如自修改代码的场景。
2. ARM64内存屏障指令
- 数据存储屏障指令(Data Memory Barrier, DMB)指令: DMB指令保证的是DMB指令之前的所有内存访问指令和DMB指令之后的所有内存访问指令的执行顺序。DMB指令不会保证内存访问指令在内存屏障指令之前完成,它仅仅保证内存屏障指令前后的内存访问的执行顺序。DMB指令仅仅影响内存访问指令、数据高速缓存指令以及高速缓存管理指令,不会影响其他指令(如算术运算指令、指令高速缓存维护指令IC)的顺序。
- 数据同步屏障(Data Synchronization Barrier, DSB)指令:比DMB指令要严格一些,仅当所有在它前面的内存访问指令都执行完毕,才会执行在它后面的指令。即任何指令都要等待DSB指令前面的内存访问指令完成。位于DSB指令前面的所有缓存(如分支预测和TLB维护)操作需要全部完成。
- 指令同步屏障(Instruction Synchronization Barrier, ISB)指令:确保所有在ISB指令之后的指令都从指令高速缓存或内存中重新预取。它刷新流水线(flush pipeline)和预取缓冲区后才会从指令高速缓存或内存中预取ISB之后的指令。ISB指令通常用来保证上下文切换(如ASIP、TLB维护操作等)的效果。
注:DMB, DSB, ISB三者区别请参阅《ARM64体系结构与编程实践》19.4节,讲得更清晰
3. DMB指令
DMB指令强调的是内存屏障前后的数据访问指令的访问次序。这里有两个要点: 一个是数据访问指令,另一个是保证访问的次序。
【例1】 CPU执行下面两条指令:
ldr x0, [x1]
str x2, [x3]
此两条指令没有数据依赖或地址依赖,CPU先执行STR或LDR,结果无差别。
如果想要确保CPU一定按照写的序列一执行,可加入一条dmb指令
ldr x0, [x1]
dmb ish
str x2, [x3]
【例2】 CPU执行下面两条指令:
ldr x0, [x1]
str x0, [x3]
这两条指令存在数据依赖,不需要内存屏障CPU也能保证上术两条指令的执行次序。
【例3】 CPU执行下面3条指令:
ldr x0, [x1]
dmb ish
add x2, x3, x4
尽管ldr和add之间有dmb内存屏障指令,但add指令有可能在ldr指令前面执行的。因为dmb指令只保证数据访问指令的次序,而add不是数据访问指令,它是算术运算指令。解决办法是把DMB替换成DSB
【例4】 CPU执行下面4条指令:
dc cvac, x6
ldr x1, [x2]
dmb ish
ldr x3, [x7]
前两条指令没有dmb指令,所以第二条 ldr可以乱序排到dc指令前面,数据高速缓存维护指令dc也算数据访问指令。
4. DSB指令
DSB指令要比DMB指令严格很多。 DSB后面的任何指令必须满足下面两个条件才能开始执行。
- DSB指令前面的所有数据访问指令(内存访问指令)必须执行完。
- DSB指令前面的高速缓存、分支预测、TLB等维护指令也必须执行完。
【例5】
ldr x0, [x1]
dsb ish
add x2, x3, x4
add指令必须等待ldr指令执行完才能开始执行。
【例6】
dc x0, [x1]
str x1, [x2]
dsb ish
add x3, x3, #1
dc和str必须在dsb之前完成,add 必须等到dsb执行完之后才开始执行。
5. DMB和DSB参数
DMB和DSB指令后面可以带参数,用于指定共享属性和访问顺序。
参数 | 访问顺序 | 共享属性 |
---|---|---|
SY | 内存读写指令 | 全系统共享域 |
ST | 内存写指令 | 全系统共享域 |
LD | 内存读指令 | 全系统共享域 |
ISH | 内存读写指令 | 内部共享域 |
ISHST | 内存写指令 | 内部共享域 |
ISHLD | 内存读指令 | 内部共享域 |
NSH | 内存读写指令 | 不指定共享域 |
NSHST | 内存写指令 | 不指定共享域 |
NSHLD | 内存读指令 | 不指定共享域 |
OSH | 内存读写指令 | 外部共享域 |
OSHST | 内存写指令 | 外部共享域 |
OSHLD | 内存读指令 | 外部共享域 |
6. 单方向内存屏障原语
- 加载-获取(load-acquire):所有加载-获取内在屏障指令后面的内存访问指令(包括读写)只能在加载获取指令执行后才开始执行。并且被其他CPU观察到。
- 存储-释放(strore-release): 所有存储-释放屏障原语之前的指令执行完了,才能执行存储-释放原语之后的指令,这样其他CPU可以观察到存储-释放原语之前的指令已经执行完了。
load-acquire和strore-release相当于单方向的DMB。
7. ISB指令
ISB指令会冲刷流水线,然后从指令高速缓存或内存中重新预取指令。
使用ISB确保在ISB之前执行的上下文更改操作的效果对在ISB执行之后的指令是可见的,更改上下文操作的效果仅在上下文同步事件之后看到。上下文同步事件包括:
- 发生一个异常
- 从一个异常返回
- 执行了ISB指令
注:修改了系统控制寄存器是需要使用ISB的,但并不是所有寄存器修改都需要ISB,如PSTATE修改不需要ISB。
【例7】 打开浮点运算单元FPU。
msr x1, cpacr_el1
orr x1, x1, #(0x3<<20)
msr cpacr_el1, x1
isb
fadd s0, s1, s2
【例8】改变页表
str x10, [x1]
dsb ish
tlbi vaelis, x11
dsb ish
isb
API
内核 API | 含义 | ARM64实现 |
---|---|---|
rmb() | 单处理器系统版本的读内存屏障指令 | #define rmb() asm volatile(“dsb ld : : : memory”) |
wmb() | 单处理器系统版本的写内存屏障指令 | #define rmb() asm volatile(“dsb st : : : memory”) |
mb() | 单处理器系统版本的读写内存屏障指令 | #define rmb() asm volatile(“dsb sy : : : memory”) |
smp_rmb() | 用于SMP环境的读内存屏障指令 | #define smp_rmb() dmb(ishld) |
smp_wmb() | 用于SMP环境的写内存屏障指令 | #define smp_wmb() dmb(ishst) |
smp_mb() | 用于SMP环境的读写内存屏障指令 | #define smp_mb() dmb(ish) |
dma_rmb() | 用于DMA 读内存屏障 | #define dma_rmb() dmb(oshld) |
dma_wmb() | 用于DMA 写内存屏障 | #define dma_wmb() dmb(oshst) |
* path: arch/arm64/include/asm/barrier.h */
#define __nops(n) ".rept " #n "\nnop\n.endr\n"
#define nops(n) asm volatile(__nops(n))
#define sev() asm volatile("sev" : : : "memory")
#define wfe() asm volatile("wfe" : : : "memory")
#define wfi() asm volatile("wfi" : : : "memory")
#define isb() asm volatile("isb" : : : "memory")
#define dmb(opt) asm volatile("dmb " #opt : : : "memory")
#define dsb(opt) asm volatile("dsb " #opt : : : "memory")
#define psb_csync() asm volatile("hint #17" : : : "memory")
#define csdb() asm volatile("hint #20" : : : "memory")
#define mb() dsb(sy)
#define rmb() dsb(ld)
#define wmb() dsb(st)
/* DMA */
#define dma_rmb() dmb(oshld)
#define dma_wmb() dmb(oshst)
/*
* Generate a mask for array_index__nospec() that is ~0UL when 0 <= idx < sz
* and 0 otherwise.
*/
#define array_index_mask_nospec array_index_mask_nospec
static inline unsigned long array_index_mask_nospec(unsigned long idx,
unsigned long sz)
{
unsigned long mask;
asm volatile(
" cmp %1, %2\n"
" sbc %0, xzr, xzr\n"
: "=r" (mask)
: "r" (idx), "Ir" (sz)
: "cc");
csdb();
return mask;
}
#define __smp_mb() dmb(ish)
#define __smp_rmb() dmb(ishld)
#define __smp_wmb() dmb(ishst)
#define __smp_store_release(p, v) \
do { \
union { typeof(*p) __val; char __c[1]; } __u = \
{ .__val = (__force typeof(*p)) (v) }; \
compiletime_assert_atomic_type(*p); \
switch (sizeof(*p)) { \
case 1: \
asm volatile ("stlrb %w1, %0" \
: "=Q" (*p) \
: "r" (*(__u8 *)__u.__c) \
: "memory"); \
break; \
case 2: \
asm volatile ("stlrh %w1, %0" \
: "=Q" (*p) \
: "r" (*(__u16 *)__u.__c) \
: "memory"); \
break; \
case 4: \
asm volatile ("stlr %w1, %0" \
: "=Q" (*p) \
: "r" (*(__u32 *)__u.__c) \
: "memory"); \
break; \
case 8: \
asm volatile ("stlr %1, %0" \
: "=Q" (*p) \
: "r" (*(__u64 *)__u.__c) \
: "memory"); \
break; \
} \
} while (0)
#define __smp_load_acquire(p) \
({ \
union { typeof(*p) __val; char __c[1]; } __u; \
compiletime_assert_atomic_type(*p); \
switch (sizeof(*p)) { \
case 1: \
asm volatile ("ldarb %w0, %1" \
: "=r" (*(__u8 *)__u.__c) \
: "Q" (*p) : "memory"); \
break; \
case 2: \
asm volatile ("ldarh %w0, %1" \
: "=r" (*(__u16 *)__u.__c) \
: "Q" (*p) : "memory"); \
break; \
case 4: \
asm volatile ("ldar %w0, %1" \
: "=r" (*(__u32 *)__u.__c) \
: "Q" (*p) : "memory"); \
break; \
case 8: \
asm volatile ("ldar %0, %1" \
: "=r" (*(__u64 *)__u.__c) \
: "Q" (*p) : "memory"); \
break; \
} \
__u.__val; \
})
#define smp_cond_load_relaxed(ptr, cond_expr) \
({ \
typeof(ptr) __PTR = (ptr); \
typeof(*ptr) VAL; \
for (;;) { \
VAL = READ_ONCE(*__PTR); \
if (cond_expr) \
break; \
__cmpwait_relaxed(__PTR, VAL); \
} \
VAL; \
})
#define smp_cond_load_acquire(ptr, cond_expr) \
({ \
typeof(ptr) __PTR = (ptr); \
typeof(*ptr) VAL; \
for (;;) { \
VAL = smp_load_acquire(__PTR); \
if (cond_expr) \
break; \
__cmpwait_relaxed(__PTR, VAL); \
} \
VAL; \
})
#include <asm-generic/barrier.h>
/* path: include/asm-generic/barrier.h */
/* 被include 到arch/arm64/include/asm/barrier.h中
* 所以此处的mb,rmb, wmb, dma_rmb, dma_wmb等都不会再定义。
*/
#ifndef mb
#define mb() barrier()
#endif
#ifndef rmb
#define rmb() mb()
#endif
#ifndef wmb
#define wmb() mb()
#endif
#ifndef dma_rmb
#define dma_rmb() rmb()
#endif
#ifndef dma_wmb
#define dma_wmb() wmb()
#endif
#ifdef CONFIG_SMP
#define smp_mb() __smp_mb()
#define smp_rmb() __smp_rmb()
#define smp_wmb() __smp_wmb()
#else /* !CONFIG_SMP */
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#endif /* CONFIG_SMP */