2. 自旋锁(Spinlock)原理与实现
2.1 自旋锁的工作机制
自旋锁是 Linux 内核中最基础的互斥原语之一,专为短临界区设计。当锁被持有时,等待线程不会睡眠,而是持续循环检查锁状态——即“自旋”。这种忙等策略避免了线程切换的开销,在临界区执行时间极短(通常 < 1000 个 CPU 周期)时效率极高。
核心特性: 自旋锁禁止抢占(preempt_disable),但允许中断。在单核系统上,自旋锁退化为抢占禁用。
适用场景: 中断处理程序、低延迟路径、短临界区(如链表头插入、引用计数更新)。
不适用场景: 长时间 I/O 等待、可能引起睡眠的代码路径。
不适用场景: 长时间 I/O 等待、可能引起睡眠的代码路径。
2.2 在 SMP 系统下的行为
在对称多处理(SMP)系统中,自旋锁的行为远比单核复杂。多个 CPU 可能同时尝试获取同一把锁,需要硬件原子操作来保证一致性。
| 场景 | CPU 数量 | 自旋锁行为 | 性能影响 |
|---|---|---|---|
| 单核 UP | 1 | 退化为 preempt_disable | 无自旋开销 |
| 双核 SMP | 2 | 一个自旋,一个执行 | 低竞争时高效 |
| 多核 SMP | 4+ | 多个 CPU 同时自旋 | 缓存一致性流量激增 |
| NUMA | 多节点 | 跨节点自旋延迟高 | 需考虑亲和性 |
缓存行乒乓(Cache Line Bouncing): 在 SMP 系统中,多个 CPU 同时自旋修改同一个锁变量会导致缓存一致性协议(MESI)频繁失效,严重时自旋锁开销可超过临界区本身。这是多核性能调优的核心关注点。
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。
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),可以在开发阶段捕获大部分自旋锁使用错误。生产环境建议关闭这些选项以避免性能开销。