并发:自旋锁

spin lock

Posted by cslqm on January 6, 2020

最近正在看 ldd,书中的并发有点难。留个笔记,感觉比较好。

1
2
3
4
参考:
[IBM](https://www.ibm.com/developerworks/cn/linux/l-cn-spinlock/index.html)
[CSDN](https://blog.csdn.net/wesleyluo/article/details/8807919)
[cnblogs](https://www.cnblogs.com/aaronLinux/p/5890924.html)

ldd 中最先讲到的就是 sem,不幸的 sem 的源码实现中有 spin lock,所以不得不先看 ldd 中之后才讲到的 spin lock。

基本定义

惯例,先说基本定义。

自旋锁的初衷:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话, 最好使用信号量。

单处理器的自旋锁:

首先,自旋锁的目的如果在系统不支持内核抢占时,自旋锁的实现也是空的,因为单核只有一个线程在执行,不会有内核抢占,从而资源也不会被其他线程访问到。 其次,支持内核抢占,由于自旋锁是禁止抢占内核的,所以不会有其他的进程因为等待锁而自旋. 最后,只有在多cpu下,其他的cpu因为等待该cpu释放锁,而处于自旋状态,不停轮询锁的状态。所以这样的话,如果一旦自旋锁内代码执行时间较长,等待该锁的cpu会耗费大量资源,也是不同于信号量和互斥锁的地方。 简单来说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。

自旋锁内睡眠禁止睡眠问题:如果自旋锁锁住以后进入睡眠,而此时又不能进行处理器抢占(锁住会disable prempt),其他进程无法获得cpu,这样也不能唤醒睡眠的自旋锁,因此不相应任何操作。

自旋锁为什么广泛用于内核:自旋锁是一种轻量级的互斥锁,可以更高效的对互斥资源进行保护。自旋锁本来就只是一个很简单的同步机制,在SMP之前根本就没这个东西,一切都是Event之类的同步机制,这类同步机制都有一个共性就是:一旦资源被占用都会产生任务切换,任务切换涉及很多东西的(保存原来的上下文,按调度算法选择新的任务,恢复新任务的上下文,还有就是要修改cr3寄存器会导致cache失效)这些都是需要大量时间的,因此用Event之类来同步一旦涉及到阻塞代价是十分昂贵的,而自旋锁的效率就远高于互斥锁。

最常用到的函数

再说到最常用的函数哈,莫过于 spin_lock、spin_unlock、spin_lock_irq,spin_lock_irqsave。

个人的体会如下

1
2
3
4
5
6
7
8
9
在 UP 系统中:
禁本 cpu 上的多进程访问  spin_lock
额外禁本 cpu 上发生中断  spin_lock_irq
保存中断               spin_lock_irqsave

在 SMP 系统中:
禁多核 cpu 上的多进程访问  spin_lock
额外禁多核 cpu 上发生中断  spin_lock_irq
保存中断                 spin_lock_irqsave

讲一个硬件上的原理:x86 处理器支持 lock 前缀指令,可以将特定内存地址加锁,实现系统总线不能访问此内存地址的功能。 从 P6 家族处理器开始,当使用 LOCK 指令访问的内存已经被处理器加载到缓存中时,LOCK# 信号通常不会被宣告。取而代之的是,仅是锁定了处理器的缓存。这里,处理器缓存的相干性(coherency)机制确保了可以原子性的对内存进行操作。

在我看的 2.6.32 的内核中,UP 和 SMP 是两套实现。

UP

UP 上完全就是直接利用了 context(x,1) 和 context(x,-1) 实现了对标志位的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# spinlock_api_up.h

#define _spin_lock(lock)			__LOCK(lock)
#define _spin_unlock(lock)			__UNLOCK(lock)
#define _spin_lock_irq(lock)			__LOCK_IRQ(lock)
#define _spin_lock_irqsave(lock, flags)		__LOCK_IRQSAVE(lock, flags)

#define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

#define __UNLOCK(lock) \
  do { preempt_enable(); __release(lock); (void)(lock); } while (0)

#define __LOCK_IRQ(lock) \
  do { local_irq_disable(); __LOCK(lock); } while (0)

#define __LOCK_IRQSAVE(lock, flags) \
  do { local_irq_save(flags); __LOCK(lock); } while (0)



# compiler.h
# define __acquire(x)	__context__(x,1)
# define __release(x)	__context__(x,-1)

SMP

SMP 中 spin_lock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# spinlock.h
#define spin_lock(lock)			_spin_lock(lock)
# spinlock_api_smp.h
#define _spin_lock(lock) __spin_lock(lock)

static inline void __spin_lock(spinlock_t *lock)
{
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock);
}

# lockdep.h
#define LOCK_CONTENDED(_lock, try, lock)			\
do {								\
	if (!try(_lock)) {					\
		lock_contended(&(_lock)->dep_map, _RET_IP_);	\
		lock(_lock);					\
	}							\
	lock_acquired(&(_lock)->dep_map, _RET_IP_);			\
} while (0)


# spinlock.h
# define _raw_spin_lock(lock)		__raw_spin_lock(&(lock)->raw_lock)


# arch/x86/include/asm/spinlock.h
static __always_inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	__ticket_spin_lock(lock);
}

# TICKET_SHIFT 不同,代码就不同,这是其中一个
# TICKET_SHIFT 8
static __always_inline void __ticket_spin_lock(raw_spinlock_t *lock)
{
	short inc = 0x0100;

	asm volatile (
		LOCK_PREFIX "xaddw %w0, %1\n"
		"1:\t"
		"cmpb %h0, %b0\n\t"
		"je 2f\n\t"
		"rep ; nop\n\t"
		"movb %1, %b0\n\t"
		/* don't need lfence here, because loads are in-order */
		"jmp 1b\n"
		"2:"
		: "+Q" (inc), "+m" (lock->slock)
		:
		: "memory", "cc");
}


LOCK_PREFIX 的定义如下:

1
2
3
4
5
6
7
8
9
10
11
#ifdef CONFIG_SMP
#define LOCK_PREFIX \
        ".section .smp_locks,\"a\"\n"   \
        _ASM_ALIGN "\n"         \
        _ASM_PTR "661f\n" /* address */ \
        ".previous\n"           \
        "661:\n\tlock; "
 
#else /* ! CONFIG_SMP */
#define LOCK_PREFIX ""
#endif
1
这里用到的是比较新的排队自旋锁,文章后边后讲到普通自旋锁。

一部分讲解: 在多处理器环境中 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 的电位拉低,而是锁住本处理器的内部缓存,然后依靠缓存一致性协议保证操作的原子性。

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

comb 比较 inc 变量的高位和低位字节是否相等,如果相等,表明锁处于未使用状态,直接跳转到标签 2 的位置退出函数。

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

可以看出 spin_lock。

  1. 只禁止内核抢占,不会关闭本地中断

  2. 为何需要关闭内核抢占:假如进程A获得spin_lock->进程B抢占进程A->进程B尝试获取spin_lock->由于进程B优先级比进程A高,先于A运行,而进程B又需要A unlock才得以运行,这样死锁。所以这里需要关闭抢占。 这个原理RTOS的mutex/semaphore是否相同?

  a. 因为ThreadX的semaphore,假如进程B获取sema失败,会一直等待,直到A进程释放,不会死锁。

  b. Mutex: mutex获取一旦失败,进程会进入sleep,直到其他进程释放;而spin_lock则不同,会一直轮训访问,且直到时间片耗完。

SMP 中 spin_unlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# spinlock.h
#define spin_unlock(lock)		_spin_unlock(lock)

# spinlock_api_smp.h
#define _spin_unlock(lock) __spin_unlock(lock)

static inline void __spin_unlock(spinlock_t *lock)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);
	_raw_spin_unlock(lock);
	preempt_enable();
}

