2. 自旋锁(Spinlock)原理与实现

2.1 自旋锁的工作机制

自旋锁是 Linux 内核中最基础的互斥原语之一,专为短临界区设计。当锁被持有时,等待线程不会睡眠,而是持续循环检查锁状态——即“自旋”。这种忙等策略避免了线程切换的开销,在临界区执行时间极短(通常 < 1000 个 CPU 周期)时效率极高。

核心特性: 自旋锁禁止抢占(preempt_disable),但允许中断。在单核系统上,自旋锁退化为抢占禁用。
适用场景: 中断处理程序、低延迟路径、短临界区(如链表头插入、引用计数更新)。
不适用场景: 长时间 I/O 等待、可能引起睡眠的代码路径。
自旋锁忙等机制示意图 CPU 0 CPU 1 CPU 2 锁已被持有 自旋等待 自旋等待 临界区执行 CPU 0 和 CPU 2 在 while 循环中持续读取锁变量,直到 CPU 1 释放锁 自旋期间 CPU 无法执行其他任务,因此临界区必须极短

2.2 在 SMP 系统下的行为

在对称多处理(SMP)系统中,自旋锁的行为远比单核复杂。多个 CPU 可能同时尝试获取同一把锁,需要硬件原子操作来保证一致性。

场景 CPU 数量 自旋锁行为 性能影响
单核 UP 1 退化为 preempt_disable 无自旋开销
双核 SMP 2 一个自旋,一个执行 低竞争时高效
多核 SMP 4+ 多个 CPU 同时自旋 缓存一致性流量激增
NUMA 多节点 跨节点自旋延迟高 需考虑亲和性
缓存行乒乓(Cache Line Bouncing): 在 SMP 系统中,多个 CPU 同时自旋修改同一个锁变量会导致缓存一致性协议(MESI)频繁失效,严重时自旋锁开销可超过临界区本身。这是多核性能调优的核心关注点。
SMP 缓存行乒乓问题 CPU 0 L1 缓存 锁变量 = 1 (Shared) CPU 1 L1 缓存 锁变量 = 1 (Shared) CPU 2 L1 缓存 锁变量 = 1 (Shared) 主存:锁变量 = 1 ⬆ 缓存行失效 → 重新加载 → 乒乓震荡 ⬇ 每个 CPU 修改锁变量时,其他 CPU 的缓存行被标记为 Invalid,导致反复从主存加载

2.3 自旋锁的变种:raw_spinlock

Linux 内核中自旋锁有两个主要变体:spinlock_t 和 raw_spinlock_t。理解它们的区别对于实时内核配置至关重要。

特性 spinlock_t raw_spinlock_t
抢占控制 通过 preempt_disable() 实现 直接操作抢占计数器
RT 内核行为 可被优先级继承互斥锁替代 保持自旋语义不变
中断上下文 允许在中断中使用 必须用于真正不可睡眠路径
性能 标准路径,有调试检查 最简路径,无额外开销
使用场景 绝大多数驱动和子系统 调度器、中断控制器、时间子系统
关键区别: 在 PREEMPT_RT 实时内核中,spinlock_t 被替换为可睡眠的互斥锁(rt_mutex),而 raw_spinlock_t 保持纯自旋语义。因此,任何在真正原子上下文中(如调度器、中断控制器)使用的自旋锁必须使用 raw_spinlock_t。
spinlock_t vs raw_spinlock_t 决策树 需要自旋锁? 是否在真正原子上下文? 是 否 raw_spinlock_t 调度器 / 中断控制器 spinlock_t 驱动 / 文件系统 / 网络 PREEMPT_RT 内核中:spinlock_t → rt_mutex(可睡眠) raw_spinlock_t → 保持纯自旋(不可睡眠) 选错类型会导致实时内核中的优先级反转或死锁

2.4 自旋锁核心 API 与实现

Linux 内核提供了一套简洁的自旋锁 API,底层基于原子操作实现。

/* 定义并初始化自旋锁 */
spinlock_t my_lock;
spin_lock_init(&my_lock);

/* 基本加锁/解锁 */
spin_lock(&my_lock);      /* 自旋等待直到获取锁 */
/* 临界区代码 */
spin_unlock(&my_lock);    /* 释放锁 */

/* 中断安全版本 */
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);   /* 保存中断状态 + 加锁 */
/* 临界区代码 */
spin_unlock_irqrestore(&my_lock, flags); /* 恢复中断状态 + 解锁 */

/* 非阻塞尝试 */
if (spin_trylock(&my_lock)) {
    /* 成功获取锁 */
    spin_unlock(&my_lock);
} else {
    /* 锁已被持有,不等待 */
}

/* raw_spinlock 使用相同 API 模式 */
raw_spinlock_t raw_lock;
raw_spin_lock_init(&raw_lock);
raw_spin_lock(&raw_lock);
raw_spin_unlock(&raw_lock);
最佳实践:
  • 始终使用 spin_lock_irqsave / spin_unlock_irqrestore 对,除非你明确知道中断状态。
  • 临界区内禁止调用任何可能睡眠的函数(如 kmalloc(GFP_KERNEL)、copy_from_user)。
  • 使用 spin_trylock 在无法获取锁时执行其他工作,避免死锁。

2.5 自旋锁实现原理(x86 架构)

自旋锁的底层实现依赖于硬件提供的原子指令。在 x86 架构上,核心是 LOCK 前缀与 cmpxchg(比较并交换)指令。

/* 简化的自旋锁实现逻辑 */
typedef struct {
    volatile int locked;  /* 0 = 未锁定, 1 = 已锁定 */
} spinlock_t;

void spin_lock(spinlock_t *lock) {
    /* 原子地尝试将 locked 从 0 改为 1 */
    while (__atomic_test_and_set(&lock->locked, __ATOMIC_ACQUIRE)) {
        /* 锁已被持有,忙等 */
        while (lock->locked) {
            /* 在 x86 上插入 PAUSE 指令优化超线程 */
            __builtin_ia32_pause();
        }
    }
}

void spin_unlock(spinlock_t *lock) {
    __atomic_clear(&lock->locked, __ATOMIC_RELEASE);
}
PAUSE 指令的作用: 在自旋循环中插入 PAUSE 指令可以降低功耗、减少内存顺序违规,并提高超线程场景下的性能。现代内核的自旋锁实现都包含这一优化。

2.6 自旋锁故障排查要点

故障现象 可能原因 排查手段
系统软锁(Soft Lockup) 自旋锁持有时间过长 检查 /proc/lock_stat,使用 ftrace
CPU 使用率 100% 多个 CPU 激烈竞争同一锁 perf top 查看热点,lockdep 检测
死锁 锁顺序不一致 启用 CONFIG_PROVE_LOCKING
实时任务延迟 raw_spinlock 在 RT 内核中阻塞 检查是否误用 spinlock_t 替代
调试建议: 内核编译时开启 CONFIG_DEBUG_SPINLOCK 和 CONFIG_PROVE_LOCKING(lockdep),可以在开发阶段捕获大部分自旋锁使用错误。生产环境建议关闭这些选项以避免性能开销。