2. 自旋锁API详解:spin_lock/spin_unlock、spin_lock_irq/spin_unlock_irq、spin_lock_irqsave/spin_unlock_irqrestore

自旋锁(Spinlock)是Linux内核中最基础的互斥原语之一,特别适用于短临界区和不可睡眠上下文。内核提供了三组核心API,分别对应不同的中断上下文场景。选择错误的变体是导致死锁和性能劣化的常见根源。

2.1 API家族总览与核心差异

三组API的核心区别在于是否操作本地CPU中断使能状态以及是否保存中断标志。下表从功能、适用场景和性能开销三个维度进行对比:

API 组合 中断使能操作 保存中断标志 适用上下文 性能开销
spin_lock / spin_unlock 不操作 否 进程上下文(无中断竞争) 最低
spin_lock_irq / spin_unlock_irq 关/开本地中断 否 进程上下文 + 中断上下文 中等
spin_lock_irqsave / spin_unlock_irqrestore 关/恢复本地中断 是 任何上下文(推荐通用) 略高(保存标志)
关键原则:如果在中断处理程序(ISR)中访问了被自旋锁保护的共享数据,则必须在加锁前关闭本地CPU中断。否则,当前进程持有锁时被中断打断,ISR尝试获取同一把锁,将导致自旋死锁。

2.2 逐组API深度解析

2.2.1 spin_lock / spin_unlock —— 纯自旋,不关中断

这是最基本的变体,仅实现忙等待轮询。它不修改本地CPU的中断使能状态。因此,它只能在确定不会与中断上下文发生竞争的场景下使用,例如:

  • 临界区仅在进程上下文中被访问(如系统调用、workqueue)。
  • 临界区仅在同一个中断处理程序的不同实例间共享(此时中断已隐式关闭)。
// 示例:仅进程上下文竞争
spinlock_t my_lock;
spin_lock_init(&my_lock);

void process_context_func(void) {
    spin_lock(&my_lock);
    // 临界区:操作共享数据
    shared_data->counter++;
    spin_unlock(&my_lock);
}
风险提示:如果在持有 spin_lock 锁时发生了中断,且该中断处理程序也尝试获取同一把锁,系统将立即死锁。这是新手最常见的错误之一。

2.2.2 spin_lock_irq / spin_unlock_irq —— 关中断,但不保存状态

该变体在加锁时调用 local_irq_disable() 关闭本地CPU的所有中断,解锁时调用 local_irq_enable() 重新开启。它解决了中断上下文竞争问题,但存在一个隐含陷阱:

  • 如果调用前中断已经是关闭状态,解锁时无条件开启中断,可能导致系统中断状态被破坏。
  • 不能在中断上下文(ISR)中调用,因为ISR本身可能是在中断关闭状态下执行的。
// 示例:进程上下文 + 中断上下文竞争(仅限进程侧加锁)
spinlock_t dev_lock;

void process_side_func(void) {
    spin_lock_irq(&dev_lock);
    // 临界区:此时本地中断已关闭,ISR不会抢占
    dev->status = BUSY;
    spin_unlock_irq(&dev_lock); // 注意:无条件开启中断
}

// 中断处理程序侧(ISR中中断已隐式关闭)
irqreturn_t dev_isr(int irq, void *dev_id) {
    spin_lock(&dev_lock); // ISR中只需spin_lock
    dev->status = IDLE;
    spin_unlock(&dev_lock);
    return IRQ_HANDLED;
}
使用限制:spin_lock_irq 只能在已知中断状态为“开启”的上下文中使用(如系统调用入口)。在中断上下文或中断关闭后调用它是不安全的。

2.2.3 spin_lock_irqsave / spin_unlock_irqrestore —— 安全通用型

这是最推荐的变体。它在关闭中断前,使用 local_irq_save(flags) 将当前中断使能状态保存到 unsigned long flags 变量中;解锁时通过 local_irq_restore(flags) 恢复到加锁前的状态,而不是无条件开启。这保证了:

  • 无论加锁前中断是开还是关,解锁后都能正确恢复。
  • 可以在任何上下文(进程、中断、软中断、NMI安全区域)中安全调用。
// 示例:通用安全模式,适用于所有上下文
spinlock_t global_lock;

void safe_critical_section(void) {
    unsigned long flags;

    spin_lock_irqsave(&global_lock, flags);
    // 临界区:安全操作共享资源
    shared_pool->allocated = true;
    spin_unlock_irqrestore(&global_lock, flags);
    // 中断状态恢复为加锁前的值
}
最佳实践:在编写驱动时,除非你能100%确定中断状态且不会在中断上下文中调用,否则始终使用 spin_lock_irqsave / spin_unlock_irqrestore。额外的 flags 变量开销微乎其微,但能避免难以调试的竞态条件。

2.3 API选择决策流程

下图展示了在实际驱动开发中,如何根据上下文快速选择正确的自旋锁API变体:

自旋锁API选择决策流程 开始选择API 是否与中断上下文竞争? 否 spin_lock 是 在中断上下文中调用? 是 spin_lock_irqsave 否 确定当前中断状态为开? 是 spin_lock_irq 否 spin_lock_irqsave 核心原则:不确定时,始终选择 spin_lock_irqsave 该变体在所有上下文中安全,且能正确恢复中断状态,避免隐式状态破坏

2.4 常见错误模式与排查要点

错误模式 表现 根因 修复方案
中断上下文死锁 系统挂起,CPU占用100% 进程持有 spin_lock 时被中断,ISR尝试获取同一锁 改用 spin_lock_irqsave
中断状态破坏 随机中断丢失,外设超时 在中断关闭状态下调用 spin_lock_irq,解锁时误开中断 改用 spin_lock_irqsave
递归加锁 进程自旋死锁 同一上下文递归调用 spin_lock 检查代码路径,或使用 spin_lock_nested
flags变量未初始化 编译警告或随机行为 spin_lock_irqsave 的 flags 未定义 确保 unsigned long flags 在栈上定义
调试技巧:当怀疑自旋锁死锁时,可以通过 /proc/lock_stat 或 lockdep 内核配置(CONFIG_PROVE_LOCKING)来检测潜在的死锁路径。在开发阶段始终开启 LOCKDEP 和 SPINLOCK_DEBUG。

2.5 性能对比:指令开销与缓存影响

从汇编指令层面看,三组API的差异主要体现在对中断标志的操作上。以下是在ARM64架构下的典型指令序列对比:

API 关键指令 额外延迟(周期) 适用场景
spin_lock ldaxr / stlxr (独占加载/存储) ~10-20 高频短临界区,无中断竞争
spin_lock_irq 上述 + msr daifset, #2 (关中断) ~30-50 进程上下文,已知中断开启
spin_lock_irqsave 上述 + mrs x0, daif (读中断标志) + msr daifset, #2 ~40-60 通用安全,推荐默认选择
性能建议:对于绝大多数驱动场景,spin_lock_irqsave 多出的20-40个CPU周期完全可以忽略。不要为了微小的性能提升而牺牲安全性。只有在经过性能分析确认锁竞争是瓶颈时,才考虑降级为 spin_lock_irq 或 spin_lock。