• 博客园logo
  • 会员
  • 众包
  • 新闻
  • 博问
  • 闪存
  • 赞助商
  • HarmonyOS
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录

SOC/IP验证工程师

  • 博客园
  • 联系
  • 订阅
  • 管理

公告

View Post

ARM中的自旋锁详解

好的,我们来详细解析 ARM 架构中的自旋锁(Spinlock)。这是一个在多核编程和操作系统底层开发中至关重要的概念。

一、核心思想:忙等待的锁

自旋锁是一种用于保护共享资源(如一段代码、一个数据结构)的同步机制。它的核心特点是:当一个处理器核心(Core)试图获取一个已经被其他核心占用的锁时,它不会立刻被挂起(休眠),而是会在一个循环中不断地尝试获取锁,直到成功为止。这个“循环尝试”的过程就像在原地“自旋”,故名“自旋锁”。

你可以把它想象成一个只有一个洗手间的公寓:

  • 门锁 = 自旋锁
  • 洗手间 = 受保护的共享资源(临界区)
  • 人 = 处理器核心(CPU Core)

规则是: 一个人进入洗手间后会把门锁上。如果另一个人想来用洗手间,发现门锁着,他不会离开去做别的事,而是会一直站在门口反复地尝试扭动门把手(自旋),直到里面的人出来并解锁门,他才能进去。


二、为什么需要自旋锁?

在多核处理器(如 ARM Cortex-A 系列的多核 CPU 或 Cortex-R52 的双核配置)中,多个核心可能同时执行代码,并访问内存中的同一块数据。如果不加保护,就会发生竞态条件(Race Condition),导致数据不一致、程序崩溃等不可预知的错误。

自旋锁的目的就是确保在任何时刻,只有一个核心能进入被锁保护的“临界区”(Critical Section)。


三、自旋锁的关键特性

  1. 忙等待(Busy-Waiting):获取锁失败的核心会持续占用着 CPU 资源进行循环检查,而不是让出 CPU。
  2. 适用于短时间持锁:因为忙等待非常消耗 CPU 资源(空转),所以自旋锁只应用于锁被持有的时间非常短的场景(例如,只是修改几个变量的值)。如果持锁时间很长,其他等待的核心会白白浪费大量 CPU 周期。
  3. 在中断上下文中的使用:在操作系统内核中,自旋锁是唯一可以在中断处理程序(Interrupt Handler)中使用的锁。因为中断上下文不能被调度器挂起,所以不能使用会导致休眠的信号量(Semaphore)等机制。
  4. 需要内存屏障(Memory Barrier):在弱内存顺序模型(如 ARM)的处理器上,编译器和处理器可能会对指令进行重排,这可能导致锁机制失效。因此,在实现自旋锁时,必须使用内存屏障指令(如 DMB, DSB)来确保锁操作之前的内存访问对所有核心都可见。

四、ARM 架构上的特殊实现

在 ARMv7 及之后的架构中,实现一个高效可靠的自旋锁依赖于一组特殊的指令:独占访问指令。

  • LDREX (LoaD Register Exclusive): 以独占模式从内存加载一个值。处理器会标记该内存地址。
  • STREX (STore Register Exclusive): 尝试向标记的内存地址写入值。只有在执行 STREX 之前该内存地址的标记仍然存在(即期间没有其他核心访问过该地址)时,写入才会成功,并返回 0。否则,写入失败,返回 1。

这套机制使得“检查锁状态”和“获取锁”这两个操作可以原子地(Atomically) 完成。

一个简单的自旋锁获取流程如下:

spin_lock:
    LDREX R1, [R0]        // R0 保存锁的地址。以独占模式将锁的值加载到 R1
    CMP R1, #0            // 检查锁是否空闲(0 表示空闲)
    WFNENE                // 如果非零(不空闲),省电指令(可选),然后循环
    STREXNE R1, R2, [R0]  // 尝试获取锁:尝试将新值(如 1)写入锁地址
    CMPNE R1, #0          // 检查 STREX 是否成功(R1==0 表示成功)
    BNE spin_lock         // 如果失败(可能因为竞争),跳回开头重试
    DMB                   // 获取锁成功后,插入内存屏障,确保临界区访问不会“溜出”锁的范围
    BX LR                 // 成功获取锁,返回

