3. Linux 同步机制之 spinlock 设计与实现(一):通用层

日期 :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 三层调用链

flowchart TD subgraph API[API 层 spinlock_t] S1[spin_lock] end subgraph RAW[raw 层 raw_spinlock_t] R1[raw_spin_lock] R2[_raw_spin_lock] R3[__raw_spin_lock] R4[do_raw_spin_lock] end subgraph ARCH[arch 层 arch_spinlock_t] A1[arch_spin_lock] end S1 --> R1 R1 --> R2 R2 --> R3 R3 --> R4 R4 --> A1
  • 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_disablearch 层退化为空

单核上"自旋"无意义(同一时刻只有一个执行流,关抢占即互斥),所以 UP 版本不需要原子指令。这就是"同一接口两套实现"的由来。

2.3 加锁的统一模式

观察所有 __raw_spin_lock_* 变体,它们遵循同一个四步模式,如图 2 所示。

图 2:加锁的统一四步模式

flowchart TD S[__raw_spin_lock 变体] --> A[1. 关中断/下半部 可选 + 关抢占 必做] A --> B[2. lockdep 记录 spin_acquire] B --> C[3. LOCK_CONTENDED 竞争等待] C --> D[4. do_raw_spin_lock 调用 arch 获取]
  1. 关中断/下半部 + 关抢占 :所有变体都 preempt_disable(底线);_irq/_irqsave/_bh 变体在关抢占前先关中断或下半部。
  2. lockdep 记录spin_acquire 登记锁依赖(CONFIG_LOCKDEP),用于死锁检测;非调试构建零开销。
  3. 竞争等待LOCK_CONTENDED 在锁有竞争时进入自旋等待。
  4. 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_irqlocal_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_enabledo_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_trylockarch_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 分派

flowchart TD M[raw_spin_lock] --> D{CONFIG_SMP} D -->|是 SMP| S[_raw_spin_lock 含 arch 自旋] D -->|否 UP| U[__LOCK 仅关抢占 arch 退化]
  • 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 分派同理,不再赘述。

六、设计哲学小结

  1. 分层解耦同步策略与原子原语:三层架构把"关抢占/中断/lockdep"(通用)与"架构原子指令"(专有)分离,新增架构只实现 arch 层。
  2. 统一四步模式:所有加锁变体共享"关抢占 + 可选关中断/bh + lockdep + arch 获取"骨架,差异仅在第一步--用一致结构容纳多种语义。
  3. 关抢占是底线 :spinlock 不可睡眠、不可被抢占,否则自旋者绝望。这是所有变体都 preempt_disable 的根本原因。
  4. SMP/UP 编译期分派:同一接口,SMP 产生自旋,UP 退化为关抢占。单核无需原子指令。
  5. 调试设施零开销 :lockdep / DEBUG_SPINLOCK 在非调试构建下编译期消除,正常运行无成本。

附录 A:延伸阅读

  • include/linux/spinlock.hspinlock_types.hspinlock_api_smp.hspinlock_api_up.h:本文通用层源头
  • kernel/locking/spinlock.cspinlock_debug.c_raw_spin_* 与调试实现
  • PREEMPT_RT:spinlock_traw_spinlock_t 分离的实时性背景(本文未展开)

附录 B:API 速查总表

本文涉及的 spinlock 通用层 API,按功能分组。持锁期间不可睡眠、不可被抢占

符号约定lockspinlock_t *flagsunsigned 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) 关中断并尝试 结合 _irqtrylock

配对原则 :加锁与解锁必须变体配对(_irqsave_irqrestore_irq_irq_bh_bh)。混用会导致中断/抢占状态错乱。


版权声明(重申)

本文所有内容(包括文字、图表、代码注释与设计分析)均为作者独立创作,著作权归作者所有。

未经作者书面授权,严禁转载、摘编、复制、建立镜像、用于任何商业目的。

Copyright © 2026. All rights reserved. Unauthorized reproduction, distribution, or commercial use is strictly prohibited.

相关推荐
念恒123062 小时前
网络基础
linux·网络·c++
皮卡狮3 小时前
Linux静态库和动态库详解
linux
撩得Android一次心动3 小时前
Linux编程笔记4【个人用】
linux·笔记·学习
七牛云行业应用4 小时前
Ollama 本地部署 DeepSeek 完全指南:macOS / Windows / Linux 三端安装 + GPU 配置 + API 调用
linux·windows·macos
顧棟4 小时前
Zookeeper热缩容解析
linux·服务器·zookeeper
三言老师5 小时前
CentOS7.9:Redis服务器部署结构化实战教程
linux·运维·服务器·数据库
味悲5 小时前
Linux提权
linux·服务器·安全
无足鸟ICT6 小时前
【RHCA+】替换变量
linux