3. 自旋锁原理(spinlock)
自旋锁是 Linux 内核中最基础的同步原语之一,专为短临界区和不可睡眠上下文设计。理解其工作机制、忙等待特性与适用边界,是掌握内核并发调试的第一步。
3.1 自旋锁的工作机制
自旋锁的核心思想是:当一个线程尝试获取已被其他线程持有的锁时,它不会进入睡眠状态,而是原地循环忙等待,不断检查锁是否已被释放。一旦持有者释放锁,等待线程立即获得锁并进入临界区。
关键行为:自旋锁的“自旋”本质是一个紧凑的 while 循环,循环体内执行原子化的读-修改-写操作(如 atomic_test_and_set_bit),直到成功获取锁为止。
自旋锁在单核与多核系统上的实现有显著差异:
| 系统类型 | 实现方式 | 说明 |
|---|---|---|
| 单核(UP) | 空操作或禁止抢占 | 单核上不存在真正的并行,自旋锁退化为抢占禁用,防止中断上下文与进程上下文竞争 |
| 多核(SMP) | 原子指令 + 内存屏障 | 使用 LOCK 前缀指令(x86)或 ldrex/strex(ARM)实现原子性,配合 dmb 保证内存序 |
内核开发者提示:在单核系统上,spin_lock() 实际上只调用 preempt_disable(),不会产生任何循环等待。这是理解自旋锁行为差异的重要前提。
3.2 忙等待特性与性能影响
忙等待(busy-waiting)是自旋锁最鲜明的特征,也是其最大的性能风险点。等待线程在自旋期间持续占用 CPU,无法执行其他任务。
忙等待带来的直接后果:
- CPU 空转:等待线程消耗完整的 CPU 时间片,无法被调度出去
- 缓存颠簸:多核场景下,自旋锁的原子操作导致缓存行在核间频繁失效
- 优先级反转:低优先级线程持有锁,高优先级线程自旋等待,低优先级线程无法获得 CPU
性能陷阱:如果临界区执行时间超过 2 次上下文切换的开销(约 1-5 μs),自旋锁的忙等待效率将低于互斥锁的睡眠等待。这是选择同步原语的核心判断依据。
3.3 适用场景
自旋锁的适用场景由其忙等待特性严格限定:
| 场景 | 是否适用 | 原因 |
|---|---|---|
| 短临界区(< 10 μs) | ✅ 适用 | 自旋开销小于上下文切换,整体效率更高 |
| 中断上下文(ISR) | ✅ 适用 | 中断上下文不可睡眠,只能使用自旋锁 |
| 软中断 / tasklet | ✅ 适用 | 软中断上下文同样不可睡眠 |
| 长临界区(> 100 μs) | ❌ 不适用 | 忙等待浪费 CPU,应使用互斥锁 |
| 需要睡眠的操作 | ❌ 不适用 | 自旋锁持有期间禁止睡眠,否则导致死锁 |
| 用户态内存访问 | ❌ 不适用 | 用户态访问可能触发缺页中断,导致睡眠 |
3.4 自旋锁的变体与选择
Linux 内核提供了多种自旋锁变体,以适应不同的并发场景:
选择自旋锁变体的核心原则:
- spin_lock() / spin_unlock():仅用于进程上下文,不涉及中断保护
- spin_lock_irqsave() / spin_unlock_irqrestore():中断上下文与进程上下文共享数据时使用,保存并禁用本地中断
- spin_lock_bh() / spin_unlock_bh():软中断与进程上下文共享数据时使用,禁用软中断
- raw_spinlock_t:实时内核(RT-PREEMPT)中,spinlock_t 可能被实现为可睡眠锁,raw_spinlock_t 保证真正的自旋行为
最佳实践:在中断上下文中访问共享数据时,必须使用 spin_lock_irqsave(),否则中断处理程序可能打断正在持有锁的进程上下文,导致死锁。
3.5 自旋锁使用示例
以下代码展示了一个典型的中断上下文自旋锁使用模式:
#include <linux/spinlock.h>
struct my_device {
int data;
spinlock_t lock;
};
/* 中断处理函数 */
irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
unsigned long flags;
spin_lock_irqsave(&dev->lock, flags);
/* 临界区:访问共享数据 */
dev->data++;
/* 临界区结束 */
spin_unlock_irqrestore(&dev->lock, flags);
return IRQ_HANDLED;
}
/* 进程上下文接口 */
ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
struct my_device *dev = filp->private_data;
unsigned long flags;
int val;
spin_lock_irqsave(&dev->lock, flags);
val = dev->data;
spin_unlock_irqrestore(&dev->lock, flags);
return copy_to_user(buf, &val, sizeof(val)) ? -EFAULT : sizeof(val);
}
关键点:中断处理函数与进程上下文共享同一把锁时,双方都必须使用 spin_lock_irqsave() / spin_unlock_irqrestore(),确保中断状态被正确保存和恢复。
3.6 自旋锁的调试与注意事项
内核提供了 CONFIG_DEBUG_SPINLOCK 和 CONFIG_DEBUG_LOCK_ALLOC 选项,用于在开发阶段检测自旋锁的误用:
| 调试选项 | 检测能力 | 性能影响 |
|---|---|---|
| CONFIG_DEBUG_SPINLOCK | 检测未初始化锁、重复解锁、锁泄漏 | 低 |
| CONFIG_DEBUG_LOCK_ALLOC | 检测锁顺序反转、死锁风险、睡眠时持有锁 | 中 |
| CONFIG_LOCK_STAT | 统计锁竞争次数、等待时间、持有时间 | 低 |
常见错误:
- 在持有自旋锁时调用可能睡眠的函数(如 copy_from_user、kmalloc(GFP_KERNEL)、mutex_lock)
- 在中断上下文中使用 spin_lock() 而非 spin_lock_irqsave()
- 忘记在错误路径中释放自旋锁,导致系统死锁