快捷搜索:

Linux内核的排队自旋锁(FIFO Ticket Spinlock)

小序

自旋锁(Spinlock)是一种 Linux 内核中广泛运用的底层同步机制。自旋锁是一种事情于多处置惩罚器情况的特殊的锁,在单处置惩罚情况中自旋锁的操作被调换为空操作。当某个处置惩罚器上的内核履行线程申请自旋锁时,假如锁可用,则得到锁,然后履行临界区操作,着末开释锁;假如锁已被占用,线程并不会转入就寝状态,而是忙等待该锁,一旦锁被开释,则第一个感知此信息的线程将得到锁。

经久以来,人们老是关注于自旋锁的安然和高效,而漠视了自旋锁的“公道”性。传统的自旋锁本色上用一个整数来表示,值为1代表锁未被占用。这种无序竞争的本色特征导致履行线程无法包管何时能取到锁,某些线程可能必要等待很长光阴。跟着谋略机处置惩罚器个数的赓续增长,这种“不公道”问题将会日益严重。

排队自旋锁(FIFO Ticket Spinlock)是 Linux 内核 2.6.25 版本引入的一种新型自旋锁,它经由过程保存履行线程申请锁的顺序信息办理了传统自旋锁的“不公道”问题。排队自旋锁的代码由 Linux 内核开拓者 Nick Piggin 实现,今朝只针对 x86 体系布局(包括 IA32 和 x86_64),信托很快就会被移植到其它平台。

传统自旋锁的实现与不够

Linux 内核自旋锁的底层数据布局 raw_spinlock_t 定义如下:

清单 1. raw_spinlock_t 数据布局

typedef struct {

unsigned int slock;

} raw_spinlock_t;

slock 虽然被定义为无符号整数,然则实际上被算作有符号整数应用。slock 值为 1 代表锁未被占用,值为 0 或负数代表锁被占用。初始化时 slock 被置为 1。

线程经由过程宏 spin_lock 申请自旋锁。假如不斟酌内核抢占,则 spin_lock 调用 __raw_spin_lock 函数,代码如下所示:

清单 2. __raw_spin_lock 函数

static inline void __raw_spin_lock(raw_spinlock_t *lock)

{

asm volatile("

1:  "

LOCK_PREFIX " ; decb %0

"

"jns 3f

"

"2:  "

"rep;nop

"

"cmpb $0,%0

"

"jle 2b

"

"jmp 1b

"

"3:

"

: "+m" (lock->slock) : : "memory");

}

LOCK_PREFIX 的定义如下:

清单 3. LOCK_PREFIX宏

#ifdef CONFIG_SMP

#define LOCK_PREFIX

".section .smp_locks,"a"

"

_ASM_ALIGN "

"

_ASM_PTR "661f

" /* address */

".previous

"

"661:

lock; "

#else /* ! CONFIG_SMP */

#define LOCK_PREFIX ""

#endif

在多处置惩罚器情况中 LOCK_PREFIX 实际被定义为 “lock”前缀。

x86 处置惩罚器应用“lock”前缀的要领供给了在指令履行时代对总线加锁的手段。芯片上有一条引线 LOCK,假如在一条汇编指令(ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG)前加上“lock” 前缀,颠末汇编后的机械代码就使得处置惩罚器履行该指令时把引线 LOCK 的电位拉低,从而把总线锁住,这样其它处置惩罚器或应用DMA的外设暂时无法经由过程同一总线造访内存。

从 P6 处置惩罚器开始,假如指令造访的内存区域已经存在于处置惩罚器的内部缓存中,则“lock” 前缀并不将引线 LOCK 的电位拉低,而是锁住本处置惩罚器的内部缓存,然后寄托缓存同等性协议包管操作的原子性。

decb 汇编指令将 slock 的值减 1。因为“减 1”是“读-改-写”操作,不是原子操作,可能会被同时申请锁的其它处置惩罚器上的线程滋扰,以是必须加上“lock”前缀。

jns 汇编指令反省 EFLAGS 寄存器的 SF(符号)位,假如为 0,阐明 slock 原本的值为 1,则线程得到锁,然后跳到标签 3 的位置停止本次函数调用。假如 SF 位为 1,阐明 slock 原本的值为 0 或负数,锁已被占用。那么线程转到标签 2 处赓续测试 slock 与 0 的大年夜小关系,要是 slock 小于或即是 0,跳转到标签 2 的位置继承忙等待;要是 slock 大年夜于 0,阐明锁已被开释,则跳转到标签 1 的位置从新申请锁。

