3、锁的分类:自旋锁(spinlock)与互斥锁(mutex)的宏观对比、适用场景

在嵌入式 Linux 内核驱动开发中,自旋锁(spinlock)与互斥锁(mutex)是最核心的两种同步原语。理解它们的本质差异与适用边界,是编写正确、高效驱动的基石。本章从宏观视角对两者进行对比,并给出清晰的场景选择指南。

3.1 核心差异:忙等 vs 睡眠

两种锁最根本的区别在于等待机制:

  • 自旋锁(spinlock):当锁被持有时,等待的线程会在一个循环中“自旋”(忙等),持续检查锁是否可用。期间 CPU 不执行其他任务,但保持运行状态。
  • 互斥锁(mutex):当锁被持有时,等待的线程会进入睡眠状态,主动让出 CPU,直到锁释放后被唤醒。
关键指标对比:自旋锁的等待开销是 CPU 空转,互斥锁的开销是上下文切换与唤醒延迟。临界区极短时自旋锁更优,临界区较长或可能睡眠时互斥锁更优。
自旋锁 vs 互斥锁 等待机制对比 自旋锁 (Spinlock) 线程 A 持有锁 线程 B 尝试获取锁 线程 B 进入 忙等循环 CPU 持续空转检查锁状态 锁释放 → 线程 B 立即获取 特点:无上下文切换,低延迟 互斥锁 (Mutex) 线程 A 持有锁 线程 B 尝试获取锁 线程 B 进入 睡眠状态 CPU 调度其他任务执行 锁释放 → 唤醒线程 B 特点:上下文切换,高延迟

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 的场景。
锁类型选择决策树 临界区需要保护? 是否在中断上下文? 是 → 自旋锁 (spin_lock_irqsave) 临界区可能睡眠? 是 → 互斥锁 (mutex_lock) 否 → 自旋锁 (spin_lock) 决策要点 中断上下文 → 只能用自旋锁;可能睡眠 → 只能用互斥锁;短临界区且无睡眠 → 自旋锁更高效

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 睡眠”的权衡。核心判断依据依次为:

  1. 执行上下文:中断上下文只能用自旋锁。
  2. 临界区是否可能睡眠:是则用互斥锁。
  3. 临界区长度:极短(< 100 指令周期)用自旋锁,较长用互斥锁。
  4. 实时性要求:自旋锁避免调度延迟,但浪费 CPU;互斥锁释放 CPU,但引入调度抖动。