如果A 不是最后一个申请者,说明中途来了新的申请者 B,那么 A必须一直等待 B 将链表构建完整,即 A 的 mcs_lock_node 结构的 next 域不再为 NULL。最后 A 将 B 的 waiting 域置为 0。
三、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 的实现。
1. raw_spinlock_t 数据结构
由于 MCS Spinlock 的申请和释放操作需要涉及同一个mcs_lock_node 结构,而且这个结构在现有的接口函数中并不存在,因此不适合使用接口函数的局部变量或是调用接口的外层函数的局部变量。一个简单的方法是在raw_spinlock_t 数据结构中为每个处理器预备一个 mcs_lock_node 结构(因为申请自旋锁的时候会关闭内核抢占,每个处理器上至多只有一个执行线程参与锁操作,所以只需要一个 mcs_lock_node)。在 NUMA 系统中,mcs_lock_node 结构可以在处理器所处节点的内存中分配,从而加快访问速度。为简化代码,本文的实现使用 mcs_lock_node 数组。
清单 1. raw_spinlock_t 数据结构
typedef struct mcs_lock_node {
volatile int waiting;
volatile struct mcs_lock_node *next;
} mcs_lock_node;
typedef volatile mcs_lock_node *mcs_lock_node_ptr;
typedef mcs_lock_node_ptr mcs_lock;
typedef struct {
mcs_lock slock;
mcs_lock_node nodes[NR_CPUS];
} raw_spinlock_t;
因为 waiting 和 next 会被其它线程异步修改,因此必须使用 volatile 关键字修饰,这样可以确保它们在任何时间呈现的都是最新的值。
2. __raw_spin_lock 函数
清单 2. __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;
pre->next = me; (d)
while (me->waiting) { (e)
continue;
}
}
raw_smp__processor_id() 函数获得所在处理器的编号,用以索引 mcs_lock_node 结构。但是此处直接使用 raw_smp__processor_id() 函数会有头文件循环依赖的问题。这是因为 raw_smp_processor_id 在 include/asm-x86/smp.h 中实现,该头文件最终会包含 include/asm-x86/spinlock.h,即 __raw_spin_lock 所在的头文件。我们可以简单地将 raw_smp__processor_id() 的代码复制一份到 spinlock.h 中来解决这个小问题。
将 lock->slock 指向本地的 mcs_lock_node 结构,使用原子交换操作。因为 me 变量随后还要使用,故用一局部变量 tmp 与 lock->slock 互换值。
锁处于空闲状态,直接返回。
设置前驱的 next 指针。
在本地 waiting 域上自旋。
3. __raw_spin_trylock 函数
清单 3. __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;
}
(a) 该函数的语义是:如果锁空闲,则获得锁并返回 1;否则直接返回 0。当且仅当 lock->slock 为 NULL 时表明锁空闲,所以使用原子比较-交换操作测试lock->slock 是否为 NULL,如不是则与 me 变量交换值。
4. __raw_spin_unlock 函数
清单 4. __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)
}
判断是否有后继申请者。
判断自己是否是最后一个申请者,若是的话就将 lock->slock 置为 NULL。
中途来了申请者,自旋等待后继申请者将链表构建完成。
通知直接后继结束自旋。
5. __raw_spin_is_locked 函数
清单 5. __raw_spin_is_locked 函数
static inline int __raw_spin_is_contended(raw_spinlock_t *lock)
{
return (lock->slock != NULL); (a)
}
lock->slock 为 NULL 就表明锁处于空闲状态。
四、总结
MCS Spinlock 是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,直接前驱负责通知其结束自旋,从而极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。笔者使用 Linux 内核开发者 Nick Piggin 的自旋锁测试程序对内核现有的排队自旋锁和 MCS Spinlock 进行性能评估,在 16 核 AMD 系统上,MCS Spinlock 的性能大约是排队自旋锁的 8.7 倍。随着大规模多核、NUMA 系统的广泛使用,MCS Spinlock 一定能大展宏图。