线程经由过程宏 spin_unlock 开释自旋锁,该宏调用 __raw_spin_unlock 函数:

清单 4. __raw_spin_unlock函数

static inline void __raw_spin_unlock(raw_spinlock_t *lock)

{

asm volatile("movb $1,%0" : "+m" (lock->slock) :: "memory");

}

可见 __raw_spin_unlock 函数仅仅履行一条汇编指令:将 slock 置为 1。

只管拥有应用简单方便、机能好的优点,自旋锁也存在自身的不够:

因为传统自旋锁无序竞争的本色特征,内核履行线程无法包管何时可以取到锁,某些履行线程可能必要等待很长光阴,导致“不公道”问题的孕育发生。这有两方面的缘故原由:

跟着处置惩罚器个数的赓续增添,自旋锁的竞争也在加剧,自然导致更长的等待光阴。

开释自旋锁时的重置操作将无效化所有其它正在忙等待的处置惩罚器的缓存,那么在处置惩罚器拓扑布局中临近自旋锁拥有者的处置惩罚器可能会更快地刷新缓存,因而增大年夜得到自旋锁的机率。

因为每个申请自旋锁的处置惩罚器均在全局变量 slock 上忙等待,系统总线将由于处置惩罚器间的缓存同步而导致繁重的流量,从而低落了系统整体的机能。

排队自旋锁的设计道理

传统自旋锁的“不公道”问题在锁竞争猛烈的办事器系统中尤为严重,是以 Linux 内核开拓者 Nick Piggin 在 Linux 内核 2.6.25 版本中引入了排队自旋锁:经由过程保存履行线程申请锁的顺序信息来办理“不公道”问题。

排队自旋锁仍旧应用原有的 raw_spinlock_t 数据布局,然则付与 slock 域新的含义。为了保存顺序信息,slock 域被分成两部分,分手保存锁持有者和未来锁申请者的票据序号(Ticket Number),如下图所示:

图 1. Next 和 Owner 域

假如处置惩罚器个数不跨越 256,则 Owner 域为 slock 的 0-7 位,Next 域为 slock 的 8-15 位,slock 的高 16 位不应用;假如处置惩罚器个数跨越 256,则 Owner 和 Next 域均为 16 位,此中 Owner 域为 slock 的低 16 位。可见排队自旋锁最多支持 216=65536 个处置惩罚器。

只有 Next 域与 Owner 域相等时,才注解锁处于未应用状态(此时也无人申请该锁)。排队自旋锁初始化时 slock 被置为 0,即 Owner 和 Next 置为 0。内核履行线程申请自旋锁时,原子地将 Next 域加 1,并将原值返回作为自己的票据序号。假如返回的票据序号即是申请时的 Owner 值,阐明自旋锁处于未应用状态,则直接得到锁;否则,该线程忙等待反省 Owner 域是否即是自己持有的票据序号,一旦相等,则注解锁轮到自己获取。线程开释锁时,原子地将 Owner 域加 1 即可,下一个线程将会发明这一变更,从忙等待状态中退出。线程将严格地按照申请顺序依次获取排队自旋锁,从而完全办理了“不公道”问题。

排队自旋锁的实现

排队自旋锁没有改变原有自旋锁的调用接口,该 API 因此 C 说话宏的形式供给给开拓职员。下表列出 6 个主要的 API 和相对应的底层实现函数:

表 1. 排队自旋锁 API

底层实现函数

描述

spin_lock_init

将锁置为初始未应用状态(值为 0)

spin_lock

__raw_spin_lock

忙等待直到 Owner 域即是本地票据序号

spin_unlock

__raw_spin_unlock

Owner 域加 1,将锁传给后续等待线程

spin_unlock_wait

__raw_spin_unlock_wait

不申请锁,忙等待直到锁处于未应用状态

spin_is_locked

__raw_spin_is_locked

测试锁是否处于应用状态

spin_trylock

__raw_spin_trylock

假如锁处于未应用状态,得到锁;否则直接返回

下面先容此中 3 个底层函数的实现细节,假定处置惩罚器个数不跨越 256。

__raw_spin_is_locked

清单 5. __raw_spin_is_locked 函数

static inline int __raw_spin_is_locked(raw_spinlock_t *lock)

{

int tmp = *(volatile signed int *)(&(lock)->slock);

return (((tmp >> 8) & 0xff) != (tmp & 0xff));

}

此函数判断 Next 和 Owner 域是否相等,假如相等,阐明自旋锁处于未应用状态,返回 0;否则返回1。

