蜗窝科技

Linux内核同步机制之(四):spin lock

linuxer · · 内核同步机制 · 阅读 140667 · 2015年4月存档

一、前言

在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock。本文主要介绍了linux kernel中的spin lock的原理以及代码实现。由于spin lock是architecture dependent代码,因此,我们在第四章讨论了ARM32和ARM64上的实现细节。

注:本文需要进程和中断处理的基本知识作为支撑。

二、工作原理

1、spin lock的特点

我们可以总结spin lock的特点如下:

(1)spin lock是一种死等的锁机制。当发生访问资源冲突的时候,可以有两个选择:一个是死等,一个是挂起当前进程,调度其他进程执行。spin lock是一种死等的机制,当前的执行thread会不断的重新尝试直到获取锁进入临界区。

(2)只允许一个thread进入。semaphore可以允许多个thread进入,spin lock不行,一次只能有一个thread获取锁并进入临界区,其他的thread都是在门口不断的尝试。

(3)执行时间短。由于spin lock死等这种特性,因此它使用在那些代码不是非常复杂的临界区(当然也不能太简单,否则使用原子操作或者其他适用简单场景的同步机制就OK了),如果临界区执行时间太长,那么不断在临界区门口“死等”的那些thread是多么的浪费CPU啊(当然,现代CPU的设计都会考虑同步原语的实现,例如ARM提供了WFE和SEV这样的类似指令,避免CPU进入busy loop的悲惨境地)

(4)可以在中断上下文执行。由于不睡眠,因此spin lock可以在中断上下文中适用。

2、 场景分析

对于spin lock,其保护的资源可能来自多个CPU CORE上的进程上下文和中断上下文的中的访问,其中,进程上下文包括:用户进程通过系统调用访问,内核线程直接访问,来自workqueue中work function的访问(本质上也是内核线程)。中断上下文包括:HW interrupt context(中断handler)、软中断上下文(soft irq,当然由于各种原因,该softirq被推迟到softirqd的内核线程中执行的时候就不属于这个场景了,属于进程上下文那个分类了)、timer的callback函数(本质上也是softirq)、tasklet(本质上也是softirq)。

先看最简单的单CPU上的进程上下文的访问。如果一个全局的资源被多个进程上下文访问,这时候,内核如何交错执行呢?对于那些没有打开preemptive选项的内核,所有的系统调用都是串行化执行的,因此不存在资源争抢的问题。如果内核线程也访问这个全局资源呢?本质上内核线程也是进程,类似普通进程,只不过普通进程时而在用户态运行、时而通过系统调用陷入内核执行,而内核线程永远都是在内核态运行,但是,结果是一样的,对于non-preemptive的linux kernel,只要在内核态,就不会发生进程调度,因此,这种场景下,共享数据根本不需要保护(没有并发,谈何保护呢)。如果时间停留在这里该多么好,单纯而美好,在继续前进之前,让我们先享受这一刻。

当打开premptive选项后,事情变得复杂了,我们考虑下面的场景:

(1)进程A在某个系统调用过程中访问了共享资源R

(2)进程B在某个系统调用过程中也访问了共享资源R

会不会造成冲突呢?假设在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,在中断返回现场的时候,发生进程切换,B启动执行,并通过系统调用访问了R,如果没有锁保护,则会出现两个thread进入临界区,导致程序执行不正确。OK,我们加上spin lock看看如何:A在进入临界区之前获取了spin lock,同样的,在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,B在访问临界区之前仍然会试图获取spin lock,这时候由于A进程持有spin lock而导致B进程进入了永久的spin……怎么破?linux的kernel很简单,在A进程获取spin lock的时候,禁止本CPU上的抢占(上面的永久spin的场合仅仅在本CPU的进程抢占本CPU的当前进程这样的场景中发生)。如果A和B运行在不同的CPU上,那么情况会简单一些:A进程虽然持有spin lock而导致B进程进入spin状态,不过由于运行在不同的CPU上,A进程会持续执行并会很快释放spin lock,解除B进程的spin状态。

多CPU core的场景和单核CPU打开preemptive选项的效果是一样的,这里不再赘述。

我们继续向前分析,现在要加入中断上下文这个因素。访问共享资源的thread包括:

(1)运行在CPU0上的进程A在某个系统调用过程中访问了共享资源R

(2)运行在CPU1上的进程B在某个系统调用过程中也访问了共享资源R

(3)外设P的中断handler中也会访问共享资源R

在这样的场景下,使用spin lock可以保护访问共享资源R的临界区吗?我们假设CPU0上的进程A持有spin lock进入临界区,这时候,外设P发生了中断事件,并且调度到了CPU1上执行,看起来没有什么问题,执行在CPU1上的handler会稍微等待一会CPU0上的进程A,等它立刻临界区就会释放spin lock的,但是,如果外设P的中断事件被调度到了CPU0上执行会怎么样?CPU0上的进程A在持有spin lock的状态下被中断上下文抢占,而抢占它的CPU0上的handler在进入临界区之前仍然会试图获取spin lock,悲剧发生了,CPU0上的P外设的中断handler永远的进入spin状态,这时候,CPU1上的进程B也不可避免在试图持有spin lock的时候失败而导致进入spin状态。为了解决这样的问题,linux kernel采用了这样的办法:如果涉及到中断上下文的访问,spin lock需要和禁止本CPU上的中断联合使用。

linux kernel中提供了丰富的bottom half的机制,虽然同属中断上下文,不过还是稍有不同。我们可以把上面的场景简单修改一下:外设P不是中断handler中访问共享资源R,而是在的bottom half中访问。使用spin lock+禁止本地中断当然是可以达到保护共享资源的效果,但是使用牛刀来杀鸡似乎有点小题大做,这时候disable bottom half就OK了。

最后,我们讨论一下中断上下文之间的竞争。同一种中断handler之间在uni core和multi core上都不会并行执行,这是linux kernel的特性。如果不同中断handler需要使用spin lock保护共享资源,对于新的内核(不区分fast handler和slow handler),所有handler都是关闭中断的,因此使用spin lock不需要关闭中断的配合。bottom half又分成softirq和tasklet,同一种softirq会在不同的CPU上并发执行,因此如果某个驱动中的sofirq的handler中会访问某个全局变量,对该全局变量是需要使用spin lock保护的,不用配合disable CPU中断或者bottom half。tasklet更简单,因为同一种tasklet不会多个CPU上并发,具体我就不分析了,大家自行思考吧。

三、通用代码实现

1、文件整理

和体系结构无关的代码如下:

(1)include/linux/spinlock_types.h。这个头文件定义了通用spin lock的基本的数据结构(例如spinlock_t)和如何初始化的接口(DEFINE_SPINLOCK)。这里的“通用”是指不论SMP还是UP都通用的那些定义。

(2)include/linux/spinlock_types_up.h。这个头文件不应该直接include,在include/linux/spinlock_types.h文件会根据系统的配置(是否SMP)include相关的头文件,如果UP则会include该头文件。这个头文定义UP系统中和spin lock的基本的数据结构和如何初始化的接口。当然,对于non-debug版本而言,大部分struct都是empty的。

(3)include/linux/spinlock.h。这个头文件定义了通用spin lock的接口函数声明,例如spin_lock、spin_unlock等,使用spin lock模块接口API的驱动模块或者其他内核模块都需要include这个头文件。

(4)include/linux/spinlock_up.h。这个头文件不应该直接include,在include/linux/spinlock.h文件会根据系统的配置(是否SMP)include相关的头文件。这个头文件是debug版本的spin lock需要的。

(5)include/linux/spinlock_api_up.h。同上,只不过这个头文件是non-debug版本的spin lock需要的

(6)linux/spinlock_api_smp.h。SMP上的spin lock模块的接口声明

(7)kernel/locking/spinlock.c。SMP上的spin lock实现。

头文件有些凌乱,我们对UP和SMP上spin lock头文件进行整理:

UP需要的头文件SMP需要的头文件
linux/spinlock_type_up.h: linux/spinlock_types.h: linux/spinlock_up.h: linux/spinlock_api_up.h: linux/spinlock.hasm/spinlock_types.h linux/spinlock_types.h: asm/spinlock.h linux/spinlock_api_smp.h: linux/spinlock.h

2、数据结构

根据第二章的分析,我们可以基本可以推断出spin lock的实现。首先定义一个spinlock_t的数据类型,其本质上是一个整数值(对该数值的操作需要保证原子性),该数值表示spin lock是否可用。初始化的时候被设定为1。当thread想要持有锁的时候调用spin_lock函数,该函数将spin lock那个整数值减去1,然后进行判断,如果等于0,表示可以获取spin lock,如果是负数,则说明其他thread的持有该锁,本thread需要spin。

内核中的spinlock_t的数据类型定义如下:

typedef struct spinlock {
struct raw_spinlock rlock;
} spinlock_t;

typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
} raw_spinlock_t;

由于各种原因(各种锁的debug、锁的validate机制,多平台支持什么的),spinlock_t的定义没有那么直观,为了让事情简单一些,我们去掉那些繁琐的成员。struct spinlock中定义了一个struct raw_spinlock的成员,为何会如此呢?好吧,我们又需要回到kernel历史课本中去了。在旧的内核中(比如我熟悉的linux 2.6.23内核),spin lock的命令规则是这样:

