Linux内核基础——内存序、屏障和原子量

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)架构中,运行时内存序受到实际架构影响,一般包括下列几种内存序:

  1. 顺序一致性(所有读写均按照编译后的顺序执行)
  2. 宽松一致性(CPU实际执行时,允许对部分类型的重排)
    • load可被重排到load后
    • load可被重排到store后
    • store可被重排到store后
    • store可被重排到load后
  3. 弱一致性(任意重排,仅受显式内存屏障限制)

一些基本的架构区别如下:

表1 各架构下的内存序

重排类型AlphaARMv7/v8MIPSRISCV WMORISCV TSOPA-RISCPOWERSPARC-RMOSPARC-PSOSPARC-TSOx86AMD64IA-64z/Architecture
Loads can be reordered after loadsYYdepend on implementationY-YYY----Y-
Loads can be reordered after storesYY^Y-YYY----Y-
Stores can be reordered after storesYY^Y-YYYY---Y-
Stores can be reordered after loadsYY^YYYYYYYYYYY
Atomic can be reordered with loadsYY^Y--YY----Y-
Atomic can be reordered with storesYY^Y--YYY---Y-
Dependent loads can be reorderedY-^-----------
Incoherent instruction cache/pipelineYY^YY-YYYYY-Y-
  1. Loads can be reordered after loads
    没有依赖关系的load/load操作,顺序可以重排,如下例中,READ_ONCE(b)可能先于READ_ONCE(a)执行。

    int x = READ_ONCE(a);
    int y = READ_ONCE(b);
    
  2. Loads can be reordered after stores
    没有依赖关系的store/load操作,顺序可以重排。如下例中,READ_ONCE(b)可能先于WRITE_ONCE(a, 1)之前执行。

    WRITE_ONCE(a, 1);
    int x = READ_ONCE(b);
    
  3. Stores can be reordered after stores
    没有依赖关系的store/store操作,顺序可以重排。如下所示,WRITE_ONCE(b, 1)可能先于WRITE_ONCE(a, 1)执行。

    WRITE_ONCE(a, 1);
    WRITE_ONCE(b, 1);
    
  4. Stores can be reordered after loads
    没有依赖关系的load/store,顺序可以重排。如下所示,WRITE_ONCE(b, 1)可能先于READ_ONCE(a)执行。

    int x = READ_ONCE(a);
    WRITE_ONCE(b, 1);
    
  5. 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);
    
  6. 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);
    
  7. Dependent loads can be reordered
    具有依赖关系的load操作,可以重排,如处理器可以将load(&b[x])提前于load(&a)执行,即使他们存在依赖关系。(仅Alpha架构的特性,其他架构一般不具有)

    int x = READ_ONCE(a);
    int y = READ_ONCE(b[x]);
    
  8. Incoherent instruction cache/pipeline
    指令缓存和流水线不一致,这意味着指令缓存(流水线)中的更改可能并不会立即反应内存的更改,这个可能发生在自修改代码这种场景下,这里暂不举例。

这类运行时的重排一般只能依赖内存屏障来实现正确的顺序。

2. 屏障

屏障就是专门设计来解决上述重排问题的方法。在内核代码中,用两种语句来实现:

2.1. 编译期屏障

编译期为防止指令重排,一般会使用编译期屏障禁止编译器对可能发生的乱序语句进行重排,对于c语言来说,这个语义如下,其中"memory"就表示对编译器的屏障约束:

asm volatile("" ::: "memory")

该语句含义是,禁止编译器将发生在此语句两侧的语义发生越界。具体可以理解为:

  1. 屏障之前的语义必须在屏障之前结束,不得晚于屏障。
  2. 屏障之后的语义必须在屏障之后开始,不得提前于屏障。

举个例子,如下列代码,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来说,分为三种类型的内存屏障ISBDMBDSB,语义依次变强。

  1. 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后面去。

  2. 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)之后开始
        }
    
  3. 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)共享域
OSHLDLoad - Load, Load - StoreOuter shareable
OSHSTStore - StoreOuter shareable
OSHAny - AnyOuter shareable
NSHLDLoad - Load, Load - StoreNon-shareable
NSHSTStore - StoreNon-shareable
NSHAny - AnyNon-shareable
ISHLDLoad - Load, Load - StoreInner shareable
ISHSTStore - StoreInner shareable
ISHAny - AnyInner shareable
LDLoad - Load, Load - StoreFull system
STStore - StoreFull system
SYAny - AnyFull system

