Cover image for ARM Atomic Operation

ARM Atomic Operation

字数 2.5k
阅读
访客
时间轴

时间轴

2025-10-30

init

本文介绍了ARM架构下的原子操作机制,详细讨论了ARMv8中基于独占监视器的Load-exclusive/Store-exclusive(LL/SC)指令工作原理、多核竞争分析及结合WFE指令的低功耗锁应用。同时,本文总结了ARMv8.1引入的LSE(Large System Extensions)原子指令,如Compare and Swap等原子内存访问操作的实现原理。

参考文档:

ARMv8.6芯片手册与独占内存访问相关的内容

  • 第B2.9章 Synchronization and semaphores
  • 第D1.16章节 Mechanisms for entering a low-power state
  • 第C3.2.13章 Compare and Swap
  • 第C3.2.13章 Atomic memory operations
  • 第C3.2.14章 Swap

为什么需要原子操作

thread_A_func和thread_B_func都尝试进行i++操作

为什么需要原子操作
为什么需要原子操作

Linux内核中的基本原子操作函数

  • Linux内核提供了atomic_t类型的原子变量,它的实现依赖于不同的架构
  • atomic_t类型的原子操作函数可以保证了一个操作的原子性和完整性
  • "读-修改-回写"机制
    • 在读取原子变量的值到通用寄存器
    • 在通用寄存器里修改原子变量的值
    • 把新值写回内存中

atomic_t
atomic_t

atomic.h
atomic.h

atomic_read 和 atomic_set
atomic_read 和 atomic_set

如果CPU仅仅是从内存中读取(load)一个变量的值,或者仅仅是往内存中写(store)一个变量的值,都是不可打断的

下面的这些操作函数API用到了"读-修改-回写"机制

读-修改-回写
读-修改-回写

原子操作函数
原子操作函数

原子操作函数
原子操作函数

ARMv8对原子操作的支持

  • ARMv8提供两种方式的原子操作
    • 传统的Load-exclusive和store-exclusive方式
      • 在ARMv8上都支持
      • LL/SC(Load-link/store-conditional)
    • LSE(Large System Extensions)支持原子操作指令
      • 在ARMv8.1上开始支持,ARMv8.1-LSE
      • 新增Compare and Swap instructions
      • 新增Atomic memory operation instructions
      • 新增Swap instruction

LSE
LSE

Load-exclusive和store-exclusive指令

  • ldxr指令:内存独占加载指令。从内存中以独占的方式加载内存地址的值到通用寄存器里
1
ldxr <xt> , [xn|sp]
  • stxr指令:内存独占存储指令。以独占的方式把新的数据存储到内存中。
1
stxr <ws> , <xt> , [xn|sp]
  • Load/Store Exclusive Pair指令
1
2
ldxp <Xt1>, <Xt2>, [Xn|SP]
stxp <Ws>, <Xt1>, <Xt2>, [<Xn|SP>]
  • 带acquire和release原语的Load/Store Exclusive指令

例子

  • 通过独占监视器(exclusive monitor)来监视这个内存的访问,独占监视器会把这个内存地址标记为独占访问模式,保证以独占的方式来访问这个内存地址,不受其他因素的影响

独占访问的例子
独占访问的例子

独占监视器(Exclusive Monitor)

  • 独占监视器一共有两个状态:
    • 开放访问状态(Open Access state)
    • 独占访问状态(Exclusive Access state)
  • ldxr指令从内存加载数据时,CPU会把这个内存地址标记为独占访问状态
  • 当CPU执行stxr指令时,需要根据独占监视器的状态来做决定
    • 如果独占监视器的状态为独占访问状态,那么stxr指令存储成功,stxr指令返回0,独占监视器的状态变成了开放访问状态
    • 如果独占监视器的状态为开放访问状态,那么stxr存储失败,stxr返回1

独占监视器
独占监视器