通用(适用于各种arch)的spin lock使用spinlock_t这样的type name,各种arch定义自己的struct raw_spinlock。听起来不错的主意和命名方式,直到linux realtime tree(PREEMPT_RT)提出对spinlock的挑战。real time linux是一个试图将linux kernel增加硬实时性能的一个分支(你知道的,linux kernel mainline只是支持soft realtime),多年来,很多来自realtime branch的特性被merge到了mainline上,例如:高精度timer、中断线程化等等。realtime tree希望可以对现存的spinlock进行分类:一种是在realtime kernel中可以睡眠的spinlock,另外一种就是在任何情况下都不可以睡眠的spinlock。分类很清楚但是如何起名字?起名字绝对是个技术活,起得好了事半功倍,可维护性好,什么文档啊、注释啊都素那浮云,阅读代码就是享受,如沐春风。起得不好,注定被后人唾弃,或者拖出来吊打(这让我想起给我儿子起名字的那段不堪回首的岁月……)。最终,spin lock的命名规范定义如下:

(1)spinlock,在rt linux(配置了PREEMPT_RT)的时候可能会被抢占(实际底层可能是使用支持PI(优先级翻转)的mutext)。

(2)raw_spinlock,即便是配置了PREEMPT_RT也要顽强的spin

(3)arch_spinlock,spin lock是和architecture相关的,arch_spinlock是architecture相关的实现

对于UP平台,所有的arch_spinlock_t都是一样的,定义如下:

typedef struct { } arch_spinlock_t;

什么都没有,一切都是空啊。当然,这也符合前面的分析,对于UP,即便是打开的preempt选项,所谓的spin lock也不过就是disable preempt而已,不需定义什么spin lock的变量。

对于SMP平台,这和arch相关,我们在下一节描述。

3、spin lock接口API

我们整理spin lock相关的接口API如下:

接口API的类型spinlock中的定义raw_spinlock的定义
定义spin lock并初始化DEFINE_SPINLOCKDEFINE_RAW_SPINLOCK
动态初始化spin lockspin_lock_initraw_spin_lock_init
获取指定的spin lockspin_lockraw_spin_lock
获取指定的spin lock同时disable本CPU中断spin_lock_irqraw_spin_lock_irq
保存本CPU当前的irq状态,disable本CPU中断并获取指定的spin lockspin_lock_irqsaveraw_spin_lock_irqsave
获取指定的spin lock同时disable本CPU的bottom halfspin_lock_bhraw_spin_lock_bh
释放指定的spin lockspin_unlockraw_spin_unlock
释放指定的spin lock同时enable本CPU中断spin_unlock_irqraw_spin_unock_irq
释放指定的spin lock同时恢复本CPU的中断状态spin_unlock_irqstoreraw_spin_unlock_irqstore
获取指定的spin lock同时enable本CPU的bottom halfspin_unlock_bhraw_spin_unlock_bh
尝试去获取spin lock,如果失败,不会spin,而是返回非零值spin_trylockraw_spin_trylock
判断spin lock是否是locked,如果其他的thread已经获取了该lock,那么返回非零值,否则返回0spin_is_lockedraw_spin_is_locked

在具体的实现面,我们不可能把每一个接口函数的代码都呈现出来,我们选择最基础的spin_lock为例子,其他的读者可以自己阅读代码来理解。

spin_lock的代码如下:

static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}

当然,在linux mainline代码中,spin_lock和raw_spin_lock是一样的,在realtime linux patch中,spin_lock应该被换成可以sleep的版本,当然具体如何实现我没有去看(也许直接使用了Mutex,毕竟它提供了优先级继承特性来解决了优先级翻转的问题),有兴趣的读者可以自行阅读,我们这里重点看看(本文也主要focus这个主题)真正的,不睡眠的spin lock,也就是是raw_spin_lock,代码如下:

#define raw_spin_lock(lock) _raw_spin_lock(lock)

UP中的实现:

#define _raw_spin_lock(lock) __LOCK(lock)

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

SMP的实现:

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
__raw_spin_lock(lock);
}

static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

UP中很简单,本质上就是一个preempt_disable而已,和我们在第二章中分析的一致。SMP中稍显复杂,preempt_disable当然也是必须的,spin_acquire可以略过,这是和运行时检查锁的有效性有关的,如果没有定义CONFIG_LOCKDEP其实就是空函数。如果没有定义CONFIG_LOCK_STAT(和锁的统计信息相关),LOCK_CONTENDED就是调用do_raw_spin_lock而已,如果没有定义CONFIG_DEBUG_SPINLOCK,它的代码如下:

static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
}

__acquire和静态代码检查相关,忽略之,最终实际的获取spin lock还是要靠arch相关的代码实现。

四、ARM平台的细节

代码位于arch/arm/include/asm/spinlock.h和spinlock_type.h,和通用代码类似,spinlock_type.h定义ARM相关的spin lock定义以及初始化相关的宏;spinlock.h中包括了各种具体的实现。

1、回忆过去

在分析新的spin lock代码之前,让我们先回到2.6.23版本的内核中,看看ARM平台如何实现spin lock的。和arm平台相关spin lock数据结构的定义如下(那时候还是使用raw_spinlock_t而不是arch_spinlock_t):

typedef struct {
volatile unsigned int lock;
} raw_spinlock_t;

一个整数就OK了,0表示unlocked,1表示locked。配套的API包括__raw_spin_lock和__raw_spin_unlock。__raw_spin_lock会持续判断lock的值是否等于0,如果不等于0(locked)那么其他thread已经持有该锁,本thread就不断的spin,判断lock的数值,一直等到该值等于0为止,一旦探测到lock等于0,那么就设定该值为1,表示本thread持有该锁了,当然,这些操作要保证原子性,细节和exclusive版本的ldr和str(即ldrex和strexeq)相关,这里略过。立刻临界区后,持锁thread会调用__raw_spin_unlock函数是否spin lock,其实就是把0这个数值赋给lock。

这个版本的spin lock的实现当然可以实现功能,而且在没有冲突的时候表现出不错的性能,不过存在一个问题:不公平。也就是所有的thread都是在无序的争抢spin lock,谁先抢到谁先得,不管thread等了很久还是刚刚开始spin。在冲突比较少的情况下,不公平不会体现的特别明显,然而,随着硬件的发展,多核处理器的数目越来越多,多核之间的冲突越来越剧烈,无序竞争的spinlock带来的performance issue终于浮现出来,根据Nick Piggin的描述:

On an 8 core (2 socket) Opteron, spinlock unfairness is extremely noticable, with a userspace test having a difference of up to 2x runtime per thread, and some threads are starved or “unfairly” granted the lock up to 1 000 000 (!) times.

多么的不公平,有些可怜的thread需要饥饿的等待1000000次。本质上无序竞争从概率论的角度看应该是均匀分布的,不过由于硬件特性导致这么严重的不公平,我们来看一看硬件block:

lock

lock本质上是保存在main memory中的,由于cache的存在,当然不需要每次都有访问main memory。在多核架构下,每个CPU都有自己的L1 cache,保存了lock的数据。假设CPU0获取了spin lock,那么执行完临界区,在释放锁的时候会调用smp_mb invalide其他忙等待的CPU的L1 cache,这样后果就是释放spin lock的那个cpu可以更快的访问L1cache,操作lock数据,从而大大增加的下一次获取该spin lock的机会。

2、回到现在:arch_spinlock_t

ARM平台中的arch_spinlock_t定义如下(little endian):

typedef struct {
union {
u32 slock;
struct __raw_tickets {
u16 owner;
u16 next;
} tickets;
};
} arch_spinlock_t;

本来以为一个简单的整数类型的变量就搞定的spin lock看起来没有那么简单,要理解这个数据结构,需要了解一些ticket-based spin lock的概念。如果你有机会去九毛九去排队吃饭(声明:不是九毛九的饭托,仅仅是喜欢面食而常去吃而已)就会理解ticket-based spin lock。大概是因为便宜,每次去九毛九总是无法长驱直入,门口的笑容可掬的靓女会给一个ticket,上面写着15号,同时会告诉你,当前状态是10号已经入席,11号在等待。

回到arch_spinlock_t,这里的owner就是当前已经入席的那个号码,next记录的是下一个要分发的号码。下面的描述使用普通的计算机语言和在九毛九就餐(假设九毛九只有一张餐桌)的例子来进行描述,估计可以让吃货更有兴趣阅读下去。最开始的时候,slock被赋值为0,也就是说owner和next都是0,owner和next相等,表示unlocked。当第一个个thread调用spin_lock来申请lock(第一个人就餐)的时候,owner和next相等,表示unlocked,这时候该thread持有该spin lock(可以拥有九毛九的唯一的那个餐桌),并且执行next++,也就是将next设定为1(再来人就分配1这个号码让他等待就餐)。也许该thread执行很快(吃饭吃的快),没有其他thread来竞争就调用spin_unlock了(无人等待就餐,生意惨淡啊),这时候执行owner++,也就是将owner设定为1(表示当前持有1这个号码牌的人可以就餐)。姗姗来迟的1号获得了直接就餐的机会,next++之后等于2。1号这个家伙吃饭巨慢,这是不文明现象(thread不能持有spin lock太久),但是存在。又来一个人就餐,分配当前next值的号码2,当然也会执行next++,以便下一个人或者3的号码牌。持续来人就会分配3、4、5、6这些号码牌,next值不断的增加,但是owner岿然不动,直到欠扁的1号吃饭完毕(调用spin_unlock),释放饭桌这个唯一资源,owner++之后等于2,表示持有2那个号码牌的人可以进入就餐了。

