自旋锁 - ARM汇编同步机制实例(三)

关于锁的基本概念见文章同步原语 - 锁(一)_生活需要深度-CSDN博客 。这里主要针对不同类型的锁给出给出其对应的汇编代码实现

1. 自旋锁

代码位于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本质上是保存在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表示操作数是立即数

3. 入场券自旋锁

入场券自旋锁(ticket spinlock)的算法类似于银行柜台的排队叫号:

(1)锁拥有排队号和服务号,服务号是当前占有锁的进程的排队号。

(2)每个进程申请锁的时候,首先申请一个排队号,然后轮询锁的服务号是否等于自己的排队号,如果等于,表示自己占有锁,可以进入临界区,否则继续轮询。

(3)当进程释放锁时,把服务号加一,下一个进程看到服务号等于自己的排队号,退出自旋,进入临界区。

ARM64架构定义的数据类型arch_spinlock_t如下所示:

arch/arm64/include/asm/spinlock_types.h

typedef struct {
#ifdef __AARCH64EB__     /* 大端字节序(高位存放在低地址) */
     u16 next;
     u16 owner;
#else                    /* 小端字节序(低位存放在低地址) */
     u16 owner;
     u16 next;
#endif
} __aligned(4) arch_spinlock_t;

成员next是排队号,成员owner是服务号。在多处理器系统中,函数spin_lock()负责申请自旋锁,ARM64架构的代码如下所示:

spin_lock() -> raw_spin_lock() -> _raw_spin_lock() -> __raw_spin_lock()  -> do_raw_spin_lock() -> arch_spin_lock()

arch/arm64/include/asm/spinlock.h

1    static inline void arch_spin_lock(arch_spinlock_t *lock)
2    {
3     unsigned int tmp;
4     arch_spinlock_t lockval, newval;
5
6     asm volatile(
7     ARM64_LSE_ATOMIC_INSN(
8     /* LL/SC */
9    "   prfm    pstl1strm, %3\n"
10   "1:   ldaxr   %w0, %3\n"
11   "   add   %w1, %w0, %w5\n"
12   "   stxr   %w2, %w1, %3\n"
13   "   cbnz   %w2, 1b\n",
14    /* 大系统扩展的原子指令 */
15   "   mov   %w2, %w5\n"
16   "   ldadda   %w2, %w0, %3\n"
17   __nops(3)
18   )
19
20   /* 我们得到锁了吗?*/
21  "   eor   %w1, %w0, %w0, ror #16\n"
22  "   cbz   %w1, 3f\n"
23  "   sevl\n"
24  "2:   wfe\n"
25  "   ldaxrh   %w2, %4\n"
26  "   eor   %w1, %w2, %w0, lsr #16\n"
27  "   cbnz   %w1, 2b\n"
28   /* 得到锁,临界区从这里开始*/
29  "3:"
30   : "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
31   : "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
32   : "memory");
33  }

第6~18行代码,申请排队号,然后把自旋锁的排队号加1,这是一个原子操作,有两种实现方法:

1)第9~13行代码,使用指令ldaxr(带有获取语义的独占加载)和stxr(独占存储)实现,指令ldaxr带有获取语义,后面的加载/存储指令必须在指令ldaxr完成之后开始执行。

2)第15~16行代码,如果处理器支持大系统扩展,那么使用带有获取语义的原子加法指令ldadda实现,指令ldadda带有获取语义,后面的加载/存储指令必须在指令ldadda完成之后开始执行。

第21~22行代码,如果服务号等于当前进程的排队号,进入临界区。

第24~27行代码,如果服务号不等于当前进程的排队号,那么自旋等待。使用指令ldaxrh(带有获取语义的独占加载,h表示halfword,即2字节)读取服务号,指令ldaxrh带有获取语义,后面的加载/存储指令必须在指令ldaxrh完成之后开始执行。

第23行代码,sevl(send event local)指令的功能是发送一个本地事件,避免错过其他处理器释放自旋锁时发送的事件。

第24行代码,wfe(wait for event)指令的功能是使处理器进入低功耗状态,等待事件。

