文章总结: 本文介绍Linux内核QueuedSpinLock原理,它是MCS自旋锁的优化变种。该锁通过4字节结构体兼容传统spinlock_t,解决了缓存行颠簸问题并保证公平性。通过定义Owner、Pender等四种角色及利用Per-CPU的qnode数组管理等待队列,该机制在高争用场景下显著提升了性能。 综合评分: 86 文章分类: 二进制安全,安全开发
Linux 内核 Queued Spin Lock 原理(1)
原创
独钓孤舟雪
MyStackTrace
2025年12月20日 23:56 北京
在前面 Linux 内核 MCS Spin Lock 原理一文中我们介绍了 MCS Spin Lock 的原理,这种自旋锁实现了先来先服务的公平性,并且在此基础上还解决了传统自旋锁在多核高争用下的性能问题(缓存行颠簸)。然而,Linux 内核并未直接使用 MCS Spin Lock,而是使用的它的优化变种 Queued Spin Lock,这篇文章我们就来介绍一下这种最优实现方案。
Linux 内核之所以没有直接使用 MCS Spin Lock 主要是它的锁结构体 mcs_spinlock 无法与传统自旋锁结构体 spinlock_t 兼容,结构体spinlock_t 其实就是一个 4 字节的整数,而 struct mcs_spinlock 一共占用 16 个字节,这两个结构的布局是无法兼容的。而 Linux 内核中提供的自旋锁相关的 API 都是以 spinlock_t 为参数的,如果要改成使用 mcs_spinlock 作为参数,那内核中所有使用自旋锁的地方都需要因为这种不兼容性而跟着改动,这显然是不合理的。
所以聪明的内核开发者们就设计出了 Queued Spin Lock,它的结构体 qspinlock 一共只有 4 个字节,因此可以兼容传统自旋锁结构,而且 Queued Spin Lock 是基于 MCS Spin Lock 实现的,既可以保证自旋锁的公平性,又可以在有众多处理器争用自旋锁的情况下避免”缓存行颠簸“这种性能问题。
与 MCS Spin Lock 不同的是,Queued Spin Lock 采用分级等待的策略,该策略并不会把所有等待自旋锁的 CPU 都放到 MCS 等待队列中,而且在不同位置上等待的 CPU 自旋检查的变量也不一样。在 Queued Spin Lock 中,试图获取自旋锁的 CPU 被划分为 4 类角色:
-
Owner:自旋锁当前的持有者。
-
Pender:自旋锁当前的第一顺位等待者,也就是当 Owner 释放自旋锁之后,会由这个第一顺位等待者获取到这个自旋锁。
-
Successor:自旋锁的 MCS 等待队列中的第一个等待者(排在队列头的等待者),也是自旋锁当前的第二顺位等待者,只有第二顺位等待者及后来的等待者才会进入 MCS 等待队列。
-
Queuer:自旋锁的 MCS 等待队列中除了队头(Successor)的其他等待者。
Queued Spin Lock 结构体如下,该结构体的设计是比较讲究的,可以看作是把 MCS 锁的思想精华压缩进了一个 32 位的整数中。
struct qspinlock 使用一个 union 将一个 32 位的 val 变量划分成不同的位域,以小端序(Little-Endian)为例,每个字段的含义如下:
-
locked(Bit 0-7):表示自旋锁是否被持有,0 表示未被持有,1 表示已被持有。自旋锁的 Pender 会自旋等待在这个 locked 变量上。
-
pending(Bit 8-15):表示是否存在第一顺位等待者(Pender),0 标识不存在,1 表示存在。
-
locked_pending(Bit 0-15):其实就是 locked 和 pending 两个字段的组合,用来判断自旋锁当前是否有持有者(Owner)或者第一顺位等待者(Pender)。自旋锁的 Successor 会自旋等待在这个 locked_pending 变量上。
-
tail(Bit 16-31):指向 MCS 等待队列的尾部节点,这里的 tail 只有 16 个 Bit,显然不可能是指向 MCS 节点指针,它只是一个索引,通过这个索引能够找到相应的 MCS 节点。
Queued Spin Lock 使用一个 Per-CPU 的 struct qnode 数组来管理 MCS 等待队列,struct qnode 里面基本上就只包了一个 struct mcs_spinlock,也就是一个 MCS 节点。每个 CPU 上都有 4 个(MAX_NODES=4)qnode,也就是 MCS 节点,这 4 个 MCS 节点分别表示该 CPU 的进程、软中断、硬中断以及 NMI 这 4 种上下文,因为一个 CPU 最多只会有这 4 种上下文会同时试图获取自旋锁(不是同一个自旋锁)。也就是说每个 CPU 有 4 个 MCS 节点,对应其 4 种可能的执行上下文,当一个 CPU 需要进入 MCS 队列排队时,它就根据当前所运行的上下文索引获取自己对应的 MCS 节点,然后把这个 MCS 节点插入 MCS 等待队列中。
那么什么时候 CPU 才能够进入 MCS 队列等待呢?如果该自旋锁当前有超过 1 个等待者,也就是除了 Pender 之外还有其他的等待者,包括 Successor 和 Queuer,这些等待者就必须在 MCS 队列中等待。在这种情况下 tail 字段的值非 0,这个非 0 的值分为下面两部分:
-
tail index(Bit 16-17,2 bits):等待队列队尾的 MCS 节点在它所属的 CPU 的 qnodes 数组中的索引。
-
tail cpu(Bit 18-31,14 bits):等待队列队尾的 MCS 节点所属的 CPU 的编号加 1(加 1 是为了区分编号为 0 的 CPU 和空队列的情况)。
内核通过 tail cpu 和 tail index 来确定并定位到 MCS 等待队列尾部的 CPU 对应的 MCS 节点(struct mcs_spinlock)。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:MyStackTrace 独钓孤舟雪《Linux 内核 Queued Spin Lock 原理(1)》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论