# spinlock.h
# define _raw_spin_unlock(lock)		__raw_spin_unlock(&(lock)->raw_lock)

# arch/x86/include/asm/spinlock.h
static __always_inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
	__ticket_spin_unlock(lock);
}

# TICKET_SHIFT 不同,代码就不同,这是其中一个 使目标数 +1 
# TICKET_SHIFT 8
static __always_inline void __ticket_spin_unlock(raw_spinlock_t *lock)
{
	asm volatile(UNLOCK_LOCK_PREFIX "incb %0"
		     : "+m" (lock->slock)
		     :
		     : "memory", "cc");
}

SMP 中 spin_lock_irq

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# spinlock.h
#define spin_lock_irq(lock)		_spin_lock_irq(lock)

# spinlock_api_smp.h
#define _spin_lock_irq(lock) __spin_lock_irq(lock)

static inline void __spin_lock_irq(spinlock_t *lock)
{
	local_irq_disable();
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock);
}

## 同 spin_lock 一致不再贴出。

相对于 spin_lock 主要多了 local_irq_disable。

  1. 禁止内核抢占,且关闭本地中断

  2. 那么在spin_lock中关闭了内核抢占,不关闭中断会出现什么情况呢?假如中断中也想获得这个锁,会出现和spin_lock中举得例子相同。所以这个时候,在进程A获取lock之后,使用spin_lock_irq将中断禁止,就不会出现死锁的情况

  3. 在任何情况下使用spin_lock_irq都是安全的。因为它既禁止本地中断,又禁止内核抢占。

  4. spin_lock比spin_lock_irq速度快,但是它并不是任何情况下都是安全的。

