Linux内核基础——内存序、屏障和原子量
文章目录
1. 内存序介绍
内存序(Memory order)可以理解为CPU对内存的访问顺序。如果进一步细分,可以分为编译期内存序和运行时内存序。
1.1. 编译期内存序
编译期内存序来自于编译器将高级语言转换成汇编语言的过程中对指令的重排行为,举一些简单的例子:
1.1.1. 基本算数运算
int x = a + b + c;
printf("x = %d", x);
对于高层次语言,我们能看到x先被计算,然后printf引用x被计算的结果。
但是,对于汇编来说,一般只有两条数值相加的指令,因此编译器会将其转换为两个加法,类似于:
x = a + b;
x = x + c;
但对于编译器来说,其只保证结果的正确性,不保证计算过程使用的语法是什么,根据加法的结合律及交换律,其也可能产生下面的语义,但并不影响程序正确性:
x = b + c;
x = a + x;
1.1.2. 间接调用情况下的序
在间接调用(利用指针来读写内存)情况下,实际读写效果受内存模型(normal/device)确定。给一个例子:
*x = *a + *b + *c;
对于普通的内存,任意读取顺序(如下面)都是正确且没有副作用的(虽然编译器不太可能直接这样优化,但是存在程序员自行实现的可能)。
方式1:
*x = *a + *b;
*x = *x + *c;
方式2:
*x = *b + *c;
*x = *x + *a;
但是对于linux来说,如果x是一个内存映射io(映射到某个设备),上面的行为就绝不可取,因为对该映射IO引入了两次写入,一次读取,会有严重的性能问题。甚至产生某些副作用,比如*x = *a + *b触发某些不允许的行为。因此一种较为安全的形式可能如下:
temp = *a + *b;
*x = temp + *c;
这要求我们不仅对内存模型比较熟悉,也要对编译器行为很了解。实际上内核程序中已经对各种类型内存的访存接口做了封装,比如对于一些外设的内存空间,我们可以使用readw()/writew()
来实现有序、安全的访问。
1.2. 运行时内存序
CPU执行指令并不是一个串行的过程,为了最大化CPU的性能,CPU会将一条指令拆分成:取指、译码、执行、写回等若干个阶段,这个过程就是一个CPU pipeline(流水线),完成各阶段的单元相互独立,同时并行,因此会对实际的指令进行优化,实际的执行顺序并不完全按照汇编指令所写的那样。像ARMv8这个流水线达到八级。
举个例子,指令4依赖指令3,指令2依赖指令1,如果完全顺序,那么指令2等待指令1,指令4等待指令3完成,都需要有很长的IDLE时间。
但是本身指令1和指令3没有什么依赖,因此把指令3提前到指令2能极大减少指令周期数,因此就非常可能出现,一条汇编指令明明先出现,但可能最后才被CPU执行的行为。对于我们来说,一些指令可能就被重排到非预期的位置上,这就是运行时的memory order。
在对称多处理器(SMP)架构中,运行时内存序受到实际架构影响,一般包括下列几种内存序:
- 顺序一致性(所有读写均按照编译后的顺序执行)
- 宽松一致性(CPU实际执行时,允许对部分类型的重排)
- load可被重排到load后
- load可被重排到store后
- store可被重排到store后
- store可被重排到load后
- 弱一致性(任意重排,仅受显式内存屏障限制)
一些基本的架构区别如下:
表1 各架构下的内存序
重排类型 | Alpha | ARMv7/v8 | MIPS | RISCV WMO | RISCV TSO | PA-RISC | POWER | SPARC-RMO | SPARC-PSO | SPARC-TSO | x86 | AMD64 | IA-64 | z/Architecture |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Loads can be reordered after loads | Y | Y | depend on implementation | Y | - | Y | Y | Y | - | - | - | - | Y | - |
Loads can be reordered after stores | Y | Y | ^ | Y | - | Y | Y | Y | - | - | - | - | Y | - |
Stores can be reordered after stores | Y | Y | ^ | Y | - | Y | Y | Y | Y | - | - | - | Y | - |
Stores can be reordered after loads | Y | Y | ^ | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y |
Atomic can be reordered with loads | Y | Y | ^ | Y | - | - | Y | Y | - | - | - | - | Y | - |
Atomic can be reordered with stores | Y | Y | ^ | Y | - | - | Y | Y | Y | - | - | - | Y | - |
Dependent loads can be reordered | Y | - | ^ | - | - | - | - | - | - | - | - | - | - | - |
Incoherent instruction cache/pipeline | Y | Y | ^ | Y | Y | - | Y | Y | Y | Y | Y | - | Y | - |
-
Loads can be reordered after loads
没有依赖关系的load/load操作,顺序可以重排,如下例中,READ_ONCE(b)
可能先于READ_ONCE(a)
执行。int x = READ_ONCE(a); int y = READ_ONCE(b);
-
Loads can be reordered after stores
没有依赖关系的store/load操作,顺序可以重排。如下例中,READ_ONCE(b)
可能先于WRITE_ONCE(a, 1)
之前执行。WRITE_ONCE(a, 1); int x = READ_ONCE(b);
-
Stores can be reordered after stores
没有依赖关系的store/store操作,顺序可以重排。如下所示,WRITE_ONCE(b, 1)
可能先于WRITE_ONCE(a, 1)
执行。WRITE_ONCE(a, 1); WRITE_ONCE(b, 1);
-
Stores can be reordered after loads
没有依赖关系的load/store,顺序可以重排。如下所示,WRITE_ONCE(b, 1)
可能先于READ_ONCE(a)
执行。int x = READ_ONCE(a); WRITE_ONCE(b, 1);
-
Atomic can be reordered with loads
没有依赖关系的原子操作可以与load重排,如READ_ONCE(b)
可能先于atomic_inc_return_relaxed(&a)
执行。int x = atomic_inc_return_relaxed(&a); int y = READ_ONCE(b);
-
Atomic can be reordered with stores
没有依赖关系的原子操作可以与store重排,如WRITE_ONCE(b, 1)
可能先于WRITE_ONCE(b, 1)
执行。int x = atomic_fetch_inc_relaxed(&a); WRITE_ONCE(b, 1);
-
Dependent loads can be reordered
具有依赖关系的load操作,可以重排,如处理器可以将load(&b[x])
提前于load(&a)
执行,即使他们存在依赖关系。(仅Alpha架构的特性,其他架构一般不具有)int x = READ_ONCE(a); int y = READ_ONCE(b[x]);
-
Incoherent instruction cache/pipeline
指令缓存和流水线不一致,这意味着指令缓存(流水线)中的更改可能并不会立即反应内存的更改,这个可能发生在自修改代码这种场景下,这里暂不举例。
这类运行时的重排一般只能依赖内存屏障来实现正确的顺序。
2. 屏障
屏障就是专门设计来解决上述重排问题的方法。在内核代码中,用两种语句来实现:
2.1. 编译期屏障
编译期为防止指令重排,一般会使用编译期屏障禁止编译器对可能发生的乱序语句进行重排,对于c语言来说,这个语义如下,其中"memory"就表示对编译器的屏障约束:
asm volatile("" ::: "memory")
该语句含义是,禁止编译器将发生在此语句两侧的语义发生越界。具体可以理解为:
- 屏障之前的语义必须在屏障之前结束,不得晚于屏障。
- 屏障之后的语义必须在屏障之后开始,不得提前于屏障。
举个例子,如下列代码,O3级别优化时,a第一个赋值语义被优化掉了,然后直接赋值b = 2,a = 3。如果有程序必须依赖a = 1到3的转换,那么这个编译器优化就是不正确的。
int a = 0;
int b = 0;
void func() {
a = 1;
b = 2;
a = 3;
}
0000000000400650 <func>:
400650: 90000100 adrp x0, 420000 <__libc_start_main@GLIBC_2.34>
400654: 9100b001 add x1, x0, #0x2c
400658: 52800043 mov w3, #0x2 // #2
40065c: 52800062 mov w2, #0x3 // #3
400660: b9002c03 str w3, [x0, #44]
400664: b9000422 str w2, [x1, #4]
400668: d65f03c0 ret
如果加两个屏障呢?如下所示,编译后三条赋值语义被完整呈现了。
int a = 0;
int b = 0;
void func() {
a = 1;
__asm__ __volatile__ ("" : : : "memory");
b = 2;
__asm__ __volatile__ ("" : : : "memory");
a = 3;
}
0000000000400650 <func>:
400650: 90000100 adrp x0, 420000 <__libc_start_main@GLIBC_2.34>
400654: 52800022 mov w2, #0x1 // #1
400658: 9100b001 add x1, x0, #0x2c
40065c: b9002c02 str w2, [x0, #44]
400660: 52800042 mov w2, #0x2 // #2
400664: b9000422 str w2, [x1, #4]
400668: 52800061 mov w1, #0x3 // #3
40066c: b9002c01 str w1, [x0, #44]
400670: d65f03c0 ret
对于内核代码来说,编译期屏障就是一个barrier()
// include/liunx/compiler.h
/* Optimization barrier */
#ifndef barrier
/* The "volatile" is due to gcc bugs */
# define barrier() __asm__ __volatile__("": : :"memory")
#endif
2.2. 运行时屏障
对于运行时的CPU对内存指令的重排行为,必须由架构提供的内存屏障来处理。
对于ARM来说,分为三种类型的内存屏障ISB
、DMB
、DSB
,语义依次变强。
-
ISB
全称 Instruction Synchronous Barrier,该指令将刷新指令pipeline和prefetch buffer,ISB之后的指令需要重新从cache或memory取指,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令。一般用于内存管理、缓存控制和上下文切换。举个一个使能FPU(浮点单元)和SIMD单元的例子。
这三条语句后加了isb()
,表示后续指令一定能看到isb()
之前的指令已完成。如果不加isb()
,则可能因为乱序导致double x
对浮点寄存器的使用先于CPU的浮点功能使能之前,导致异常。int cpacr_el1 = read_sysreg(CPACR_EL1); cpacr_el1 |= CPACR_EL1_FPEN; // 使能浮点寄存器和SIMD寄存器的访问 write_sysreg(cpacr_el1, CPACR_EL1); isb(); double x = 3.1415926; // 若没有isb,上面使能过程可能在x之后才执行,触发EL异常
注意:ISB只刷新pipeline和prefetch buffer,不对内存序进行控制,其之前的语句依旧是乱序的,只是限制其不会移动到ISB后面去。
-
DMB
全称 Data Memory Barrier,仅当所有在它前面的内存访问都执行完毕后,才执行它后面的内存访问动作(注意只对内存访问敏感),其它非内存访问指令(如算数指令等)依然可以乱序执行。如下例所示,
dmb(ishld)
限制了CPU在执行指令时,READ_ONCE(a)
一定在WRITE_ONCE(y, b)
之前完成。
但dmb
并不对非访存指令(ADD/SUB/MOV/OR/XOR/XOR/MSR/MRS等)进行约束,因此下面的NOP可能被CPU放到任意位置处执行。int x = 1, y = 2, a = 3, b = 4; void func(void) { x = READ_ONCE(a); // 必须在dmb(ishld)之前完成 dmb(ishld); asm volatile("NOP\n"); // 不受dmb(ishld)影响,可以在任意位置处被执行 WRITE_ONCE(y, b); // 必须在dmb(ishld)之后开始 }
-
DSB
全称Data Synchronous Barrier,相对于DMB来说是一种更强的内存屏障。仅当所有在它前面的内存访问都执行完毕后,才执行它在后面的指令(亦即任何指令都要等待)。
如下例所示,下面的WRITE_ONCE(x, 0)
要在dsb
之前执行,READ_ONCE(y)
和NOP
必须要在dsb
之后执行,但NOP
可以和READ_ONCE(y)
重排。int x = 1, y = 2, ; void func(void) { WRITE_ONCE(x, 0); // 必须在dsb(ish)之前完成 dsb(ish); int z = READ_ONCE(y); // 必须在dsb(ish)之后开始执行 asm volatile("NOP\n"); // 必须在dsb(ish)之前的所有访存指令完成之后,才能执行 }
这里类似于ishld
指令参数,说明了具体的访问控制类型和共享域,如下所示,可以不需要了解的这么细,一般操作系统只需要使用标准的屏障接口就好了。
表2 屏障访问控制及共享域
参数 | 访问控制(before-after) | 共享域 |
---|---|---|
OSHLD | Load - Load, Load - Store | Outer shareable |
OSHST | Store - Store | Outer shareable |
OSH | Any - Any | Outer shareable |
NSHLD | Load - Load, Load - Store | Non-shareable |
NSHST | Store - Store | Non-shareable |
NSH | Any - Any | Non-shareable |
ISHLD | Load - Load, Load - Store | Inner shareable |
ISHST | Store - Store | Inner shareable |
ISH | Any - Any | Inner shareable |
LD | Load - Load, Load - Store | Full system |
ST | Store - Store | Full system |
SY | Any - Any | Full system |
访问控制描述了屏障前后的load和store行为,有三类:
- Load - Load/Store,屏障前的Load必须在屏障前完成,屏障前的Store可以被重排;屏障后的Load和Store必须在屏障之后开始。
- Store - Store,屏障前的Store必须在屏障前完成,屏障后的Store必须在屏障后开始。不对Load进行要求
- Any - Any,屏障前的Load和Store都必须在屏障前完成,屏障后的Load和Store都必须在屏障后开始。
共享域指的是内存访问序的可见范围如下,对越大范围的区域进行保护意味着更大的开销:
- Non-shareable: 指只有某个core能看到。
- Inner shareable:可被多核共享,一个系统中可能有多个Inner shareable区域。
- Outer shareable:所有Inner Shareable和外设组成的区域。
- Full System:指整个系统能看到一致的内存访问序。
内核一般使用封装好的接口来调用该类屏障,需要注意的是,因为实现都是以编译器屏障语句asm volatile("" ::: "memory")
对编译器进行约束的,因此Linux内核中所有的运行时屏障都隐含编译期屏障的作用。
#define mb() asm volatile("dsb sy" ::: "memory")
#define wmb() asm volatile("dsb st" ::: "memory")
#define rmb() asm volatile("dsb ld" ::: "memory")
#define smp_mb() asm volatile("dmb ish" ::: "memory")
#define smp_wmb() asm volatile("dmb ishst" ::: "memory")
#define smp_rmb() asm volatile("dmb ishld" ::: "memory")
/* arm下独有的屏障,映射为dmb osh操作 */
#define dma_mb() asm volatile("dmb osh" ::: "memory")
#define dma_wmb() asm volatile("dmb oshst" ::: "memory")
#define dma_rmb() asm volatile("dmb oshld" ::: "memory")
#define isb() asm volatile("isb" : : : "memory")
- 全系统屏障
mb()
、wmb()
、rmb()
:确保整个系统的访问顺序一致,也就是说无论内存模型是内存映射的IO,还是普通内存,都是一致的。 - 处理器间屏障
smp_mb()
、smp_wmb()
、smp_rmb()
:仅确保多处理器间的内存顺序一致。 - DMA内存屏障
dma_mb()
,dma_wmb()
、dma_rmb()
:确保dma操作如读写寄存器的操作访存一致性。这个是arm64独有的io屏障,其他架构下io屏障可能被映射为mb()
。 - 指令同步屏障
isb()
,刷新指令流,确保之前的指令执行完成,后面的指令从头开始取指,一般用于上下文切换等场景。
简单讲:
5. 保证到屏障时,所有指令执行完才能执行后续指令,如上下文切换,缓存同步,用isb()
。
6. 涉及到与外设/dma等io相关的内存映射,可以用dma_mb()
或mb()
。如果架构没有提供dma_mb()
屏障,则内核也会保证将dma_mb()
屏障转换为mb()
。目前ARM64在使用该类屏障时,自动将其转换为dma_mb()
屏障。
```c
// arch/arm64/include/asm/io.h
#define __iowmb() dma_wmb()
#define __iomb() dma_mb()
#define readb(c) ({ u8 __v = readb_relaxed(c); __iormb(__v); __v; })
#define readw(c) ({ u16 __v = readw_relaxed(c); __iormb(__v); __v; })
#define readl(c) ({ u32 __v = readl_relaxed(c); __iormb(__v); __v; })
#define readq(c) ({ u64 __v = readq_relaxed(c); __iormb(__v); __v; })
#define writeb(v,c) ({ __iowmb(); writeb_relaxed((v),(c)); })
#define writew(v,c) ({ __iowmb(); writew_relaxed((v),(c)); })
#define writel(v,c) ({ __iowmb(); writel_relaxed((v),(c)); })
#define writeq(v,c) ({ __iowmb(); writeq_relaxed((v),(c)); })
```
- 仅涉及到多核、多线程、多进程之间的普通内存序时,用
smp_mb()
,smp_wmb()
,smp_rmb()
即可。
下图是读写屏障的作用范围:
简单讲,虚线是指屏障,smp_mb()
彻底禁止两侧的load/store重排,但是smp_rmb()
和smp_wmb()
会有一定的放宽,比如,读屏障不限制屏障前的内存写重排,写屏障不限制所有读重排。
举个读写屏障的例子:
CPU A CPU B
WRITE_ONCE(x, 1);
smp_wmb();
WRITE_ONCE(ready, 1); while(READ_ONCE(ready) != 1);
smp_rmb();
WRITE_ONCE(y, x);
这个例子中,smp_wmb()
和smp_rmb()
成对出现,CPU A只有在完成x
写入后才能将ready
置为1,通知CPU B可以通过y
取x
的数据了。
- 若缺少
smp_wmb()
,WRITE_ONCE(ready, 1)
可能先于WRITE_ONCE(x, 1)
执行,数据没准备好CPU B就启动了; - 若缺少
smp_rmb()
,WRITE_ONCE(y, x)
可能先于*READ_ONCE(ready)
执行,没有执行正确的逻辑。
2.3. load_acquire/store_release
除了上述显式屏障,ARM架构还提供了load_acquire
和store_release
这类带单向屏障语义的读写指令。这类指令比上面的DMB
,DSB
指令更弱,以减少显式内存屏障带来的性能影响。
如果说load_acquire
/store_release
能满足需求,就没必要用显式的屏障。这类语义一般用在原子量的操作上,可以减少一些显式屏障的使用,后续介绍原子量时会有例子。
2.3.1. load_acquire
load_acquire(ldar指令)可以看做是load指令+单向屏障,使得load_acquire之后的读写不得乱序到load_acquire之前,具体讲:
- ldar之前的load/store不受影响。
- ldar之后的load/store不能早于ldar之前执行。
简单理解:load_acquire表示获取/进入一个临界区,进入临界区后的所有访存语句不得先于load_acquire执行。
2.3.2. store_release
store_release(stlr指令)可以看做是stxr+单向屏障,使得store_release之前的读写不得乱序到store_release之后,具体将:
- stxr之前的load/store必须在stxr之前完成。
- stxr之后的load/store不受影响。
简单理解:store_release表示离开/释放一个临界区,在离开临界区之前的访存语句必须全部执行完才能离开。
3. 原子量
一般来讲,对一个内存的操作一般要执行3个操作,如下所示:
- 将内存值加载到寄存器
- 寄存器计算新值
- 寄存器写回内存
如果该过程被打断,比如两个CPU同时给某个内存值+1(假设初始值为0),可能就会出现不一致的行为:
CPU A CPU B
从内存加载0到寄存器
寄存器值加1
从内存加载0到寄存器
寄存器值加1
将寄存器值1写回内存
将寄存器值1写回内存
即使是同一个CPU,也可能因为进程调度导致类似的问题:
进程 A 进程 B
从内存加载0到寄存器
寄存器值加1
从内存加载0到寄存器
寄存器值加1
将寄存器值1写回内存
将寄存器值1写回内存
原子量(atomic)是一种非常常见的轻量级的互斥访问操作,相比于各种锁(mutex会休眠产生上下文切换开销,spinlock要自旋忙等)来说效率要高很多。
3.1. 原子量的接口
内核中原子量atomic_t就是一个4字节的整数:
typedef struct {
int counter;
} atomic_t;
原子操作的接口有很多,分为RMW(read-modify-write)型和非RMW型操作。非RMW型原子操作只有两个atomic_set
和atomic_read
,即只会包含一个read/write操作。
这里只对其中通用的接口和部分特殊接口进行介绍:
-
set/read原子指令
这两个是非RMW型原子操作,内核提供了两种形态,包括普通的atomic_set/atomic_read
及带单向屏障语义的形式atomic_read_acquire/atomic_set_release
/* 标准形式atomic_read,等价于READ_ONCE(v->counter) */ int atomic_read(const atomic_t *v); /* 带load_acquire语义的atomic_read */ int atomic_read_acquire(const atomic_t *v); /* 标准形式atomic_set,等价于WRITE_ONCE(v->counter, i)*/ int atomic_set(atomic_t *, int i); /* 带set_release语义的atomic_set */ int atomic_set_release(atomic_t *v, int i);
因为原子操作实际上也是内存操作,一般的
atomic_set/atomic_read
只保证了对原子量的访问,并没有对其前后的访存指令有任何屏障语义。因此,如果要用atomic_set/atomic_read
来通知状态量的变化,需要配合显示屏障,比如:CPU A CPU B WRITE_ONCE(x, 1); smp_wmb(); atomic_set(&ready, 1); while(atomic_read(&ready) != 1); smp_rmb(); WRITE_ONCE(y, x);
但如果使用带load_acquire/set_release语义,该语句可以被简化:
CPU A CPU B WRITE_ONCE(x, 1); atomic_set_release(&ready, 1); while(atomic_read_acquire(&ready) != 1); WRITE_ONCE(y, x);
-
cmpxchg
cmpxchg是实现除了set/read之外所有RMW型原子操作的基础,即atomic_add/atomic_or/atomic_xor/atomic_and/atomic_andnot等都是由atomic_compxchg底层的比较交换逻辑实现的。cmpxchg可以用
LL/SC
或LSE
实现,具体参考下一章节。cmpxchg的通用形式如下:
int atomic_cmpxchg(atomic_t *v, int old, int new); bool atomic_try_cmpxchg(atomic_t *v, int *old, int new);
atomic_cmpxchg
标准作用是,比较*v
和old
是否相同,如果相同,将*v
改写为new
;否则,不修改;返回值则为*v
的旧值。atomic_try_cmpxchg
作用是,比较*v
和old
是否相同,如果相同,将*v
改写为new
;否则,将*old
置为*v
,返回值为比较结果,相同则为true
,不同则为false
。除此外,atomic_cmpxchg这种不带_relaxed结尾的是强序语义,即带双向屏障;如果想用弱序形式,还可以用:
// 带双向屏障的原子交换,即要求两侧的访存指令不能发生越界 int atomic_cmpxchg(atomic_t *v, int old, int new); // 带load_acquire语义屏障的原子交换,即该原子指令后的访存指令不能早于该原子指令执行 int atomic_cmpxchg_acquire(atomic_t *v, int old, int new); // 带store_release语义屏障的原子交换,即该原子指令前的访存指令必须在该原子指令前完成 int atomic_cmpxchg_release(atomic_t *v, int old, int new); // 不带任何屏障的原子交换,前后语句可以产生重排 int atomic_cmpxchg_relaxed(atomic_t *v, int old, int new)
-
add/or/xor/and/andnot等原子指令
此类指令都是RMW型原子操作,实现基本上类似,只是在load内存到寄存器后,做了不同操作(add/or/xor/and/andnot)等,只以add作介绍:基本形式如下,都会带有默认的双向屏障:
/* 原子加法,不返回值 */ void atomic_add(int i, atomic_t *v); /* 原子加法,返回修改后的值 */ int atomic_add_return(int i, atomic_t *v); /* 原子加法,返回修改前的值 */ int atomic_fetch_add(int i, atomic_t *v);
如果想要带有单向屏障或无屏障的形式,可以使用以下操作:
int atomic_add_return_acquire(int i, atomic_t *v); int atomic_add_return_release(int i, atomic_t *v); int atomic_add_return_relaxed(int i, atomic_t *v); int atomic_fetch_add_acquire(int i, atomic_t *v); int atomic_fetch_add_release(int i, atomic_t *v); int atomic_fetch_add_release(int i, atomic_t *v);
3.2. 原子量在ARM下的实现
对于ARM来说,实施原子操作有两种方式,LSE (Large System Extensions)和LLSC(Load-Linked/Store-Conditional)是ARM架构下的两种方式,区别如下:
-
LSE:用于大型系统的扩展指令集,由硬件保证的原子指令,一条指令就可以实现原子的内存访问和操作:
LDADD/STADD
:原子加法
# 原子加,返回旧值 ldadd x0, x1, [x20] # 等价于x0 = [x20], [x20] = x1 + [x20] # 原子加,不返回旧值 stadd x1, [x20] # 等价于[x20] = x1 + [x20]
LDCLR/STCRL
:原子清零LDEOR/STEOR
:原子异或LDSET/STSET
:原子设置SWP
:原子交换
swp x0, x1, [x20] # 等价于x0 = [x20], [x20] = x1
CAS
:比较交换
这类指令只需要一条指令下发给硬件,直接就能完成原子操作,原子性由硬件保证。
-
LLSC,更泛用的方法,ARM必须支持的基础指令集,用排他性(exclusive)指令实现原子,而非真正意义上的原子指令。LLSC实现原子操作至少3条指令,指令还会失败,比如
- 原子加法指令(假设[x20]是被操作的atomic_t,x3是加值):
1:ldxr x0, [x20] # 将[x20]内存加载到x0 add x0, x0, x3 # x0 = x0 + x3 stxr w1, x0, [x20] # [x20] = x0,操作结果写入w1 cbnz w1, 1b # 写失败?跳至1处继续循环
- 原子交换指令(假设[x20]是被操作的atomic_t,x3是要交换的值):
1: ldxr x0, [x20] # 将[x20]内存加载到x0 stxr w1, x3, [x20] # [x20] = w3,操作结果写入w1 cbnz %w1, 1b # 写失败?跳至1处继续循环