引言
自旋锁(Spinlock)是一种在 Linux 内核 [1] 中广泛运用的底层同步机制,长期以来,人们总是关注于自旋锁的安全和高效,而忽视了自旋锁的“公平”性。排队自旋锁(FIFO Ticket Spinlock)是内核开发者 Nick Piggin 在Linux Kernel 2.6.25 版本中引入的一种新型自旋锁,它通过保存执行线程申请锁的顺序信息解决了传统自旋锁的“不公平”问题 [4]。
排队自旋锁仍然使用原有的 raw_spinlock_t 数据结构,但是赋予 slock 域新的含义。为了保存顺序信息,slock 域被分成两部分,低位部分保存锁持有者的票据序号(Ticket Number),高位部分则保存未来锁申请者的票据序号。只有 Next 域与 Owner 域相等时,才表明锁处于未使用状态(此时也无执行线程申请该锁)。排队自旋锁初始化时 slock 被置为 0,即 Owner 和 Next 置为 0。内核执行线程申请自旋锁时,原子地将 Next 域加 1,并将原值返回作为自己的票据序号。如果返回的票据序号等于申请时的 Owner 值,说明自旋锁处于未使用状态,则直接获得锁;否则,该线程忙等待检查 slock 的 Owner 部分是否等于自己持有的票据序号,一旦相等,则表明锁轮到自己获取。线程释放锁时,原子地将 Owner 域加 1 即可,下一个线程将会发现这一变化,从忙等待状态中退出。线程将严格地按照申请顺序依次获取排队自旋锁,从而完全解决了“不公平”问题。
但是在大规模多处理器系统和 NUMA系统中,排队自旋锁(包括传统自旋锁)存在一个比较严重的性能问题:由于执行线程均在同一个共享变量 slock 上自旋,申请和释放锁的时候必须对 slock 进行修改,这将导致所有参与排队自旋锁操作的处理器的缓存变得无效。如果排队自旋锁竞争比较激烈的话,频繁的缓存同步操作会导致繁重的系统总线和内存的流量,从而大大降低了系统整体的性能。
回页首
MCS Spinlock 的原理
为了解决自旋锁可扩展性问题,学术界提出了许多改进版本,其核心思想是:每个锁的申请者(处理器)只在一个本地变量上自旋。MCS Spinlock [2] 就是其中一种基于链表结构的自旋锁(还有一些基于数组的自旋锁)。MCS Spinlock的设计目标如下:
MCS Spinlock采用链表结构将全体锁申请者的信息串成一个单向链表,如图 1 所示。每个锁申请者必须提前分配一个本地结构 mcs_lock_node,其中至少包括 2 个域:本地自旋变量 waiting 和指向下一个申请者 mcs_lock_node 结构的指针变量 next。waiting 初始值为 1,申请者自旋等待其直接前驱释放锁;为 0 时结束自旋。而自旋锁数据结构 mcs_lock 是一个永远指向最后一个申请者 mcs_lock_node 结构的指针,当且仅当锁处于未使用(无任何申请者)状态时为 NULL 值。MCS Spinlock 依赖原子的“交换”(swap)和“比较-交换”(compare_and_swap)操作,缺乏后者的话,MCS Spinlock 就不能保证以先进先出的顺序获取锁,从而可能造成“饥饿”(Starvation)。
MCS Spinlock 申请操作描述如下:
MCS Spinlock 释放操作描述如下:
回页首
MCS Spinlock 的实现
目前 Linux 内核尚未使用 MCS Spinlock。根据上节的算法描述,我们可以很容易地实现 MCS Spinlock。本文的实现针对x86 体系结构(包括 IA32 和 x86_64)。原子交换、比较-交换操作可以使用带 LOCK 前缀的 xchg(q),cmpxchg(q)[3] 指令实现。
为了尽量减少工作量,我们应该重用现有的自旋锁接口[4]。下面详细介绍 raw_spinlock_t 数据结构,函数__raw_spin_lock、__raw_spin_unlock、 __raw_spin_is_locked 和 __raw_spin_trylock 的实现。
raw_spinlock_t 数据结构
MCS Spinlock 的申请和释放操作需要涉及同一个mcs_lock_node 结构,这个mcs_lock_node 结构独立于锁的数据结构。为了重用 Linux Kernel 现有的自旋锁接口函数,我们使用了一个简单的方法,在raw_spinlock_t 数据结构中为每个处理器预备一个 mcs_lock_node 结构(因为申请自旋锁的时候会关闭内核抢占,每个处理器上至多只有一个执行线程参与锁操作,所以只需要一个 mcs_lock_node)。在 NUMA 系统中,mcs_lock_node 结构可以在处理器所处节点的内存中分配,从而加快访问速度。为简化代码,本文的实现使用 mcs_lock_node 数组。
typedef struct _mcs_lock_node { volatile int waiting; struct _mcs_lock_node *volatile next; } ____cacheline_aligned_in_smp mcs_lock_node; typedef mcs_lock_node *volatile mcs_lock; typedef struct { mcs_lock slock; mcs_lock_node nodes[NR_CPUS]; } raw_spinlock_t; |
因为 waiting 和 next 会被其它处理器异步修改,因此必须使用 volatile 关键字修饰,这样可以确保它们在任何时间呈现的都是最新的值。加上 ____cacheline_aligned_in_smp 修饰在多处理器环境下会增加mcs_lock_node 结构的大小,但是可以使其按高速缓存管线(cache line)大小对齐以消除 False Sharing[5]。这是因为由于 mcs_lock_node 结构比较小,每个等待的处理器在自己的 mcs_lock_node 的 waiting 域上自旋的时候,相邻处理器的 mcs_lock_node 结构会一齐放在同一个高速缓存管线中(一般 L1,L2 的高速缓存管线为 64 字节),一旦锁拥有者处理器在释放锁阶段修改其直接后继的 waiting 域时,会无效化整个高速缓存管线,因此可能造成一些后续等待者处理器的相应高速缓存管线也被迫更新,增加了系统总线的无谓开销。
__raw_spin_lock 函数
static __always_inline void __raw_spin_lock(raw_spinlock_t *lock) { int cpu; mcs_lock_node *me; mcs_lock_node *tmp; mcs_lock_node *pre; cpu = raw_smp_processor_id(); (a) me = &(lock->nodes[cpu]); tmp = me; me->next = NULL; pre = xchg(&lock->slock, tmp); (b) if (pre == NULL) { /* mcs_lock is free */ return; (c) } me->waiting = 1; (d) smp_wmb(); (e) pre->next = me; (f) while (me->waiting) { (g) asm volatile (“pause”); } } |
__raw_spin_trylock 函数
static __always_inline int __raw_spin_trylock(raw_spinlock_t *lock) { int cpu; mcs_lock_node *me; cpu = raw_smp_processor_id(); me = &(lock->nodes[cpu]); me->next = NULL; if (cmpxchg(&lock->slock, NULL, me) == NULL) (a) return 1; else return 0; } |
__raw_spin_unlock 函数
static __always_inline void __raw_spin_unlock(raw_spinlock_t *lock) { int cpu; mcs_lock_node *me; mcs_lock_node *tmp; cpu = raw_smp_processor_id(); me = &(lock->nodes[cpu]); tmp = me; if (me->next == NULL) { (a) if (cmpxchg(&lock->slock, tmp, NULL) == me) { (b) /* mcs_lock I am the last. */ return; } while (me->next == NULL) (c) continue; } /* mcs_lock pass to next. */ me->next->waiting = 0; (d) } |
__raw_spin_is_locked 函数
static inline int __raw_spin_is_contended(raw_spinlock_t *lock) { return (lock->slock != NULL); (a) } |
回页首
总结
MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。笔者使用 Linux 内核开发者 Nick Piggin 的自旋锁压力测试程序对内核现有的排队自旋锁和 MCS Spinlock 进行性能评估,在 16 核 AMD 系统中,MCS Spinlock 的性能大约是排队自旋锁的 8.7 倍。随着大规模多核、NUMA 系统的广泛使用,MCS Spinlock 一定能大展宏图。