访问控制描述了屏障前后的load和store行为,有三类:

  1. Load - Load/Store,屏障前的Load必须在屏障前完成,屏障前的Store可以被重排;屏障后的Load和Store必须在屏障之后开始。
  2. Store - Store,屏障前的Store必须在屏障前完成,屏障后的Store必须在屏障后开始。不对Load进行要求
  3. Any - Any,屏障前的Load和Store都必须在屏障前完成,屏障后的Load和Store都必须在屏障后开始。

共享域指的是内存访问序的可见范围如下,对越大范围的区域进行保护意味着更大的开销:

  1. Non-shareable: 指只有某个core能看到。
  2. Inner shareable:可被多核共享,一个系统中可能有多个Inner shareable区域。
  3. Outer shareable:所有Inner Shareable和外设组成的区域。
  4. 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")
  1. 全系统屏障mb()wmb()rmb():确保整个系统的访问顺序一致,也就是说无论内存模型是内存映射的IO,还是普通内存,都是一致的。
  2. 处理器间屏障smp_mb()smp_wmb()smp_rmb():仅确保多处理器间的内存顺序一致。
  3. DMA内存屏障dma_mb(),dma_wmb()dma_rmb():确保dma操作如读写寄存器的操作访存一致性。这个是arm64独有的io屏障,其他架构下io屏障可能被映射为mb()
  4. 指令同步屏障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)); })
```
  1. 仅涉及到多核、多线程、多进程之间的普通内存序时,用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可以通过yx的数据了。

  1. 若缺少smp_wmb()WRITE_ONCE(ready, 1)可能先于WRITE_ONCE(x, 1)执行,数据没准备好CPU B就启动了;
  2. 若缺少smp_rmb()WRITE_ONCE(y, x)可能先于*READ_ONCE(ready)执行,没有执行正确的逻辑。

2.3. load_acquire/store_release

除了上述显式屏障,ARM架构还提供了load_acquirestore_release这类带单向屏障语义的读写指令。这类指令比上面的DMBDSB指令更弱,以减少显式内存屏障带来的性能影响。

如果说load_acquire/store_release能满足需求,就没必要用显式的屏障。这类语义一般用在原子量的操作上,可以减少一些显式屏障的使用,后续介绍原子量时会有例子。

2.3.1. load_acquire

load_acquire(ldar指令)可以看做是load指令+单向屏障,使得load_acquire之后的读写不得乱序到load_acquire之前,具体讲:

  1. ldar之前的load/store不受影响。
  2. ldar之后的load/store不能早于ldar之前执行。

简单理解:load_acquire表示获取/进入一个临界区,进入临界区后的所有访存语句不得先于load_acquire执行。
在这里插入图片描述

2.3.2. store_release

store_release(stlr指令)可以看做是stxr+单向屏障,使得store_release之前的读写不得乱序到store_release之后,具体将:

  1. stxr之前的load/store必须在stxr之前完成。
  2. stxr之后的load/store不受影响。

简单理解:store_release表示离开/释放一个临界区,在离开临界区之前的访存语句必须全部执行完才能离开。

在这里插入图片描述

3. 原子量

一般来讲,对一个内存的操作一般要执行3个操作,如下所示:

  1. 将内存值加载到寄存器
  2. 寄存器计算新值
  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_setatomic_read,即只会包含一个read/write操作。

这里只对其中通用的接口和部分特殊接口进行介绍:

  1. 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);
    
  2. cmpxchg
    cmpxchg是实现除了set/read之外所有RMW型原子操作的基础,即atomic_add/atomic_or/atomic_xor/atomic_and/atomic_andnot等都是由atomic_compxchg底层的比较交换逻辑实现的。

    cmpxchg可以用LL/SCLSE实现,具体参考下一章节。

    cmpxchg的通用形式如下:

    int atomic_cmpxchg(atomic_t *v, int old, int new);
    bool atomic_try_cmpxchg(atomic_t *v, int *old, int new);
    

    atomic_cmpxchg标准作用是,比较*vold是否相同,如果相同,将*v改写为new;否则,不修改;返回值则为*v的旧值。

    atomic_try_cmpxchg作用是,比较*vold是否相同,如果相同,将*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)
    
  3. 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架构下的两种方式,区别如下:

  1. 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:比较交换
      这类指令只需要一条指令下发给硬件,直接就能完成原子操作,原子性由硬件保证。
  2. 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处继续循环
    
  • 15
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值