SMP 中 spin_lock_irqsave

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# spinlock.h
#define spin_lock_irqsave(lock, flags)			\
	do {						\
		typecheck(unsigned long, flags);	\
		_spin_lock_irqsave(lock, flags);	\
	} while (0)

# spinlock_api_smp.h
#define _spin_lock_irqsave(lock) __spin_lock_irqsave(lock)

static inline unsigned long __spin_lock_irqsave(spinlock_t *lock)
{
	unsigned long flags;

	local_irq_save(flags);
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	/*
	 * On lockdep we dont want the hand-coded irq-enable of
	 * _raw_spin_lock_flags() code, because lockdep assumes
	 * that interrupts are not re-enabled during lock-acquire:
	 */
#ifdef CONFIG_LOCKDEP
	LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock);
#else
	_raw_spin_lock_flags(lock, &flags);
#endif
	return flags;
}
  1. 禁止内核抢占,关闭中断,保存中断状态寄存器的标志位

  2. spin_lock_irqsave在锁返回时,之前开的中断,之后也是开的;之前关,之后也是关。但是spin_lock_irq则不管之前的开还是关,返回时都是开的(?)。

  3. spin_lock_irq在自旋的时候,不会保存当前的中断标志寄存器,只会在自旋结束后,将之前的中断打开。

对于这些函数何时用呢:

总结:

如果被保护的共享资源只在进程上下文访问和软中断上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,因此对于这种情况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。

当然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。但是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其他两个快。

如果被保护的共享资源只在进程上下文和tasklet或timer上下文访问,那么应该使用与上面情况相同的获得和释放锁的宏,因为tasklet和timer是用软中断实现的。

如果被保护的共享资源只在一个tasklet或timer上下文访问,那么不需要任何自旋锁保护,因为同一个tasklet或timer只能在一个CPU上运行,即使是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其需要被调度时已经把该tasklet绑定到当前CPU,因此同一个tasklet决不可能同时在其他CPU上运行。

timer也是在其被使用add_timer添加到timer队列中时已经被帮定到当前CPU,所以同一个timer绝不可能运行在其他CPU上。当然同一个tasklet有两个实例同时运行在同一个CPU就更不可能了。

如果被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅需要用spin_lock和spin_unlock来保护,不必使用_bh版本,因为当tasklet或timer运行时,不可能有其他tasklet或timer在当前CPU上运行。