注意事项

  • 独占监视器本身不是用来阻止CPU核心来访问被标记的内存,不会lock总线
  • 独占监视器仅仅是起到监视的作用,监视状态的变化
  • 不能把独占监视器看成是一个硬件的锁

独占监视器状态机
独占监视器状态机

独占监视器的组成架构

  • 通常一个系统由多级独占监视器组成(由芯片设计时定义)
    • 本地独占监视器(Local monitor),适用于非共享(Non-shareable)的内存
    • 缓存一致性的全局独占监视器(Internal coherent global monitor),适用于普通类型的内存
    • 外部的全局独占监视器(External global monitor),适用于设备类型的内存
  • 有的Soc不支持外部的全局独占监视器。例如树莓派4b上使用的BMC2711
  • 在MMU没有使能的情况下,我们访问物理内存变成了访问设备类型的内存,此时使用ldxr和stxr指令会产生不可预测的错误

独占监视器的组成架构
独占监视器的组成架构

  • ldxr指令的使用会有很多限制,要求memory是normal memory且是shareable
  • 如果访问device memory,例如MMU没有打开的情况,那就需要CPU IP核心支持以独占方式访问device memory,这个需要查询具体CPU IP手册的描述

独占监视器的粒度(Granularity of Exclusive Monitor)

  • CTR_EL0寄存器中的ERG(Exclusives Reservation Granule)定义了独占监视器的最小单位
  • ERG可以定义的范围是4 words~512words,不过通常是一个cache line的大小
  • 举例:
    • 假设ERG是2^4,即16字节。当使用ldxrb指令对0x341B4地址进行独占地读操作,那么从0x341b0~0x341bf都会标记为exclusive access

案例1:atomic_add()函数的实现

案例1 atomic_add
案例1 atomic_add

案例2:简单锁(spinlock)的实现

案例2 简单锁实现
案例2 简单锁实现

cbnz w2, retry

多核情况下的ldxr和stxr分析

CPU0和CPU1同时执行get_lock()操作

多核情况下的ldxr和stxr分析
多核情况下的ldxr和stxr分析

T0时刻 初始化状态

T0时刻 初始化状态
T0时刻 初始化状态

T1和T2时刻 CPU0执行ldxr指令

T1时刻和T2时刻 CPU0执行ldxr指令
T1时刻和T2时刻 CPU0执行ldxr指令

T3时刻 CPU1执行ldxr指令

T3时刻 CPU1执行ldxr指令
T3时刻 CPU1执行ldxr指令

T4时刻 CPU0通过stxr指令来获取了锁

T4时刻 CPU0通过stxr指令获取了锁
T4时刻 CPU0通过stxr指令获取了锁

T5时刻 CPU1通过stxr指令尝试获取锁

T5时刻 CPU1通过stxr指令尝试获取锁
T5时刻 CPU1通过stxr指令尝试获取锁

WFE指令在锁实现中的应用

  • 如果CPU0获取了锁,CPUn在等待锁的时候,让CPU进入低功耗模式,那么能节省功耗和提升性能
  • 获取锁的示例代码

获取锁的示例代码
获取锁的示例代码

  • 释放锁的示例代码

释放锁的示例代码
释放锁的示例代码

使用 LDXR/STXR 实现原子自增(无序)

1
2
3
4
5
loop:
ldxr w0, [addr] // 加载值(不带内存屏障)
add w0, w0, #1
stxr w1, w0, [addr] // 试图写回
cbnz w1, loop // 如果失败重试

这种写法不保证内存顺序,仅保证原子性,适合数据原子更新但不涉及线程同步的场景。


例2:使用 LDAXR/STLXR 实现锁(有序)

1
2
3
4
5
6
7
// try_lock
loop:
ldaxr w0, [lock] // 加载锁值(acquire)
cbnz w0, loop // 如果锁已被持有则重试
mov w0, #1
stlxr w1, w0, [lock] // 尝试设置锁(release)
cbnz w1, loop // 如果失败重试

