硬件信号量与自旋锁:多核共享资源的原子获取/释放
更新时间:2026-09-19。前置:round-robin 仲裁公平性、AHB 总线矩阵、内存屏障。当多个 CPU 核或多个 DMA/硬件主设备要独占同一外设寄存器、共享队列或共享内存结构时,需要互斥机制。
本文要回答的问题
- 多核为什么不能用普通读-改-写实现锁?
- 硬件 spinlock 寄存器如何用「一次读」原子获取?
- LL/SC、LR/SC、AMO 原子指令与专门硬件信号量有何区别?
- 多硬件主设备(不只 CPU)怎么加锁,死锁和释放归属怎么防?
一、为什么普通读写不够
锁的核心是「判断空闲并占用」必须不可分割。若用普通 load 然后判断再 store:
bash
核A: load lock=0 ─┐
核B: load lock=0 ─┴─ 两者都读到 0
核A: store lock=1 ─ 都以为拿到锁,同时进临界区 ✗
核B: store lock=1总线上两次访问间可能插入其他主设备,软件无法保证原子。解决方法分两类:CPU 原子指令和专用硬件锁/信号量外设。
二、CPU 原子指令
- LL/SC(MIPS)、LR/SC(RISC-V/ARM LDAXR/STXR):链接加载后监控该地址,期间若被其他核改写,条件存储失败;软件用「LR→判空→SC,失败重试」实现锁,天然检测竞争(类似 RISC-V LR/SC 序列)。
- AMO(atomic memory-op):单指令原子读-改-写(AMOSWAP/AMOADD),常用于「swap 出旧值并写入锁标记」。
- 这些依赖内存子系统支持独占监控(exclusive monitor)或在总线上发起 locked/exclusive 传输(AXI 的 AxLOCK/exclusive、AHB 的 HMASTLOCK)。局限:只有会执行这些指令的 CPU/总线主能用;纯硬件 DMA、不具备原子指令的处理器、或异构加速器之间无法直接用。
三、专用硬件 spinlock/信号量外设
SoC 常在总线上挂一个硬件锁模块(如 TI Spinlock、各类 hwsem),对所有总线主(CPU/DMA/加速器)开放。核心是一个「读即原子获取」的锁存语义:
bash
空闲锁: value=0
主X 读 lock 寄存器:
硬件在单次访问中:返回「获取成功 + 当前持有者=X」,并立刻把锁置忙
→ 即使两主同一周期读,内部仲裁只让一个成功(另一个返回 fail/忙)
释放: 持有者写(或写指定 key/TAG)释放,锁回空闲- 原子性由硬件仲裁保证:读请求经过模块内仲裁器(见 round-robin),同一时刻只服务一个「获取」,从根本上消除 load/test/store 窗口。
- 持有者标识(owner ID/tag):获取时记录主 ID,释放时校验只有持有者能释放,防止 A 加的锁被 B 误清。
- 获取失败处理:返回 busy/fail 后软件自旋重读或挂起到等待队列;硬件可提供「锁空闲中断」避免纯空转。
- 多锁号(lock number/bank):模块提供 N 个独立锁(如 32/256),不同资源用不同锁号,提高并发而非一把全局锁。
四、公平性与高级形态
- 排队锁 / ticket lock(硬件 FIFO):简单自旋在高竞争下有饿死和「惊群」;硬件可实现请求 FIFO/ticket,按到达顺序授权(先进先出),保证公平、降低缓存乒乓。
- 信号量计数(counting semaphore):不止 0/1,可允许 N 个持有者(资源池)。
- 中断/事件通知:锁释放时唤醒等待者,可接到中断控制器(见 事件路由),低功耗核可睡眠等待而非轮询。
- 与 cache 一致性:锁寄存器所在内存通常设为 non-cacheable/Device,或经一致性互连正确广播 invalidates,否则某核 cache 里缓存旧锁值会永久自旋(见 DMA 与 cache 一致性、CHI)。
五、典型使用规则
c
// 硬件自旋锁(读获取/写释放)伪码
while (readl(HWSPINLOCK(n)) != ACQUIRED) { cpu_relax(); } // 原子读即试锁
// ---- 临界区:独占访问共享外设/队列 ----
writel(owner_tag, HWSPINLOCK_RELEASE(n)); // 释放(校验归属)- 获取成功后、访问共享资源前、释放前后的顺序必须用 内存屏障 保证(获取是 acquire 语义、释放是 release 语义),否则临界区的读写可能被重排到锁外。
- 中断/异常处理:不能在 ISR 与主流程之间用同一把不可重入锁而不防死锁;持锁期间被中断、ISR 也取该锁即自锁。
六、常见的坑
- 用普通 load-test-store 实现多核锁:非原子,竞争下双进入。
- 锁变量被 cache 缓存:各核看本地旧值永久自旋,必须 Device/non-cacheable 或一致性正确。
- 获取/释放缺屏障:临界区访存被重排到锁外,互斥失效。
- 不校验 owner 就释放:A 的锁被 B 清,临界区再次并发。
- 持锁进中断再取同锁:死锁;要用关中断区间或独立锁。
- 获取后异常/提前返回不释放:锁泄漏,系统最终所有资源被占死,需超时/看门狗检测。
- 粗粒度全局锁:256 个独立资源只用 1 把锁,多核退化串行;按资源分锁。
- 纯自旋高功耗/低优先级饿死:高竞争应用排队锁+中断,避免惊群和优先级反转。
- 在锁内做长操作/DMA 等待:持锁等慢速事件放大串行化;锁只保护数据结构不覆盖整个 IO。
- DMA/加速器取锁未参与缓存一致协议:硬件主和 CPU 看到不同锁状态,要走硬件信号量或 coherent 端口。
- 复位后锁状态残留:外设复位/系统复位要把所有锁归位,否则启动即见忙锁。
- exclusive monitor 被 AXI 从机不支持:LR/SC 永远失败,需确认从设备实现 exclusive 访问。
相关与延伸
仲裁基础见 round-robin 公平性;多主互连见 AHB 总线矩阵、AXI Crossbar;顺序见 内存屏障;共享内存一致性见 DMA 与 cache、CHI。
总结
多核互斥的本质是「判断空闲并占用」必须原子:CPU 用 LR/SC、LL/SC、AMO 配合独占监控或总线 locked/exclusive 传输实现,而非 CPU 的多个 DMA/加速器则用挂在总线上、对所有主开放的硬件自旋锁——其「一次读经内部仲裁原子地返回获取成功并立即置忙、持有者凭 owner ID 写释放」,配多锁号、FIFO 排队和空闲中断提升并发与公平;锁内存必须 non-cacheable/一致,获取-释放要有 acquire/release 屏障,并严防持锁进中断、异常不释放和 owner 不校验——非原子读写、缓存旧锁值、缺屏障和锁泄漏是四大死锁根因。