Linux内核分析之内核同步-01

This language version is unavailable; showing the other language.

16.1 自旋锁 —— spinlock_t 与 ticket lock

自旋锁(spinlock)是内核中最基础的锁原语:当锁不可用时,获取者不让出 CPU,而是在原地"自旋"反复检测锁状态,直到持锁者释放。这种策略在临界区极短的场景下远优于睡眠——省去了两次上下文切换的代价,但代价是持锁期间必须禁止抢占,且等待者白白消耗 CPU。Linux 7.0.10 中,自旋锁的实现呈现"四层结构":spin_lock() API 层之下是 x86_64 的排队自旋锁(qspinlock),而经典的票号锁(ticket lock)则保留在 include/asm-generic/ticket_spinlock.h 中作为其他架构的备选实现。本节将结合 Linux 7.0.10 内核源码,逐行分析自旋锁的分层设计、ticket lock 与 qspinlock 两种算法,以及快慢路径的完整流转。


16.1.1 自旋锁的使用规则与 preempt_count

什么时候用自旋锁

自旋锁适用的判据只有一条:临界区足够短,且不能睡眠。典型场景是保护一段纯内存操作的数据结构(链表摘除、计数器更新、状态翻转)。如果临界区内可能睡眠(分配内存、拷贝用户空间数据、获取互斥锁),自旋锁会造成两个问题:一是其他 CPU 的自旋者被无限拉长等待时间;二是本 CPU 在持有自旋锁时禁止了抢占,睡眠会破坏调度器的公平性假设,might_sleep() 检查会直接报错。

持有自旋锁期间内核给出的硬约束,全部通过 preempt_count 计数器实现。include/linux/preempt.h 中定义了这个 32 位字的位域划分:

// include/linux/preempt.h:33-53
 *         PREEMPT_MASK:    0x000000ff
 *         SOFTIRQ_MASK:    0x0000ff00
 *         HARDIRQ_MASK:    0x000f0000
 *             NMI_MASK:    0x00f00000
 * PREEMPT_NEED_RESCHED:    0x80000000
 */
#define PREEMPT_BITS    8
#define SOFTIRQ_BITS    8
#define HARDIRQ_BITS    4
#define NMI_BITS    4

#define PREEMPT_SHIFT   0
#define SOFTIRQ_SHIFT   (PREEMPT_SHIFT + PREEMPT_BITS)
#define HARDIRQ_SHIFT   (SOFTIRQ_SHIFT + SOFTIRQ_BITS)
#define NMI_SHIFT   (HARDIRQ_SHIFT + HARDIRQ_BITS)

preempt_count 从低位到高位分为四段:抢占计数(8 位)、软中断禁用计数(8 位)、硬中断计数(4 位)、NMI 计数(4 位),最高位是 PREEMPT_NEED_RESCHED 标志(由架构相关代码管理,见 13.4 节)。spin_lock() 的第一步 preempt_disable() 就是把 PREEMPT 段加 1:

// include/linux/preempt.h:211-233
#define preempt_disable() \
do { \
    preempt_count_inc(); \
    barrier(); \
} while (0)
...
#define preempt_enable() \
do { \
    barrier(); \
    if (unlikely(preempt_count_dec_and_test())) \
        __preempt_schedule(); \
} while (0)

preempt_enable() 的嵌套语义(line 228-232):只有当整个计数器减到 0 时才调用 __preempt_schedule() 触发抢占。这正是"嵌套关抢占"的基础——schedule_preempt_disabled()(互斥锁慢路径所用)和软中断退出时的抢占检查都依赖这一语义。barrier() 是编译器屏障(15.2 节),保证计数器操作不会被编译器重排到临界区之外。

自旋锁的变体家族

API 关中断 禁软中断 适用场景
spin_lock() 否 否 持锁者与所有竞争者都在进程上下文,且不需要与中断处理共享数据
spin_lock_bh() 否 是 与软中断(如 tasklet)共享数据,进程上下文一侧
spin_lock_irq() 是 否 与硬中断共享数据,且确定退出时中断总是开的
spin_lock_irqsave() 是(保存标志) 否 与硬中断共享数据,不确定进入时中断状态(最通用、最安全)

选择的原则是代价最小够用:spin_lock() 最便宜(只关抢占),spin_lock_irqsave() 最贵(多一次标志读写加关中断指令)。反过来说,如果持锁路径可能被中断打断、而中断处理程序又会获取同一把锁,就必须用 irq 变体,否则会死锁——中断在本 CPU 上抢不到自己持有的锁,而持锁者又无法运行到释放点。

16.1.2 spinlock_t 的四层结构

类型定义:raw_spinlock_t 与 spinlock_t

Linux 7.0.10 把锁类型定义拆分到独立的头文件中。最内层是 raw_spinlock_t:

// include/linux/spinlock_types_raw.h:14-24
context_lock_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
};
typedef struct raw_spinlock raw_spinlock_t;

核心字段只有一个:arch_spinlock_t raw_lock,即架构相关的底层锁字。x86_64 上它是 struct qspinlock(16.1.4 节)。CONFIG_DEBUG_SPINLOCK 下附加的 magic/owner_cpu/owner 用于调试时识别锁的持有者;CONFIG_DEBUG_LOCK_ALLOC 下的 dep_map 是 lockdep 的登记入口(16.4 节)。

context_lock_struct(name) 是本树新引入的容器定义宏(include/linux/compiler-context-analysis.h:98-117):当 clang 开启线程安全上下文分析时,它展开为带锁属性注解的结构体声明,供编译器做静态锁检查;普通编译下退化为普通的 struct name {...}。本树的各类锁容器(raw_spinlock/spinlock/mutex/seqlock)统一通过它定义,配套的锁 API 上大量出现 __no_context_analysis 注解,把运行时检查交给编译器前端。这是 Linux 内核正在推进的"编译器辅助锁分析"方向,阅读源码时遇到这两个标记知道其作用即可。

外层 spinlock_t 是驱动和子系统实际使用的类型:

// include/linux/spinlock_types.h:16-30
/* Non PREEMPT_RT kernels map spinlock to raw_spinlock */
context_lock_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];
            struct lockdep_map dep_map;
        };
#endif
    };
};
typedef struct spinlock spinlock_t;

union 的巧妙之处(line 18-28):spinlock_t 与 raw_spinlock_t 二进制布局完全一致——启用 lockdep 时,dep_map 通过 __padding 精确对齐到 raw_spinlock.dep_map 的偏移处。这样 spin_lock() 只需取 &lock->rlock 就能直接转发,两种类型之间零转换开销。

在 CONFIG_PREEMPT_RT 实时内核下,这个映射被整体替换:spinlock_t 变成 struct rt_mutex_base lock(spinlock_types.h:51-56),自旋锁变为可睡眠的 rt_mutex,只有 raw_spinlock_t 仍然是真正的自旋锁。这与 13.1 节所述的 RT 口径一致:实时内核中"普通的自旋锁被转换为基于 rt_mutex 的可睡眠锁"。

四层调用链

从 spin_lock() 到底层锁字,本树展开为清晰的四层:

spin_lock(&lock)                      // include/linux/spinlock.h:339
  |  取 &lock->rlock,纯转发
  v
raw_spin_lock(&lock->rlock)           // include/linux/spinlock.h:218
  |  debug/lockdep 构建下包装 _raw_spin_lock()
  v
_raw_spin_lock()                      // kernel/locking/spinlock.c:152
  |  __lockfunc 导出符号,SMP 实现入口
  v
__raw_spin_lock(lock)                 // include/linux/spinlock_api_smp.h:154
     preempt_disable()
     spin_acquire(&lock->dep_map, ...)   // lockdep 登记
     LOCK_CONTENDED(lock, trylock, lock) // 真正的锁操作

内层实现:

// include/linux/spinlock_api_smp.h:154-171
static inline void __raw_spin_lock(raw_spinlock_t *lock)
    __acquires(lock) __no_context_analysis
{
    preempt_disable();
    spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
    __releases(lock)
{
    spin_release(&lock->dep_map, _RET_IP_);
    do_raw_spin_unlock(lock);
    preempt_enable();
}

操作顺序是"先关抢占,再拿锁"(line 155-158),解锁时对称地"先放锁,再开抢占"。这个顺序不能颠倒:如果先拿锁再关抢占,可能在两步之间被抢占到其他 CPU,锁的持有者就"消失"在别的运行队列上了。spin_acquire()/spin_release() 是 lockdep 的登记点(无 lockdep 构建下为空操作)。

LOCK_CONTENDED 宏(include/linux/lockdep.h:441-448)是 lockstat 的包装:开启 CONFIG_LOCK_STAT 时先 trylock 统计"无争用命中率",失败再进慢路径并统计等待时间;普通构建下退化为直接调用 do_raw_spin_lock(lock),即最终落到 arch_spin_lock()——x86_64 上就是 queued_spin_lock()(16.1.5 节)。

可抢占内核下的自旋循环

在 CONFIG_PREEMPT 内核中,_raw_spin_lock() 用 BUILD_LOCK_OPS 宏生成一个"自旋中保持可抢占"的循环:

// kernel/locking/spinlock.c:67-78
#define BUILD_LOCK_OPS(op, locktype)                    \
static void __lockfunc __raw_##op##_lock(locktype##_t *lock)        \
{                                   \
    for (;;) {                          \
        preempt_disable();                  \
        if (likely(do_raw_##op##_trylock(lock)))        \
            break;                      \
        preempt_enable();                   \
                                    \
        arch_##op##_relax(&lock->raw_lock);         \
    }                               \
}                                   \

trylock-repanic 循环(line 69-77):每次先 preempt_disable() 后尝试 trylock;失败立即 preempt_enable() 交还调度权,让本 CPU 上的高优先级任务得以运行,然后再进入下一轮。这样"等待者"本身不会被饿死调度。BUILD_LOCK_OPS(spin, raw_spinlock) 在 spinlock.c:126 实例化,生成的 _raw_spin_lock() 等 noinline 函数放在 .spinlock.text 段(spinlock.h:84 的 __lockfunc 标记),使 spin_lock() 这个 __always_inline 薄封装不必把慢路径代码内联到每个调用点。

16.1.3 ticket lock —— 票号算法

在排队自旋锁(qspinlock)出现之前,x86 内核长期使用票号锁。Linux 7.0.10 中它退居 include/asm-generic/ticket_spinlock.h,供嵌入式等架构选用(本树 x86 不使用,arch/x86/Kconfig:140 已硬性 select ARCH_USE_QUEUED_SPINLOCKS),但其算法仍是理解所有公平自旋锁的起点。

取号与叫号

票号锁把 32 位锁字分成两个 16 位半字:低 16 位是服务号(head,当前正在被服务的票),高 16 位是取号机(tail,下一个发出的票):

// include/asm-generic/ticket_spinlock.h:33-51
static __always_inline void ticket_spin_lock(arch_spinlock_t *lock)
{
    u32 val = atomic_fetch_add(1<<16, &lock->val);
    u16 ticket = val >> 16;

    if (ticket == (u16)val)
        return;

    /*
     * atomic_cond_read_acquire() is RCpc, but rather than defining a
     * custom cond_read_rcsc() here we just emit a full fence. ...
     */
    atomic_cond_read_acquire(&lock->val, ticket == (u16)VAL);
    smp_mb();
}

取号(line 35-36):atomic_fetch_add(1<<16, &lock->val) 一次原子操作完成"读旧值 + tail 加 1",旧值的高半字就是本任务领到的票号 ticket。

快速无争用路径(line 37-38):如果领到的票恰好等于当前服务号(ticket == (u16)val,即锁无人持有),直接返回,整个加锁就是一条 lock xadd 指令。

等待(line 43-45):否则进入 atomic_cond_read_acquire() 自旋,条件是"服务号推进到自己的票号"。这个原语在 x86 上展开为带 pause 指令的自旋读循环(减少流水线冲刷与总线流量),最终读到匹配值时附带 acquire 语义;随后的 smp_mb() 补足注释中所说的全序要求。

trylock 与 unlock 的对应实现(ticket_spinlock.h:53-69):

// include/asm-generic/ticket_spinlock.h:53-69
static __always_inline bool ticket_spin_trylock(arch_spinlock_t *lock)
{
    u32 old = atomic_read(&lock->val);

    if (old >> 16)
        return false;

    return atomic_try_cmpxchg(&lock->val, &old, old + (1 << 16));
}

static __always_inline void ticket_spin_unlock(arch_spinlock_t *lock)
{
    /*
     * unlock() needs release semantics:
     */
    u16 val = atomic_add_return_release(1, &lock->val) >> 16;
    ...
}

trylock 的判据是 old >> 16 == 0——tail 为 0 表示"没有任何人排队",此时一次 cmpxchg 把 tail 加 1 就等于领到了正在被服务的票。unlock 则是服务号 head 加 1(release 语义),等价于柜台"叫下一个号"。

公平性与代价

票号锁的核心价值是严格 FIFO 公平:票号顺序即获取顺序,任何等待者最多等 (N-1) 张票,不存在"刚来的抢走等了很久的"这一不公平现象(x86 早期无队列的 xchg 自旋锁就有此问题)。

代价同样明显:

  1. 所有等待者自旋同一个缓存行。lock->val 所在的缓存行被 unlock 时的写操作打回失效,所有 CPU 的自旋者同时发起缓存行争用(cache line ping-pong),CPU 数越多风暴越烈。
  2. 自旋无差别耗电。等待者无法区分"即将轮到我"和"前面还有几十个",只能一律忙等。
  3. 唤醒风暴。unlock 瞬间 N 个自旋者同时看到服务号变化、同时重试缓存行访问。

qspinlock 的设计目标正是消除这三点:用 per-CPU 的 MCS 队列节点把"自旋在全局锁字上"改为"自旋在自己的缓存行上",并用 pending 位在锁字里"原地消化"第二个竞争者。

16.1.4 qspinlock 的 32 位编码

锁字布局

x86_64 的 arch_spinlock_t 是一个 32 位字(include/asm-generic/qspinlock_types.h:14-44):

// include/asm-generic/qspinlock_types.h:14-44
typedef struct qspinlock {
    union {
        atomic_t val;
        /*
         * By using the whole 2nd least significant byte for the
         * pending bit, we can allow better optimization of the lock
         * acquisition for the pending bit holder.
         */
#ifdef __LITTLE_ENDIAN
        struct {
            u8  locked;
            u8  pending;
        };
        struct {
            u16 locked_pending;
            u16 tail;
        };
#else
        ...
#endif
    };
} arch_spinlock_t;

位域划分由 qspinlock_types.h:67-93 的宏推导:

// include/asm-generic/qspinlock_types.h:67-93
#define _Q_SET_MASK(type)   (((1U << _Q_ ## type ## _BITS) - 1)\
                << _Q_ ## type ## _OFFSET)
#define _Q_LOCKED_OFFSET    0
#define _Q_LOCKED_BITS      8
#define _Q_PENDING_OFFSET   (_Q_LOCKED_OFFSET + _Q_LOCKED_BITS)
#if CONFIG_NR_CPUS < (1U << 14)
#define _Q_PENDING_BITS     8
#else
#define _Q_PENDING_BITS     1
#endif
#define _Q_TAIL_IDX_OFFSET  (_Q_PENDING_OFFSET + _Q_PENDING_BITS)
#define _Q_TAIL_IDX_BITS    2
#define _Q_TAIL_CPU_OFFSET  (_Q_TAIL_IDX_OFFSET + _Q_TAIL_IDX_BITS)
#define _Q_TAIL_CPU_BITS    (32 - _Q_TAIL_CPU_OFFSET)
#define _Q_TAIL_OFFSET      _Q_TAIL_IDX_OFFSET
#define _Q_LOCKED_VAL       (1U << _Q_LOCKED_OFFSET)
#define _Q_PENDING_VAL      (1U << _Q_PENDING_OFFSET)
字段 位范围 含义
locked(bit 0-7) _Q_LOCKED_VAL = 1 锁被持有(0=空闲,非 0=持有)
pending(bit 8) _Q_PENDING_VAL = 0x100 已有一个"等待中的第二竞争者"在锁字上自旋
tail idx(2 位) bit 16-17 队列节点在该 CPU 的嵌套层级编号
tail cpu+1(14 位) bit 18-31 队尾节点所在的 CPU 编号加 1

三个编码细节值得注意:

  1. pending 独占整个第二字节(本树 CONFIG_NR_CPUS < 16384 时 8 位),注释说明这是为了让 pending 持有者的优化不受 locked 字节读写的影响(line 18-21)。
  2. CPU 编号统一 +1 编码:tail = 0 才能表示"队列为空",这与 OSQ 的 encode_cpu()(16.2.4 节)同一个套路。
  3. 若系统支持 16384 个以上 CPU,pending 缩为 1 位给 tail 腾位;slowpath 入口处的 BUILD_BUG_ON(CONFIG_NR_CPUS >= (1U << _Q_TAIL_CPU_BITS))(qspinlock.c:136)在编译期把关 tail 字段的容量。

locked_pending 与 tail 两个 16 位视图(line 26-28)服务于"一次性读写低半字/高半字"的优化路径。

16.1.5 queued_spin_lock —— 快路径与三态状态机

快路径:一次 cmpxchg

// include/asm-generic/qspinlock.h:107-129
static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
    int val = 0;

    if (likely(atomic_try_cmpxchg_acquire(&lock->val, &val, _Q_LOCKED_VAL)))
        return;

    queued_spin_lock_slowpath(lock, val);
}
...
static __always_inline void queued_spin_unlock(struct qspinlock *lock)
{
    /*
     * unlock() needs release semantics:
     */
    smp_store_release(&lock->locked, 0);
}

无争用加锁只是一条 lock cmpxchg(line 108-112):把锁字从 0 换成 _Q_LOCKED_VAL,成功即获得锁,acquire 语义保证临界区访问不会被重排到加锁之前。失败时 cmpxchg 把锁字当前值留在 val 里传给慢路径——慢路径不需要再读一次锁字,这个细节避免了重复的缓存行访问。

解锁同样是单条指令级操作(line 119-127):smp_store_release() 在 x86 上退化为对 locked 字节的普通 store(x86 的 store 自带 release 语义),但不解锁 MCS 队列——队首唤醒由拿锁者传递(16.1.8 节),这是 qspinlock 与 ticket lock 的本质区别之一。

三态状态机

kernel/locking/qspinlock.c:116-129 的注释给出了完整状态机,(*,*,*) 记为 (pending, tail, locked):

 *              fast     :    slow                                  :    unlock
 *                       :                                          :
 * uncontended  (0,0,0) -:--> (0,0,1) ------------------------------:--> (*,*,0)
 *                       :       | ^--------.------.             /   :
 * pending               :    (0,1,1) +--> (0,1,0)   \           |   :
 * uncontended           :    (n,x,y) +--> (n,0,0) --'           |   :
 *   queue               :       | ^--'                          |   :
 * contended             :    (*,x,y) +--> (*,0,0) ---> (*,0,1) -'   :
 *   queue               :         ^--'                             :

三态设计的本质是让锁按竞争烈度逐级"升温":

  1. uncontended:锁字在 (0,0,0) 与 (0,0,1) 之间翻转,纯 cmpxchg 路径。
  2. pending:第二个竞争者把锁字推到 (0,1,x),只允许一个任务在锁字旁"原地自旋",其余不再涌入。
  3. queue:第三个及以后的竞争者进入 per-CPU 的 MCS 队列 (n,x,y),各自自旋在自己的缓存行上。

16.1.6 slowpath 阶段一:pending 位

慢路径入口 queued_spin_lock_slowpath()(qspinlock.c:130 起)的第一段处理 pending 位:

// kernel/locking/qspinlock.c:156-206
    /*
     * If we observe any contention; queue.
     */
    if (val & ~_Q_LOCKED_MASK)
        goto queue;

    /*
     * trylock || pending
     *
     * 0,0,* -> 0,1,* -> 0,0,1 pending, trylock
     */
    val = queued_fetch_set_pending_acquire(lock);

    if (unlikely(val & ~_Q_LOCKED_MASK)) {

        /* Undo PENDING if we set it. */
        if (!(val & _Q_PENDING_MASK))
            clear_pending(lock);

        goto queue;
    }

    if (val & _Q_LOCKED_MASK)
        smp_cond_load_acquire(&lock->locked, !VAL);

    /*
     * take ownership and clear the pending bit.
     *
     * 0,1,0 -> 0,0,1
     */
    clear_pending_set_locked(lock);
    lockevent_inc(lock_pending);
    return;

逐段解读:

  • line 158-160:进入慢路径时观察到的锁字若已含 pending 或 tail 位(val & ~_Q_LOCKED_MASK),说明竞争已经很热,直接跳过 pending 阶段去排队,避免在锁字上堆积。
  • line 165-166:queued_fetch_set_pending_acquire() 尝试原子地"置 pending 位并读回旧值"——x86 上这是一条 btsl(arch/x86/include/asm/qspinlock.h:24-31)。谁成功置上 pending,谁就是唯一的"锁字旁等待者"。
  • line 168-175:如果置位后发现有别人也在排队(旧值含 pending/tail),说明自己抢 pending 失败,回滚(clear_pending())并转入队列。
  • line 177-178:pending 持有者只需等 locked 字节清零。smp_cond_load_acquire() 在 x86 上展开为带 pause 的自适应自旋读,acquire 语义直接接住锁的所有权。
  • line 183-184:clear_pending_set_locked() 一次写完成"清 pending + 置 locked"((0,1,0) → (0,0,1)),状态机中标注的这条转换路径就是本阶段。

bounded 自旋:若锁持有者迟迟不放,x86 定义 _Q_PENDING_LOOPS (1 << 9)(arch/x86/include/asm/qspinlock.h:14),pending 等待者自旋 512 次后放弃,转入队列,避免 pending 位成为新的风暴点。

16.1.7 slowpath 阶段二:MCS 队列入队

per-CPU 节点数组

队列节点来自 per-CPU 数组(qspinlock.c:80):

static DEFINE_PER_CPU_ALIGNED(struct qnode, qnodes[_Q_MAX_NODES]);

_Q_MAX_NODES = 4(kernel/locking/qspinlock.h:16-28),四个槽位对应四种嵌套上下文:task、softirq、hardirq、NMI。同一 CPU 在不同嵌套层级中同时争用同一把锁时,各用各的节点,互不覆盖。若 NMI 中再发生第五层嵌套(实际不可能),代码退化为"不排队、直接自旋"(qspinlock.c:230-235)——正确性由 NMI 的极短临界区保证。

tail 编码与链入

// kernel/locking/qspinlock.h:52-69
static inline __pure u32 encode_tail(int cpu, int idx)
{
    u32 tail;

    tail  = (cpu + 1) << _Q_TAIL_CPU_OFFSET;
    tail |= idx << _Q_TAIL_IDX_OFFSET; /* assume < 4 */

    return tail;
}
...
static inline struct qnode *decode_tail(u32 tail, struct qnode *qnodes)
{
    int cpu = ((tail >> _Q_TAIL_CPU_OFFSET) - 1) % nr_cpu_ids;
    int idx = (tail & _Q_TAIL_MASK) >> _Q_TAIL_IDX_OFFSET;

    return per_cpu_ptr(&qnodes[idx], cpu);
}

tail 字段只存 (cpu+1, idx) 这一对编号——锁字里不存指针,32 位锁字的容量瓶颈就此消除。入队主体在 qspinlock.c:212-291:

// kernel/locking/qspinlock.c:212-291
queue:
    lockevent_inc(lock_slowpath);
pv_queue:
    node = this_cpu_ptr(&qnodes[0].mcs);
    idx = node->count++;
    tail = encode_tail(smp_processor_id(), idx);
    ...
    old = xchg_tail(lock, tail);
    next = NULL;

    /*
     * if there was a previous node; link it and wait until reaching the
     * head of the waitqueue.
     */
    if (old & _Q_TAIL_MASK) {
        prev = decode_tail(old, qnodes);

        /* Link @node into the waitqueue. */
        WRITE_ONCE(prev->next, node);

        pv_wait_node(node, prev);
        arch_mcs_spin_lock_contended(&node->locked);

关键操作序列:

  1. xchg_tail(line 227 附近):原子地把锁字的 tail 字段换成自己的编码,拿回旧 tail。这条 xchg 是整个排队机制的"登记动作",之后锁字上不再有本任务的事。
  2. 链入队列(line 288):旧 tail 非空说明有前驱,WRITE_ONCE(prev->next, node) 把自己挂到前驱的 next 指针上。
  3. 自旋在自己的节点上(line 291):arch_mcs_spin_lock_contended(&node->locked) 等待自己节点的 locked 标志——前驱解锁时会写这个字段。等待发生在本 CPU 私有的缓存行上,与前驱的解锁写、后继的读各不相扰,cache line ping-pong 问题就此消除。

16.1.8 slowpath 阶段三:队首拿锁与锁传递

排队者升到队首后,进入拿锁循环(qspinlock.c:325-379):

// kernel/locking/qspinlock.c:325-379
    /*
     * We're pending, we're at the head of the waitqueue, wait for the
     * owner to release the lock.
     */
    ...
    for (;;) {
        if (atomic_cond_read_acquire(&lock->val,
                         !(VAL & _Q_LOCKED_PENDING_MASK)))
            break;
    }

    /* take ownership and clear the tail */
    set_locked(lock);
    ...
    if (next) {
        arch_mcs_spin_unlock_contended(&next->locked);
        ...
    }

队首的双重等待(line 328 附近):队首节点可以安全地盯回全局锁字——因为自己是唯一有资格拿锁的等待者,atomic_cond_read_acquire() 等条件 !(VAL & _Q_LOCKED_PENDING_MASK),即 locked 与 pending 都清零。读到后 set_locked()(qspinlock.h:79-99,直接置 _Q_LOCKED_VAL)接管锁。

锁传递(line 368-372):持锁者用完锁,queued_spin_unlock() 清掉 locked 字节;队首拿锁者不读全局锁字等待,而是由前驱显式把后继节点的 locked 字段置 1——arch_mcs_spin_unlock_contended(&next->locked)(line 370)。这样"下一个是谁"的信息始终走 MCS 链传播,全局锁字上永远只有至多一个竞争读,风暴被彻底隔离。随后释放节点计数(line 379,node->count--),本次加锁完成。

16.1.9 pvqspinlock —— 半虚拟化扩展

在 KVM 虚拟机里,自旋者可能在宿主机上被调度走(vCPU 抢占),此时"自旋"纯属烧别人的 CPU。CONFIG_PARAVIRT_SPINLOCKS(arch/x86/Kconfig:827-836,默认关闭)启用 pvqspinlock:内核在启动时通过 PV OP 探测 hypervisor 是否支持,把慢路径替换为 __pv_queued_spin_lock_slowpath()。

实现技巧在 qspinlock.c:386-410:整个文件被二次 #include 自身(定义 _GEN_PV_LOCK_SLOWPATH 后再包含一遍),由预处理生成一份带 PV 钩子的慢路径副本;kernel/locking/qspinlock_paravirt.h 提供 pv 侧逻辑,用 _Q_SLOW_VAL = 3<<0(qspinlock_paravirt.h:24)标记"锁字已被 PV 哈希表登记",空闲 vCPU 上的自旋者通过 pv_wait() 交给 hypervisor 睡眠,由持锁者的 pv_kick() 唤醒。虚拟化场景分析(第 49-51 章)会再次遇到这套机制。

16.1.10 使用实例与总结

驱动中保护通知链注册的典型 irqsave 用法:

// drivers/char/random.c:156-168
int __cold execute_with_initialized_rng(struct notifier_block *nb)
{
    unsigned long flags;
    int ret = 0;

    spin_lock_irqsave(&random_ready_notifier.lock, flags);
    if (crng_ready())
        nb->notifier_call(nb, 0, NULL);
    else
        ret = raw_notifier_chain_register((struct raw_notifier_head *)&random_ready_notifier.head, nb);
    spin_unlock_irqrestore(&random_ready_notifier.lock, flags);
    return ret;
}

临界区只有"读一个标志或挂一个链表节点"两条纯内存操作,用自旋锁最合适;因为回调可能在任意上下文触发,故用 irqsave 变体(14.3 节的 siglock 规则与此同理)。

本章要点回顾:

层 内容 位置
API 层 spin_lock() = preempt_disable + 底层锁 include/linux/spinlock.h:339
生成层 BUILD_LOCK_OPS 可抢占自旋循环 kernel/locking/spinlock.c:67
算法层 ticket lock(FIFO 票号,其他架构可选) include/asm-generic/ticket_spinlock.h:33
算法层 qspinlock(x86 默认,三态升温) kernel/locking/qspinlock.c:130

qspinlock 的三态设计——"一个 cmpxchg、一个 pending 位、一条 MCS 队列"——体现了内核锁实现的典型演进路径:先让最常见情况接近免费,再让次常见情况可忍受,最后才付出队列的完整代价。读写场景下的同类演进见 16.3 节的 qrwlock。


16.2 互斥锁 —— mutex_t 与乐观等待

互斥锁(mutex)是内核为"可能睡眠的临界区"提供的标准互斥原语:拿不到锁时,任务不空转烧 CPU,而是挂到等待队列上睡眠,让出处理器给更有价值的工作。它的名字常被写作 mutex_t,但内核源码中的实际类型是 struct mutex(本节沿 SUMMARY 目录沿用 "mutex_t" 作为主题名,正文以源码为准)。互斥锁的设计精髓在"乐观等待":锁被短暂持有时,先赌一把在原地自旋几个周期,赌赢就省下两次上下文切换;赌输了再排队睡眠,而且排队时还要经过 OSQ 队列排队自旋这一级缓冲。本节将结合 Linux 7.0.10 内核源码,逐行分析 struct mutex 的编码设计、加锁五级阶梯(快路径 → trylock → 乐观自旋 → 睡眠等待 → handoff),以及解锁路径的唤醒逻辑。


16.2.1 struct mutex 的数据结构

类型定义

// include/linux/mutex_types.h:41-54
context_lock_struct(mutex) {
    atomic_long_t       owner;
    raw_spinlock_t      wait_lock;
#ifdef CONFIG_MUTEX_SPIN_ON_OWNER
    struct optimistic_spin_queue osq; /* Spinner MCS lock */
#endif
    struct list_head    wait_list;
#ifdef CONFIG_DEBUG_MUTEXES
    void            *magic;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map  dep_map;
#endif
};

注意一个常见误解:本树的 struct mutex 没有 count 字段。老教材描述的 "count 0↔1、负数表示有等待者" 的三态计数器,在本树中已完全编码进 owner 一个字。

四个核心字段:

  • owner(atomic_long_t):整个锁的状态机。值为 0 表示未锁;否则高位的任务指针记录持锁者,低 3 位是状态标志。
  • wait_lock(raw_spinlock_t):保护 wait_list 与 owner 变更的内部自旋锁。它必须是 raw_spinlock_t——在 PREEMPT_RT 内核中普通自旋锁会变睡眠锁,而锁内部的簿记锁绝不能睡眠。
  • osq(optimistic_spin_queue):乐观自旋者之间的 MCS 排队锁(16.2.4 节),仅在 SMP 且开启 CONFIG_MUTEX_SPIN_ON_OWNER(kernel/Kconfig.locks:227-229,默认 y)时存在。
  • wait_list:struct mutex_waiter 双向链表,睡眠等待者按 FIFO 排队。

owner 低 3 位标志

// kernel/locking/mutex.h:23-36
 * @owner: contains: 'struct task_struct *' to the current lock owner,
 * NULL means not owned. ...
 * Bit0 indicates a non-empty waiter list; unlock must issue a wakeup.
 * Bit1 indicates unlock needs to hand the lock to the top-waiter
 * Bit2 indicates handoff has been done and we're waiting for pickup.
 */
#define MUTEX_FLAG_WAITERS  0x01
#define MUTEX_FLAG_HANDOFF  0x02
#define MUTEX_FLAG_PICKUP   0x04

#define MUTEX_FLAGS     0x07
标志 值 语义
MUTEX_FLAG_WAITERS 0x01 等待队列非空;解锁者必须做唤醒
MUTEX_FLAG_HANDOFF 0x02 解锁时必须把锁直接移交给队首等待者(防抢锁饿死)
MUTEX_FLAG_PICKUP 0x04 移交已完成,持锁资格待队首"拾取"

任务指针与标志共存的编码依赖任务指针的天然对齐——task_struct 按 L1_CACHE_BYTES 对齐(见 8.1 节),低 3 位永远为 0,可安全挪作标志位。__mutex_owner()/__owner_task()(kernel/locking/mutex.h:38-48)负责在指针与标志间拆分。

等待者结构

// kernel/locking/mutex.h:14-21
struct mutex_waiter {
    struct list_head    list;
    struct task_struct  *task;
    struct ww_acquire_ctx   *ww_ctx;
#ifdef CONFIG_DEBUG_MUTEXES
    void            *magic;
#endif
};

mutex_waiter 挂在睡眠任务的内核栈上(__mutex_lock_common() 中的栈变量 struct mutex_waiter waiter),而非堆分配——等待者数量天然等于睡眠任务数,无需预分配。这要求互斥锁的睡眠等待绝不能跨任务栈生命周期,也解释了为什么 signal_pending 一类条件退出必须在同一栈帧内完成清理。

16.2.2 mutex_lock() 五级阶梯总览

从最快到最慢,mutex_lock() 的获取路径是五级阶梯:

mutex_lock()
  |
  |-- [1] __mutex_trylock_fast()      owner: 0 -> current       (无锁 cmpxchg)
  |        失败
  |-- [2] __mutex_trylock()           含 WAITERS/HANDOFF 标志语义的 trylock
  |        失败
  |-- [3] mutex_optimistic_spin()     OSQ 排队 + 盯 owner 自旋   (不睡眠)
  |        失败
  |-- [4] wait_list 排队 + schedule_preempt_disabled() 睡眠     (FIFO)
  |        被唤醒
  |-- [5] __mutex_trylock_or_handoff() HANDOFF 语义拾取
  v
  获得锁

这个阶梯的思想与 qspinlock 三态(16.1.5 节)一脉相承:竞争越轻的路径越便宜,代价高昂的睡眠永远是最后选项。

第 1 级:无争用快路径

// kernel/locking/mutex.c:152-170
static __always_inline bool __mutex_trylock_fast(struct mutex *lock)
{
    unsigned long curr = (unsigned long)current;
    unsigned long zero = 0UL;

    MUTEX_WARN_ON(lock->magic != lock);

    if (atomic_long_try_cmpxchg_acquire(&lock->owner, &zero, curr))
        return true;

    return false;
}

无争用加锁 = 一次 cmpxchg:把 owner 从 0 换成自己的任务指针。acquire 语义确保临界区访问不越过加锁点。入口函数(mutex.c:285-291)只是快路径失败后转入慢路径:

// kernel/locking/mutex.c:285-291
void __sched mutex_lock(struct mutex *lock)
{
    might_sleep();

    if (!__mutex_trylock_fast(lock))
        __mutex_lock_slowpath(lock);
}

might_sleep() 是一个运行期契约检查:在原子上下文(preempt_count 非 0,15.1 节)调用 mutex_lock 会在此处报 BUG——互斥锁绝不能在中断、软中断或持自旋锁时使用。解锁的对称快路径 __mutex_unlock_fast()(mutex.c:165-170)是 release 语义的 cmpxchg:owner 从 current 换回 0。

一个本树特有的事实:mutex_lock() 整个函数被 #ifndef CONFIG_DEBUG_LOCK_ALLOC 包裹(mutex.c:282-293),深度调试构建下直接进入 nested 慢路径入口 __mutex_lock(),让 lockdep 完整接管参数检查——快路径只在正常构建下生效。

16.2.3 __mutex_lock_common() —— 慢路径主体

慢路径入口 __mutex_lock_common()(mutex.c:577-770)的流程骨架:

// kernel/locking/mutex.c:613-692
    preempt_disable();
    mutex_acquire_nest(&lock->dep_map, subclass, 0, nest_lock, ip);

    trace_contention_begin(lock, LCB_F_MUTEX | LCB_F_SPIN);
    if (__mutex_trylock(lock) ||
        mutex_optimistic_spin(lock, ww_ctx, NULL)) {
        /* got the lock, yay! */
        lock_acquired(&lock->dep_map, ip);
        ...
        preempt_enable();
        return 0;
    }

    raw_spin_lock_irqsave(&lock->wait_lock, flags);
    /*
     * After waiting to acquire the wait_lock, try again.
     */
    if (__mutex_trylock(lock)) {
        ...
        goto skip_wait;
    }
    ...
    if (!use_ww_ctx) {
        /* add waiting tasks to the end of the waitqueue (FIFO): */
        __mutex_add_waiter(lock, &waiter, &lock->wait_list);
    }
    ...
    set_current_state(state);
    for (;;) {
        bool first;
        ...
        if (__mutex_trylock(lock))
            goto acquired;
        ...
        raw_spin_unlock_irqrestore_wake(&lock->wait_lock, flags, &wake_q);

        schedule_preempt_disabled();

慢路径的五步:

  1. 第 2 级 trylock(line 617):__mutex_trylock() 与快路径不同,它理解 owner 标志语义(16.2.5 节),能处理"有等待者但锁刚好空出"等状态。
  2. 第 3 级乐观自旋(line 617):mutex_optimistic_spin() 自旋期间不睡眠,成功则直接返回(此时 preempt 已关)。
  3. 锁 wait_lock 后第三次 trylock(line 627-632):进入队列前的最后一次尝试——wait_lock 的获取本身有时间窗口,期间锁可能已被释放。
  4. FIFO 入队(line 642-645):__mutex_add_waiter() 把栈上的 waiter 挂到 wait_list 尾部,并把 owner 置上 MUTEX_FLAG_WAITERS。
  5. 睡眠循环(line 653-692):set_current_state(state) 设睡眠状态(互斥锁用 TASK_UNINTERRUPTIBLE),释放 wait_lock,schedule_preempt_disabled() 交出 CPU。被唤醒后回到循环顶部再 trylock——醒来不等于拿到锁,这与 8.2 节 "唤醒与获得锁是两回事" 的状态机规则完全一致。

trylock-repanic 语义(line 660-664):每次被唤醒都重新持 wait_lock、重试 trylock,防止虚假唤醒和竞争窗口破坏一致性。

第 2 级 trylock 的原子语义

// kernel/locking/mutex.c:84-134
static inline bool __mutex_trylock_common(struct mutex *lock, bool handoff)
{
    unsigned long curr, owner = atomic_long_read(&lock->owner);

    for (;;) {
        unsigned long flags;

        /* We own the lock already? */
        if (__owner_task(owner) == current) {
            /*
             * o -> 0: ... trylock, but we already own the lock
             */
            MUTEX_WARN_ON(!owner & MUTEX_FLAG_WAITERS);
            return false;
        }

        flags = owner & MUTEX_FLAGS;

        if (flags & MUTEX_FLAG_PICKUP) {
            if (!handoff)
                return false;

            flags &= ~MUTEX_FLAG_PICKUP;
        } else if (flags & MUTEX_FLAG_HANDOFF) {
            if (!handoff)
                return false;
        }

        curr = (unsigned long)current | flags;

        if (atomic_long_try_cmpxchg_acquire(&lock->owner,
                            &owner, curr)) {
            ...
            return true;
        }
    }
}

普通 trylock(handoff=false)只接受"无标志的空闲锁"(owner==0);带 PICKUP/HANDOFF 标志的锁只能由指定路径接收。cmpxchg 循环处理标志在竞争窗口内的变化——每次失败后用最新的 owner 值重算。

handoff 拾取(16.2.6 节):睡眠循环中队首等待者用 __mutex_trylock_or_handoff(lock, first)(mutex.c:708)接收移交的锁,此时 cmpxchg 保留 HANDOFF→清除 PICKUP 的转换路径。

16.2.4 第 3 级:乐观自旋与 OSQ

mutex_optimistic_spin()

// kernel/locking/mutex.c:444-481
static __always_inline bool
mutex_optimistic_spin(struct mutex *lock, struct ww_acquire_ctx *ww_ctx,
              struct mutex_waiter *waiter)
{
    if (!waiter) {
        /*
         * The purpose of the mutex_can_spin_on_owner() function is
         * to eliminate the overhead of osq_lock() and osq_unlock()
         * in case spinning isn't possible. ...
         */
        if (!mutex_can_spin_on_owner(lock))
            goto fail;

        /*
         * In order to avoid a stampede of mutex spinners trying to
         * acquire the mutex all at once, the spinners need to take a
         * MCS (queued) lock first before spinning on the owner field.
         */
        if (!osq_lock(&lock->osq))
            goto fail;
    }

    for (;;) {
        struct task_struct *owner;

        /* Try to acquire the mutex... */
        owner = __mutex_trylock_or_owner(lock);
        if (!owner)
            break;

        if (!mutex_spin_on_owner(lock, owner, ww_ctx, waiter))
            goto fail_unlock;
        ...

流程:

  1. 预检(line 450):mutex_can_spin_on_owner() 先看持锁者是否正在某个 CPU 上运行(owner_on_cpu()),若持锁者已睡眠,自旋毫无意义;同时若本任务 need_resched() 也放弃(mutex.c:403-404)。
  2. OSQ 排队自旋(line 457-459):注释点明动机——防止"自旋者踩踏"(stampede)。所有想乐观自旋的任务先在 lock->osq 这把 MCS 锁上排队,同一时刻只有一个自旋者盯着 owner 字段。这把全局自旋风暴重新隔离到 per-CPU 节点上,与 qspinlock 的 MCS 队列(16.1.7 节)同一设计哲学。
  3. 自旋主循环(line 464-472):__mutex_trylock_or_owner() 一次原子操作返回两种结果——拿到锁返回 NULL(break),拿不到返回当前 owner;非 NULL 则检查 mutex_spin_on_owner() 决定继续与否。

放弃自旋的条件

// kernel/locking/mutex.c:355-421
static bool mutex_spin_on_owner(struct mutex *lock,
                struct task_struct *owner,
                struct ww_acquire_ctx *ww_ctx,
                struct mutex_waiter *waiter)
{
    bool ret = true;
    ...
    lockdep_assert_preemption_disabled();

    for (;;) {
        int prev = READ_ONCE(waiter->ww_ctx ? waiter->ww_ctx->deadlock : 0);
        ...
        /*
         * If the owner changed, we must not spin.
         */
        if (!owner_on_cpu(owner)) {
            ret = false;
            break;
        }

        /*
         * Provide a ST_SPIN_ON_OWNER spark for debugging.
         */
        if (need_resched()) {
            ...
            ret = false;
            break;
        }
        ...
        cpu_relax();
    }
    ...
}

两条退出线(mutex.c:377-380):

  • 持锁者睡眠:owner_on_cpu() 为假——owner 不在任何 CPU 上运行,说明临界区比"短"长得多,继续自旋纯属浪费。
  • 本任务需要调度:need_resched()——自旋者不是最高优先级了,必须退出让路。

退出后若 need_resched() 为真,mutex_optimistic_spin() 走 schedule_preempt_disabled() 再返回(mutex.c:508-515),保证让出 CPU 而非带着 pending 的调度请求继续运行。OSQ 排队自旋的价值在于自旋者的退出自动把机会让给队里下一位,全队共享一个"是否值得自旋"的判断。

OSQ:乐观自旋队列的实现

OSQ 本体只是一个 32 位 tail 字段(include/linux/osq_lock.h:10-16):

// kernel/locking/osq_lock.c:15-42
struct optimistic_spin_node {
    struct optimistic_spin_node *next, *prev;
    int locked; /* 1 if lock acquired */
    int cpu; /* encoded CPU # + 1 value */
};

static DEFINE_PER_CPU_SHARED_ALIGNED(struct optimistic_spin_node, osq_node);

/*
 * We use the value 0 to represent "no CPU", thus the encoded value
 * will be the CPU number incremented by 1.
 */
static inline int encode_cpu(int cpu_nr)
{
    return cpu_nr + 1;
}

节点同样是 per-CPU 单实例(line 21 的 DEFINE_PER_CPU_SHARED_ALIGNED)。其安全性依赖文件头注释(osq_lock.c:6-13)声明的两条前提:OSQ 只用于睡眠锁的乐观自旋(不会在中断上下文被调用),且自旋期间抢占已关——否则同一 CPU 嵌套两层自旋会覆盖同一个节点。

// kernel/locking/osq_lock.c:93-148
bool osq_lock(struct optimistic_spin_queue *lock)
{
    struct optimistic_spin_node *node = this_cpu_ptr(&osq_node);
    struct optimistic_spin_node *prev, *next;
    int curr = encode_cpu(smp_processor_id());
    int old;

    node->locked = 0;
    node->next = NULL;
    node->cpu = curr;

    /*
     * We need both ACQUIRE (pairs with corresponding RELEASE in
     * unlock() uncontended, or fastpath) and RELEASE (to publish
     * the node fields we just initialised) semantics when updating
     * the lock tail.
     */
    old = atomic_xchg(&lock->tail, curr);
    if (old == OSQ_UNLOCKED_VAL)
        return true;

    prev = decode_cpu(old);
    node->prev = prev;
    ...
    smp_wmb();

    WRITE_ONCE(prev->next, node);
    ...
    /*
     * Wait to acquire the lock or cancellation. Note that need_resched()
     * will come with an IPI, which will wake smp_cond_load_relaxed() ...
     */
    if (smp_cond_load_relaxed(&node->locked, VAL || need_resched() ||
                  vcpu_is_preempted(node_cpu(node->prev))))
        return true;

快路径(line 107-109):atomic_xchg 把 tail 换成自己的 CPU 编码;旧值为 OSQ_UNLOCKED_VAL = 0(osq_lock.h:18)即无人排队,直接获得自旋资格。

排队(line 111-124):smp_wmb() 保证节点字段初始化先于指针发布(release 语义),随后 prev->next = node 链入队列——与 qspinlock 的 WRITE_ONCE(prev->next, node)(16.1.7 节)完全同构。

带逃生口的等待(line 138-147):smp_cond_load_relaxed 的条件里内建了两个退出项——need_resched()(调度需求经 IPI 到达,能中断自旋读)和 vcpu_is_preempted()(虚拟机里前驱 vCPU 可能被抢占,等待无意义)。需要退出时进入"取消排队"流程:先 cmpxchg 摘除自己(osq_lock.c:159-183 稳定 prev),再 osq_wait_next() 稳定 next(:192),最后重连 prev->next 与 next->prev(:204-205)——双向链表的无锁删除三步曲。

解锁路径 osq_unlock()(osq_lock.c:210-234)三分支:无竞争 cmpxchg 归零;node->next 已就位则直接 WRITE_ONCE(next->locked, 1) 踢醒下一位;否则 osq_wait_next() 兜底等待后继稳定后传递。

16.2.5 第 4 级:睡眠等待与第 5 级 handoff

为什么需要 handoff

睡眠等待有一个固有的不公平:锁释放瞬间,新来的乐观自旋者(还在 CPU 上跑着)几乎总能抢过刚被唤醒、还在穿内核的睡眠者。极端情况下等待队列头部会一直被"起跑线更靠前的新人"截胡——队首等待者被唤醒后再次 trylock 失败、再次睡眠,形成抢锁饿死(lock stealing starvation)。

MUTEX_FLAG_HANDOFF 就是解药。解锁者发现队首等待者已被唤醒过一次仍没拿到锁(或直接由 __mutex_unlock_slowpath() 决定),就给 owner 置上 HANDOFF 标志:这把锁已经有主了,别人休想偷。

移交与拾取

移交动作在 __mutex_handoff()(mutex.c:235-253):

// kernel/locking/mutex.c:235-253
static void __mutex_handoff(struct mutex *lock,
                struct task_struct *task)
{
    unsigned long owner = atomic_long_read(&lock->owner);

    for (;;) {
        unsigned long old, new;

        MUTEX_WARN_ON(__owner_task(owner) != current);
        MUTEX_WARN_ON(!(owner & MUTEX_FLAG_WAITERS));

        old = owner;
        new = (unsigned long)task;
        if (task) {
            new |= MUTEX_FLAG_PICKUP;
            new &= ~MUTEX_FLAG_HANDOFF;
        }
        ...
        if (atomic_long_try_cmpxchg(&lock->owner, &old, new))
            break;
        ...
    }
}

owner 的指针部分直接换成队首任务,标志位做 HANDOFF → PICKUP 的转换——锁的所有权在 cmpxchg 中显式过户。队首任务醒来后 __mutex_trylock_common(handoff=true) 沿 PICKUP 路径拾取(16.2.3 节)。整个链路中,乐观自旋者看到带 HANDOFF/PICKUP 标志的 owner 一律放弃偷锁(__mutex_trylock_or_owner() 对标志位敏感)。

waiter 醒来后的二次乐观自旋:__mutex_lock_common() 中队首等待者被唤醒时把栈上 waiter 传入 mutex_optimistic_spin(lock, ww_ctx, &waiter)(mutex.c:711-723)——此时不再 OSQ 排队(if (!waiter) 分支),因为队首身份本身就是"自旋资格"。这是手握 HANDOFF 的等待者被进一步插队时的最后防线。

睡眠状态的唤醒路径

mutex_unlock() 的慢路径负责唤醒(mutex.c:931-982):

// kernel/locking/mutex.c:940-981
    /*
     * Release the lock before (potentially) taking the spinlock such that
     * other contenders can get on with things ASAP.
     *
     * Except when HANDOFF, in that case we must not clear the owner field,
     * but instead set it to the top waiter.
     */
    owner = atomic_long_read(&lock->owner);
    for (;;) {
        MUTEX_WARN_ON(__owner_task(owner) != current);

        if (owner & MUTEX_FLAG_HANDOFF)
            break;

        if (atomic_long_try_cmpxchg_release(&lock->owner, &owner, __owner_flags(owner))) {
            if (owner & MUTEX_FLAG_WAITERS)
                break;

            return;
        }
    }

    raw_spin_lock_irqsave(&lock->wait_lock, flags);
    debug_mutex_unlock(lock);
    if (!list_empty(&lock->wait_list)) {
        /* get the first entry from the wait-list: */
        struct mutex_waiter *waiter =
            list_first_entry(&lock->wait_list,
                     struct mutex_waiter, list);
        ...
        wake_q_add(&wake_q, next);
    }

解锁慢路径的三个关键点:

  1. 先放锁再拿 wait_lock(line 940-953 注释):释放锁字(owner 换回 0,保留标志)发生在获取 wait_lock 之前,让等锁者尽快前进。唯一例外是 HANDOFF——此时 owner 不能清零,要等 __mutex_handoff() 过户。
  2. release cmpxchg(line 950):atomic_long_try_cmpxchg_release() 保证临界区写先于锁释放可见(15.2 节的 RELEASE 语义)。
  3. wake_q 批量唤醒(line 963-979):从 wait_list 取队首 waiter,wake_q_add() 挂入 wake_q(一种延迟唤醒列表,避免持自旋锁期间直接 try_to_wake_up() 的开销,见 13.2 节调度器侧的 wake_q 处理),在释放 wait_lock 后统一唤醒。

mutex_unlock() 本体(mutex.c:546-554)只是 fastpath 失败后转入此慢路径。

16.2.6 mutex 与 spinlock 的选择、使用实例与总结

选择判据

维度 spinlock mutex
等待时行为 自旋(烧 CPU) 睡眠(让出 CPU)
临界区限制 不能睡眠、不能长 可以睡眠、可以长
可用上下文 进程/软中断/硬中断 仅进程上下文
无争用开销 preempt_disable + cmpxchg might_sleep + cmpxchg
公平性 qspinlock FIFO(排队后) FIFO + HANDOFF 防饿死
典型场景 中断共享数据、极短临界区 子系统全局状态、可睡眠路径

两条经验法则:临界区内会睡眠 → mutex(spinlock 下 might_sleep() 直接报错);中断上下文会获取 → spinlock(mutex 在原子上下文是致命错误)。两者之间的灰色地带(临界区短但进程上下文、竞争不激烈)通常选 mutex——睡眠的代价被乐观自旋消化掉大半。

mm 子系统的 mutex 实例

mm/ksm.c 用一把全局 mutex 串行化 KSM 的启停控制:

// mm/ksm.c:3513-3546
static ssize_t run_store(struct kobject *kobj, struct kobj_attribute *attr,
             const char *buf, size_t count)
{
    unsigned int flags;
    int err;

    err = kstrtouint(buf, 10, &flags);
    if (err)
        return -EINVAL;
    ...
    mutex_lock(&ksm_thread_mutex);
    wait_while_offlining();
    if (ksm_run != flags) {
        ksm_run = flags;
        if (flags & KSM_RUN_UNMERGE) {
            set_current_oom_origin();
            err = unmerge_and_remove_all_rmap_items();
            clear_current_oom_origin();
            if (err) {
                ksm_run = KSM_RUN_STOP;
                count = err;
            }
        }
    }
    mutex_unlock(&ksm_thread_mutex);

ksm_thread_mutex(mm/ksm.c:487 的 DEFINE_MUTEX)保护的临界区内调用 unmerge_and_remove_all_rmap_items()——内部大量内存分配与页面扫描,必须可睡眠。这正是"用 mutex 不用 spinlock"的教科书场景。

要点总结:

  • 状态机浓缩在 atomic_long_t owner 一个字:指针 + WAITERS/HANDOFF/PICKUP 三标志,本树无独立 count 字段。
  • 五级阶梯:cmpxchg 快路径 → 标志感知 trylock → OSQ 排队乐观自旋 → FIFO 睡眠 → HANDOFF 防饿死移交。
  • 乐观自旋的两条纪律——持锁者在跑才值得等、need_resched() 立刻让路——把"自旋"限制在真正划算的窗口内。
  • handoff 机制用"所有权显式过户"化解睡眠锁固有的抢锁不公平。

互斥锁的等待队列、睡眠唤醒与调度器深度联动,其 schedule_preempt_disabled() 之后的细节(如何进入 __schedule()、如何被 ttwu 唤醒)在 13.2 节已有完整分析。


16.3 读写锁与顺序锁

读多写少是内核数据结构的普遍形态:路由表、挂载表、配额统计……绝大多数访问是查询,修改只占零头。为这类负载准备两套原语——读写锁允许多个读者并行、写者独占;顺序锁则更进一步,让读者完全无锁,靠"序号对不上就重试"实现乐观读取。Linux 7.0.10 中 x86_64 采用排队读写锁(qrwlock),其设计与 qspinlock(16.1 节)一脉相承;顺序锁家族则演进出了绑定关联锁的类型变体(seqcount_spinlock_t 等)与双副本乒乓的 latch 模式。本节将结合 Linux 7.0.10 内核源码,逐行分析 qrwlock 的计数编码与公平性设计、seqcount/seqlock 的奇偶序号协议,以及它们在时间子系统中的真实应用。


16.3.1 读写锁的问题域

读写锁的语义契约:

  • 读-读共享:任意多个读者可同时持锁;
  • 读-写互斥、写-写互斥:写者独占。

设计读写锁要回答两个尖锐问题:

  1. 读者会饿死写者吗? 若读者源源不断,计数永远非零,写者无限等待。
  2. 写者会饿死读者吗? 若写者一到就封锁新读者,而已有写者在长临界区内,读者也无限等待。

Linux 的 qrwlock 给出的答案是写者优先排队、读者不饿死:写者到达即立起"我在等"的标志,新读者看到标志就不再进入,已进入的读者排空后写者拿锁;写者退出后读者恢复进入。下面看实现。

16.3.2 qrwlock 的计数编码

锁字布局

// include/asm-generic/qrwlock_types.h:13-32
struct qrwlock {
    atomic_t        cnts;
    u8          wlocked;
    arch_spinlock_t     wait_lock;
};

cnts 是 32 位计数核心,位域由 qrwlock.h:27-31 定义:

// include/asm-generic/qrwlock.h:27-31
#define _QW_WAITING 0x100       /* A writer is waiting     */
#define _QW_LOCKED  0x0ff       /* A writer holds the lock */
#define _QW_WMASK   0x1ff       /* Writer mask         */
#define _QR_SHIFT   9       /* Reader count shift      */
#define _QR_BIAS    (1U << _QR_SHIFT)
字段 位范围 含义
_QW_LOCKED(bit 0-7) 0x0ff 写者持锁标志(写者拿锁时写入整个低字节)
_QW_WAITING(bit 8) 0x100 有写者在排队——读者的"止步线"
读者计数(bit 9-31) _QR_BIAS = 1<<9 每个读者加一次 _QR_BIAS

wlocked 字段是 cnts 低字节的别名视图(union,qrwlock_types.h:14-25),供解锁时的字节写入优化。wait_lock 是一把内嵌的 qspinlock,专管写者之间的排队——rwlock 是用 spinlock 搭出来的。

x86_64 的启用链:arch/x86/Kconfig:139 select ARCH_USE_QUEUED_RWLOCKS → kernel/Kconfig.locks:252-254 把 CONFIG_QUEUED_RWLOCKS 默认置 y(SMP 且非 PREEMPT_RT)。arch/x86/include/asm/qrwlock.h:5-6 只是 include asm-generic 的实现,无架构特化代码。

调用链

读写锁 API 的分层与自旋锁完全平行(16.1.2 节):read_lock()(include/linux/rwlock.h:31-45)→ _raw_read_lock()(include/linux/rwlock_api_smp.h:159-241,preempt_disable + LOCK_CONTENDED)→ do_raw_read_lock()(rwlock.h:31-45)→ arch_read_lock(),最后经 include/asm-generic/qrwlock.h:139-145 的宏重映射落到 queued_read_lock()。写侧对称。API 层的 __raw_read_lock() 示例:

// include/linux/rwlock_api_smp.h:225-239
static inline void __raw_write_lock(rwlock_t *lock)
    __acquires(lock) __no_context_analysis
{
    preempt_disable();
    rwlock_acquire(&lock->dep_map, 0, 0, _RET_IP_);
    LOCK_CONTENDED(lock, do_raw_write_trylock, do_raw_write_lock);
}

与自旋锁完全一致:先 preempt_disable() 再拿锁,LOCK_CONTENDED 里是真正的锁操作。

16.3.3 读者路径:一次原子加计数

// include/asm-generic/qrwlock.h:78-92
static inline void queued_read_lock(struct qrwlock *lock)
{
    int cnts;

    cnts = atomic_add_return_acquire(_QR_BIAS, &lock->cnts);
    if (likely(!(cnts & _QW_WMASK)))
        return;

    /* The slowpath will decrement the reader count, if necessary. */
    queued_read_lock_slowpath(lock);
}

快路径是一条 lock add(line 80-84):atomic_add_return_acquire(_QR_BIAS) 原子地把读者计数加 1 并拿回新值。检查新增值的高位写者域:_QW_WMASK(0x1ff)覆盖 _QW_LOCKED 和 _QW_WAITING——只要没有任何写者持锁或排队,读者立即放行。注意语义细节:用 _QR_BIAS 加进高位读者域,绝不会碰低 9 位的写者域,读写两个域在同一个字里互不侵蚀。

写者出现后(_QW_WAITING 置位),新读者加完计数发现自己撞线,转入慢路径(慢路径会把 _QR_BIAS 退掉再排队,见 16.3.6 节)。解锁对称(qrwlock.h:108-114):atomic_sub_return_release(_QR_BIAS),release 语义保证读者临界区的读操作先于计数递减可见。

16.3.4 写者路径:快路径与排队

快路径

// include/asm-generic/qrwlock.h:94-102
static inline void queued_write_lock(struct qrwlock *lock)
{
    int cnts = 0;
    /* Optimize for the unfair lock case where the fair flag is 0. */
    if (likely(atomic_try_cmpxchg_acquire(&lock->cnts, &cnts, _QW_LOCKED)))
        return;

    queued_write_lock_slowpath(lock);
}

写锁快路径要求整个 cnts 恰好为 0:无读者(高 23 位全零)、无排队写者(bit 8 = 0)、无持锁写者(低 8 位 = 0),一次 cmpxchg 把低字节写成 _QW_LOCKED 即独占。注释里的 "unfair lock case" 指的是:即使 _QW_WAITING 已置位,只要此刻计数清零,仍可能被恰好路过的写者用这条路径抢走——公平性由慢路径兜底而非绝对保证。

慢路径:立 WAITING、等排空

// kernel/locking/qrwlock.c:66-91
void __lockfunc queued_write_lock_slowpath(struct qrwlock *lock)
{
    int cnts;

    trace_contention_begin(lock, LCB_F_SPIN | LCB_F_WRITE);

    /* Put the writer into the wait queue */
    arch_spin_lock(&lock->wait_lock);

    /* Try to acquire the lock directly if no reader is present */
    if (!(cnts = atomic_read(&lock->cnts)) &&
        atomic_try_cmpxchg_acquire(&lock->cnts, &cnts, _QW_LOCKED))
        goto unlock;

    /* Set the waiting flag to notify readers that a writer is pending */
    atomic_or(_QW_WAITING, &lock->cnts);

    /* When no more readers or writers, set the locked flag */
    do {
        cnts = atomic_cond_read_relaxed(&lock->cnts, VAL == _QW_WAITING);
    } while (!atomic_try_cmpxchg_acquire(&lock->cnts, &cnts, _QW_LOCKED));
unlock:
    arch_spin_unlock(&lock->wait_lock);

    trace_contention_end(lock, 0);
}

四步流程:

  1. 排队(line 73-74):先抢内嵌的 wait_lock——多个写者在此串行化,同一时刻只有一个写者操作 cnts 的写者域。这把锁是 qspinlock,享受 16.1 节的全部分层优化。
  2. 直接尝试(line 77-79):排队成功后先试一次"计数全零 → 直接拿锁",覆盖"读者刚好排空"的运气场景。
  3. 立起止步线(line 82-83):atomic_or(_QW_WAITING) ——从此刻起新读者必然撞上 _QW_WMASK 而进入慢路径,读者洪峰被截断。
  4. 等排空再拿锁(line 86-89):atomic_cond_read_relaxed() 自旋等计数退化为纯 _QW_WAITING(现有读者全部退出),随后 cmpxchg 把 _QW_WAITING 原地升级为 _QW_LOCKED。循环+cmpxchg 的组合处理竞争窗口——条件满足到拿锁之间计数可能再变。

这就是"写者优先排队、读者不饿死写者"的实现:写者只需等"已在场"的读者退场,不必等无穷无尽的新读者;而读者虽被拦在线外,却只在写者实际持锁期间(通常极短)等待,写者释放后(低 9 位清零)读者立即恢复进入——两边都不会被饿死。

读侧慢路径(qrwlock.c:21-59)做了对称的礼貌处理:非中断上下文的读者先回退 _QR_BIAS 再抢 wait_lock 排队,保证同一时刻只有一个"排队读者"在等写者退出,其余读者在 wait_lock 上自旋(而非全都压在 cnts 缓存行上);中断上下文的读者则被允许越过"仅在等待(_QW_WAITING 置位但未持锁)"的写者直接进——中断处理必须尽快完成,不能因一个还没拿到锁的写者而卡死,这也规避了"写者持 wait_lock 时中断读者抢 wait_lock"的自死锁。

解锁

写解锁(qrwlock.h:113-123)是 smp_store_release(&lock->wlocked, 0)——利用 union 别名直接清低字节,一条 release store 完事,被拦的读者与新写者随即竞争。读解锁如 16.3.3 节所述是 release 语义的计数递减。

16.3.5 seqcount —— 读者无锁的序号协议

动机与奇偶约定

读写锁仍要求读者执行"原子加计数 + 检查",并且写者要付出拦截全部读者的代价。seqcount 把代价推到极限不对称:读者零原子操作、零缓存行写,只在最后核对一次序号;写者独占式地翻动序号。类型定义:

// include/linux/seqlock_types.h:33-38
typedef struct seqcount {
    unsigned sequence;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
    struct lockdep_map dep_map;
#endif
} seqcount_t;

整个原语只有 unsigned sequence 一个字。写者协议是奇偶约定:序号为偶数表示"无写进行中",奇数表示"写进行中"。写者进入写段时 ++(变奇),退出时再 ++(变偶):

// include/linux/seqlock.h:421-455
#define raw_write_seqcount_begin(s)                 \
do {                                    \
    if (seqprop_preemptible(s))                 \
        preempt_disable();                  \
                                    \
    do_raw_write_seqcount_begin(seqprop_ptr(s));            \
} while (0)

static inline void do_raw_write_seqcount_begin(seqcount_t *s)
{
    kcsan_nestable_atomic_begin();
    s->sequence++;
    smp_wmb();
}
...
static inline void do_raw_write_seqcount_end(seqcount_t *s)
{
    smp_wmb();
    s->sequence++;
    kcsan_nestable_atomic_end();
}

两处 smp_wmb() 的位置就是协议的内存序核心(line 436-439 与 448-451):

  • 进入写段:sequence++(变奇)之后写屏障——保证序号变奇这一事件先于任何数据修改对其他 CPU 可见。读者若看到奇数序号,它随后读到的必是"写到一半"的数据,重试即可。
  • 退出写段:smp_wmb() 之后 sequence++(变偶)——保证全部数据修改先于序号翻回偶数可见。读者看到"偶数且序号未变"就意味着读到的数据是完整一致的快照。

这两道屏障与 15.2 节的 RELEASE/ACQUIRE 配对分析同理:序号是这个协议里唯一的"发布点"。

seqprop_preemptible()(seqlock.h:442-448 的宏分支)决定写段是否自动 preempt_disable()——裸 seqcount_t 可抢占,绑定锁的变体(16.3.7 节)按锁属性决定。

读者:读-验证-重试

// include/linux/seqlock.h:272-320, 386-413
#define __read_seqcount_begin(s)                    \
({                                  \
    unsigned __seq;                         \
                                    \
    while (unlikely((__seq = seqprop_sequence(s)) & 1))     \
        cpu_relax();                        \
                                    \
    kcsan_atomic_next(KCSAN_SEQLOCK_REGION_MAX);            \
    __seq;                              \
})
...
#define raw_read_seqcount(s)                        \
({                                  \
    unsigned __seq = seqprop_sequence(s);               \
                                    \
    kcsan_atomic_next(KCSAN_SEQLOCK_REGION_MAX);            \
    __seq;                              \
})
...
static inline int do___read_seqcount_retry(const seqcount_t *s, unsigned start)
{
    kcsan_atomic_next(0);
    return unlikely(READ_ONCE(s->sequence) != start);
}
...
static inline int do_read_seqcount_retry(const seqcount_t *s, unsigned start)
{
    smp_rmb();
    return do___read_seqcount_retry(s, start);
}

标准读循环三步:

seq = read_seqcount_begin(&s);   // 等到偶数序号并记下
...读共享数据...                 // 纯读,无任何原子操作
if (read_seqcount_retry(&s, seq)) // 序号变了 → 有写穿插,重来
        goto 重试;

do_read_seqcount_retry() 的 smp_rmb()(line 395-399):确保读者对数据的读取完成后再核对序号——没有这道屏障,弱序架构上数据读取可能被重排到序号读取之后,出现"序号没变但数据是新旧混合"的撕裂。x86 上它是空操作(强序),但抽象层必须写。__read_seqcount_begin() 的自旋(line 275-280)只在起点撞上奇数序号时发生——等写者完成;raw_seqcount_begin()(seqlock.h:362-369)提供了更激进的变体:奇数序号不等待,直接 (seq & ~1) 闯入读,把代价留给 retry 判定(读到的必是写了一半的数据,但 retry 必然失败,浪费的只是这一次读)。

seqlock_t:seqcount + spinlock 的组合

裸 seqcount_t 的写者必须自己保证写段互斥。seqlock_t 把互斥锁直接打包进来:

// include/linux/seqlock_types.h:84-92
context_lock_struct(seqlock) {
    /*
     * Make sure that readers don't starve writers on PREEMPT_RT: use
     * seqcount_spinlock_t instead of seqcount_t. Check __SEQ_LOCK().
     */
    seqcount_spinlock_t seqcount;
    spinlock_t lock;
};
typedef struct seqlock seqlock_t;
// include/linux/seqlock.h:877-896
static inline void write_seqlock(seqlock_t *sl)
    __acquires(sl) __no_context_analysis
{
    spin_lock(&sl->lock);
    do_write_seqcount_begin(&sl->seqcount.seqcount);
}

static inline void write_sequnlock(seqlock_t *sl)
    __releases(sl) __no_context_analysis
{
    do_write_seqcount_end(&sl->seqcount.seqcount);
    spin_unlock(&sl->lock);
}

写者 = spin_lock + 翻号,读者完全无锁(read_seqbegin()/read_seqretry() 在 seqlock.h:835-856,只是带注解的 seqcount 接口)。9.3 节遇到过的 mount_lock(seqlock_t,保护挂载点引用计数与序号读取)就是此类型的实际用户。变体 write_seqlock_bh()/write_seqlock_irq()/write_seqlock_irqsave()(seqlock.h:905-985)对应自旋锁的 bh/irq 家族;另有排他读接口 read_seqlock_excl*(:1011-1130)与"先乐观后退让"的 read_seqbegin_or_lock()/need_seqretry()/done_seqretry()(:1146-1186)——后者用于遍历很长的结构时先乐观试一遍、失败率太高就升级为持锁读。

16.3.6 绑定关联锁的 seqcount 变体

裸 seqcount_t 对写者零保护——忘记加锁的写者只会造成静默数据竞争。本树把"写者串行化锁"做成 seqcount 类型的一部分:

// include/linux/seqlock.h:227-231
SEQCOUNT_LOCKNAME(raw_spinlock, raw_spinlock_t,  false,    raw_spin)
SEQCOUNT_LOCKNAME(spinlock,     spinlock_t,      __SEQ_RT, spin)
SEQCOUNT_LOCKNAME(rwlock,       rwlock_t,        __SEQ_RT, read)
SEQCOUNT_LOCKNAME(mutex,        struct mutex,    true,     mutex)

生成 seqcount_raw_spinlock_t、seqcount_spinlock_t、seqcount_rwlock_t、seqcount_mutex_t 四个类型(定义于 seqlock_types.h:62-72),内嵌对应的锁指针。SEQCOUNT_LOCKNAME() 的第三个参数(preemptible 列)决定写段是否自动关抢占:seqcount_mutex_t 可抢占(mutex 自己会调度),spinlock 变体在 PREEMPT_RT 下由 __SEQ_RT 标记特殊处理。

双收益:其一,write_seqcount_begin() 自动取关联锁,写者想漏都漏不掉;其二,lockdep 能验证写侧临界区确实被正确加锁——seqprop_assert_lock_held() 检查把锁序错误提前到运行时。初始化(seqlock.h:131-134)形如 seqcount_spinlock_init(s, name, lock),静态版 SEQCNT_RAW_SPINLOCK_ZERO() 等(:244-248)。类型分发用 _Generic(__seqprop,seqlock.h:250-264)——同一套 read_seqcount_begin()/write_seqcount_begin() 接口对四种类型透明。

内核的倾向很明确:新代码应使用绑定锁的变体而非裸 seqcount_t,14.2 节信号子系统的 seqlock_t stats_lock(signal.h:160)与时间子系统的用法均属此类。

16.3.7 seqcount_latch —— 双副本乒乓

序号协议有一个死角:读者在写段的中间到达怎么办?标准 seqlock 的答案是读者重试,但若读者处在 NMI 这类不可阻塞、不可重试失败太多次的上下文(如 perf 采样读时钟),重试的代价无法接受。latch(门闩)方案引入两份数据副本:

// include/linux/seqlock.h:606-612
typedef struct seqcount_latch {
    seqcount_t seqcount;
} seqcount_latch_t;

写者每写一轮就翻动一次序号(write_seqcount_latch(),seqlock.h:694-700),序号的奇偶恰好充当副本选择器:偶数读副本 0,奇数读副本 1。写者按"翻号 → 写另一份 → 再翻号 → 写回第一份"的节奏更新,任何时刻读者按当前奇偶读对应副本,读到的都是某一份完整数据——读者永远不需要重试。写段本身完全可抢占,甚至可以被 NMI 中的读者随时打断(seqlock.h:600-609 注释),代价是数据要存两份、写入两遍。典型用户是时间子系统的 clocksource 切换(tk_core 旁的 latch 副本),与 19.5 节 TLB、20.4 节 fixmap 中的时间读取路径呼应。

16.3.8 时间子系统的真实用例

get_jiffies_64() 的序号读循环

32 位系统上 64 位 jiffies_64 无法单条指令读取,序号协议派上用场:

// kernel/time/jiffies.c:43-59
__cacheline_aligned_in_smp DEFINE_RAW_SPINLOCK(jiffies_lock);
__cacheline_aligned_in_smp seqcount_raw_spinlock_t jiffies_seq =
    SEQCNT_RAW_SPINLOCK_ZERO(jiffies_seq, &jiffies_lock);
...
u64 get_jiffies_64(void)
{
    unsigned int seq;
    u64 ret;

    do {
        seq = read_seqcount_begin(&jiffies_seq);
        ret = jiffies_64;
    } while (read_seqcount_retry(&jiffies_seq, seq));
    return ret;
}

jiffies_seq 是 seqcount_raw_spinlock_t(16.3.6 节的绑定锁变体),写侧在 tick 处理中持 jiffies_lock 并 write_seqcount_begin。这个 do-while 循环是 seqcount 读侧的标准形态,get_jiffies_64() 在系统负载下偶发的重试完全被锁变量免去——读者之间零争用。

ktime_get() —— 高精度时间的序号读

// kernel/time/timekeeping.c:814-832
static __always_inline ktime_t __ktime_get_fast_ns(...)
...
ktime_t ktime_get(void)
{
    struct timekeeper *tk = &tk_core.timekeeper;
    unsigned int seq;
    ktime_t base;
    u64 nsecs;

    do {
        seq = read_seqcount_begin(&tk_core.seq);
        base = tk->tkr_mono.base;
        nsecs = timekeeping_get_ns(&tk->tkr_mono);
    } while (read_seqcount_retry(&tk_core.seq, seq));
    ...
}

ktime_get() 是内核调用最频繁的时钟接口之一(打印时间戳、定时器、网络协议栈都要读),这正是它必须无锁的原因:如果每个读者都要抢一把自旋锁,时钟读取本身会成为系统的串行点。写侧——update_wall_time() 更新 timekeeper——才持锁并翻动 tk_core.seq。

三种原语的适用对照

原语 读者开销 写者开销 适用场景
rwlock(qrwlock) 原子加计数 + 检查 拦截全部读者 读临界区较长、读者不需重试的确定性
seqcount 零原子,纯读 + 末尾核对 翻号两次 + 写屏障 读多写极少、读侧可容忍重试、数据小(重读便宜)
seqcount_latch 零原子、零重试 双副本写两遍 NMI 等不可重试读者的读

选择 seqcount 的隐性条件:重试意味着整个读循环重来,因此被保护的数据必须小到"重读不心疼"。大结构遍历用 read_seqbegin_or_lock() 退让模式或干脆回到 rwlock。

要点总结:

  • qrwlock 用一个 32 位字同时编码读者计数(bit 9+)与写者域(低 9 位),写者慢路径以 _QW_WAITING 为读者止步线,实现写者优先排队且双方不饿死。
  • seqcount 把读者开销压到零原子操作,代价是写者翻号与读者可能重试;两道 smp_wmb() 锚定序号与数据的可见顺序。
  • 绑定锁变体(seqcount_spinlock_t 等)让 lockdep 接管写侧检查,是新代码的推荐形态;latch 双副本则服务于 NMI 读者。
  • 时间子系统(jiffies_seq、tk_core.seq)是这套原语最密集的用户,"高频读 + 偶发写"正是它们的诞生理由。

16.4 死锁检测与 lockdep

死锁是并发编程中最阴险的缺陷:四条发生条件(互斥、持有并等待、不可剥夺、循环等待)在代码里往往相隔数千行、跨越数十条路径,code review 很难发现,而触发时系统通常已经挂死。lockdep(lock dependency validator,锁依赖验证器)的思路是釜底抽薪:在运行时把每一次锁获取记录成依赖图的边,一旦图上出现环、或同一把锁在中断使能与使用上下文上不一致,立刻报告完整证据链——死锁还没发生就被定罪。它的代价(慢一个数量级、内存开销大)注定只用于调试内核,但几乎所有内核死锁类 bug 修复背后都有它。本节将结合 Linux 7.0.10 内核源码,逐行分析 lockdep 的核心数据结构、依赖边与环检测、中断上下文一致性检查,以及它在锁实现各层(16.1-16.3 节)中的接入点。


16.4.1 开启方式与总体架构

Kconfig 开关

lockdep 由一组嵌套的配置项控制(lib/Kconfig.debug):

配置项 位置 作用
LOCK_DEBUGGING_SUPPORT Kconfig.debug:1444-1447 lockdep 基础框架的使能前提
PROVE_LOCKING Kconfig.debug:1449-1472 "证明锁正确性"总开关,select LOCKDEP、DEBUG_SPINLOCK、DEBUG_LOCK_ALLOC、TRACE_IRQFLAGS 等
DEBUG_LOCK_ALLOC Kconfig.debug:1576-1590 锁类被释放/重初始化时的检查与 dep_map 接入
LOCKDEP Kconfig.debug:1591-1596 依赖跟踪引擎本体
DEBUG_LOCKDEP Kconfig.debug:1641-1648 引擎自身的数据结构自检

PROVE_LOCKING 是日常用语"开 lockdep"的实际所指。除锁依赖外它还连带开启中断使能轨迹跟踪(TRACE_IRQFLAGS),这是 16.4.4 节中断一致性检查的基础。

四个核心数据结构

lockdep 的世界由四个静态大表构成(kernel/locking/lockdep.c:207-225, 3326-3328, 413):

lock_classes[MAX_LOCKDEP_KEYS]        锁类数组          (lockdep.c:208)
list_entries[]                        依赖边数组        (lockdep.c:210)
lock_chains[]                         锁链缓存          (lockdep.c:3326)
chain_hlocks[]                        锁链的索引存储    (lockdep.c:3328)

容量由编译期参数决定(include/linux/lockdep_types.h:202-204、kernel/locking/lockdep_internals.h:99-125、lib/Kconfig.debug:1601-1639):

  • MAX_LOCKDEP_KEYS_BITS = 13 → 最多 8192 个锁类;
  • MAX_LOCKDEP_ENTRIES = 1 << CONFIG_LOCKDEP_BITS(默认 2^15 = 32768 条依赖边);
  • MAX_LOCKDEP_CHAINS = 1 << CONFIG_LOCKDEP_CHAINS_BITS(默认 2^16 = 65536 条锁链)。

运行中超限会报 BUG: MAX_LOCKDEP_KEYS too low!(register_lock_class 内,lockdep.c:1331 附近),提示增大对应配置并重编。理解这四个表的关系是理解 lockdep 的一切的前提。

16.4.2 锁类与锁的实例

lock_class_key:静态实例的身份

lockdep 区分锁类(lock class)与锁实例(lock instance)。同一个数组的一万个元素是同一把"逻辑锁"(同一个锁类)的不同实例——对依赖分析而言它们行为一致,只需记录一次。锁类由编译期静态对象的地址标识:

// include/linux/lockdep_types.h:70-83
struct lockdep_subclass_key {
    char __one_byte;
} __attribute__ ((packed));

struct lock_class_key {
    struct lockdep_subclass_key subkeys[MAX_LOCKDEP_SUBCLASSES];
};

每把锁声明处都隐含一个 static struct lock_class_key __key(由 DEFINE_SPINLOCK/DEFINE_MUTEX 等宏在展开时生成,取 &__key 的地址作身份)。运行时通过哈希表 classhash_table(lockdep.c:413)把 key 地址映射到锁类。

// include/linux/lockdep_types.h:98-147
struct lock_class {
    struct hlist_node       hash_entry;
    struct list_head        lock_entry;

    struct lockdep_subclass_key *key;
    unsigned int            subclass;
    unsigned int            dep_gen_id;

    /*
     * IRQ/softirq usage tracking bits:
     */
    unsigned long           usage_mask;
    struct stack_trace      usage_traces[XXX_LOCK_USAGE_STATES];
    ...
    /*
     * These fields represent a directed graph of lock dependencies,
     * round-robin-dispatched...
     */
    struct list_head        locks_after, locks_before;
    ...
    const char          *name;
    int             name_version;
    ...
};

两个关键字段:

  • usage_mask:这把锁在各类上下文中的使用历史位图(16.4.4 节);
  • locks_after/locks_before:依赖图的邻接表——"本锁 → 之后获取的锁"与反方向的边列表。

lockdep_map 是锁实例侧的登记结构(lockdep_types.h:186-204),嵌在每个锁容器里(16.1.2 节见过的 dep_map 字段):key 指向锁类身份,class_cache[2] 缓存最近解析出的 lock_class 指针避免反复查哈希,name 供报告打印。

register_lock_class:锁类注册

// kernel/locking/lockdep.c:1285-1394
static struct lock_class *
register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
{
    struct lock_class *class = NULL;
    struct hlist_head *hash_head;
    struct lockdep_subclass_key *key;
    ...
    key = lock->key->subkeys + subclass;
    hash_head = classhash_table + hash_pointer(key);
    ...
    /*
     * We have to do the hash-walk again, to avoid taking
     * a lock-class whose key is identical to this one...
     */
    hlist_for_each_entry_rcu(class, hash_head, hash_entry) {
        if (class->key == key) {
            ...
            goto out_unlock_set;
        }
    }
    ...
    /*
     * Allocate a new key from the static array...
     */
    if (nr_lock_classes >= MAX_LOCKDEP_KEYS) {
        ...
        dump_lock_classes();
        ...
        pr_err("BUG: MAX_LOCKDEP_KEYS too low!\n");
        ...
        return NULL;
    }
    ...
    /*
     * We haven't seen this lock before, so we need to
     * allocate a new class...
     */
    class = lock_classes + nr_lock_classes++;
    ...
    class->name = lock->name;
    class->key = key;
    class->subclass = subclass;
    ...
    hlist_add_head_rcu(&class->hash_entry, hash_head);
    ...

首次获取一把锁时:key 哈希查找未命中 → 从 lock_classes[] 顺序分配一个槽位 → 填 name/key/subclass → 头插进哈希桶 → 写回 lock->class_cache[0]。这个惰性注册意味着 lockdep 的依赖图随内核运行逐步生长——没走过的代码路径不产生锁类,没发生过的锁序不产生依赖边,所以"跑了三天没报"只是"三天内走过的路径都干净"。

16.4.3 依赖图与死锁检测

held_lock:任务的持锁栈

每个任务在 task_struct 里有一个持有锁的数组(curr->held_locks,容量 MAX_LOCK_DEPTH = 48,include/linux/sched.h:1270):

// include/linux/lockdep_types.h:206-257
struct held_lock {
    u64             prev_chain_key;
    unsigned long           acquire_ip;
    struct lockdep_map      *instance;
    struct lockdep_map      *nest_lock;
#ifdef CONFIG_LOCK_STAT
    u64             waittime_stamp;
    u64             holdtime_stamp;
#endif
    unsigned int            class_idx:MAX_LOCKDEP_KEYS_BITS;
    ...
    unsigned int            read:2;      /* 0: exclusive, 1: read, 2: recursive read */
    unsigned int            check:1;
    unsigned int            hardirqs_off:1;
    unsigned int            softirqs_off:1;
    unsigned int            references:12;
    unsigned int            pin_count;
};

lock_acquire()/lock_release()(lockdep.c:5825-5894,各锁 API 经 spin_acquire/LOCK_CONTENDED 调到这里)本质上就是在维护这个数组:获取时压栈一条 held_lock,释放时弹出并在 check_prevs_add() 中与栈上更早的锁建边。

主流程:__lock_acquire()

lock_acquire() 关中断、递增 lockdep_recursion(防止 lockdep 自身用的锁被反复跟踪)后进入 __lock_acquire()(lockdep.c:5077 起),按序执行:

  1. 解析锁类:register_lock_class()(16.4.2 节)查/建锁类,填 hlock->class_idx;
  2. check_wait_context:等待类型一致性(wait_type_outer/inner,本树 lockdep_map 新增字段的用途);
  3. mark_usage:按当前上下文更新锁类 usage 位(16.4.4 节);
  4. 计算 chain key:把"当前持锁栈 + 新锁"的类序号哈希成一个 64 位键,查 lock_chains[] 缓存——命中则本条锁序历史上已验证过,直接返回,这是 lockdep 的主要性能优化;
  5. check_deadlock:同锁重入检查;
  6. check_prevs_add:为栈上每对相邻锁建依赖边并做环检测。

check_deadlock:同锁重入

// kernel/locking/lockdep.c:3056-3097
static int
check_deadlock(struct task_struct *curr, struct held_lock *next)
{
    ...
    for (i = 0; i < curr->lockdep_depth; i++) {
        prev = curr->held_locks + i;

        if (prev->instance == next->nest_lock)
            nest = prev;

        if (hlock_class(prev) != hlock_class(next))
            continue;

        /*
         * Allow read-after-read recursion of the same
         * lock class (i.e. read_lock(lock)+read_lock(lock)):
         */
        if ((next->read == 2) && prev->read)
            continue;
        ...
        if (nest)
            return 2;

        print_deadlock_bug(curr, prev, next);
        return 0;
    }
    return 1;
}

规则:同任务重复获取同一锁类的非读锁 → 直接报 DEADLOCK(print_deadlock_bug 打印两处获取点的栈);读写锁的读-读重入放行(next->read == 2 && prev->read);nest_lock(mutex_lock_nested 等的嵌套声明)命中时返回 2 表示合法递归。SINGLE_DEPTH_NESTING(10.4 节 Cow 相关分析中出现的同类机制)就是绕过此检查的显式嵌套声明。

check_prev_add:建边与 BFS 环检测

// kernel/locking/lockdep.c:3155-3170
    /*
     * Prove that the new <prev> -> <next> dependency would not
     * create a circular dependency in the graph. (We do this by
     * a breadth-first search into the graph starting at <next>,
     * and check whether we can reach <prev>.)
     *
     * The search is limited by the size of the circular queue (i.e.,
     * MAX_CIRCULAR_QUEUE_SIZE) which keeps track of a breadth of nodes
     * in the graph whose neighbours are to be checked.
     */
    ret = check_noncircular(next, prev, trace);
    if (unlikely(bfs_error(ret) || ret == BFS_RMATCH))
        return 0;

    if (!check_irq_usage(curr, prev, next))
        return 0;

环检测的问法值得细品:新边是 prev → next(先拿 prev 再拿 next)。要判断它是否成环,lockdep 从 next 出发做 BFS,看能否反向走到 prev——若能,则存在 next →…→ prev → next 的循环等待,即 A-B 死锁在未来某次调度中必然可复现。报错时打印的正是 BFS 找到的完整环上每条边的两端获取栈——这就是 lockdep 报告里 "Possible unsafe locking scenario" 部分的来源。每条新边还会经 check_irq_usage()(lockdep.c:2780)做中断维度的一致性证明(16.4.4 节),通过后 add_lock_to_list() 把边挂进 locks_after/locks_before 双向邻接表(lockdep.c:3235 起,冗余边剔除在 :3219-3223)。

为什么"没死锁"不等于"安全":依赖图记录的是所有可能同时发生的锁序组合。CPU 0 上 A→B、CPU 1 上 B→A 只需各发生一次(哪怕相隔几小时、从未并发),图上就已有环——lockdep 不等真实死锁,它证明的是未来必然可能。

16.4.4 中断上下文一致性:usage 位

usage 位图

死锁的另一大来源是"在持锁时开了中断,而中断处理要拿同一把锁"。lockdep 用 usage 位跟踪每把锁类在哪里被使用、在哪里被开启中断:

// kernel/locking/lockdep_internals.h:13-24
enum lock_usage_bit {
#define LOCKDEP_STATE(__STATE)      \
    LOCK_USED_IN_##__STATE,     \
    LOCK_USED_IN_##__STATE##_READ,  \
    LOCK_ENABLED_##__STATE,     \
    LOCK_ENABLED_##__STATE##_READ,
#include "lockdep_states.h"
#undef LOCKDEP_STATE
    LOCK_USED,
    LOCK_USED_READ,
    LOCK_USAGE_STATES,
};

lockdep_states.h:7-8 目前只有 HARDIRQ 与 SOFTIRQ 两个状态,展开成 2 状态 × 4 类 = 8 个位,加 LOCK_USED/LOCK_USED_READ 共 10 位。四类语义:USED_IN_*(该锁曾在此上下文中被持有)、ENABLED_*(持有该锁期间此上下文被开启过)、_READ 变体(读方式持有)。

mark_usage:采集上下文

// kernel/locking/lockdep.c:4617-4645
static int
mark_usage(struct task_struct *curr, struct held_lock *hlock, int check)
{
    if (!check)
        goto lock_used;

    /*
     * If non-trylock use in a hardirq or softirq context, then
     * mark the lock as used in these contexts:
     */
    if (!hlock->trylock) {
        if (hlock->read) {
            if (lockdep_hardirq_context())
                if (!mark_lock(curr, hlock,
                        LOCK_USED_IN_HARDIRQ_READ))
                    return 0;
        ...
        } else {
            if (lockdep_hardirq_context())
                if (!mark_lock(curr, hlock, LOCK_USED_IN_HARDIRQ))
                    return 0;
    ...

__lock_acquire() 在每次获取锁时调用它:当前在硬中断里 → 置 LOCK_USED_IN_HARDIRQ(读为 _READ 变体);同时若该锁是在中断未关闭时获取的(hlock->hardirqs_off == 0),置 LOCK_ENABLED_HARDIRQ——即"持这把锁期间,硬中断可能闯进来"。mark_lock()(lockdep.c:4712-4774)置位后对 new_bit < LOCK_USED 的情况调用 mark_lock_irq()(:4256)→ valid_state()(:4051):比对锁类的 usage 位组合,发现"曾 ENABLED_HARDIRQ 又 USED_IN_HARDIRQ"这类矛盾即报 inconsistent lock usage(BUG: inconsistent lock state),报告同样附带两条路径的完整栈。

一个直观例子:驱动在 spin_lock(&lock)(未关中断)保护的临界区里调用了会开中断的函数,而中断处理程序又 spin_lock_irqsave(&lock)——第一次组合使 lock 记下 ENABLED_HARDIRQ,中断到达时又记下 USED_IN_HARDIRQ,lockdep 当场报错,而真实死锁可能一周才发生一次。

16.4.5 lockdep 在锁实现中的接入点与运行视图

接入点回顾

前三节的锁实现里,lockdep 的钩子无处不在,串起来看:

接入点 所在实现 作用
dep_map 字段 spinlock_types_raw.h:20-22、mutex_types.h:51-53、seqcount 每锁实例的登记入口
preempt_disable() + spin_acquire() spinlock_api_smp.h:154-160 持锁栈压栈(16.1.2 节)
LOCK_CONTENDED lockdep.h:441-471 trylock 统计 + 等待登记
mutex_acquire_nest() mutex.c:614 mutex 慢路径登记(支持 nest_lock 声明)
rwlock_acquire() rwlock_api_smp.h:227-231 读写锁登记(read 标志区分)
seqprop_assert_lock_held() seqlock.h:158-188 校验 seqcount 写侧持锁(16.3.6 节)
RCU_LOCKDEP_WARN() rcupdate.h 多处 RCU 读侧上下文校验(17.1 节)

任务的退场清理

第 14.1 节分析 do_exit() 时出现过 lockdep_free_task()——任务退出时释放其 held_locks 相关状态。配套地,lockdep_init()(3.1 节 start_kernel 中调用)在启动时初始化哈希表与静态数组,locking_selftest()(kernel/locking/ 下 selftest 用例)用预设的锁场景自检引擎本身。用户态视图由 kernel/locking/lockdep_proc.c 提供:/proc/lockdep(锁类列表,lockdep_proc.c:120)、/proc/lockdep_chains(:185)、/proc/lockdep_stats(:231,四张表的占用率)——调试时先看 stats 判断是否撞容量上限。

实践要点

  • 每个 DEFINE_* 一把逻辑锁:同名锁若被 kfree 后重建(如 per-file 锁),需 lockdep_register_key()/lockdep_unregister_key() 换身份,否则锁类串味。
  • subclass 表达锁序:lock_acquire_nested(lock, subclass) 显式声明同类锁的层次(如 inode 锁 i_mutex 系列的 I_MUTEX_PARENT/I_MUTEX_CHILD),lockdep 按子类分别跟踪,允许"父→子"禁止"子→父"。
  • 误报处理:lockdep 报告也可能源于设计上就安全的锁序(如双向迁移场景),此时用 lockdep_set_subclass 或 lock_acquire_nest 标注豁免,而不是关掉检查。
  • 成本:PROVE_LOCKING 内核慢一个数量级、内存涨数 MB、held_locks 使 task_struct 变大——只用于 debug 构建、CI 压测与 syzkaller 这类 fuzzing 基建。

要点总结:

  • lockdep 把死锁检测从"事后救火"变成"事前证明":运行时收集锁序依赖边,BFS 判环,中断使能/使用做交叉验证。
  • 四张静态表(锁类/依赖边/锁链/链索引)各有编译期上限,超限即 BUG——lockdep 的图是有界的。
  • 每条新依赖边触发反向 BFS + 中断一致性检查,靠 chain key 缓存避免重复验证。
  • 接入点遍布 16.1-16.3 节所有锁容器(dep_map + 各 acquire 钩子),与 PREEMPT_RT、seqcount 关联锁、RCU 校验共同构成内核的运行时并发正确性体系。

至此第 16 章四种锁原语的实现与防护网都已展开。锁解决的是"互斥",下一章的 RCU 将展示另一极:读侧连原子操作都不要的同步。