函数spin_unlock()负责释放自旋锁,ARM64架构的代码如下所示:

spin_unlock() -> raw_spin_unlock() -> _raw_spin_unlock() -> __raw_spin_unlock()  -> do_raw_spin_unlock() -> arch_spin_unlock()

arch/arm64/include/asm/spinlock.h

1   static inline void arch_spin_unlock(arch_spinlock_t *lock)
2   {
3    unsigned long tmp;
4   
5    asm volatile(ARM64_LSE_ATOMIC_INSN(
6    /* LL/SC */
7    "    ldrh   %w1, %0\n"
8    "    add   %w1, %w1, #1\n"
9    "    stlrh   %w1, %0",
10   /* 大多统扩展的原子指令 */
11   "    mov   %w1, #1\n"
12   "    staddlh   %w1, %0\n"
13   __nops(1))
14   : "=Q" (lock->owner), "=&r" (tmp)
15   :
16   : "memory");
17  }

把自旋锁的服务号加1,有两种实现方法:

(1)第7~9行代码,使用指令ldrh(加载,h表示halfword,即2字节)和stlrh(带有释放语义的存储)实现,指令stlrh带有释放语义,前面的加载/存储指令必须在指令stlrh开始执行之前执行完。因为一次只能有一个进程进入临界区,所以只有一个进程把自旋锁的服务号加1,不需要是原子操作。

(2)第11~12行代码,如果处理器支持大系统扩展,那么使用带有释放语义的原子加法指令staddlh实现,指令staddlh带有释放语义,前面的加载/存储指令必须在指令staddlh开始执行之前执行完。

在单处理器系统中,自旋锁是空的。

include/linux/spinlock_types_up.h

typedef struct { } arch_spinlock_t;

函数spin_lock()只是禁止内核抢占。

spin_lock() -> raw_spin_lock() -> _raw_spin_lock()

include/linux/spinlock_api_up.h

#define _raw_spin_lock(lock)             __LOCK(lock)

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

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

4. MCS自旋锁 

入场券自旋锁存在性能问题:所有等待同一个自旋锁的处理器在同一个变量上自旋等待,申请或者释放锁的时候会修改锁,导致其他处理器存放自旋锁的缓存行失效,在拥有几百甚至几千个处理器的大型系统中,处理器申请自旋锁时竞争可能很激烈,缓存同步的开销很大,导致系统性能大幅度下降。

MCS(MCS是“Mellor-Crummey”和“Scott”这两个发明人的名字的首字母缩写)自旋锁解决了这个缺点,它的策略是为每个处理器创建一个变量副本,每个处理器在申请自旋锁的时候在自己的本地变量上自旋等待,避免缓存同步的开销。

4.1.   传统的MCS自旋锁

传统的MCS自旋锁包含:

(1)一个指针tail指向队列的尾部。

(2)每个处理器对应一个队列节点,即mcs_lock_node结构体,其中成员next指向队列的下一个节点,成员locked指示锁是否被其他处理器占有,如果成员locked的值为1,表示锁被其他处理器占有。 

结构体的定义如下所示:

typedef struct __mcs_lock_node {   
    struct __mcs_lock_node *next;
    int locked;
} ____cacheline_aligned_in_smp mcs_lock_node;

typedef struct {
    mcs_lock_node *tail;
    mcs_lock_node nodes[NR_CPUS];/* NR_CPUS是处理器的数量 */
} spinlock_t;

其中“____cacheline_aligned_in_smp”的作用是:在多处理器系统中,结构体的起始地址和长度都是一级缓存行长度的整数倍。

当没有处理器占有或者等待自旋锁的时候,队列是空的,tail是空指针。

 

图 4.1 处理器0申请MCS自旋锁

如图 4.1所示,当处理器0申请自旋锁的时候,执行原子交换操作,使tail指向处理器0的mcs_lock_node结构体,并且返回tail的旧值。tail的旧值是空指针,说明自旋锁处于空闲状态,那么处理器0获得自旋锁。

 

图 4.2 处理器1申请MCS自旋锁

