自旋锁:轻量级锁的底层哲学
自旋锁,说白了就是让CPU在那「空转」等待。我刚开始接触内核时,觉得这玩意儿挺傻的——干嘛不直接睡过去等?后来才明白,有些场景下,睡觉的成本比空转高得多。
它的核心思想很简单:如果锁被占用,当前线程就不断循环检查锁是否释放,而不是进入睡眠状态。这就像你在等厕所,如果知道里面的人马上就出来,你宁愿站在门口等,而不是去沙发上躺一会儿再过来看。
自旋锁的工作原理
自旋锁基于一个原子操作——Test-and-Set。硬件保证这个操作要么全部完成,要么完全不执行。我当年调试一个驱动时,就因为没理解这个原子性,差点把服务器搞挂了。
伪代码看起来是这样的:
// 自旋锁的核心逻辑(简化版)
void spin_lock(lock_t *lock) {
while (test_and_set(&lock->flag, 1) == 1) {
// 啥也不干,就空转
// CPU在这里不断循环
}
}
void spin_unlock(lock_t *lock) {
lock->flag = 0;
}
注意看那个while循环。只要test_and_set返回1(表示锁被占用),CPU就一直在那转。这就是「自旋」二字的由来。
关键点:自旋锁的等待线程不会睡眠,它一直占用CPU。这意味着如果你在单核CPU上用了自旋锁,持有锁的线程被调度走了,那其他线程就永远等不到锁释放——死锁了。
自旋锁的API
Linux内核提供了丰富的自旋锁API。我整理了一张表,方便你查阅:
| API | 说明 | 使用场景 |
|---|---|---|
| spin_lock_init | 初始化自旋锁 | 创建锁时调用一次 |
| spin_lock | 获取自旋锁 | 普通上下文,可睡眠 |
| spin_lock_irq | 获取锁并关闭本地中断 | 中断上下文,防止死锁 |
| spin_lock_irqsave | 获取锁并保存中断状态 | 需要恢复原中断状态时 |
| spin_unlock | 释放自旋锁 | 对应spin_lock |
| spin_unlock_irq | 释放锁并开启中断 | 对应spin_lock_irq |
| spin_unlock_irqrestore | 释放锁并恢复中断状态 | 对应spin_lock_irqsave |
| spin_trylock | 尝试获取锁,失败立即返回 | 不想阻塞的场景 |
嗯,这里有个细节要注意:spin_lock_irqsave和spin_unlock_irqrestore是成对出现的。我见过有人混用spin_lock_irq和spin_unlock_irqrestore,结果中断状态乱套了。
使用场景与注意事项
自旋锁不是万能的。它最适合以下场景:
- 临界区非常短:比如就修改一个变量,几纳秒的事
- 不能睡眠的上下文:中断处理函数、软中断等
- 多核CPU:自旋锁在SMP系统上才能真正发挥作用
避坑指南:我曾经在中断处理函数里用了普通的spin_lock,结果持有锁的中断处理函数被另一个中断打断了——死锁。后来改用spin_lock_irq才解决。记住:中断上下文一定要用带irq的版本。
还有几个坑你得注意:
- 自旋锁保护的代码不能睡眠——这包括kmalloc(GFP_KERNEL)、copy_from_user等操作
- 自旋锁不能递归使用——同一个线程不能重复获取同一个自旋锁
- 自旋锁持有时间要短——否则其他CPU空转太浪费
实战案例:驱动中的共享计数器
来看一个实际例子。假设我们写一个驱动,需要统计某个中断触发的次数:
#include <linux/spinlock.h>
struct my_device {
spinlock_t lock;
unsigned long irq_count;
};
// 初始化
void my_device_init(struct my_device *dev) {
spin_lock_init(&dev->lock);
dev->irq_count = 0;
}
// 中断处理函数
irqreturn_t my_irq_handler(int irq, void *dev_id) {
struct my_device *dev = (struct my_device *)dev_id;
unsigned long flags;
// 保存中断状态并获取锁
spin_lock_irqsave(&dev->lock, flags);
dev->irq_count++; // 临界区:就这一行
spin_unlock_irqrestore(&dev->lock, flags);
return IRQ_HANDLED;
}
// 读取计数(可能在进程上下文调用)
unsigned long my_get_count(struct my_device *dev) {
unsigned long count;
unsigned long flags;
spin_lock_irqsave(&dev->lock, flags);
count = dev->irq_count;
spin_unlock_irqrestore(&dev->lock, flags);
return count;
}
这个例子展示了几个要点:
- 中断上下文和进程上下文共享数据时,都用
spin_lock_irqsave - 临界区只有一行代码,自旋锁的开销几乎可以忽略
- 用
irqsave/restore版本,保证中断状态正确恢复
个人经验:我习惯在驱动初始化时就分配好所有资源,这样临界区里就不会有内存分配操作。你想想看,如果在持有自旋锁时调用kmalloc,万一内存紧张触发页面回收,那就睡眠了——内核直接报错。
自旋锁的知识体系
下面这张图帮你理清自旋锁的核心逻辑:
这张图把自旋锁的整个流程串起来了。你注意看那个「CPU空转等待」的循环——这就是自旋锁的精髓所在。
最后说一句,自旋锁虽然简单,但用不好就是灾难。我见过一个同事在持有自旋锁时调用了printk,结果控制台输出太慢,导致其他CPU空转了十几毫秒。嗯,从那以后,我写代码时都会问自己一句:这段代码能在5微秒内执行完吗?
公众号:蓝海资料掘金营,微信deep3321