tmp 这种繁杂的赋值操作是为了直接从内存中取值,避免处置惩罚器缓存的影响。

__raw_spin_lock

清单 6. __raw_spin_lock 函数

static inline void __raw_spin_lock(raw_spinlock_t *lock)

{

short inc = 0x0100;

__asm__ __volatile__ (

LOCK_PREFIX "xaddw %w0, %1

"

"1:  "

"cmpb %h0, %b0

"

"je 2f

"

"rep ; nop

"

"movb %1, %b0

"

/* don't need lfence here, because loads are in-order */

"jmp 1b

"

"2:"

:"+Q" (inc), "+m" (lock->slock)

:

:"memory", "cc");

}

LOCK_PREFIX 宏在前文中已经先容过,便是“lock”前缀。

xaddw 汇编指令将 slock 和 inc 的值互换,然后把这两个值相加后的和存到 slock 中。也便是说,该指令履行完毕后,inc 存有原本的 slock 值作为票据序号,而 slock 的 Next 域被加 1。

comb 对照 inc 变量的高位和低位字节是否相等,假如相等,注解锁处于未应用状态,直接跳转到标签 2 的位置退出函数。

假如锁处于应用状态,则不绝地将当前的 slock 的 Owner 域复制到 inc 的低字节处(movb 指令),然后重复 c 步骤。不过此时 inc 变量的高位和低位字节相等注解轮到自己获取了自旋锁。

__raw_spin_unlock

清单 7. __raw_spin_unlock 函数

static inline void __raw_spin_unlock(raw_spinlock_t *lock)

{

__asm__ __volatile__(

UNLOCK_LOCK_PREFIX "incb %0"

:"+m" (lock->slock)

:

:"memory", "cc");

}

在 IA32 体系布局下,假如应用 PPro SMP 系统或者启用了 X86_OOSTORE,则 UNLOCK_LOCK_PREFIX 被定义为“lock”前缀;否则被定义为空。

incb 指令将 slock 最低位字节也便是 Owner 域加 1。

Windows 操作系统的排队自旋锁(Queued Spinlock)先容

排队自旋锁并不是一个新设法主见,某些操作系统早已采纳了类似观点,只是实现要领有所区别。例如在 Windows 操作系统中排队自旋锁被称为 Queued Spinlock。

Queued Spinlock 的事情要领如下:每个处置惩罚器上的履行线程都有一个本地的标志,经由过程该标志,所有应用该锁的处置惩罚器(锁拥有者和等待者)被组织成一个单向行列步队。当一个处置惩罚器想要得到一个已被其它处置惩罚器持有的 Queued Spinlock 时,它把自己的标志放在该 Queued Spinlock 的单向行列步队的末端。假如当前锁持有者开释了自旋锁,则它将该锁移交到行列步队中位于自己之后的第一个处置惩罚器。同时,假如一个处置惩罚器正在忙等待 Queued Spinlock,它并不是反省该锁自身的状态,而是反省针对自己的标志;在行列步队中位于该处置惩罚器之前的处置惩罚器开释自旋锁时会设置这一标志,以注解轮到这个正在等待的处置惩罚器了。

与 Linux 的排队自旋锁比拟,Queued Spinlock 的设计更为繁杂,然则 Queued Spinlock 拥有自己的上风:

忙等待 Queued Spinlock 的每个处置惩罚器在针对该处置惩罚器的标志上扭转,而不是在全局的自旋锁上测试扭转,是以处置惩罚器之间的同步比 Linux 的排队自旋锁少得多。

Queued Spinlock 拥有真实的行列步队布局,是以便于扩充更高档的功能。

扩展排队自旋锁的一点设法主见

排队自旋锁设计简单、实现轻易且机能优秀,是以肯定会受到开拓职员的迎接。本节评论争论一下排队自旋锁未来可能有用的一些扩展功能:

超时(Timeout)

只管排队自旋锁包管了内核履行线程严格按照申请顺序获取锁,然则因为锁的竞争剧烈(例如处置惩罚器个数达到64或更多),线程仍旧可能会等待过长的光阴。当该线程得到锁时,情况大概已发生变更而导致无法完成义务。是以申请线程可以预先指定一个等待阈值,一旦跨越该阈值且尚未得到锁,则自动从等待步队中退出,并返回代表超时的差错值。

优先级(Priority)

当前的实现中,所有的线程一律平等,严格按照申请顺序等待。某些履行关键操作的线程大概必要特殊对待,即付与更高的优先级。一旦它们申请自旋锁,就把他们插入到等待行列步队的前部优先履行。

您可能还会对下面的文章感兴趣: