在 《Linux 内核 MCS Spin Lock 原理》 一文中,我们探讨了 MCS Spin Lock 的实现。这种锁机制在实现先来先服务的公平性同时,也有效解决了传统自旋锁在多核高争用下因“缓存行颠簸”导致的性能瓶颈。然而,Linux 内核并未直接采用原始的 MCS 锁,而是基于其思想发展出了一种更优的变体——Queued Spin Lock(排队自旋锁)。本文将深入剖析其工作原理。
Linux 内核选择不直接使用 MCS 锁,一个核心原因在于结构兼容性问题。传统的 spinlock_t 结构体本质上仅占用 4 个字节,而 struct mcs_spinlock 则需要 16 字节,二者在内存布局上无法兼容。考虑到内核中大量自旋锁相关 API 都以 spinlock_t 为参数,强行更改为 mcs_spinlock 将引发连锁改动,这无疑是不可行的。

因此,内核开发者巧妙地设计出了 Queued Spin Lock。其核心数据结构 struct qspinlock 同样只有 4 个字节,能够无缝替换传统的 spinlock_t。更重要的是,它在底层继承了 MCS 锁的精髓,在保障公平性的同时,依然能够有效避免多处理器争抢锁时的缓存行颠簸问题。
Queued Spin Lock 引入了一种分级等待策略,与 MCS Lock 将所有等待 CPU 都放入队列不同,它将竞争者划分为 4 种角色,并在不同位置上自旋:
- Owner:锁的当前持有者。
- Pender:第一顺位等待者。当 Owner 释放锁时,它将直接获得锁。
- Successor:MCS 等待队列的队头节点,即第二顺位等待者。第二顺位及之后的等待者才会进入 MCS 队列。
- Queuer:MCS 等待队列中,位于 Successor 之后的其他等待者。
struct qspinlock 的设计堪称精妙,它将 MCS 锁的思想压缩进了一个 32 位整数中:

该结构体使用一个联合体(union)将一个 32 位的 val 变量划分为不同的位域(以小端序为例):
- locked (Bit 0-7):表示锁是否被持有(0=空闲,1=已持有)。Pender 角色会在此位上自旋等待。
- pending (Bit 8-15):表示是否存在第一顺位等待者(Pender),0 为否,1 为是。
- locked_pending (Bit 0-15):
locked 与 pending 字段的组合,用于判断锁当前是否有持有者(Owner)或第一顺位等待者(Pender)。Successor 角色会在此组合位上自旋等待。
- tail (Bit 16-31):指向 MCS 等待队列的尾部节点。这 16 位并非直接指针,而是一个索引,用于定位具体的 MCS 节点。
Queued Spin Lock 使用一个每处理器(Per-CPU)的 struct qnode 数组来管理 MCS 队列。每个 qnode 主要包含一个 struct mcs_spinlock 节点。每个 CPU 上预分配了 4 个(MAX_NODES=4)qnode,分别对应进程、软中断、硬中断以及 NMI 这四种可能获取自旋锁的上下文,因为一个 CPU 上最多只会有这四种上下文试图竞争锁(针对不同锁)。当 CPU 需要进入 MCS 队列排队时,它会根据当前运行的上下文,选取对应的 MCS 节点并插入队列。

那么,CPU 何时需要进入 MCS 队列等待呢?当锁的等待者超过 1 个(即除了 Pender 外,还有 Successor 或 Queuer)时,后续的等待者就必须在 MCS 队列中排队。此时,tail 字段的值非零,它由两部分构成:
- tail index (Bit 16-17, 2 bits):队尾 MCS 节点在其所属 CPU 的
qnodes 数组中的索引(0-3)。
- tail cpu (Bit 18-31, 14 bits):队尾 MCS 节点所属 CPU 的编号加 1(加 1 是为了区分 CPU 0 和空队列状态)。
通过 tail cpu 和 tail index,内核即可精确定位到 MCS 等待队列尾部 CPU 所对应的 struct mcs_spinlock 节点。这种设计对于理解 Linux 内核并发编程至关重要,相关的高级同步机制都可以在我们的 网络/系统 板块找到深度解析。
|