如图 4.2所示,当处理器0占有自旋锁的时候,处理器1申请自旋锁,执行原子交换操作,使tail指向处理器1的mcs_lock_node结构体,并且返回tail的旧值。tail的旧值是处理器0的mcs_lock_node结构体的地址,说明自旋锁被其他处理器占有,那么使处理器0的mcs_lock_node结构体的成员next指向处理器1的mcs_lock_node结构体,把处理器1的mcs_lock_node结构体的成员locked设置为1,然后处理器1在自己的mcs_lock_node结构体的成员locked上面自旋等待,等待成员locked的值变成0。

 

图 4.3 处理器0释放MCS自旋锁

如图 4.3所示,处理器0释放自旋锁,发现自己的mcs_lock_node结构体的成员next不是空指针,说明有申请者正在等待锁,于是把下一个节点的成员locked设置为0,处理器1获得自旋锁。

处理器1释放自旋锁,发现自己的mcs_lock_node结构体的成员next是空指针,说明自己是最后一个申请者,于是执行原子比较交换操作:如果tail指向自己的mcs_lock_node结构体,那么把tail设置为空指针。

4.2.   小巧的MCS自旋锁

传统的MCS自旋锁存在的缺陷是:结构体的长度太大,因为mcs_lock_node结构体的起始地址和长度都必须是一级缓存行长度的整数倍,所以MCS自旋锁的长度是(一级缓存行长度 + 处理器数量 * 一级缓存行长度),而入场券自旋锁的长度只有4字节。自旋锁被嵌入到内核的很多结构体中,如果自旋锁的长度增加,会导致这些结构体的长度增加。

经过内核社区技术专家的努力,成功地把MCS自旋锁放进4个字节,实现了小巧的MCS自旋锁。自旋锁的定义如下所示:

include/asm-generic/qspinlock_types.h

typedef struct qspinlock {
     atomic_t  val;
} arch_spinlock_t;

另外,为每个处理器定义1个队列节点数组,如下所示:

kernel/locking/qspinlock.c

#ifdef CONFIG_PARAVIRT_SPINLOCKS
#define MAX_NODES  8
#else
#define MAX_NODES  4
#endif

static DEFINE_PER_CPU_ALIGNED(struct mcs_spinlock, mcs_nodes[MAX_NODES]);

配置宏CONFIG_PARAVIRT_SPINLOCKS用来启用半虚拟化的自旋锁,给虚拟机使用,本文不考虑这种使用场景。每个处理器需要4个队列节点,原因如下:

(1)       申请自旋锁的函数禁止内核抢占,所以进程在等待自旋锁的过程中不会被其他进程抢占。

(2)       进程在等待自旋锁的过程中可能被软中断抢占,然后软中断等待另一个自旋锁。

(3)       软中断在等待自旋锁的过程中可能被硬中断抢占,然后硬中断等待另一个自旋锁。

(4)       硬中断在等待自旋锁的过程中可能被不可屏蔽中断抢占,然后不可屏蔽中断等待另一个自旋锁。

综上所述,一个处理器最多同时等待4个自旋锁。

和入场券自旋锁相比,MCS自旋锁增加的内存开销是数组mcs_nodes。

队列节点的定义如下所示:

kernel/locking/mcs_spinlock.h

struct mcs_spinlock {
     struct mcs_spinlock *next;
     int locked;
     int count;
};

其中成员next指向队列的下一个节点;成员locked指示锁是否被前一个等待者占有,如果值为1,表示锁被前一个等待者占有;成员count是嵌套层数,也就是数组mcs_nodes已分配的数组项的数量。

自旋锁的32个二进制位被划分成4个字段:

(1)       locked字段,指示锁已经被占有,长度是一个字节,占用第0~7位。

(2)       一个pending位,占用第8位,第1个等待自旋锁的处理器设置pending位。

(3)       index字段,是数组索引,指示队列的尾部节点使用数组mcs_nodes的哪一项。

(4)       cpu字段,存放队列的尾部节点的处理器编号,实际存储的值是处理器编号加上1,cpu字段减去1才是真实的处理器编号。

index字段和cpu字段合起来称为tail字段,存放队列的尾部节点的信息,布局分两种情况:

(1)       如果处理器的数量小于2的14次方,那么第9~15位没有使用,第16~17位是index字段,第18~31位是cpu字段。

(2)       如果处理器的数量大于或等于2的14次方,那么第9~10位是index字段,第11~31位是cpu字段。

把MCS自旋锁放进4个字节的关键是:存储处理器编号和数组索引,而不是存储尾部节点的地址。

内核对MCS自旋锁做了优化:第1个等待自旋锁的处理器直接在锁自身上面自旋等待,不是在自己的mcs_spinlock结构体上自旋等待。这个优化带来的好处是:当锁被释放的时候,不需要访问mcs_spinlock结构体的缓存行,相当于减少了一次缓存没命中。后续的处理器在自己的mcs_spinlock结构体上面自旋等待,直到它们移动到队列的首部为止。

自旋锁的pending位进一步扩展这个优化策略。第1个等待自旋锁的处理器简单地设置pending位,不需要使用自己的mcs_spinlock结构体。第2个处理器看到pending被设置,开始创建等待队列,在自己的mcs_spinlock结构体的locked字段上自旋等待。这种做法消除了两个等待者之间的缓存同步,而且第1个等待者没使用自己的mcs_spinlock结构体,减少了一次缓存行没命中。

在多处理器系统中,申请MCS自旋锁的代码如下所示:

spin_lock() -> raw_spin_lock() -> _raw_spin_lock() -> __raw_spin_lock()  -> do_raw_spin_lock() -> arch_spin_lock()

include/asm-generic/qspinlock.h

1     #define arch_spin_lock(l)         queued_spin_lock(l)
2    
3     static __always_inline void queued_spin_lock(struct qspinlock *lock)
4     {
5     u32 val;
6    
7     val = atomic_cmpxchg_acquire(&lock->val, 0, _Q_LOCKED_VAL);
8     if (likely(val == 0))
9          return;
10   queued_spin_lock_slowpath(lock, val);
11   }

第7行代码,执行带有获取语义的原子比较交换操作,如果锁的值是0,那么把锁的locked字段设置为1。获取语义保证后面的加载/存储指令必须在函数atomic_cmpxchg_acquire()完成之后开始执行。函数atomic_cmpxchg_acquire()返回锁的旧值。

第8~9行代码,如果锁的旧值是0,说明申请锁的时候锁处于空闲状态,那么成功地获得锁。

第10行代码,如果锁的旧值不是0,说明锁不是处于空闲状态,那么执行申请自旋锁的慢速路径。

申请MCS自旋锁的慢速路径如下所示:

kernel/locking/qspinlock.c

1     void queued_spin_lock_slowpath(struct qspinlock *lock, u32 val)
2     {
3     struct mcs_spinlock *prev, *next, *node;
4     u32 new, old, tail;
5     int idx;
6    
7     ...
8     if (val == _Q_PENDING_VAL) {
9          while ((val = atomic_read(&lock->val)) == _Q_PENDING_VAL)
10             cpu_relax();
11   }
12  
13   for (;;) {
14        if (val & ~_Q_LOCKED_MASK)
15             goto queue;
16  
17        new = _Q_LOCKED_VAL;
18        if (val == new)
19             new |= _Q_PENDING_VAL;
20  
21        old = atomic_cmpxchg_acquire(&lock->val, val, new);
22        if (old == val)
23             break;
24  
25        val = old;
26   }
27  
28   if (new == _Q_LOCKED_VAL)
29        return;
30  
31   smp_cond_load_acquire(&lock->val.counter, !(VAL & _Q_LOCKED_MASK));\
32  
33   clear_pending_set_locked(lock);
34   return;
35  
36   queue:
37   node = this_cpu_ptr(&mcs_nodes[0]);
38   idx = node->count++;
39   tail = encode_tail(smp_processor_id(), idx);
40  
41   node += idx;
42   node->locked = 0;
43   node->next = NULL;
44   ...
45  
46   if (queued_spin_trylock(lock))
47        goto release;
48  
49   old = xchg_tail(lock, tail);
50   next = NULL;
51  
52   if (old & _Q_TAIL_MASK) {
53        prev = decode_tail(old);
54        smp_read_barrier_depends();
55  
56        WRITE_ONCE(prev->next, node);
57  
58        ...
59        arch_mcs_spin_lock_contended(&node->locked);
60  
61        next = READ_ONCE(node->next);
62        if (next)
63             prefetchw(next);
64   }
65  
66   ...
67   val = smp_cond_load_acquire(&lock->val.counter, !(VAL & _Q_LOCKED_PENDING_MASK));
68  
69   locked:
70   for (;;) {
71        if ((val & _Q_TAIL_MASK) != tail) {
72             set_locked(lock);
73             break;
74        }
75  
76        old = atomic_cmpxchg_relaxed(&lock->val, val, _Q_LOCKED_VAL);
77        if (old == val)
78             goto release;
79  
80        val = old;
81   }
82  
83   if (!next) {
84        while (!(next = READ_ONCE(node->next)))
85             cpu_relax();
86   }
87  
88   arch_mcs_spin_unlock_contended(&next->locked);
89   ...
90  
91   release:
92   __this_cpu_dec(mcs_nodes[0].count);
93   }

