ARM 架构下 spin_lock 实现
2026/9/13 13:04:44 网站建设 项目流程

阅读该文章前,需要对原子指令有所了解,推荐阅读 聊一聊原子操作和弱内存序

1、概念

内核当发生访问资源冲突的时候,可以有两种锁的解决方案选择:

  • 一个是原地等待
  • 一个是挂起当前进程,调度其他进程执行(睡眠)

Spinlock 是内核中提供的一种比较常见的锁机制,自旋锁是“原地等待”的方式解决资源冲突的,即,一个线程获取了一个自旋锁后,另外一个线程期望获取该自旋锁,获取不到,只能够原地“打转”(忙等待)。由于自旋锁的这个忙等待的特性,注定了它使用场景上的限制 —— 自旋锁不应该被长时间的持有(消耗 CPU 资源)。

2、源码实现

2.1 内联汇编

asm(code : output operand list : input operand list : clobber list);

这种嵌入汇编的形式一共分为四个部分:

  • code:汇编的操作代码,一条或者多条指令,如果是多条指令,需要在指令间使用 \n\t 隔开。与通用的汇编代码有一些不同:因为支持 C 变量的操作,所以在操作由第二、三部分提供的操作数时,使用 %n 来替代操作数;
  • output operand list:表示输出的操作数,通常是一个或者多个 C 函数中的变量;
  • input operand list:表示输入的操作数,通常是一个或者多个 C 函数中的变量;
  • clobber list:告诉编译器这段汇编代码会修改哪些寄存器或状态

2.2 ARM32 上 spinlock 实现

arch\arm\include\asm\spinlock.h

#defineTICKET_SHIFT16typedefstruct{union{u32 slock;struct__raw_tickets{#ifdef__ARMEB__u16 next;u16 owner;#elseu16 owner;u16 next;#endif}tickets;};}arch_spinlock_t;/* * ARMv6 ticket-based spin-locking. * * A memory barrier is required after we get a lock, and before we * release it, because V6 CPUs are assumed to have weakly ordered * memory. */staticinlinevoidarch_spin_lock(arch_spinlock_t*lock){unsignedlongtmp;u32 newval;arch_spinlock_tlockval;prefetchw(&lock->slock);__asm____volatile__("1: ldrex %0, [%3]\n"" add %1, %0, %4\n"" strex %2, %1, [%3]\n"" teq %2, #0\n"" bne 1b":"=&r"(lockval),"=&r"(newval),"=&r"(tmp):"r"(&lock->slock),"I"(1<<TICKET_SHIFT):"cc");while(lockval.tickets.next!=lockval.tickets.owner){wfe();lockval.tickets.owner=READ_ONCE(lock->tickets.owner);}smp_mb();}staticinlinevoidarch_spin_unlock(arch_spinlock_t*lock){smp_mb();lock->tickets.owner++;dsb_sev();}

在内联汇编中,%0、%1、%2… 是自动编号的操作数,它们在 “输出” 和 “输入” 约束中按顺序出现。例如:%0 代指 lockval; %1 代指 newval; %4 代指 (1 << TICKET_SHIFT)…

汇编C语言解释
1: ldrex %0, [%3]lockval = lock读取锁的值赋值给 lockval
add %1, %0, %4newval = lockval + (1 << 16)将 next++ 之后的值存在 newval 中
strex %2, %1, [%3]lock = newval更新 lock 中的值,将是否成功结果存入在 tmp 中
teq %2, #0if(tmp == 0)判断上条指令是否成功,如果不成功执行 ”bne 1b” 跳到标号1执行

注:

  1. arch_spinlock_t是一个联合体,其内部仅包含一个slock成员。因此,对lock->slock的访问实质上就是对整个arch_spinlock_t结构的访问
  2. 在 ARM32 架构下,该自旋锁的实现本质上是一个基于队列的排队锁(ticket lock)。每次调用arch_spin_lock()获取锁时,都会对锁的next字段执行原子加一操作。next的值相当于调用者在等待队列中排到的“号码”
  3. 解锁操作(通常由 arch_spin_unlock() 实现)会对 owner 字段执行原子加一,使得下一个排队等待的线程可以获取锁;
  4. 上面所说的 “原子加一” 实际上是通过原子指令 ldrex、strex 实现的。原子指令,确保了排队的正确性,不会出现两个任务获取到相同号码的情况

这是一种基于 “排队取号” 模型的公平自旋锁,由 next(发号计数器)和 owner(服务计数器)两个变量协同工作:

  • 拿号排队:每个试图加锁的线程都会原子性地从 next领取一个专属号码,并推动 next 递增。这确立了线程获取锁的先后次序
  • 轮询等待:线程通过循环不断比对 自身号码 与 当前服务号码(owner)。只有当两者一致时,才意味着轮到该线程执行,从而获得锁
  • 叫号放行:锁释放时,线程只需将 owner 原子加 1。这一操作隐式地通知了队列中的下一个线程:“现在轮到你了”,从而实现锁的平滑移交

该机制通过将竞争转化为有序的队列等待,有效避免了线程饥饿问题。

3、WFE/SEV 的事件通知机制

3.1 WFE 为什么出现在自旋循环中

如果等待者一直执行普通的紧凑轮询,它会高频读取共享缓存行,增加功耗和内存系统压力WFE(Wait For Event)向处理器表明当前执行流正在等待事件;具体处理器实现可以在等待期间暂停取指,甚至进入低功耗状态

当事件到达时,WFE返回,代码重新读取owner。唤醒并不等于获得锁,事件可能与当前锁无关,也可能同时唤醒多个处理器,所以判断条件必须放在循环中反复检查。这和条件变量常见的“被唤醒后仍需检查条件”是同一个原则。

3.2 SEV 如何通知等待者

SEV(Send Event)向系统中的处理器发送事件。当前持锁者执行owner++后再执行SEV,等待者从WFE返回并重新检查owner。只有票号等于新owner的等待者可以进入临界区,其余等待者继续执行WFE

ARM 为每个处理器维护事件寄存器,可以把它理解成一个一位的事件标志:

  • 标志已经置位时执行WFE,指令会清除标志并直接返回;
  • 标志未置位时执行WFE,处理器可以等待后续事件;
  • SEV会使对应的事件标志置位,并使正在等待事件的处理器恢复执行。

这个机制可以避免典型的“先通知、后等待”丢失唤醒问题。即使释放者恰好在等待者即将执行WFE之前发出了SEV,事件也会先记录在事件寄存器中;等待者随后执行WFE时会消费该事件并立即返回,然后重新读取owner

3.3 WFE 和 WFI 的区别

指令全称主要等待对象在此处的用途
WFEWait For Event体系结构定义的事件,SEV是其中的重要来源等待锁状态变化,释放方可主动发送事件
WFIWait For Interrupt体系结构定义的 WFI 唤醒事件(WFI wake-up events)常用于 CPU 空闲路径,不适合直接构成这里的锁交接协议

按照 Arm 的术语,WFI会等待体系结构定义的WFI wake-up event;不宜笼统表述为“手动唤醒”或“异常唤醒”。SEV不是WFI的配对通知指令。具体唤醒条件会随体系结构版本、处理器状态和实现而有所不同,请查阅对应版本的 Arm Architecture Reference Manual。

本文只需关注:自旋锁等待的是另一个 CPU 对共享锁状态的更新,解锁方会执行SEV,因此等待方使用与之配合的WFE,而不是WFI

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询