3、锁的分类:自旋锁(spinlock)与互斥锁(mutex)的宏观对比、适用场景
在嵌入式 Linux 内核驱动开发中,自旋锁(spinlock)与互斥锁(mutex)是最核心的两种同步原语。理解它们的本质差异与适用边界,是编写正确、高效驱动的基石。本章从宏观视角对两者进行对比,并给出清晰的场景选择指南。
3.1 核心差异:忙等 vs 睡眠
两种锁最根本的区别在于等待机制:
- 自旋锁(spinlock):当锁被持有时,等待的线程会在一个循环中“自旋”(忙等),持续检查锁是否可用。期间 CPU 不执行其他任务,但保持运行状态。
- 互斥锁(mutex):当锁被持有时,等待的线程会进入睡眠状态,主动让出 CPU,直到锁释放后被唤醒。
关键指标对比:自旋锁的等待开销是 CPU 空转,互斥锁的开销是上下文切换与唤醒延迟。临界区极短时自旋锁更优,临界区较长或可能睡眠时互斥锁更优。
3.2 宏观对比表
| 对比维度 | 自旋锁 (spinlock) | 互斥锁 (mutex) |
|---|---|---|
| 等待行为 | 忙等(自旋) | 睡眠(阻塞) |
| 上下文切换 | 无 | 有(进入/退出睡眠) |
| 临界区时长 | 极短(微秒级) | 可较长(毫秒级或更长) |
| 中断上下文 | 可用(需关中断变体) | 不可用(可能睡眠) |
| 持有期间能否睡眠 | 禁止 | 允许 |
| CPU 占用 | 等待时 100% 占用 | 等待时释放 CPU |
| 适用处理器 | 多核 / 单核(需关抢占) | 多核 / 单核 |
| 递归加锁 | 不支持(死锁) | 不支持(标准 mutex) |
经验法则:如果临界区执行时间 少于 100 个指令周期,优先考虑自旋锁;如果临界区可能包含 I/O 操作、内存分配、或任何可能睡眠的调用,必须使用互斥锁。
3.3 适用场景分析
3.3.1 自旋锁的典型场景
- 中断处理程序(ISR):中断上下文不可睡眠,只能使用自旋锁(或 spin_lock_irqsave 变体)。
- 硬件寄存器访问:对设备寄存器进行短时、高频的读写保护。
- 链表/队列的原子操作:如 sk_buff 链表、workqueue 链表等内核数据结构的快速操作。
- 多核间共享计数器:如统计计数、状态标志的更新。
3.3.2 互斥锁的典型场景
- 设备文件操作:驱动中的 open/read/write/ioctl 等可能睡眠的文件操作。
- 内存分配:临界区内调用 kmalloc(GFP_KERNEL)、vmalloc 等可能触发页面回收的函数。
- 用户空间数据拷贝:使用 copy_from_user/copy_to_user 时,可能发生缺页异常导致睡眠。
- 长时间临界区:临界区执行时间超过 1ms 的场景。
3.4 使用注意事项
⚠️ 常见陷阱:
- 自旋锁持有期间调用可能睡眠的函数(如 copy_from_user、kmalloc(GFP_KERNEL))会导致内核崩溃。
- 互斥锁在中断上下文中使用会导致“scheduling while atomic”错误。
- 长时间持有自旋锁在多核系统上会严重浪费 CPU 时间,降低系统实时性。
- 忘记释放锁(如异常路径未解锁)会导致死锁。
3.5 代码示例:场景对比
以下代码展示两种锁在典型场景下的使用差异:
/* 场景1:中断上下文 - 必须使用自旋锁 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
/* 快速操作:更新硬件寄存器、统计计数 */
writel(value, reg_addr);
spin_unlock_irqrestore(&my_lock, flags);
return IRQ_HANDLED;
}
/* 场景2:文件操作 - 使用互斥锁 */
static ssize_t my_read(struct file *filp, char __user *buf,
size_t count, loff_t *f_pos)
{
ssize_t ret;
mutex_lock(&my_mutex);
/* 可能睡眠的操作:拷贝到用户空间 */
ret = copy_to_user(buf, kernel_buf, count);
mutex_unlock(&my_mutex);
return ret;
}
/* 场景3:短临界区 - 使用自旋锁 */
void update_shared_counter(void)
{
spin_lock(&counter_lock);
shared_counter++;
spin_unlock(&counter_lock);
}
性能参考:在 ARM Cortex-A72 @1.5GHz 上,自旋锁的获取/释放开销约为 10-20ns,而互斥锁的上下文切换开销约为 1-5μs(取决于调度器负载)。对于临界区小于 1μs 的场景,自旋锁优势明显。
3.6 小结
自旋锁与互斥锁的选择本质上是“忙等 vs 睡眠”的权衡。核心判断依据依次为:
- 执行上下文:中断上下文只能用自旋锁。
- 临界区是否可能睡眠:是则用互斥锁。
- 临界区长度:极短(< 100 指令周期)用自旋锁,较长用互斥锁。
- 实时性要求:自旋锁避免调度延迟,但浪费 CPU;互斥锁释放 CPU,但引入调度抖动。