1. 自旋锁基础概念
1.1 自旋锁的核心工作原理
自旋锁(Spinlock)是 Linux 内核中最基础的同步原语之一,专为 短临界区 设计。其核心思想是:当一个线程尝试获取已被其他 CPU 持有的锁时,该线程不会进入睡眠状态,而是在原地 “自旋”(即循环检测锁状态),直到锁被释放。
关键特性: 自旋锁的持有者不允许睡眠或主动调度(schedule()),否则将导致死锁。
自旋锁的典型工作流程如下:
- 尝试获取: 调用
spin_lock(),原子性地检查锁变量是否为“未锁定”状态。 - 成功: 若锁空闲,立即将锁标记为“已锁定”,并进入临界区执行代码。
- 失败(忙等待): 若锁已被其他 CPU 持有,当前 CPU 进入紧凑循环,反复读取锁变量,直到锁被释放。
- 释放: 调用
spin_unlock(),原子性地将锁状态恢复为“未锁定”。
1.2 忙等待机制详解
忙等待(Busy-waiting)是自旋锁区别于其他锁机制最显著的特征。当锁被持有时,等待线程在一个紧凑的循环中反复执行 原子加载指令(如 ldrex / cmpxchg),持续消耗 CPU 周期。
性能陷阱: 在单核系统上,若持有锁的线程被中断打断,而中断处理程序又尝试获取同一自旋锁,将导致 立即死锁。这是驱动开发中最常见的调试噩梦。
忙等待的底层实现(ARM64 架构简化示例):
/* 自旋锁忙等待循环的伪汇编表示 */
spin_lock:
ldrex r1, [r0] /* 独占加载锁变量 */
cmp r1, #0 /* 检查是否空闲 */
strexeq r1, #1, [r0] /* 若空闲,尝试原子写入 */
cmpeq r1, #0 /* 检查写入是否成功 */
bne spin_lock /* 失败则重试 */
dmb /* 内存屏障,保证后续操作可见性 */
bx lr
1.3 自旋锁与互斥锁的全面对比
理解自旋锁与互斥锁(Mutex)的差异,是正确选择同步机制的前提。下表从多个维度进行对比:
| 对比维度 | 自旋锁 (Spinlock) | 互斥锁 (Mutex) |
|---|---|---|
| 等待行为 | 忙等待(CPU 循环检测) | 睡眠等待(进程切换) |
| 适用场景 | 短临界区(微秒级) | 长临界区(毫秒级或更长) |
| 上下文限制 | 可用于中断上下文 | 禁止在中断上下文使用 |
| CPU 开销 | 等待时持续占用 CPU | 等待时释放 CPU 给其他进程 |
| 调度影响 | 无上下文切换开销 | 涉及两次上下文切换 |
| 持有者限制 | 禁止睡眠、禁止调度 | 允许睡眠、允许调度 |
| 实现复杂度 | 简单,基于原子操作 | 复杂,涉及等待队列 |
| 多核扩展性 | 高(仅影响等待的 CPU) | 中(涉及全局调度器) |
1.4 选择策略:何时使用自旋锁
在 Linux 驱动开发中,选择自旋锁还是互斥锁应遵循以下原则:
- 中断上下文必须使用自旋锁: 中断处理程序(Top Half)和软中断(Softirq)中禁止睡眠,只能使用自旋锁变体(如
spin_lock_irqsave())。 - 临界区极短(< 100 条指令)优先自旋锁: 上下文切换的开销(约 1-5 μs)可能超过忙等待的时间。
- 临界区涉及 I/O 操作或内存分配: 必须使用互斥锁,因为这些操作可能触发睡眠。
- 多核高竞争场景: 自旋锁的忙等待会浪费 CPU 周期,若临界区较长,互斥锁的睡眠等待更高效。
调试建议: 在开发初期,若不确定锁类型,优先使用
spin_lock_irqsave() 和 spin_unlock_irqrestore() 这对组合。它们会自动保存和恢复本地中断状态,避免中断上下文死锁。后续可通过性能分析工具(如 perf)评估是否需要切换为互斥锁。