原子操作¶
原子操作底层与架构紧密相关,ARMv8.0 的实现如下:
#define ATOMIC_OP(op, asm_op, constraint) \
static inline void \
__ll_sc_atomic_##op(int i, atomic_t *v) \
{ \
unsigned long tmp; \
int result; \
\
asm volatile("// atomic_" #op "\n" \
" prfm pstl1strm, %2\n" \
"1: ldxr %w0, %2\n" \
" " #asm_op " %w0, %w0, %w3\n" \
" stxr %w1, %w0, %2\n" \
" cbnz %w1, 1b\n" \
: "=&r" (result), "=&r" (tmp), "+Q" (v->counter) \
: __stringify(constraint) "r" (i)); \
}
注意:ARMv8.1增加的LSE(Large System Extension)feature使用的是ldadd
指令。
独占内存访问指令¶
LDXR
和STXR
指令,用于实现对变量的原子操作。
LDXR
是独占内存加载指令,它以独占的方式加载内存地址的值到通用寄存器。
STXR
是独占内存存储指令,它以独占的方式将通用寄存器中的值存储到内存地址。执行的结果放在 Ws 寄存器中,如果该寄存器为0则执行成功。
LDXP
和STXP
是多字节的独占内存访问指令。
独占监视器¶
独占内存访问指令LDXR
和STXR
通过独占监视器来监控对内存的访问。
独占监视器会把对应内存地址标记为独占访问模式,
最后一行通过判断 W3 寄存器的值来判断是否执行成功,如果不为0则跳转到标签1处重新执行。
注意,LDXR
和STXR
指令必须配对使用,位于这两条指令之间的代码是原子的。
独占监视器一共有两种状态——开放访问状态和独占访问状态。
当 CPU 通过LDXR
指令从内存加载数据时,CPU 会把这个内存地址标记为独占访问,然后 CPU 内部的独占监视器的状态就变为独占访问状态。当执行到STXR
指令时,需要根据独占监视器的状态来做决定:
- 如果是独占访问状态并且
STXR
指令要存储的地址正好是刚才标记过的地址,那么STXR
指令执行成功,返回0,并且独占监视器的状态变为开放访问状态。 - 如果是开放访问状态,那么
STXR
指令执行失败,返回1,并且独占监视器的状态仍然保持开放访问状态。
ARMv8 体系结构根据缓存一致性的层次关系分为了多个监视器:
- 本地独占监视器:监视本地 CPU
- 内部缓存一致性全局独占监视器:监视内部缓存一致性
- 外部全局独占监视器:监视外部缓存一致性
原子内存访问指令¶
ARMv8 体系结构中新增了原子内存访问指令,该指令需要 AMBA 5总线中的 CHI(Coherent Hub Interface)的支持。AMBA 5总线引入了原子事务(atomic transaction)的概念,允许将原子操作发送到数据,并且允许原子操作在靠近数据的地方执行,而不需要加载到高速缓存中处理。原子事务非常适合要操作的数据离处理器核心比较远的地方,比如数据在内存中。
原子内存访问指令与独占内存访问指令最大区别在于效率。设想一个 SMP 系统,假如共享资源存储在内存中,使用独占内存访问指令会导致所有 CPU 核心都将锁加载到 L1 高速缓存中,然后不停地尝试获取锁和检查独占监视器的状态,在锁竞争激烈的时候会造成高速缓存颠簸现象,并且整个过程还需要 MESI(缓存一致性)协议来处理 L1 高速缓存一致性。这个场景在 NUMA 架构下更加明显,远端节点的 CPU 需要不断地跨节点访问数据。另外一个问题是不公平,当锁持有者释放锁时,所有的 CPU 都需要争抢这把锁,有可能最先申请锁的 CPU 反而没有抢到锁。