ARM 架构下 spin_lock 实现
发布时间:2026/9/13 13:04:48 作者:尧图编辑部 阅读量:1,286

阅读该文章前需要对原子指令有所了解推荐阅读 聊一聊原子操作和弱内存序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(1TICKET_SHIFT):cc);while(lockval.tickets.next!lockval.tickets.owner){wfe();lockval.tickets.ownerREAD_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读取锁的值赋值给 lockvaladd %1, %0, %4newval lockval (1 16)将 next 之后的值存在 newval 中strex %2, %1, [%3]lock newval更新 lock 中的值将是否成功结果存入在 tmp 中teq %2, #0if(tmp 0)判断上条指令是否成功如果不成功执行 ”bne 1b” 跳到标号1执行注arch_spinlock_t是一个联合体其内部仅包含一个slock成员。因此对lock-slock的访问实质上就是对整个arch_spinlock_t结构的访问在 ARM32 架构下该自旋锁的实现本质上是一个基于队列的排队锁ticket lock。每次调用arch_spin_lock()获取锁时都会对锁的next字段执行原子加一操作。next的值相当于调用者在等待队列中排到的“号码”解锁操作通常由 arch_spin_unlock() 实现会对 owner 字段执行原子加一使得下一个排队等待的线程可以获取锁上面所说的 “原子加一” 实际上是通过原子指令 ldrex、strex 实现的。原子指令确保了排队的正确性不会出现两个任务获取到相同号码的情况这是一种基于 “排队取号” 模型的公平自旋锁由 next发号计数器和 owner服务计数器两个变量协同工作拿号排队每个试图加锁的线程都会原子性地从 next领取一个专属号码并推动 next 递增。这确立了线程获取锁的先后次序轮询等待线程通过循环不断比对 自身号码 与 当前服务号码owner。只有当两者一致时才意味着轮到该线程执行从而获得锁叫号放行锁释放时线程只需将 owner 原子加 1。这一操作隐式地通知了队列中的下一个线程“现在轮到你了”从而实现锁的平滑移交该机制通过将竞争转化为有序的队列等待有效避免了线程饥饿问题。3、WFE/SEV 的事件通知机制3.1 WFE 为什么出现在自旋循环中如果等待者一直执行普通的紧凑轮询它会高频读取共享缓存行增加功耗和内存系统压力。WFEWait For Event向处理器表明当前执行流正在等待事件具体处理器实现可以在等待期间暂停取指甚至进入低功耗状态。当事件到达时WFE返回代码重新读取owner。唤醒并不等于获得锁事件可能与当前锁无关也可能同时唤醒多个处理器所以判断条件必须放在循环中反复检查。这和条件变量常见的“被唤醒后仍需检查条件”是同一个原则。3.2 SEV 如何通知等待者SEVSend 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。