从无序竞争到有序排队:Linux内核TicketSpinLock原理

admin 2025-12-27 02:04:53 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: Linux内核TicketSpinlock通过引入排队机制解决传统自旋锁的公平性问题,利用next和current字段实现先来先服务。核心基于atomic_fetch_add原子操作分配唯一排队号,线程循环等待匹配。该设计杜绝了饥饿现象,但频繁读取同一变量可能引发缓存风暴,影响性能。 综合评分: 88 文章分类: 二进制安全


cover_image

从无序竞争到有序排队:Linux 内核 Ticket Spin Lock 原理

原创

独钓孤舟雪

MyStackTrace

2025年12月11日 00:15 上海

Linux 内核中的 Ticket Spinlock 是一种非常精巧的设计,它通过引入排队的机制,优雅地解决了传统自旋锁的公平性问题。在 Ticket Spinlock 出现之前,传统的自旋锁(用一个整数表示,0 为空闲,1 为占用)存在显著的不公平性,所有竞争锁的 CPU 在锁释放时会同时尝试抢锁,这可能导致离锁内存地址近的、缓存状态更优的 CPU 更容易成功,而其他 CPU 可能长时间饥饿,Ticket Spinlock 的核心目标就是来解决这种问题,实现先来先服务(FIFO)的公平性。

Ticket Spinlock 的设计灵感来源于银行柜台的排队叫号系统,它的核心数据结构通常包含两个关键字段:

  • 取号机上的最新号码:next new ticket,下一个请求锁的线程将获取到的这个值作为自己的 ticket,然后 next new ticket 自动加 1。
  • 当前服务的号码:current service ticket,当前持有自旋锁的线程的 ticket,当前持有自旋锁的线程解锁的时候会对 current service ticket 加 1,相当于叫号了,持有对应号的线程就会抢到自旋锁。

当自旋锁被初始化时,next new ticket 和 current service ticket 都被设置为 0。当 next new ticket 和 current service ticket 相等时,表示自旋锁处于空闲状态,没有线程持有该自旋锁,否则自旋锁肯定是被某个线程持有的。

下面我们来看下在 Linux 内核中是如何在 x86 平台上实现 Ticket Spinlock 的,不同硬件平台上对  Ticket Spinlock 的实现略有不同,但思想都是一样的。

对于 Ticket Spinlock 来说,自旋锁最终的定义为 arch_spinlock_t 结构体,该结构体中就只有一个 atomic_t 类型的成员 val,这个原子变量用来表示该自旋锁是否被持有。这个原子变量是一个 32 位的变量,它的高 16 位表示 next new ticket,低 16 位表示 current service ticket。

函数 ticket_spin_lock 为 Ticket Spinlock 的加锁函数。一进该函数,首先是调 atomic_fetch_add, 这是一个原子性的“读-修改-写”操作,它一次性完成:读取 lock->val 的当前值到 val(这相当于是取号),然后将 1<<16 加到 lock->val 上,这是把 lock->val 的高 16 位加 1 (这相当是取号机上的号码刚被取走了一个,自动加一)。这里需要注意的是返回值 val:是加操作之前的旧值,假设当前 lock->val 为0,那么执行后 val = 0,lock->val 变为 0x00010000(高16位变为1)。这个原子操作(atomic_fetch_add)确保了每个竞争者都能获取到一个唯一且递增的排队号。

接下来的代码 “u16 ticket = val >> 16;” 表示从旧值 val 中提取出高 16 位,这就是当前线程的排队号(ticket),如果提取到的 ticket 和 val 的低 16 位,也就是 current service ticket 相等,则说明当前没有线程持有该自旋锁,那么当前排队线程自然就获取到自旋锁了,直接退出 ticket_spin_lock 函数。如果当前排队线程的 ticket 和 val 的低 16 位不相等,则说明该自旋锁当前已被其他线程持有,那么当前排队线程就需要等待(spin),等待的代码如下:

atomic_cond_read_acquire(&lock->val, ticket == (u16)VAL);

这是一个在循环中等待条件成立的宏,它会不断地读取 lock->val,直到传入的条件为真,这里的条件是:lock->val 的低 16 位,也就是 current service ticket(即 (u16)VAL)等于当前线程持有的 ticket。

下面是 Ticket Spinlock 的解锁函数 ticket_spin_unlock,解锁过程的核心是“叫下一个号”。

解锁函数的第一句代码如下:

u16 *ptr = (u16 *)lock&nbsp;+ IS_ENABLED(CONFIG_CPU_BIG_ENDIAN);

这行代码的目的是得到一个指向 lock->val 中 current service ticket 字段(低16位)的指针。(u16 *)lock 将 lock 指针转换为 16 位无符号整数指针,指向其低地址部分。IS_ENABLED(CONFIG_CPU_BIG_ENDIAN) 在小端序(Little-Endian)CPU 上为 0,因此最终 ptr 指针指向 lock->val 低 16 位的地址,所以这第一句代码的意图是获取 current service ticket 字段的地址。拿到 current service ticket 字段的地址之后,通过 smp_store_release(ptr, (u16)val + 1) 来把 current service ticket 字段加 1 并写回去,这个操作相当于“叫号”。一旦 current service ticket 值被更新,正在等待的线程中,那个持有匹配 ticket的线程就会从 atomic_cond_read_acquire 中返回,从而获得锁。

这个 Ticket Spinlock 的实现非常精炼,其核心优势在于公平性,严格遵循先到先得的原则,避免了传统自旋锁由于“乱抢”导致的饥饿问题,它通过 atomic_fetch_add原子性地分配排队号,并通过 atomic_cond_read_acquire 和 smp_store_release 这类具有明确内存序语义的指令,高效且正确地在多核环境下实现同步。

在 Ticket Spinlock 中,所有等待的 CPU 都频繁读取同一个 current service ticket 变量,当一个 CPU 解锁更新 current service ticket 时,会导致其他所有 CPU 上缓存了该变量的缓存行失效,从而引发“缓存风暴”影响性能,所以Linux 内核又引入了一个更高级的排队自旋锁(queued_spin_lock)可以缓解这个问题,这个我们后面再介绍。


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:MyStackTrace 独钓孤舟雪《从无序竞争到有序排队:Linux 内核 Ticket Spin Lock 原理》

评论:0   参与:  4