如果被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源需要用spin_lock和spin_unlock来保护,因为同样的软中断可以同时在不同的CPU上运行。

如果被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源当然更需要用spin_lock和spin_unlock来保护,不同的软中断能够同时在不同的CPU上运行。

如果被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,因此,在进程或软中断上下文需要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

而在中断处理句柄中使用什么版本,需依情况而定,如果只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅需要spin_lock和spin_unlock来保护对共享资源的访问就可以了。

因为在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。但是如果有不同的中断处理句柄访问该共享资源,那么需要在中断处理句柄中使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。

在使用spin_lock_irq和spin_unlock_irq的情况下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪一个也需要依情况而定,如果可以确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些。

因为它比spin_lock_irqsave要快一些,但是如果你不能确定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因为它将恢复访问共享资源前的中断标志而不是直接使能中断。

当然,有些情况下需要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。

spin_lock用于阻止在不同CPU上的执行单元对共享资源的同时访问以及不同进程上下文互相抢占导致的对共享资源的非同步访问,而中断失效和软中断失效却是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问

普通自旋锁

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

1
2
3
typedef struct {
    unsigned int slock;
} raw_spinlock_t;

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

线程通过宏 spin_lock 申请自旋锁。如果不考虑内核抢占,则 spin_lock 调用__raw_spin_lock函数,代码如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    asm volatile("\n1:\t"
             LOCK_PREFIX " ; decb %0\n\t"
             "jns 3f\n"
             "2:\t"
             "rep;nop\n\t"
             "cmpb $0,%0\n\t"
             "jle 2b\n\t"
             "jmp 1b\n"
             "3:\n\t"
             : "+m" (lock->slock) : : "memory");
}

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函数:

1
2
3
4
static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
    asm volatile("movb $1,%0" : "+m" (lock->slock) :: "memory");
}

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

尽管拥有使用简单方便、性能好的优点,自旋锁也存在自身的不足:

  1. 由于传统自旋锁无序竞争的本质特点,内核执行线程无法保证何时可以取到锁,某些执行线程可能需要等待很长时间,导致“不公平”问题的产生。这有两方面的原因:
    • 随着处理器个数的不断增加,自旋锁的竞争也在加剧,自然导致更长的等待时间。
    • 释放自旋锁时的重置操作将无效化所有其它正在忙等待的处理器的缓存,那么在处理器拓扑结构中临近自旋锁拥有者的处理器可能会更快地刷新缓存,因而增大获得自旋锁的机率。
  2. 由于每个申请自旋锁的处理器均在全局变量 slock 上忙等待,系统总线将因为处理器间的缓存同步而导致繁重的流量,从而降低了系统整体的性能。

排队自旋锁

排队自旋锁的设计原理

传统自旋锁的“不公平”问题在锁竞争激烈的服务器系统中尤为严重,因此 Linux 内核开发者 Nick Piggin 在 Linux 内核 2.6.25 版本中引入了排队自旋锁:通过保存执行线程申请锁的顺序信息来解决“不公平”问题。

排队自旋锁仍然使用原有的 raw_spinlock_t 数据结构,但是赋予 slock 域新的含义。为了保存顺序信息,slock 域被分成两部分,分别保存锁持有者和未来锁申请者的票据序号(Ticket Number),所示: 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 和相对应的底层实现函数:

底层实现函数 描述
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

1
2
3
4
5
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
    short inc = 0x0100;
 
    __asm__ __volatile__ (
        LOCK_PREFIX "xaddw %w0, %1\n"
        "1:\t"
        "cmpb %h0, %b0\n\t"
        "je 2f\n\t"
        "rep ; nop\n\t"
        "movb %1, %b0\n\t"
        /* don't need lfence here, because loads are in-order */
        "jmp 1b\n"
        "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

1
2
3
4
5
6
7
8
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 拥有真实的队列结构,因此便于扩充更高级的功能。