时间轴

2025-10-30

init

参考文档:

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.h

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

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 ID手册的描述

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

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

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

案例1 atomic_add

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

案例2 简单锁实现

cbnz w2, retry

多核情况下的ldxr和stxr分析

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

多核情况下的ldxr和stxr分析

T0时刻 初始化状态

T0时刻 初始化状态

T1和T2时刻 CPU0执行ldxr指令

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

T3时刻 CPU1执行ldxr指令

T3时刻 CPU1执行ldxr指令

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

T4时刻 CPU0通过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

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

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

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指令在Linux内核中的使用

  • cmpxchg函数原型

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

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 原子的有符号数的最大值
SMIX 原子的有符号数的最小值操作
UMAX 原子的无符号数的最大值
UMIX 原子的无符号数的最小值操作

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

ldumax

原子交换指令

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

相当于

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