3、接口实现

同样的,这里也只是选择一个典型的API来分析,其他的大家可以自行学习。我们选择的是arch_spin_lock,其ARM32的代码如下:

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;

prefetchw(&lock->slock);------------------------(1)
__asm__ __volatile__(
“1: ldrex %0, [%3]\n”-------------------------(2)
" add %1, %0, %4\n"
" strex %2, %1, [%3]\n"------------------------(3)
" teq %2, #0\n"----------------------------(4)
" bne 1b"
“=&r” (lockval), “=&r” (newval), “=&r” (tmp)
“r” (&lock->slock), “I” (1 « TICKET_SHIFT)
“cc”);

while (lockval.tickets.next != lockval.tickets.owner) {------------(5)
wfe();-------------------------------(6)
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------(7)
}

smp_mb();------------------------------(8)
}

(1)和preloading cache相关的操作,主要是为了性能考虑

(2)将slock的值保存在lockval这个临时变量中

(3)将spin lock中的next加一

(4)判断是否有其他的thread插入。更具体的细节参考Linux内核同步机制之(一):原子操作中的描述

(5)判断当前spin lock的状态,如果是unlocked,那么直接获取到该锁

(6)如果当前spin lock的状态是locked,那么调用wfe进入等待状态。更具体的细节请参考ARM WFI和WFE指令中的描述。

(7)其他的CPU唤醒了本cpu的执行,说明owner发生了变化,该新的own赋给lockval,然后继续判断spin lock的状态,也就是回到step 5。

(8)memory barrier的操作,具体可以参考memory barrier中的描述。

arch_spin_lock函数ARM64的代码(来自4.1.10内核)如下:

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned int tmp;
arch_spinlock_t lockval, newval;

asm volatile(
/* Atomically increment the next ticket. */
" prfm pstl1strm, %3\n"
“1: ldaxr %w0, %3\n”-----(A)-----------lockval = lock
" add %w1, %w0, %w5\n"-------------newval = lockval + (1 « 16),相当于next++
" stxr %w2, %w1, %3\n"--------------lock = newval
" cbnz %w2, 1b\n"--------------是否有其他PE的执行流插入?有的话,重来。
/* Did we get the lock? */
" eor %w1, %w0, %w0, ror #16\n"--lockval中的next域就是自己的号码牌,判断是否等于owner
" cbz %w1, 3f\n"----------------如果等于,持锁进入临界区
/*
* No: spin on the owner. Send a local event to avoid missing an
* unlock before the exclusive load.
*/
" sevl\n"
“2: wfe\n”--------------------否则进入spin
" ldaxrh %w2, %4\n"----(A)---------其他cpu唤醒本cpu,获取当前owner值
" eor %w1, %w2, %w0, lsr #16\n"---------自己的号码牌是否等于owner?
" cbnz %w1, 2b\n"----------如果等于,持锁进入临界区,否者回到2,即继续spin
/* We got the lock. Critical section starts here. */
“3:”
“=&r” (lockval), “=&r” (newval), “=&r” (tmp), “+Q” (*lock)
“Q” (lock->owner), “I” (1 « TICKET_SHIFT)
“memory”);
}

基本的代码逻辑的描述都已经嵌入代码中,这里需要特别说明的有两个知识点:

(1)Load-Acquire/Store-Release指令的应用。Load-Acquire/Store-Release指令是ARMv8的特性,在执行load和store操作的时候顺便执行了memory barrier相关的操作,在spinlock这个场景,使用Load-Acquire/Store-Release指令代替dmb指令可以节省一条指令。上面代码中的(A)就标识了使用Load-Acquire指令的位置。Store-Release指令在哪里呢?在arch_spin_unlock中,这里就不贴代码了。Load-Acquire/Store-Release指令的作用如下:

-Load-Acquire可以确保系统中所有的observer看到的都是该指令先执行,然后是该指令之后的指令(program order)再执行

-Store-Release指令可以确保系统中所有的observer看到的都是该指令之前的指令(program order)先执行,Store-Release指令随后执行

(2)第二个知识点是关于在arch_spin_unlock代码中为何没有SEV指令?关于这个问题可以参考ARM ARM文档中的Figure B2-5,这个图是PE(n)的global monitor的状态迁移图。当PE(n)对x地址发起了exclusive操作的时候,PE(n)的global monitor从open access迁移到exclusive access状态,来自其他PE上针对x(该地址已经被mark for PE(n))的store操作会导致PE(n)的global monitor从exclusive access迁移到open access状态,这时候,PE(n)的Event register会被写入event,就好象生成一个event,将该PE唤醒,从而可以省略一个SEV的指令。

注:

(1)+表示在嵌入的汇编指令中,该操作数会被指令读取(也就是说是输入参数)也会被汇编指令写入(也就是说是输出参数)。
(2)=表示在嵌入的汇编指令中,该操作数会是write only的,也就是说只做输出参数。
(3)I表示操作数是立即数

原创文章,转发请注明出处。蜗窝科技

Change log:

1、2015/11/5,加入ARM64的代码实现部分的分析

2、2015/11/17,增加ARM64代码中的两个知识点的描述

标签:spinlock自旋锁

继续讨论

新讨论请用 GitHub 账号在下方发言——评论会保存在本仓库的 Discussions 里,与站点同寿; 再下方的「历史评论」是原站的只读存档。

评论(126 条,只读存档)