第8~11行代码,如果锁的状态是pending,即{tail=0,pending=1,locked=0},那么等待锁的状态变成locked,即{tail=0,pending=0,locked=1}。

第14~15行代码,如果锁的tail字段不是0或者pending位是1,说明已经有处理器在等待自旋锁,那么跳转到标号queue,本处理器加入等待队列。

第17~21行代码,如果锁处于locked状态,那么把锁的状态设置为locked & pending,即{tail=0,pending=1,locked=1};如果锁处于空闲状态(占有锁的处理器刚刚释放自旋锁),那么把锁的状态设置为locked。

第28~29行代码,如果上一步锁的状态从空闲变成locked,那么成功地获得锁。

第31行代码,等待占有锁的处理器释放自旋锁,即锁的locked字段变成0。

第32行代码,成功地获得锁,把锁的状态从pending改成locked,即清除pending位,把locked字段设置为1。

从第2个等待自旋锁的处理器开始,需要加入等待队列,处理如下:

(1)       第37~43行代码,从本处理器的数组mcs_nodes分配一个数组项,然后初始化。

(2)       第46~47行代码,如果锁处于空闲状态,那么获得锁。

(3)       第49行代码,把自旋锁的tail字段设置为本处理器的队列节点的信息,并且返回前一个队列节点的信息。

(4)       第52行代码,如果本处理器的队列节点不是队列首部,那么处理如下:

1)第56行代码,把前一个队列节点的next字段设置为本处理器的队列节点的地址。

2)第59行代码,本处理器在自己的队列节点的locked字段上面自旋等待,等待locked字段从0变成1,也就是等待本处理器的队列节点移动到队列首部。

(5)       第67行代码,本处理器的队列节点移动到队列首部以后,在锁自身上面自旋等待,等待自旋锁的pending位和locked字段都变成0,也就是等待锁的状态变成空闲。

(6)       锁的状态变成空闲以后,本处理器把锁的状态设置为locked,分两种情况:

1)第71行代码,如果队列还有其他节点,即还有其他处理器在等待锁,那么处理如下:

q第72行代码,把锁的locked字段设置为1。

q第83~86行代码,等待下一个等待者设置本处理器的队列节点的next字段。

q第88行代码,把下一个队列节点的locked字段设置为1。

2)第76行代码,如果队列只有一个节点,即本处理器是唯一的等待者,那么把锁的tail字段设置为0,把locked字段设置为1。

(7)       第92行代码,释放本处理器的队列节点。

释放MCS自旋锁的代码如下所示:

spin_unlock() -> raw_spin_unlock() -> _raw_spin_unlock() -> __raw_spin_unlock()  -> do_raw_spin_unlock() -> arch_spin_unlock()

include/asm-generic/qspinlock.h

1     #define arch_spin_unlock(l)       queued_spin_unlock(l)
2    
3     static __always_inline void queued_spin_unlock(struct qspinlock *lock)
4     {
5     (void)atomic_sub_return_release(_Q_LOCKED_VAL, &lock->val);
6     }