五、举例说明

假设我们有一个双核 ARM 处理器(Core 0 和 Core 1),它们需要共同更新一个全局计数器 shared_counter。

没有锁的情况(灾难):

  1. shared_counter 初始值为 5。
  2. Core 0 读取 shared_counter (值为 5)。
  3. Core 1 也读取 shared_counter (值也为 5)。
  4. Core 0 将值加 1,得到 6,并写回内存。
  5. Core 1 也将它之前读到的值 (5) 加 1,得到 6,并写回内存。
  6. 最终结果 shared_counter 是 6,而不是正确的 7。Core 0 的操作被 Core 1 的操作覆盖了。

使用自旋锁的情况:

我们定义一个自旋锁 counter_lock 来保护 shared_counter。

// 伪代码示意
volatile int counter_lock = 0; // 0 表示锁是空闲的
int shared_counter = 5;

// Core 0 上运行的代码
void core0_function(void) {
    while(spin_try_lock(&counter_lock) != SUCCESS) { // 尝试获取锁
        // 在这里自旋!不断尝试,直到 Core 1 释放锁
    }
    // 成功获取锁,进入临界区
    shared_counter++; // 安全地修改
    spin_unlock(&counter_lock); // 释放锁
}

// Core 1 上运行的代码(与 Core 0 同时运行)
void core1_function(void) {
    while(spin_try_lock(&counter_lock) != SUCCESS) { // 尝试获取锁
        // 在这里自旋!不断尝试,直到 Core 0 释放锁
    }
    // 成功获取锁,进入临界区
    shared_counter++; // 安全地修改
    spin_unlock(&counter_lock); // 释放锁
}

执行时序:

  1. counter_lock 初始为 0(空闲)。
  2. Core 0 率先执行 spin_try_lock。LDREX 读到 0,STREX 成功将其置为 1(已上锁),成功获取锁。
  3. Core 0 开始执行 shared_counter++(假设此时发生了上下文切换,Core 1 开始运行)。
  4. Core 1 执行 spin_try_lock。LDREX 读到 1(已上锁),STREX 会失败。Core 1 陷入自旋循环,不停地重试 LDREX/STREX。
  5. Core 0 继续执行,完成 shared_counter++(值变为 6),然后调用 spin_unlock,将 counter_lock 重置为 0(空闲)。这个解锁操作会被所有核心看到。
  6. 正在自旋的 Core 1 在下次循环中,LDREX 读到了 0,并且它的 STREX 成功地将锁置为 1,从而成功获取锁。
  7. Core 1 进入临界区,读取 shared_counter(现在是 6),加 1 后变为 7,写回内存。
  8. Core 1 释放锁。
  9. 最终结果 shared_counter 为 7,正确无误。

六、总结与注意事项

特性 说明
目的 保护多核环境下的短期共享资源访问。
行为 获取失败时忙等待(循环检查)。
实现 依赖于 ARM 的独占访问指令 (LDREX/STREX) 实现原子操作。
优点 避免上下文切换开销,适用于极短期等待和中断上下文。
缺点 浪费 CPU 周期,不适用于持锁时间长的操作。
关键点 必须配合内存屏障指令使用,以确保正确的内存可见性。

简单来说,自旋锁就是一种“不拿到锁绝不罢休(但也不睡觉)”的锁机制,它通过让 CPU 核心空转循环来等待锁变得可用,适用于等待时间非常短的场景,是构建多核系统的基础同步原语。

posted on 2025-09-19 20:36  SOC验证工程师  阅读(31)  评论(0)    收藏  举报

刷新页面返回顶部
 
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3