以下为原站的历史评论(只读存档);继续讨论请用上方的评论区。

  • heziq

    大哥,你研究的太深了了,佩服五体投地啊。你招小弟不,把我收了吧!后面顺便把把内核其它锁给讲讲吧!
    • linuxer

      @heziq:客气客气,大家一起研究一起进步。其他内核中的锁的机制一定会写的,只不过需要时间
  • callme_friend

    许久不见更新,原来是在专研锁啊。 不知道上次说的tty的分析文档会出吗?
    • linuxer

      @callme_friend:坦率的讲,系统级的一些模块(例如内核同步)写起来更有快感,更吸引我,因此,heziq网友发了一篇spin lock文档立刻激发我写spin lock的兴趣。驱动级别的模块(例如tty)虽然可以写,不过驱动力不是很强,不过既然答应你了,一定会写的,呵呵~~~ 不过,实际上更多的工程师更关注驱动级别的代码,我感觉写这些内容会更受欢迎,看来需要在自己写的爽和别人读的爽之间平衡啊。 好象还答应tigger写tasklet的,还想要修改ARM gerneric timer的那份文档,唉,时间太少了.......
      • callme_friend

        @linuxer:linuxer如果没时间,指导我写也行~! 我这段时间基本上有时间就看tty;思路稍微清晰了一些。 对于上次问你的统一设备模型和字符设备驱动以及洋葱问题,有了认识。 对于linux来说,按照一切即文件的思路,app调用后首先应该是进到vfs,然后根据文件类型,找到字符设备子系统,再那里找到之前注册的字符设备驱动程序,完成vfs的跨越。 而对于tty,我看了一部分了,也是沿着上面的思路,往下继续封装、分层,但是tty还和输入子系统等其他方面有联系,还没看全。 至于统一设备模型和设备驱动程序,我现在是这样认识:按照狭义设备驱动来说,只有字符设备、块设备、网络设备驱动等;总线下的驱动、设备,其实仅仅是个机制,跟狭义的字符设备驱动完全不同。不知道这样理解对不对?
        • linuxer

          @callme_friend:如果你能写一系列tty的文档那当然太好了,可以考虑给你开一个专栏就叫tty子系统,你可以慢慢写,写成一个系列,有问题大家可以交流,也可以帮忙review
          • callme_friend

            @linuxer:好的,已经看的差不多了。写了思路和笔记,真要发表,担心文字表达跟你差距太大~! 另外,在哪可以写?
            • linuxer

              @callme_friend:登录之后,在管理界面有一个写文章的标签,点击进去就可以写文章了。当然,也可以offline写,这时候可以使用Windows Live Writer
            • linuxer

              @callme_friend:不要担心质量什么的,能否分享给大家就是很好的开端
      • tigger

        @linuxer:吭吭 这篇文章我看的很爽啊~~~~~~ 非常有快感~~~~ 更新了我对spinlock的认识呢,尤其是之前内核与现在的区别,耳目一新啊
  • tigger

    semaphore可以允许多个thread进入 这个怎么理解? 另外64位的spinlock 好像窝窝写过一次 在wfe wfi文档里面
    • linuxer

      @tigger:mutex和spin lock都是允许一个thread进入临界区的lock机制。 semaphore可以允许多个thread进入。就好像九毛九有10张桌子,当然允许10个人(thread)进入,当第11人到来的时候,只能等待了(thread将无法获取锁从而挂入等待队列,进入睡眠)。 不停的spin是对电源管理是一个硬伤,因此ARM处理器提供了WFE指令,从而让准备获取spin lock的那个thread不需不停的spin,只需要调用WFE指令,进入低功耗状态,其他处理器会在unlock的时候会触发唤醒动作的。我文档中引用了那份专门描述WFI WFE的文档,呵呵~~~
      • tigger

        @linuxer:我把semaphore 看成了mutex 还在纳闷呢
  • tigger

    等这样一篇有干货的文章,足足从3月26号等到4月22号 不过值啦,哈哈。每一篇都能学到好多东西
    • linuxer

      @tigger:呵呵~~~也是没有办法的事情,毕竟是业余时间搞,更重要的是家庭原因,还需要照顾家庭,和孩子玩... 不过,在陪孩子上早教的时候,看到教室中的一句荀子的话还是很有道理的:不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之。我把自己的理解的知识整理出来也勉强算是到了“见之”这个阶段,真正掌握还需要“行之” 同样的道理,如果读者有缘看到这份文档也就是到了“闻之”阶段,离“知之”和“行之”还是有一段距离。 拽一段古文,与tigger同学共勉,哈哈!
      • tigger

        @linuxer:我在想,会不会增加这样的操作比较好。 比如你看到比较好的文章,推荐出来, 这样就不限于你自己写了, 当然我希望你推荐英文的文档,国内的文档作者太不负责任了。 与其看到错误的文档,不如不看~~~~ ps:我这样说的原因是,我有很深的体会,英文的文档,我总是找不到我想要看的,总找不到有很多干货的,要不就是我的渠道有问题。
        • linuxer

          @tigger:我基本上都是看git的log,有时候也会看lwn上的一些文档,还有就是stackoverflow上的一些问题解答。推荐英文文档当然没有问题,不过我上面的话主要是委婉催促你写点什么,哈哈,真的,相信我,越写越聪明
          • tigger

            @linuxer:哈哈 ok
  • 新手上路

    我刚开始拜读大牛文章。很有些茅塞顿开的感觉。下面一段没看懂,能麻烦解释一下为什么要disable么? “使用spin lock+禁止本地中断当然是可以达到保护共享资源的效果,但是使用牛刀来杀鸡似乎有点小题大做,这时候disable bottom half就OK了。” 我感觉spin lock是在isr handle里面做的,因为很快,不需要放到bottom half里面。我的理解是否有误?
    • linuxer

      @新手上路:到底用哪一种方式的锁机制是由访问共享资源的上下文决定的,当然,我们希望访问spin lock的共享资源执行比较快,例如某一个counter变量的操作。但是,你不能保证对这个counter变量的访问总是在中断handler中执行,因为对该counter的操作可能依赖于一系列比较耗时的操作,需要推到bottom half中执行,因此,驱动代码中的逻辑就是在bottom half中执行: 1、首先进行一系列比较耗时的操作 2、counter++ 此外,还有进程上下文操作这个counter变量,这时候,就是需要用spin_lock_bh和spin_unlock_bh这样的保护机制
      • 新手上路

        @linuxer:谢谢大牛指点。没想到回复这么快。 我理解这句话,“使用spin lock+禁止本地中断当然是可以达到保护共享资源的效果,但是使用牛刀来杀鸡似乎有点小题大做,这时候disable bottom half就OK了。” 就是用spin lock的时候,如果case 很简单, 就可以抛弃bottom half了对么?
        • linuxer

          @新手上路:不是这个意思。如何使用锁的机制来保护临界区始终是和场景相关,我们假设访问共享资源的thread包括: (1)运行在CPU0上的进程A在某个系统调用过程中访问了共享资源R (2)运行在CPU1上的进程B在某个系统调用过程中也访问了共享资源R (3)外设P的bottom half中也会访问共享资源R 这时候,使用spin lock+禁止本地中断当然是可以达到保护共享资源的效果,但是disable中断会影响系统的中断延迟。在这种场景下,最适合的策略是:使用spin lock+禁止bottom half的方法。
          • 新手上路

            @linuxer:受教了! 我的基本概念还有待加强。
          • learner

            @linuxer:请教下,有个问题想大不通,spin lock+禁止bottom half的方法,我的理解是由P的中断handler直接访问R了吧?那如何么防止在和A运行在同一个CPU时和A产生死锁?
  • bear20081015

    楼主,请问ARM里面spinlock的实现究竟是否依赖于L1和L2 cache呢?我看了下ldr和str(即ldrex和strexeq)的文档,感觉不是很明白。或者这样说,在cache被disable的情况下,能直接调用spin lock/unlock么?还是需要自己去实现spinlock锁?谢谢!
    • linuxer

      @bear20081015:当然不会,spinlock依赖的是对内存中的一个u32数据,怎么会和cache是否enable or disable相关呢?能说明你对ldrex和strex哪些不明白?我好象还没有搞清楚你要问什么问题
      • bear20081015

        @linuxer:不好意思,我可能没有描述清楚。这是从ARM网站上看到的描述(http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/CJAGCFAF.html): Each processor that supports exclusive accesses has a local monitor. Exclusive accesses to memory locations marked as Non-shareable are checked only against this local monitor. Exclusive accesses to memory locations marked as Shareable are checked against both the local monitor and the global monitor. 我个人理解,ldrex和strex指令的实现是依赖于local monitor和global monitor组成的状态机的,而这两者需要在L1和L2 cache中实现。那么当cache被disable的话这两个monitor也就不能工作了吧?这时候是不是就不能使用strex和ldrex指令了?
        • linuxer

          @bear20081015:你摘录的英文都是在描述Shareability,但是你的问题却在说cache,你是不是把Shareability和Cacheability搞混了?
          • bear20081015

            @linuxer:linux,我摘录的那一段都是在描述arm中如何支持exclusive access的。我的理解是:spinlock从本质上来说确实就是内存中的一个u32数据,但如何支持ldx和stx这两条指令的原子性呢?硬件上肯定要有东西来支撑它,我理解就是local monitor和global monitor。正如这句话“Each processor that supports exclusive accesses has a local monitor.”。进一步从arm的文档中看,local和global monitor是实现在L1和L2 cache中的。所以我才怀疑当L1,L2 cache被disable掉的时候,ldx和stx指令都无效了,也就不能使用spinlock了。我这两天看了下ARM的trust firmware里的bakery lock,我不确定自己理解的对不对,但是看起来就是一个软件上的锁似乎就是为了代替在cpu刚上电时cache关掉时各个CPU的race condition,如果能够直接用spinlock的话似乎就没这个必要了吧。
            • linuxer

              @bear20081015:local和global monitor是实现在L1和L2 cache中的 --------------------------------------------------- 我看原文上是说local和global monitor是实现在L1和L2 memory system中,我是觉得local timer以及global timer应该是和L1和L2 cache是否enable或者disable无关,即便是L1和L2 cache disable了,local monitor和global monitor也应该是可以工作的 当然,这也是我的猜测而已,仅供参考
              • bear20081015

                @linuxer:我个人还是感觉是有点关系的,否则不可能arm trustfirm要多此一举去实现一个软件的spinlock锁,而且这个锁就是在cache没有enable的情况下使用的。但还是感谢你耐心的回答!另外多问一句,我看到你们的pm runtime的系列文章似乎只开了个头,我最近在这一块有点疑问,不知道你们后续文章有时间表么?或者就在评论区里面大家讨论一下?
              • linuxer

                @linuxer:之前对于Exclusive access instructions理解有误,重新整理如下: 1、Exclusive access instructions不适合用在device memory中,基本上在控制多个thread访问某个外设的时候,可以使用基于normal memory的资源控制方法(例如spin lock或者mutex),而不是依赖Exclusive access instructions。在有些系统上,对device memory使用Exclusive access instructions会产生bus error。 2、对于normal memory,可以分成Shareable memory regions和Non-Shareable memory regions。对于前者,需要local monitor和global monitor,对于后者,仅仅需要local monitor 3、低于local monitor和global monitor而言,不同的系统实现也不同。对于特定的系统,如A9双核SOC,local monitor在L1 cache中,global monitor在SCU中。 4、在MMU on(告知Shareable属性)和cache on(让monitors on)的情况下,一切工作应该是正常的,local monitor和global monitor都是正常工作的,否则,应该Exclusive access instructions不能正常工作
            • wowo

              @bear20081015:我来插一句: ARM架构提供了non-blocking的同步机制(也称作synchronization primitives),确实比较高效、实用。但该机制需要一些额外机制的支撑,也就是您所提到的local monitor和global monitor。monitor特别是global monitor的实现, 和cache、MMU等没有直接关系。但是,对那些特殊位置的memory,如non-cacheable、cache-disable等,需要在PE之外实现monitor功能,而并不是所有厂商都会做这些事情。 对arm trusted firmware来说,它无法假设厂商的具体行为,因此为了确保同步的有效,就不得不使用软件的互斥机制(bakery lock)。 而对Linux kernel而言,kernel是在单核、preemptive disable的情况下启动的,等到真正使用spin lock的时候,一切都准备就绪了,因此也就没什么问题了。 具体可参考DDI0487A_d_armv8_arm.pdf: For Shareable memory locations, in some implementations and for some memory types, the properties of the global monitor require functionality outside the PE. Some system implementations might not implement this functionality for all locations of memory. In particular, this can apply to: • Any type of memory in the system implementation that does not support hardware cache coherency. • Non-cacheable memory, or memory treated as Non-cacheable, in an implementation that does support hardware cache coherency. In such a system, it is defined by the system: • Whether the global monitor is implemented. • If the global monitor is implemented, which address ranges or memory types it monitors ... Therefore, architecturally-compliant software that requires mutual exclusion must not rely on using the Load-Exclusive/Store-Exclusive mechanism, and must instead use a software algorithm such as Lamport’s Bakery algorithm to achieve mutual exclusion.
              • bear20081015

                @wowo:谢谢!看起来非常合理啊。唯一有点疑问的:“而对Linux kernel而言,kernel是在单核、preemptive disable的情况下启动的,等到真正使用spin lock的时候,一切都准备就绪了,因此也就没什么问题了”。我的理解是确实在启动情况下没什么问题,但是对于SMP来说,当cluster进入到power down的状态,如果有多个cpu同时醒来的话,那么还是有竞争关系,仍然需要类似于spinlock的锁来保护吧,如果这时候local/global monitor还不能用的话。
            • wowo

              @bear20081015:至于runtime PM,确实只是开了一个头。因为RPM的使用比较简单,但内部实现涉及到的知识点就多了,因此还没有时间去写。可以直接评论交流,非常欢迎~~~~
              • bear20081015

                @wowo:wowo,我最近在梳理pm runtime和suspend之间的关系。碰到有一种case有点不太理解,请帮我看看: 1.一个设备在系统suspend之前处于RPM_SUSPENDED状态;此时dev->power.usage_count=0,dev->power.runtime_state = RPM_SUSPENDED; 2.在dev->prepare函数, system suspend框架会调用pm_runtime_get_noresume,此时dev->power.usage_count=1, runtime_state仍然是RPM_SUSPENEDED; 3.接着,system suspend会调用device_suspend_late,这个函数中调用了__pm_runtime_disable()。如果恰好在此之前,有人希望通过pm_runtime_get来resume这个设备,那么就会把一个pending的resume request放到pm runtime work queue上;此时usage_count=2, runtime_state = RPM_SUSPENDED; 4. 在__pm_runtime_disable中,函数会调用__pm_runtime_barrier,这会取消step3中的pending resume请求。 但不会将usage_count减1。此时usage_count=2, runtime_state = RPM_SUSPENDED。 5. 当系统resume回来的时候,它会调用pm_runtime_put来将usage_count减1。 此时usage_count=1, runtime_state = RPM_SUSPENDED。 可以看到,经过这几个步骤后,device的runtime_state和usage_count出现了不匹配的情况,runtime_state=RPM_SUSPENDED的时候usage_count应该是0,但现在变成了1。我做的实验也是这样。不知道这算不算runtime的bug呢?还是我上面哪一步有问题?请指教,谢谢!
            • bsp

              @bear20081015:你指的cache disable分两种(arm): 1:设置cp15(c1)寄存器中的IC/DC,这时cpu 访存不会经过cache而已,但是cache monitor还是工作的,exclusive access是可以正常工作的。从linux内核代码中也可以看到CONFIG_CPU_DCACHE_DISABLE/CONFIG_CPU_ICACHE_DISABLE并不会影响spin-lock,spin-lock中ldrex并没有因为cache disable而做额外的动作,说明ldrex在cache disable时也能正常工作。 2:cache power off,这时arm core一定也是power offer的,不会出现core active而cache power off的情况。所以在cache power off时,不会出现arm core access memory的情况。 3:至于outer cache,其power off时arm core一般也是power off的;如果outer cache power off && arm core power on,exclusive access应该会by-pass 这一级cache的monitor。
  • wowo

    @bear20081015: cluster的power down以及醒来,是要经过arm trust firmware哦。
    • bear20081015

      @wowo:是的,但是arm trust firmware是在ARMV8之后才有的。我的意思是说在之前的armv7架构上,cluster的power down以及醒来的初始代码还是要有类似于bakery lock的锁吧,只不过不像ATF里面那样是标准的,可能是各个vendor自己实现了吧。
      • wowo

        @bear20081015:嗯,这就清楚了。 另外关于RPM的我要先看看,另外你看到的kernel版本是哪个?电源管理是kernel比较活跃的模块,一直在进化,有问题也是可能的。
        • bear20081015

          @wowo:我的版本的3.14的。但我看到最新的3.18里面我提的这个问题牵涉到的相关代码似乎没有什么变化,所以3.18里我相信也会有这个问题的。
  • tigger

    如果我只知道,arch_spinlock_t里面的owner跟next,我有方法知道是那个进程拿到了锁么??或者说哪个core。我想到有一种情况 比如我现在有8个core,core 1到core7 都在等同一把锁。owner 跟 next相差7,说明有7个thread在等,根据spinlock的原理,那么肯定是那个不再等的core上面拿了锁。 但是我又如何证明锁是被哪个core拿了呢? 再仅有owner跟next的情况下,是不是没法确定,必须打开debug的宏才知道呢?
    • bear20081015

      @tigger:我理解应该没办法知道。除非恰好这个spinlock是全局变量,然后可以通过system.map知道lock的名字。再看看code看能不能知道是哪段code拿到的。局部变量只能打开debug宏了。
    • linuxer

      @tigger:仅有owner跟next的情况下应该是无法知道持有锁的是哪一个core。不过你为何要知道哪一个core持有锁呢?具体的应用场景是什么?
      • tigger

        @linuxer:因为会出现一种情况,几个core都在等同一把锁。这个时候就要知道哪个core拿锁。是不是core出现了问题,没有释放
  • cinmun

    我想问楼主 如果我在操作gpio口的时候 用spinlock 在多核cpu里是 锁 当前核 还是所有核。 为什么 打开spinlcok的时候 后台播放音乐会卡顿暂停呢
    • linuxer

      @cinmun:spinlock 当然是 lock all cpu core 你确定 后台播放音乐会卡顿和spinlock相关吗?你的spinlock是否配合其他的内核同步机制?例如关中断什么的,能否进一步描述如何使用spinlock的?
      • 维尼

        @linuxer:我用的终极spinlock-》spin_lock_irqsave 我锁定大约200ms 如果程序一直跑 那就一直锁定 后台放音乐卡顿 是不是就是内核不能去解码音乐了
        • linuxer

          @维尼:用spin_lock_irqsave锁定200ms实在是太残暴了,你的临界区真正的那么长吗?如果那么长,完全考虑其他的内核同步机制了,例如semaphore或者mutex 如果后台放音乐的进程要竞争这个残暴的临界区,那么卡是一定的了
          • 维尼

            @linuxer:我是在操作gpio口的时候用spin_lock_irqsave 用其他同步semaphore或者mutex io都有延迟 或者错误
      • dadanio

        @linuxer:spinlock我的理解是锁住当前cpu。 spinlock其实就是CPU一直循环查询lock状态,如果是锁住所有的cpu,那么就有可能导致其他cpu没有机会释放spinlock从而导致当前cpu一直忙。不知道理解对不对
        • linuxer

          @dadanio:spinlock只是锁住临界区,允许一个thread进入而已。
  • cinmun

    spin lock相关的接口API如下: 这里 unlock的好多函数名都是错误的
    • linuxer

      @cinmun:多谢指正,晚上回家就修改之
  • descent

    請問一下禁止中斷的話, 有沒有可能漏掉中斷, 若漏了中斷, 是不是有可能會發生什麼問題呢?ex: 資料接收不完整之類的。
    • wowo

      @descent:中斷被禁止的過程中,如果來一次可以觸發中斷的事件,是不會丟的,下一次打開中斷之後,會接著觸發中斷。如果來多次,要看相應的硬體行為了。 另外,一般中斷被禁止的時間不會很長,那些硬件模塊的buffer足以應付了。
  • 坚持到底

    hi, 我有个疑问: ticket spinlock 的实现上,只在初始化的时候把 owner, next 置为 0, 就一直 ++, ++,什么时候会被清空?理论上 16bit 的数据 65535 次后就溢出了。
    • linuxer

      @坚持到底:即便是溢出了,整个逻辑还是对的,你可以自己再分析一下
      • 坚持到底

        @linuxer:是的。请问 wowotech 有群不?方便大家交流一下。
  • zhr2130

    @linuxer:问一个比较low但一直困扰我的问题。 关于spin lock(或其他同步机制如信号量,互斥锁),是如何和它要保护的共享资源(或临界区)关联起来的。是根据spin lock定义的位置么。比如要用spin lock保护一个共享的struct,那么在这个struct中添加一个spin lock的element应该是可行的。放在struct外面可以么。另外对于其它类型的共享资源,如何和spin lock关联呢? 看了很多参考资料,都只是介绍在进入临界区时要获得spin lock,却没有说这个spin lock应该放在哪。难道这个spin lock 可以随便定义在任何位置么?? 非常感谢
    • wowo

      @zhr2130:其实您问这个问题的时候应该已经有答案啦,即iushi随便放了,这完全是软件结构的事情了,和spin lock无关的。或者换个思路,如果要保护的仅仅是一个变量,没有结构体,spin lock放哪里呢?
      • zhr2130

        @wowo:@wowo,那意思就是在进临界区时,只要获取一个kernel中任意一个spinlock就行,只要这个spinlock可以获取到.添加新code时增加的新spinlock,完全是因为我们不清楚旧的spinlock有谁在用,如果用旧有的spinlock可能会造成死锁.是这样么.
        • wowo

          @zhr2130:是的。
          • zhr2130

            @wowo:@wowo,终于明白了,非常感谢
    • 郭健

      @zhr2130:其实这和数据结构的设计有关,如果你的数据是全局范围的,那么多半定义如下: 1、struct xxxx foo。foo就是多个thread并发访问的资源 2、DEFINE_SPINLOCK(foo_lock)。foo_lock就是控制并发访问foo这个全局资源的spin lock,不在struct xxxx,应该和foo这个需要控制并发的资源定义在一起。 如果你的数据是局部的,比如struct xxxx是动态分配的,有A B C D.......等等若干个实例,而并发控制如下: 1、对A资源的并发是需要控制在线程t1、t2、t3.....之间 2、对B资源的并发是需要控制在线程t1、t2、t3.....之间 .... 这时候,定义全局的spinlock显然不合适,最好的设计是相关的数据放在一起,因此,spin lock应该放在struct xxxx中,成为其成员。
      • zhr2130

        @郭健:非常感谢,这下就清楚了
      • zhr2130

        @郭健:@郭健:不好意思还有个问题,如你描述的第一种情况,“foo_lock就是控制并发访问foo这个全局资源的spin lock,不在struct xxxx,应该和foo这个需要控制并发的资源定义在一起。”虽然它们定义在一起,可它们并没有相关性。那么其它进程要访问该全局数据时,并不一定要获取foo_lock,才能访问。可这样就没有定义锁的意义了吧。还是说因为它们做为全局变量定义在一起,所以spin lock把它们所在的存储区整个保护起来了。非常感谢
        • 郭健

          @zhr2130:其实我不太清楚你的问题,不过我强调2点: 1、foo_lock和foo的定义相关性非常,非常....的大,对于一个代码洁癖的工程师而言,他们必须在源代码中定义在一起。 2、访问foo必须获取foo_lock
          • zhr2130

            @郭健:@郭健 不好意思,我对这块的概念比较模糊.其实我的问题就是foo_lock和foo是怎么关联到一起的.特别是foo_lock并未定义在foo结构里的时候. 1、foo_lock和foo的定义相关性非常,非常....的大, 它们的相关性是如何产生的,我没有找到它们能关联在一起的方法.spinlock_init的过程也没有把锁和相关资源关联起来啊. 非常感谢你的耐心回答
            • 郭健

              @zhr2130:我还是换一个实际的例子好了,比如在clocksource.c文件中,有如下代码: static LIST_HEAD(clocksource_list); static DEFINE_MUTEX(clocksource_mutex); clocksource_list就是共享的资源,而clocksource_mutex就是保护这个资源的锁。任何对clocksource_list这个资源的访问都必须获取clocksource_mutex,以便确定在任何时候,系统只有一个thread在临界区,从而不会产生竞态条件: mutex_lock(&clocksource_mutex); 访问临界区的代码 mutex_unlock(&clocksource_mutex); 这里的mutex并不会定义在struct clocksource中,因为mutex控制的资源是全局性的,是系统所有clocksource的实例构成的链表。
              • zhr2130

                @郭健:@郭健: 又仔细看了看文章终于搞明白了,非常感谢你的解答
  • simonzhang

    新的内核这里改变比较大,确实改善了公平性,保证了先来先得。 但是next和owner都是16 bits,最大也就是65536,如果next overflow,但是owner不变的话,可能有两个thread可以同时获得lock。不过就目前的CPU硬件规模,(如最大cores数目100?)应该不会有问题了,就是说没有65536个cpu thread去竞争这个spin lock了。 当然如果next和owner同时overflow的话也自然也不会有问题,只要不相差65536以上。
    • hony

      @simonzhang:#if (CONFIG_NR_CPUS < 256) typedef u8 __ticket_t; typedef u16 __ticketpair_t; #else typedef u16 __ticket_t; typedef u32 __ticketpair_t; #endif 从作者的定义看cpu数不会超过16位。
  • randy

    请问下,spinlock的使用限制,除了不能阻塞性操作、耗时太久,critical section中是不是也不能开中断?对于spinlock_irq而言,内部实现为关local irq,如果critical section中开了中断是不是就会有问题?如果是的话,为啥很多资料里面没有提到这点呢?
    • linuxer

      @randy:spinlock的接口有多个,例如spin_lock、spin_lock_bh、spin_lock_irq,不同的场景,使用不同的接口。对于spin_lock而言,其实是没有关闭本地中断的需求的,因此,在进入临界区的时候,中断是打开的,如果在临界区有对中断的操作也是OK的。对于spin_lock_irq,其临界区是关中断的,因此,在临界区内当然不能打开中断,这是常识,因此没有必要提出。
      • randy

        @linuxer:了解, linuxer,thanks
  • Jeffle

    你好,我用的内核版本是3.6.10,其中看到 spin_unlock对owner进行修改时并没有使用strex,而是直接令owner++,我觉得会有一个问题,那就是如果一个进程并不调用spin_lock,而是一上来直接调用spin_unlock,这里就存在一个bug, 作者的意思是“only one CPU can be performing an unlock() operation for a given lock, this doesn't need to be exclusive”,是不是说作者认为这种bug应该由编程者自己来解决,而不是自旋锁的机制本身来解决呢? static inline void arch_spin_unlock(arch_spinlock_t *lock) { - unsigned long tmp; - u32 slock; - smp_mb(); - - __asm__ __volatile__( -" mov %1, #1\n" -"1: ldrex %0, [%2]\n" -" uadd16 %0, %0, %1\n" -" strex %1, %0, [%2]\n" -" teq %1, #0\n" -" bne 1b" - : "=&r" (slock), "=&r" (tmp) - : "r" (&lock->slock) - : "cc"); - + lock->tickets.owner++; dsb_sev(); }
    • hony

      @Jeffle:应该是由使用者保证的,就像使用互斥锁时使用不当就会产生死锁一样道理。
  • ron

    @linuxner: 在arm32中,next和owner初始化都为0, 当第一个thread获取spin_lock的时候 汇编中 next++, 但是owner仍然为0, 做完汇编之后就会判断 这两个值是否相等。 while (lockval.tickets.next != lockval.tickets.owner) {------------(5) wfe();-------------------------------(6) lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------(7) } 由于owner从未改变所以此时岂不是一直在循环里出不来吗? 多谢!
    • linuxer

      @ron:lock->tickets.owner的值怎么会不变呢?其他thread终究会unlock从而修改owner的。
      • ron

        @linuxer:谢谢@linuxer,我的意思是 第一次获取锁的时候, next和owner初始化都为0, 当进入arch_spin_lock()的时候,next和owner都没有改变, 但是在汇编里 "add %1, %0, %4\n" 此时next++, 后面接着就while 判断,理论上这时候是unlock的,而且锁应该是谁申请 谁释放。 spin_lock --------> 第一次进入的时候 next++, 而owner未改变,应该会while循环 do_citical() spin_unlock 如果上述情况成立,那其他thread应该先spin_lock申请锁,此时上面的锁还在while循环。。。。
        • linuxer

          @ron:也许汇编代码影响了你的逻辑判断,我用c代码重新写过spinlock的过程(当然,这里无法保证原子操作了,不过我们主要是理解逻辑): static inline void arch_spin_lock(arch_spinlock_t *lock) { unsigned long tmp; u32 newval; arch_spinlock_t lockval; lockval = lock->slock;----获得了next和owner值, lock->tickets.next++;----lock中的next++,lockval不变 如果这时候当前的号码牌(lockval.tickets.next)和owner相等,说明可以持有锁长驱直入。 while (lockval.tickets.next != lockval.tickets.owner) { wfe(); lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);---用当前spinlock中的owner值更新lockval中的owner值 } smp_mb(); } 你把lock和lockval这两个变量搞混了。
          • ron

            @linuxer:@linuxer 明白了, 是我把两个变量的值混淆了, 一语惊醒梦中人啊... 非常感谢。
  • michael

    @linuxer 您的文章中这样写到: 最后,我们讨论一下中断上下文之间的竞争。同一种中断handler之间在uni core和multi core上都不会并行执行,这是linux kernel的特性。如果不同中断handler需要使用spin lock保护共享资源,对于新的内核(不区分fast handler和slow handler),所有handler都是关闭中断的,因此使用spin lock不需要关闭中断的配合。bottom half又分成softirq和tasklet,同一种softirq会在不同的CPU上并发执行,因此如果某个驱动中的sofirq的handler中会访问某个全局变量,对该全局变量是需要使用spin lock保护的,不用配合disable CPU中断或者bottom half。tasklet更简单,因为同一种tasklet不会多个CPU上并发,具体我就不分析了,大家自行思考吧 下面是我对这段话的分析,帮忙分析是否正确: (1)中断handler+spinlock 1.同一种中断不能并发运行在多个CPU上,因此同一种中断访问时不需要spinlock保护. 2.如果两个中断handler运行同一个CPU上,由于新的内核disable local irq,因此这种情况不需要spinlock保护. 3.如果两个中断handler运行在不同的CPU上,这种情况需要spinlock保护,且调用函数spin_lock_irqsave进行保护? (2)softirq+spinlock 4.同一种softirq可以运行在多个CPU上,因此需要spinlock保护,调用哪个API进行保护? 5.不同的softirq可以运行在多个CPU上,因此需要spinlock保护,调用哪个API进行保护? (3)tasklet+spinlock 6.同一种tasklet只能运行在同一个CPU上串行执行,因此不需要spinlock保护. 7.不同的tasklet可以运行在不同的CPU上执行,因此需要进行spinlock保护,调用哪个API进行保护? (4)workqueue+spinlock workqueue属于线程化,但优先级仍高于普通进程。 8.同一个work在某一时间点上只能在同一CPU上运行,是不是就不需要spinlock保护? 9.不同的work访问共享资源时,由于可以运行在不同的CPU上,就需要spinlock保护,调用哪个API进行保护?
    • michael

      @michael:追加两个问题: 1.我的理解,中断也是一种抢占,它是一种异步抢占,是不是调用了preempt_disable禁止了本CPU的抢占,是不是也disable中断了? 2.所谓的抢占,是不是指同一CPU上进程、中断间争夺CPU的控制权,而两个CPU之间,不存在抢占?也就是说,运行在不同CPU上的两个线程间,不存在抢占了
      • John

        @michael:同样追加两个回答: 1.我的理解,中断也是一种抢占,它是一种异步抢占,是不是调用了preempt_disable禁止了本CPU的抢占,是不是也disable中断了? A:preempt_disable只是一种软件方法,达到同步的目的。而disable中断是一个涉及硬件的行为。所以diable中断是一个更底层的行为,它的结果可以影响软件,比如使scheduler没有办法执行,更不能执行preempt。 2.所谓的抢占,是不是指同一CPU上进程、中断间争夺CPU的控制权,而两个CPU之间,不存在抢占?也就是说,运行在不同CPU上的两个线程间,不存在抢占了 A:我的理解是这里的抢占指的是进入临界区。获得CPU的控制权是手段,目的还是要进入临界区完成一些操作。
    • John

      @michael:我的理解是linuxer表达的意思是所有的临界区都是在相同的handler中共享的。比如同种softirq的handler之间或者不同softirq handler之间,他们会访问全局的变量。并不考虑softirq handler与硬中断handler(top handler)之间的临界区这种情况。 以下是我的一些理解,回答如下: (1)中断handler+spinlock 1.同一种中断不能并发运行在多个CPU上,因此同一种中断访问时不需要spinlock保护. A:我的理解是你的观点是正确的。spin lock主要是防止其他的CPU进入临界区,如果这个临界区只在这个中断handler中访问的话,那么由于只有一个handler(同一种中断不能并发运行在多个CPU上),所以不需要spin lock保护。 2.如果两个中断handler运行同一个CPU上,由于新的内核disable local irq,因此这种情况不需要spinlock保护. A:由于中断handler执行时是关中断的,所以不存在两个中断handler运行同一个CPU上的情况。 3.如果两个中断handler运行在不同的CPU上,这种情况需要spinlock保护,且调用函数spin_lock_irqsave进行保护? A:两个中断handler运行在不同的CPU上,这种情况需要spinlock保护,但是并不需要关闭中断。因为中断handler执行时local cpu是关中断的。所以只需使用spin_lock就行了。 (2)softirq+spinlock 4.同一种softirq可以运行在多个CPU上,因此需要spinlock保护,调用哪个API进行保护? A:同1,基于linuxer原文的意思,不考虑softirq handler和硬中断handler临界区情况,所以只需要spin_lock就可以了。 5.不同的softirq可以运行在多个CPU上,因此需要spinlock保护,调用哪个API进行保护? A:同4。 (3)tasklet+spinlock 6.同一种tasklet只能运行在同一个CPU上串行执行,因此不需要spinlock保护. A:我的理解是你的观点是正确的。 7.不同的tasklet可以运行在不同的CPU上执行,因此需要进行spinlock保护,调用哪个API进行保护? A:只需要spin_lock就可以了 (4)workqueue+spinlock workqueue属于线程化,但优先级仍高于普通进程。 8.同一个work在某一时间点上只能在同一CPU上运行,是不是就不需要spinlock保护? A:我不确定同一个work是否可以同时运行在多个CPU上。如果是的话,必须使用spin lock防止其他CPU干扰。 9.不同的work访问共享资源时,由于可以运行在不同的CPU上,就需要spinlock保护,调用哪个API进行保护? A:基于linuxer原文的意思,不考虑work与其他人(softirq handler,thread,top handler)共享临界区的情况,spin_lock就可以了。
  • benjoying

    (1)运行在CPU0上的进程A在某个系统调用过程中访问了共享资源R (2)运行在CPU1上的进程B在某个系统调用过程中也访问了共享资源R (3)外设P的bottom half中也会访问共享资源R 这时候,使用spin lock+禁止本地中断当然是可以达到保护共享资源的效果,但是disable中断会影响系统的中断延迟。在这种场景下,最适合的策略是:使用spin lock+禁止bottom half的方法。 @linuxer, 在这里有点不太明白,bottom half一般是用来做一些比较费时的事情,那如果把这个disable掉,那这部分如何处理呢,感谢!
    • linuxer

      @benjoying:一般而言,这种临界区不会太长,一旦离开临界区,打开了bottom half的话,delay的bottom half会立刻执行。
      • benjoying

        @linuxer:@linuxer 感谢感谢,之前把这个意思曲解了谢谢!
  • zoro

    如果CPU0上有个线程A获得了锁后在执行临界代码,这个时候CPU0上发生了中断,中断中也申请同一个锁,这样的话这个CPU0岂不是就进入低功耗等待模式了?不能再去做其他事情了。 如果CPU0进入了低功耗等待模式,那么其他的CPU能不能去释放这个锁,同时把CPU0从低功耗等待模式退出呢?
    • linuxer

      @zoro:你说的场景属于错误的使用了锁的机制,如果一个临界区会被中断和线程并非访问,那么你需要的内核同步机制是spin lock+disable local irq
  • fy

    hi 说到spin lock 说一个老生常谈的问题吧。 如果一个临界区X是有进程上下文和中断上下文都会访问的。那按理 spin lock应该做到 1.禁止本地中断 2.禁止抢占 3.自旋 那又由于内核抢占真正能运行的前提是 从一个中断返回时(A) 并且当前允许抢占(B)并且当前中断开启(C)。。。 因为上面1.中已经禁止本地中断了,即使2.禁止抢占不做也没关系了。 我个人觉得确实是不需要2.禁止抢占的。只要1禁止本地中断和3.自旋 一起就可以达到保护临界区X 如果真的不加上 2.禁止抢占,会不会有问题,我想了一些case,暂时没想到。 当然,我是默认临界区X内不会主动放弃CPU为前提的。(临界区也确实不应该睡眠才是) 不知道linuxer对此什么看法。
    • wowo

      @fy:我有点看不太明白这个问题,不过可以试着说一下我的理解: spin lock有两大类: 一类是spin_lock,做的事情就是禁止抢占+自旋,这样可以保护那些没有中断参与的临界区,代价也小一些。 一类是spin_lock_irqsave,做的事情是禁止中断+(禁止抢占+自旋),这样在第一类的基础上也可以保护有中断参与的临界区。 你好像在质疑第二类中禁止抢占的必要性,确实,没什么必要。可spin_lock_irqsave要调用spin_lock啊,再封出来一个接口?岂不是有点画蛇添足了?
      • fy

        @wowo:是的。我只是就这里的“禁止抢占”非必要,问问wowo的个人理解。 确实是非必要的。 非常感谢。
        • LOONG

          @fy:前来膜拜郭大侠:) 那天看文嘉转了郭大侠在另一个群里讨论这个问题(关抢占有没有必要),觉得受益匪浅,学习了。 当时就想:或者作者当初也并不是一气呵成呢?这样定然有commit log可以查询。试着搜了一下,发现目前的git库已经没有相关记录了。于是上网搜到了这个问题: https://stackoverflow.com/questions/13263538/linux-kernel-spinlock-smp-why-there-is-a-preempt-disable-in-spin-lock-irq-sm 基本跟郭大侠分析的一样:) 不知@fy有何理解?或许可以直接问Dave Miller,说不定他跟郭大侠一样nice:)
          • linuxer

            @LOONG:多谢支持!问题是一样的问题,但是在stackoverflow上也没有得到回答,不过在前几天的微信群里,这里问题已经讨论的比较清楚了。
    • linuxer

      @fy:非常好的问题,非常精彩的思考点。 禁止了中断的确等于了禁止抢占,但是并不意味着它们两个完全等同,因为在preempt disable---preempt enable这个的调用过程中,在打开抢占的时候有一个抢占点,内核控制路径会在这里检查抢占,如果满足抢占条件,那么会立刻调度schedule函数进行进程切换,但是local irq disable---local irq enable的调用中,并没有显示的抢占检查点,当然,中断有点特殊,因为一旦打开中断,那么pending的中断会进来,并且在返回中断点的时候会检查抢占,但是也许下面的这个场景就无能为力了。进程上下文中调用如下序列: (1)local irq disable (2)wake up high level priority task (3)local irq enable 当唤醒的高优先级进程被调度到本CPU执行的时候,按理说这个高优先级进程应该立刻抢占当前进程,但是这个场景无法做到。在调用try_to_wake_up的时候会设定need resched flag并检查抢占,但是由于中断disable,因此不会立刻调用schedule,但是在step (3)的时候,由于没有检查抢占,这时候本应立刻抢占的高优先级进程会发生严重的调度延迟.....直到下一个抢占点到来。
      • wowo

        @linuxer:这个场景还是挺有意思的,不过话说回来了,spin_lock_xxx的目的是保护临界区,关preempt的抢占点什么事呢?爱抢占不抢占啊~ 本质上还是spin_lock需要通过开关抢占进行临界区保护,到spin_lock_irqxxx的时候,顺便做一下,再顺便帮preempt一个忙而已。 所以这个场景并不能构成“spin_lock_irqsave关抢占是否有必要的理由”,最后的答案还是没必要(因为“保护”的目的已经达到),至于是不是可以增加一个抢占点,在local_irq_restore的时候,调用一下preempt_check_resched岂不是更直接?
  • randy

    想请问一个arm32实现版本的spin unlock的问题,我注意到spin_unlock的实现中仅仅是owner++,没有ldrex/strex的实现,请问是否有问题: cpu0 cpu1 ldrex next | owner+1 next+1 | owner strex 所以在cpu0完成strex后,把老的owner值覆盖回去了,cpu1的对owner++操作被覆盖,关键在于unlock操作没有原子,请问Linux内核为何没有问题?
    • smcdef

      @randy:你可以看到在ARM64上,arch_spin_unlock()同样也是类似owner++的操作。我就根据ARM64的认知,推测以下ARM32的原理吧。 CPU0 执行ldrex后,monitor的状态是Exclusive Access。当CPU1把owner写成功以后,就会导致monitor的状态切换到Open Access。然后,CPU0的strex操作就会失败。因此,不会出现你担心的情况。 虽然owner++是RMW操作,但是由于spin_unlock()在任意时刻都只有一个进程调用。spin_unlock()和spin_lock()之间也是安全的。
      • randy

        @smcdef:我理解普通的ldr/str指令不会导致open/exclusive状态机的切换,除非把Unlock的实现为ldrex/strex指令对,而非普通的owner++。
        • smcdef

          @randy:在ARM64上,普通的str操作也会clear monitor。你既然说不会,可能你已经查阅了相关ARM的文档。我只是说出了ARM64的设计,供你参考。因为,我确实不了解ARM的设计(一直在搞ARM64)。
          • randy

            @smcdef:sorry,你的理解是对的,重新读了下文档,arm32下str指令也会导致exclusive->open态的切换,我之前一直理解为只有strex这样的指令才会,多谢!
  • cp

    请教一个问题 T1 T2 next +1 | owner next+1 | owner+1 WFE 这种情况下,T1如何才能被唤醒?
    • cp

      @cp:意思就是 一个cpu的SEV 在另一个CPU的next++ 和WFE之间完成会怎么样呢? 多谢
      • bsp

        @cp:这就是linuxer说的:有其它thread/PE的插入,strex会执行失败,重新ldrex。
        • 白白

          @bsp:大佬有没有联系方式 想咨询一下EMMC读写方面的问题
  • crash

    spin_lock获取不到锁资源spin等待期间会被中断切走吗
    • linuxer

      @crash:会的,spin lock在spin的时候其实是处于WFE状态,中断来了之后,cpu会从WFE状态解除,从而跳转到中断上下文去执行。 当然,前提是你的spin lock是单纯的spin lock,没有附件disable interrupt
  • linuxzz

    大神,加了你们的官方QQ群,求通过,一起学习
  • bsp

    (1)和preloading cache相关的操作,主要是为了性能考虑 做一点补充: prefetch mem 指令其工作主要是通知memory系统(cache line file)在后台执行,然后立马返回,而不会阻塞在等待prefetch完成后(linefill complete))才返回,所以会有效加速性能(10%以上)。 from: https://developer.arm.com/documentation/100403/0200/functional-description/level-1-memory-system/data-prefetching The Cortex-A75 core supports the AArch64 Prefetch Memory (PRFM) instructions and the AArch32 Prefetch Data (PLD) and Preload Data With Intent To Write (PLDW) instructions. These instructions signal to the memory system that memory accesses from a specified address are likely to occur soon. The memory system acts by taking actions that aim to reduce the latency of the memory access when they occur. PRFM instructions perform a lookup in the cache, and if they miss and are to a cacheable address, a linefill starts. However, the PRFM instruction retires when its linefill is started, rather than waiting for the linefill to complete. This enables other instructions to execute while the linefill continues in the background.
  • linuxxx

    linuxer你好: 请教一个问题:进程上下文中在临界区有cooy_to_user的操作,中断上下文也需要写同样的buffer, 使用了copy_to_user就无法使用spinlock,但是中断里锁机制只能使用spinlock该如何解决这种冲突呢?谢谢
    • linuxer

      @linuxxx:建议不要这么设计软件,可以考虑中断中queue一个work,然后在其callback中使用copy_to_user
  • dream 主页

    linuxer: 看到这段话,有些不理解,还请解惑,非常感谢。 "A在进入临界区之前获取了spin lock,同样的,在A访问共享资源R的过程中发生了中断,中断唤醒了沉睡中的,优先级更高的B,B在访问临界区之前仍然会试图获取spin lock,这时候由于A进程持有spin lock而导致B进程进入了永久的spin……怎么破?linux的kernel很简单,在A进程获取spin lock的时候,禁止本CPU上的抢占(上面的永久spin的场合仅仅在本CPU的进程抢占本CPU的当前进程这样的场景中发生)。" 即使当前进程在持有锁的时候,被高优先级进程抢占也访问了锁,进入自旋状态,那么内核不是支持时间片轮转的调度嘛,当产生 tick 中断的时候,高优先级的进程时间片用完,那么低优先级的进程就有再次执行的机会,只要低优先级有执行的机会,自然就能完成临界区的代码执行,然后释放锁。释放锁,高优先级进程也就自然也能获取锁。
    • Yaksama

      @dream:个人认为spinlock本身就是一种轻量级的锁,设计初衷除了考虑中断访问临界区资源的情况,还有一个就是避免过多进程上下文切换带来的开销。 如果不disable preempt,的确拥有锁的线程早晚有得到执行的一天,但这种情况为什么不直接用mutex呢,而且线程获取spinlock之后执行的任务都是轻量级的,如果让其调度出去再调度回来才能执行完,这和disable preempt相比未免有些得不偿失。 另外个人对调度器内容不太了解,但如果调度器本身也是个线程,可以获取spinlock的话...
    • bsp

      @dream:B如果为rt线程,SCHED_FIFO时,是不考虑时间片的;
    • icy_river

      @dream:在你假设的场景是,功能是没问题的; 但是这样B的时间片不是完全浪费了吗?
    • ja

      @dream:我看完這段也有相同的想法,引用 @dream 最後一段的說法 即使当前进程在持有锁的时候,被高优先级进程抢占也访问了锁,进入自旋状态,那么内核不是支持时间片轮转的调度嘛,当产生 tick 中断的时候,高优先级的进程时间片用完,那么低优先级的进程就有再次执行的机会,只要低优先级有执行的机会,自然就能完成临界区的代码执行,然后释放锁。释放锁,高优先级进程也就自然也能获取锁 這樣B进程不會进入"永久"的spin,而只是暫時的spin,使用"永久"這個詞是因為上述做法會因為某些原因而無法達成嗎?
  • bsp

    ticket-based-spinlock有个问题, CPU0获取了spinlock,CPU1、CPU2、CPU3...在等锁时,CPU0通过spinunlock中的sev唤醒其它所有所有CPU去check是否轮到自己;既然我们通过ticket知道了lock->next是哪个CPU,其实只需要唤醒对应CPU上的task即可。 也许这就是qspinlock应运而生的一个原因?
    • icy_river

      @bsp:1. sev只有sev/sevl两种, 无法给指定core发event; 2. 同时自旋的core太多的话, cache同步开销就不能忽略了; 3. kernel 5.10, ticket spinlock已经换成qspinlock;
      • bsp

        @icy_river:嗯,我其实就在说qspinlock 替代ticket-based-spinlock的原因。 通过qspinlock,只需要唤醒lock->next所在的cpu,其它cpu继续自旋。
      • bsp

        @icy_river:嗯,我其实就在说qspinlock 替代ticket-based-spinlock的原因。 通过qspinlock,只需要唤醒lock->next所在的cpu,其它cpu继续自旋。
      • bsp

        @icy_river:bsp 2023-02-01 13:11 ticket-based-spinlock有个重大问题: 假如当CPU0获取了spinlock,而CPU1、CPU2、CPU3...在等锁,在CPU0 spin_unlock时通过sev/stlr唤醒其它所有所有CPU去check是否轮到自己;我们通过ticket可以知道lock->next是哪个CPU,其实只需要唤醒对应CPU上的task即可。 于是qspinlock应运而生: 1:当CPU0获取了spinlockA,而CPU1-->CPU2-->CPU3依次在等锁(lock->next是CPU1上的task)时,只需让CPU1自旋在spinlockA上(即qspinlock中的pending)、CPU2/CPU3自旋在percpu上的另外一把锁B(即qspinlock中的mcs锁),percpu的锁可以减少cache-boucing(具体怎么减 自行脑补); 2:当CPU0释放了spinlockA(armv8的stlr可以只唤醒对应地址上的waiter即CPU1,这点比armv7的sev好用),只会唤醒自旋在spinlockA上的CPU1,其它CPU则暂时不会被唤醒; 3:同理CPU1获取了spinlockA,需要把把lock->next标记为CPU2,并让CPU2从自旋在percpu的spinlockB转换到spinlockA;即qspinlock中对应的mcs队列; 4:同理CPU1释放了spinlockA,stlr(load-acquire/store-release)只会唤醒CPU2,;CPU2获取了spinlockA,把lock->next标记为CPU3,并让CPU3从自旋在percpu的spinlockB转换到spinlockA;即qspinlock中对应的mcs队列; 依次往下,这便是qspinlock的中心思想吧。至于怎么设计和实现,我觉得有不同的方法。
  • Harry Song

    文中对 ticket 的机制进行举例和实际的汇编实现有偏差: "最开始的时候,slock被赋值为0,也就是说owner和next都是0,owner和next相等,表示unlocked。" --> 实际上应该是默认值是1,第一位来排队的,先 next+1 = 1,然后 当前owner == 当前next,拿到了锁;如果不是第一位来排队的,那就是等待 owner何时 == 自己的next值; 而不是来排队的先 next+1 把号码给后来的人。 我通过 ARM32 的arch_spin_lock 分析的,你看对不对。