这种写法是有序的,保证:

  • 获取锁前的操作不会越过锁;
  • 释放锁后的操作不会被提前执行。

WFE唤醒

  • 通过WFE睡眠的CPU,下面方式唤醒
    • unmasked interrupt
    • Event(唤醒事情)
  • 触发唤醒事件的方式:
    • 执行了sev指令
    • 本地CPU执行了sevl指令
    • clear独占监视器,从独占状态变成开放状态
  • 当持有锁的CPU通过stlr指令写入lock区域释放锁的时候,会触发一个唤醒事件,正在睡眠等待的spinlock的CPU会被唤醒

WFE wake-up event
WFE wake-up event

原子内存访问操作(Atomic Memory Access)

  • ARMv8.1 上支持下面三种原子内存访问操作(Large System Extensions)
    • Compare and Swap instructions, CAS and CASP
    • Atomic memory operation instructions
    • Swap instruction
  • 通过ID_AA64ISAR0_EL1寄存器中的atomic域来判断是否支持LSE

ID_AA64ISAR0_EL1
ID_AA64ISAR0_EL1

比较并交换(Compare and Swap)指令

  • 比较并交换指令:检查ptr指向的值与expected是否相等。若相等,则把new的值赋给ptr;否则什么也不做。不管是相等,最终都会返回ptr的旧值

比较并交换指令
比较并交换指令

  • ARMv8.1上的CAS指令
1
CAS <Xs>, <Xt>, [Xn|SP]

如果Xn的值(Xn是一个地址)==Xs,那么把Xt的值存储在Xn,返回值为Xs,等于Xn的旧值

cas指令
cas指令

CAS指令在Linux内核中的使用

  • cmpxchg函数原型

cmpxchg原子地比较ptr的值是否与old的值相等,若相等,则把new的值设置到ptr地址中,返回old的值

cmpxchg
cmpxchg

mov x30, %x[old]

  • 期望值old复制到寄存器x30
  • x30作为 CASAL 指令的比较寄存器

casal x30, %x[new], %[v]

  • 执行 CASAL 指令

  • 参数:

    • x30:存储旧值,比较使用
    • %x[new](x2):新值
    • %[v](*ptr):内存地址
  • 功能:

    1. 比较内存中的值与x30(old)
    2. 如果相等,写入%x[new]到内存地址v
    3. 如果不等,x30被更新为当前内存值
  • 原子性 + Acquire-Release 内存序语义, 形成了一个双向内存屏障(Full fence)

    1
    [前面的写]  ----必须在----> CASAL ----必须在----> [后面的读写]

CASAL 会在比较成功时把新值写入[x0](即内存地址 *ptr),比较失败则返回内存中的旧值到寄存器x30

mov %x[ret], x30

  • 将操作后的值写回返回值寄存器[ret](绑定在 x0)
  • 返回的值可以告诉调用者:CAS 成功或失败

原子内存操作指令

  • 原子加载指令(Atomic loads)
1
LD<OP> <Xs>, <Xt>,[<Xn|SP>]

相当于

1
2
3
tmp = *Xn;
*Xn = *Xn <OP> Xs;
Xt = tmp;

原子存储指令

1
ST<OP> <Xs>,[<Xn|SP>]

相当于

1
*Xn = *Xn <OP> Xs;
  • OP
OP 操作描述
ADD原子加法
CLR原子的比特位清除
SET原子的比特位置位
EOR原子的异或操作
SMAX原子的有符号数的最大值
SMIN原子的有符号数的最小值操作
UMAX原子的无符号数的最大值
UMIN原子的无符号数的最小值操作

举例:使用ldumax指令实现简单的spinlock

ldumax
ldumax

原子交换指令

1
swp <Xs>, <Xt>, [<Xn|SP>]

相当于

1
2
3
tmp = *Xn;
*Xn = Xs;
Xt = tmp;