第5行代码,执行带释放语义的原子减法操作,把锁的locked字段设置为0,释放语义保证前面的加载/存储指令在函数atomic_sub_return_release()开始执行之前执行完。

MCS自旋锁的配置宏是CONFIG_ARCH_USE_QUEUED_SPINLOCKS 和CONFIG_QUEUED_SPINLOCKS,目前只有x86处理器架构使用MCS自旋锁,默认开启MCS自旋锁的配置宏,如下所示:

arch/x86/kconfig

config X86

     def_bool y

     ...

     select ARCH_USE_QUEUED_SPINLOCKS

     ...

kernel/kconfig.locks

config ARCH_USE_QUEUED_SPINLOCKS

     bool

config QUEUED_SPINLOCKS

     def_bool y if ARCH_USE_QUEUED_SPINLOCKS

     depends on SMP

最后给出x86_64自旋锁


//自旋锁结构
typedef struct
{
     volatile u32_t lock;//volatile可以防止编译器优化,保证其它代码始终从内存加载lock变量的值 
} spinlock_t;
//锁初始化函数
static inline void x86_spin_lock_init(spinlock_t * lock)
{
     lock->lock = 0;//锁值初始化为0是未加锁状态
}
//加锁函数
static inline void x86_spin_lock(spinlock_t * lock)
{
    __asm__ __volatile__ (
    "1: \n"
    "lock; xchg  %0, %1 \n"//把值为1的寄存器和lock内存中的值进行交换
    "cmpl   $0, %0 \n" //用0和交换回来的值进行比较
    "jnz    2f \n"  //不等于0则跳转后面2标号处运行
    "jmp 3f \n"     //若等于0则跳转后面3标号处返回
    "2:         \n" 
    "cmpl   $0, %1  \n"//用0和lock内存中的值进行比较
    "jne    2b      \n"//若不等于0则跳转到前面2标号处运行继续比较  
    "jmp    1b      \n"//若等于0则跳转到前面1标号处运行,交换并加锁
    "3:  \n"     :
    : "r"(1), "m"(*lock));
}
//解锁函数
static inline void x86_spin_unlock(spinlock_t * lock)
{
    __asm__ __volatile__(
    "movl   $0, %0\n"//解锁把lock内存中的值设为0就行
    :
    : "m"(*lock));
}

上述代码的中注释已经很清楚了,关键点在于 xchg 指令,xchg %0, %1 。其中,%0 对应 “r”(1),表示由编译器自动分配一个通用寄存器,并填入值 1,例如 mov eax,1。而 %1 对应"m"(*lock),表示 lock 是内存地址。把 1 和内存中的值进行交换,若内存中是 1,则不会影响;因为本身写入就是 1,若内存中是 0,一交换,内存中就变成了 1,即加锁成功。自旋锁依然有中断嵌套的问题,也就是说,在使用自旋锁的时候我们仍然要注意中断。在中断处理程序访问某个自旋锁保护的某个资源时,依然有问题,所以我们要写的自旋锁函数必须适应这样的中断环境,也就是说,它需要在处理中断的过程中也能使用,如下所示。


static inline void x86_spin_lock_disable_irq(spinlock_t * lock,cpuflg_t* flags)
{
    __asm__ __volatile__(
    "pushfq                 \n\t"
    "cli                    \n\t"
    "popq %0                \n\t"
    "1:         \n\t"
    "lock; xchg  %1, %2 \n\t"
    "cmpl   $0,%1       \n\t"
    "jnz    2f      \n\t"
    "jmp    3f      \n"  
    "2:         \n\t"
    "cmpl   $0,%2       \n\t" 
    "jne    2b      \n\t"
    "jmp    1b      \n\t"
    "3:     \n"     
     :"=m"(*flags)
    : "r"(1), "m"(*lock));
}
static inline void x86_spin_unlock_enabled_irq(spinlock_t* lock,cpuflg_t* flags)
{
    __asm__ __volatile__(
    "movl   $0, %0\n\t"
    "pushq %1 \n\t"
    "popfq \n\t"
    :
    : "m"(*lock), "m"(*flags));
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值