日期 :2026-07-25 · 参考源码版本:Linux 内核 5.x
版权声明 本文为原创技术文章,著作权归作者所有。未经作者书面授权,严禁任何形式的转载、摘编、复制、建立镜像、商业使用。
写在前面
多核并发是内核绕不开的主题。当多个 CPU 同时访问共享数据时,需要同步机制保证数据一致。内核提供了两类基本锁:spinlock(自旋锁) 与 mutex(互斥锁)。本系列聚焦 spinlock,分为两部分:
- 第一部分 :spinlock 的通用层设计---从
spin_lock一行调用到arch_spin_lock架构接口的完整调用链,以及 irq/irqsave/bh 等变体的语义差异。架构无关。 - 第二部分 :具体硬件架构(如ARM32架构的票号锁(ticket lock))实现,即调用链最底层的
arch_spin_lock。
阅读本文你将了解:
- spinlock 与 mutex 的选型依据
- spinlock / raw_spinlock / arch_spinlock 三层架构的分工
__raw_spin_lock的统一四步模式:关抢占 + lockdep + 竞争等待 + arch 获取- irq / irqsave / bh / nested 变体到底关掉了什么
- SMP 与 UP 为何能共用同一接口
一、问题:为什么需要 spinlock
1.1 spinlock vs mutex 选型
spinlock 和 mutex 都能保护共享资源,但语义不同:spinlock 让等待者忙等(CPU 空转重试),mutex 让等待者睡眠(让出 CPU)。选型依据:
- 中断/原子上下文:必须 spinlock(不能睡眠)
- 进程上下文 :
- 临界区极短(< 上下文切换开销):spinlock
- 需调用可能睡眠的函数:mutex
- 持锁时间长或不确定:mutex(更安全)
| 场景 | spinlock | mutex |
|---|---|---|
| 高竞争、短临界区 | 好(避免调度开销) | 差(频繁睡眠/唤醒) |
| 低竞争、短临界区 | 一般(空转浪费 CPU) | 好 |
| 长临界区 | 非常差(浪费大量 CPU) | 好 |
| 中断上下文 | 可用 | 不可用(会睡眠) |
1.2 spinlock 的本质
spinlock 的核心约束是持锁期间不能睡眠、不能被抢占。否则一个持锁的 CPU 若被调度走,其他 CPU 会无限自旋等待---整个系统可能卡死。因此"关抢占"是 spinlock 的底线,而"关中断"用于进程上下文与中断上下文共享同一把锁的场景。理解这一点,后面所有变体的设计都顺理成章。
二、核心设计思想
2.1 三层架构
内核把 spinlock 分成三层,各司其职。其调用链如图 1 所示。
图 1:spinlock 三层调用链
- API 层 (
spinlock_t):面向驱动/子系统的接口,spin_lock等只是包装,转发给raw_spin_lock(&lock->rlock)。 - raw 层 (
raw_spinlock_t):实现关抢占/关中断/lockdep 等通用逻辑,SMP/UP 在此区分。 - arch 层 (
arch_spinlock_t):架构相关的原子获取(ARM、x86 等),是真正的"自旋"发生处。
为什么拆三层?把"同步策略(关抢占/中断/lockdep)"与"原子原语(架构指令)"解耦:同步策略全架构通用,原子原语因架构而异。新增架构只需实现
arch_spin_*几个接口,上层完全复用。
2.2 SMP vs UP:同一接口两套实现
raw 层的 _raw_spin_lock 在编译期按 CONFIG_SMP 分派:
- SMP :
_raw_spin_lock->__raw_spin_lock,含preempt_disable+ lockdep + 真正的arch_spin_lock自旋。 - UP (单核):
_raw_spin_lock->__LOCK,仅preempt_disable,arch 层退化为空。
单核上"自旋"无意义(同一时刻只有一个执行流,关抢占即互斥),所以 UP 版本不需要原子指令。这就是"同一接口两套实现"的由来。
2.3 加锁的统一模式
观察所有 __raw_spin_lock_* 变体,它们遵循同一个四步模式,如图 2 所示。
图 2:加锁的统一四步模式
- 关中断/下半部 + 关抢占 :所有变体都
preempt_disable(底线);_irq/_irqsave/_bh变体在关抢占前先关中断或下半部。 - lockdep 记录 :
spin_acquire登记锁依赖(CONFIG_LOCKDEP),用于死锁检测;非调试构建零开销。 - 竞争等待 :
LOCK_CONTENDED在锁有竞争时进入自旋等待。 - arch 获取 :
do_raw_spin_lock->arch_spin_lock真正原子获取。
解锁顺序与加锁对称:spin_release(lockdep)-> do_raw_spin_unlock(arch)-> 恢复抢占/中断/下半部。
三、数据结构与初始化
3.1 两层结构体
include/linux/spinlock_types.h
c
typedef struct spinlock {
union {
struct raw_spinlock rlock; // 实际的锁
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE]; // 填充,使 dep_map 与 raw_spinlock 中位置一致
struct lockdep_map dep_map; // 锁依赖跟踪
};
#endif
};
} spinlock_t;
include/linux/spinlock_types_raw.h
c
typedef struct raw_spinlock {
arch_spinlock_t raw_lock; // 架构相关的锁实现
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu; // 调试用
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
spinlock_t 内含一个 raw_spinlock_t rlock,外层只是给它套上 debug 设施。raw_spinlock_t 内含 arch_spinlock_t raw_lock--这才是架构相关的实际锁值(ARM32 上是 next/owner 票号对)。三层数据结构对应三层调用。
历史上
spinlock_t曾直接包含锁字段;后来为支持实时内核(PREEMPT_RT)把可抢占的spinlock_t与不可抢占的raw_spinlock_t分离。本文按分离后的结构讲解。
3.2 初始化
include/linux/spinlock_types.h
c
#define __SPIN_LOCK_UNLOCKED(lockname) \
(spinlock_t) __SPIN_LOCK_INITIALIZER(lockname)
#define DEFINE_SPINLOCK(x) spinlock_t x = __SPIN_LOCK_UNLOCKED(x)
include/linux/spinlock.h
c
#define spin_lock_init(_lock) \
do { \
spinlock_check(_lock); \
*(_lock) = __SPIN_LOCK_UNLOCKED(_lock); \
} while (0)
DEFINE_SPINLOCK 是静态定义(编译期初始化为未锁),spin_lock_init 用于运行时(动态分配的结构体)。
四、关键操作原理
4.1 加锁:__raw_spin_lock 的四步
include/linux/spinlock_api_smp.h
c
#define LOCK_CONTENDED(_lock, try, lock) \
do { \
if (!try(_lock)) { \
lock_contended(&(_lock)->dep_map, _RET_IP_); \
lock(_lock); \
} \
lock_acquired(&(_lock)->dep_map, _RET_IP_); \
} while (0)
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable(); // ① 关抢占
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); // ② lockdep
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); // ③④
}
- ①
preempt_disable:持锁期间禁止抢占(spinlock 底线)。 - ②
spin_acquire:lockdep 登记获取点,用于检测锁序导致的死锁。 - ③
LOCK_CONTENDED:先用do_raw_spin_trylock试抢;抢不到则lock_contended(lockdep 标记"正在竞争")后调用do_raw_spin_lock真正自旋等待。 - ④
do_raw_spin_lock->arch_spin_lock:架构相关的原子获取。
LOCK_CONTENDED 的设计是先试后等 :无竞争时一次 trylock 即成功,避免无谓的 lockdep 竞争标记;有竞争才进入自旋。
4.2 变体:irq / irqsave / bh 的差异
各变体在四步模式的第一步(关什么)上分化。以 __raw_spin_lock_irqsave 为例:
include/linux/spinlock_api_smp.h
c
static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
unsigned long flags;
local_irq_save(flags); // 关中断并保存原状态
preempt_disable(); // 关抢占
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
#ifdef CONFIG_LOCKDEP
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock); // lockdep:避免获取期间开中断
#else
do_raw_spin_lock_flags(lock, &flags); // 非 lockdep:允许获取期间短暂开中断
#endif
return flags; // 返回用于恢复
}
local_irq_save(flags):关中断并保存原中断状态 到flags,解锁时用local_irq_restore(flags)恢复。用于调用方不知自己是否已关中断的场景。spin_lock_irq:local_irq_disable关中断但不保存状态,解锁时local_irq_enable无条件开中断--仅适用于调用前中断已开启的场景,否则解锁会错误打开中断。spin_lock_bh:__local_bh_disable_ip关软中断下半部(不关硬中断),用于进程上下文与软中断共享锁。spin_lock_nested:加锁逻辑同spin_lock,仅给 lockdep 标记子类号,用于合法的同类嵌套加锁(避免 lockdep 误报死锁)。
所有变体都关抢占;差异只在"额外关什么"。
_irqsave保存状态、_irq不保存、_bh关下半部、_nested仅影响 lockdep 分类。
4.3 解锁:与加锁对称的恢复
include/linux/spinlock_api_smp.h
c
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
spin_release(&lock->dep_map, _RET_IP_); // lockdep 释放
do_raw_spin_unlock(lock); // arch 释放
preempt_enable(); // 恢复抢占
}
static inline void __raw_spin_unlock_irqrestore(raw_spinlock_t *lock,
unsigned long flags)
{
spin_release(&lock->dep_map, _RET_IP_);
do_raw_spin_unlock(lock);
local_irq_restore(flags); // 恢复中断状态
preempt_enable();
}
解锁顺序与加锁对称:先 spin_release(lockdep 释放)、再 do_raw_spin_unlock(arch 释放),最后恢复抢占/中断/下半部--加锁时最先关的抢占/中断最后恢复,确保持锁全程关闭。_irqrestore 必须配对 _irqsave(用保存的 flags 恢复),_irq 配对 local_irq_enable。
顺序不能反:若先
preempt_enable再do_raw_spin_unlock,窗口期内被抢占会导致其他 CPU 自旋等待--违反 spinlock 不可睡眠/不可抢占约束。
4.4 trylock:非阻塞快速路径
include/linux/spinlock_api_smp.h
c
static inline int __raw_spin_trylock(raw_spinlock_t *lock)
{
preempt_disable();
if (do_raw_spin_trylock(lock)) { // 一次性尝试 arch 获取
spin_acquire(&lock->dep_map, 0, 1, _RET_IP_);
return 1; // 成功
}
preempt_enable(); // 失败则恢复抢占
return 0;
}
trylock 不自旋:一次 do_raw_spin_trylock(arch_spin_trylock)成功则持锁,失败立即返回 0。用于"拿不到就跳过"的场景,避免死等。
4.5 操作语义对照表
| API | 关中断 | 关抢占 | 关下半部 | 典型用途 |
|---|---|---|---|---|
spin_lock |
✗ | ✓ | ✗ | 进程上下文、与进程上下文共享 |
spin_lock_irq |
✓ | ✓ | ✗ | 进程与硬中断共享,调用前中断开启 |
spin_lock_irqsave |
✓ 保存 | ✓ | ✗ | 进程与硬中断共享,调用前中断状态未知 |
spin_lock_bh |
✗ | ✓ | ✓ | 进程与软中断共享 |
spin_lock_nested |
✗ | ✓ | ✗ | 同类锁嵌套(仅 lockdep 分类) |
spin_trylock |
✗ | ✓ | ✗ | 非阻塞尝试 |
所有变体都关抢占;选哪个取决于"临界区会被哪种上下文打断"。完整 API 速查见 附录 B。
五、SMP/UP 分派机制
raw 层在编译期按 CONFIG_SMP 选择实现:
c
#define raw_spin_lock(lock) _raw_spin_lock(lock)
SMP 版本 (kernel/locking/spinlock.c):
c
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
__raw_spin_lock(lock); // §4.1 的四步
}
UP 版本 (include/linux/spinlock_api_up.h):
c
#define _raw_spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); ___LOCK(lock); } while (0)
分派关系如图 3 所示。
图 3:SMP/UP 分派
- SMP :
_raw_spin_lock调用__raw_spin_lock,经do_raw_spin_lock->arch_spin_lock真正自旋。 - UP :
_raw_spin_lock展开为__LOCK,仅preempt_disable;___LOCK(lock)在 UP 上是空操作。
同一份上层代码,SMP 上产生原子自旋,UP 上退化为关抢占--这就是 spinlock 能跨配置复用的关键。
irq/irqsave/bh变体的 SMP/UP 分派同理,不再赘述。
六、设计哲学小结
- 分层解耦同步策略与原子原语:三层架构把"关抢占/中断/lockdep"(通用)与"架构原子指令"(专有)分离,新增架构只实现 arch 层。
- 统一四步模式:所有加锁变体共享"关抢占 + 可选关中断/bh + lockdep + arch 获取"骨架,差异仅在第一步--用一致结构容纳多种语义。
- 关抢占是底线 :spinlock 不可睡眠、不可被抢占,否则自旋者绝望。这是所有变体都
preempt_disable的根本原因。 - SMP/UP 编译期分派:同一接口,SMP 产生自旋,UP 退化为关抢占。单核无需原子指令。
- 调试设施零开销 :lockdep /
DEBUG_SPINLOCK在非调试构建下编译期消除,正常运行无成本。
附录 A:延伸阅读
include/linux/spinlock.h、spinlock_types.h、spinlock_api_smp.h、spinlock_api_up.h:本文通用层源头kernel/locking/spinlock.c、spinlock_debug.c:_raw_spin_*与调试实现- PREEMPT_RT:
spinlock_t与raw_spinlock_t分离的实时性背景(本文未展开)
附录 B:API 速查总表
本文涉及的 spinlock 通用层 API,按功能分组。持锁期间不可睡眠、不可被抢占。
符号约定 :
lock为spinlock_t *;flags为unsigned long(保存中断状态);subclass为 lockdep 子类号。"关中断"列:✓ 表示关,✗ 表示不关,保存表示保存并关。
B.1 初始化
| API | 语义 | 说明 |
|---|---|---|
DEFINE_SPINLOCK(x) |
静态定义并初始化 | 编译期,x = __SPIN_LOCK_UNLOCKED(x) |
__SPIN_LOCK_UNLOCKED(name) |
静态初始化器 | 用于已有结构体的初始化列表 |
spin_lock_init(lock) |
运行时初始化 | 动态分配的锁用此 |
B.2 加锁
| API | 关中断 | 关抢占 | 关下半部 | 说明 |
|---|---|---|---|---|
spin_lock(lock) |
✗ | ✓ | ✗ | 基础加锁 |
spin_lock_irq(lock) |
✓ | ✓ | ✗ | 关中断,不保存状态 |
spin_lock_irqsave(lock, flags) |
保存 | ✓ | ✗ | 保存并关中断 |
spin_lock_bh(lock) |
✗ | ✓ | ✓ | 关软中断下半部 |
spin_lock_nested(lock, subclass) |
✗ | ✓ | ✗ | 嵌套加锁,仅 lockdep 分类 |
B.3 解锁
| API | 语义 | 说明 |
|---|---|---|
spin_unlock(lock) |
基础解锁 | 恢复抢占 |
spin_unlock_irq(lock) |
开中断解锁 | 配对 spin_lock_irq |
spin_unlock_irqrestore(lock, flags) |
恢复中断状态解锁 | 配对 spin_lock_irqsave |
spin_unlock_bh(lock) |
使能下半部解锁 | 配对 spin_lock_bh |
B.4 查询与尝试
| API | 语义 | 说明 |
|---|---|---|
spin_is_locked(lock) |
锁是否已被持有 | 快照,仅瞬间有效 |
spin_trylock(lock) |
非阻塞尝试 | 成功持锁,失败返回 0 |
spin_trylock_irq(lock) |
关中断并尝试 | 结合 _irq 与 trylock |
配对原则 :加锁与解锁必须变体配对(
_irqsave配_irqrestore、_irq配_irq、_bh配_bh)。混用会导致中断/抢占状态错乱。
版权声明(重申)
本文所有内容(包括文字、图表、代码注释与设计分析)均为作者独立创作,著作权归作者所有。
未经作者书面授权,严禁转载、摘编、复制、建立镜像、用于任何商业目的。
Copyright © 2026. All rights reserved. Unauthorized reproduction, distribution, or commercial use is strictly prohibited.