Linux内核:进程管理——自旋锁

一、自旋锁基本原理

  自旋锁是一种对临界资源进行互斥访问的手段,获得锁的的任务能对临界进行操作,操作完后解锁,没有获得锁的任务则循环等待锁可用,相当于原地打转自旋。下面用一个例子说明一下自旋锁的工作原理。

把自旋锁看做内存中的一个变量flag,当flag=1时表示已经上锁,flag=0表示为解锁状态。线程或进程要操作临界资源时先上锁(设置flag=1),操作完成后解锁(设置flag=0)。如果上锁的时候发现已经flag=1,则循环等待查询flag值,直到flag=0.

假设SMP系统中有两个cpu,cpu1和cpu2,cpu1是运行着线程1,cpu2上运行着线程2.

1、cpu1的线程1要操作临界资源,先查看flag值是否为0,此时flag值为0,顺利完成加锁并操作临界资源

2、线程1未临界操作完成时cpu2的线程2也要操作临界资源

3、cpu2的线程2先看flag值是1,则进入循环等待查询flag值,直到flag等0才能成功加锁。

4、cpu1的线程1处理完成临界资源后解锁,设置flag=0

5、cpu2的线程2成功等待到flag值为0,并设置flag=1加锁处理临界资源,处理完成后解锁。

以上是不同cpu直接竞争临界资源的情况,相同cpu不同线程情况也是这样。实际处理中设置flag=1和falg=0需要原子操作。原子操作可参考链接:https://www.bilibili.com/read/cv16561601/

这么操作会带来一个弊端,在多核多线程强占情况下,谁优先获得锁完全是无序的,就像大家一哄而上,谁抢到就是谁的,并没有先来后到。我们希望先等待的线程能先获得锁,就像餐馆门口取号排队一样,先来先就餐。

  假设一个餐馆只有一张桌子,一次只能有一个人吃饭。门口有个自动取票机,取票机上显示两个值,ower和next,ower表示当前可以就餐的号码,next表示取到的号码都是next,每取出一个号,next就会加1.每个吃饭的顾客要吃饭先取票,然后把自己的号码取票机上的ower号码比较,如果和自己的一样,就可以进去就餐,否则等待。

  初始状态下取票机上的ower=next=0,

  1、来了第一个顾客,取到的号码是0,然后取票机会自动next++,这个顾客看到自己的号码和取票机上的ower号码一样都是0,就进去就餐。

  2、第一个顾客很快吃完,自动取票机把ower++,表示下一个可以吃饭的号码为1.这时ower=next=1。

  3、来了第二个顾客,这个顾客取到的号码是1,然后看一下取票机上的ower也是1,就进去就餐。

  4、第二个顾客还没吃完的时候又来了第三个顾客,第三个顾客取到的号码是2,此时取票号机上的ower=1,这个顾客需要等待。

  5、第二个顾客吃饭很慢,这时又来了第四个顾客,第四个顾客取到的号码是3,此时取票号机上的ower=1,这个顾客需要等待。

  6、第二个顾客终于吃完了,自动取号机把ower++,此时ower=2。

  7、号码为2的顾客看到ower的值和自己的一样,就进去就餐。

  8、第三个顾客也很快吃完,自动取号机把ower++,此时ower=3。

  9、第四个顾客看到ower值和自己去一样,就进去就餐

  如果把餐桌比作临界资源,吃饭顾客比作cpu。通过这种排队取号机制,可以让cpu有序的竞争临界资源。linux自旋锁也是采用这种机制。

二、自旋锁使用

2.1自旋锁操作函数

  linux中的自旋锁用结构体spinlock_t 表示,定义在include/linux/spinlock_type.h。自旋锁的接口函数全部定义在include/linux/spinlock.h头文件中,实际使用时只需include<linux/spinlock.h>即可

方式1:
include<linux/spinlock.h>
spinlock_t lock;      //定义自旋锁
spin_lock_init(&lock);  //初始化自旋锁
spin_lock(&lock);     //获得锁,如果没获得成功则一直等待
.......            //处理临界资源
spin_unlock(&lock);   //释放自旋锁

方式2:
include<linux/spinlock.h>
spinlock_t lock;      //定义自旋锁
spin_lock_init(&lock);  //初始化自旋锁
if (spin_trylock(&lock))   //尝试获得自旋锁,获得成功返回true,不成功返回false,不会等待
{
 .......            //处理临界资源
  spin_unlock(&lock);   //释放自旋锁
}

spin_lock只能解决进程间互斥问题,如果中断中也要访问临界资源,还需要关中断才行。linux提供了相应的接口函数:

spin_lock_irq() = spin_lock() + local_irq_disable()

spin_unlock_irq() = spin_unlock() + local_irq_enable()

spin_lock_irqsave() = spin_lock() + local_irq_save()

spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()

spin_lock_bh() = spin_lock() + local_bh_disable()

spin_unlock_bh() = spin_unlock() + local_bh_enable()

2.2

三、自旋锁源码分析

3.1 自旋锁相关文件介绍

  对于UP系统和SMP系统,自旋锁的实现不同,对于UP系统,自旋锁实现很简单,只是禁止cpu调度即可,SMP系统实现会稍微复杂。

  include/linux/spinlock.h  

  这个头文件定义了自旋锁的接口函数,spin_lock_init、spin_lock、spin_trylock、spin_unlock等。

    **-->*include/linux/spinlock_types.h ***

  这个头文件定义了自旋锁基本类型spinlock_t、raw_spinlock_t。

      -->asm/spinlock_types.h

  这个是SMP系统相关的头文件,这头文件定义了体系结构相关的自旋锁结构体,arm是在arch/arm/include/asm/spinlock_types.h,arm64是在arch/arm64/include/asm/spinlock_types.h

      -->linux/spinlock_types_up.h

  这个是UP系统相关头文件,定义了UP系统自旋锁结构体,Debug版本用的

    -->asm/spinlock.h  

  这个是SMP系统相关的头文件,这个头文件是自旋锁的实现,跟体系结构有关,arm是在arch/arm/include/asm/spinlock.h,arm64是在arch/arm64/include/asm/spinlock.h。

    -->include/linux/spinlock_up.h

  这个是UP系统相关的头文件,Debug版本用到

    -->linux/spinlock_api_smp.h

  SMP系统自旋锁的接口定义

    -->linux/spinlock_api_up.h

3.2 自旋锁调用过程  

SMP系统加锁函数调用流程

-->spin_lock  //include/linux/spinlock.h

  -->raw_spin_lock   

    -->_raw_spin_lock  

      -->__raw_spin_lock  //对于SMP系统,linux/spinlock_api_smp.h

        -->preempt_disable  //关闭内核抢占

        -->spin_acquire    //检查锁的有效性

        -->LOCK_CONTENDED  

          -->arch_spin_lock  //这里是真正的加锁实现,见下面源码分析

UP系统加锁函数调用流程

-->spin_lock  //include/linux/spinlock.h

  -->raw_spin_lock   

    -->_raw_spin_lock  

      -->__LOCK  //对于UP系统,include/linux/spinlock_api_up.h

        -->preempt_disable  //UP系统加锁很简单,仅仅禁止内核抢占即可

3.3 arm32自旋锁源码分析

arm32的自旋锁结构体定义在arch/arm/include/asm/spinlock_types.h中

#define TICKET_SHIFT    16

typedef struct {
    union {
        u32 slock;
        struct __raw_tickets {
#ifdef __ARMEB__            //大端模式
            u16 next;
            u16 owner;
#else
            u16 owner;
            u16 next;
#endif
        } tickets;
    };
} arch_spinlock_t;          //自旋锁

#define __ARCH_SPIN_LOCK_UNLOCKED    { { 0 } }

typedef struct {
    u32 lock;
} arch_rwlock_t;            //读写锁

#define __ARCH_RW_LOCK_UNLOCKED        { 0 }

#endif

arch_spinlock_t的内存分布如下所示:

arch_spinlock_t总共32位,next占用低16bit,ower占用高16bit

arm自旋锁和读写锁的函数实现都是定义在arch/arm/include/asm/spinlock.h中

加锁源码分析:

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

    prefetchw(&lock->slock);     //预取指令,向将lock->slock取到cash缓存,以提高读写速度
    __asm__ __volatile__(
"1:    ldrex    %0, [%3]\n"      //原子访问,lockval=*lock,这里先保存一份lock,因为传进来的lock是多个线程公用的,有可能被别的线程改掉
"    add    %1, %0, %4\n"       //newval = lockval + (1<<16),相当于newval=lockcal.tickets.next++
"    strex    %2, %1, [%3]\n"    //*lock = *((arch_spinlock_t)&newval),相当于执行了lock->tickets.netx++
"    teq    %2, #0\n"         //判断原子操作是否成功,如果不成功则返回到第一步继续原子操作
"    bne    1b"
    : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
    : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
    : "cc");

    while (lockval.tickets.next != lockval.tickets.owner) {  //如果不相等表示锁还未解开,
        wfe();                              //WFE指令,让cpu进入休眠,解锁的线程会用sev指令唤醒cpu
        lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);  //其它线程已经用完临界资源解锁,调用sev指令唤醒该cpu,还要需要重新去加载lock的owner值
    }

    smp_mb();                            //设置内存屏障
}

从加锁的源码中可以看到,没有获得锁的cpu会一直休眠等待被唤醒,无法进行线程调度。如果处理临界资源需要较多时间,用自旋锁会影响cpu效率。

解锁源码分析

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
    smp_mb();                  //设置内存屏障
    lock->tickets.owner++;          //owner自增
    dsb_sev();                  //执行sev指令唤醒休眠中cpu
}

可以看到解锁owner++不需要原子操作,因为获得锁的线程只有一个,能解锁的线程也只有一个,解锁没有竞争存在

3.4 arm64自旋锁源码分析

arm64自旋锁结构体定义在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;    //arm64自旋锁接头体

#define __ARCH_SPIN_LOCK_UNLOCKED    { 0 , 0 }

typedef struct {
    volatile unsigned int lock;    
} arch_rwlock_t;            //arm64读写锁结构体

arm64自旋锁和读写锁的函数实现都是定义在arch/arm64/include/asm/spinlock.h中

加锁源码分析:

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. */
    ARM64_LSE_ATOMIC_INSN(
    /* LL/SC */
"    prfm    pstl1strm, %3\n"
"1:    ldaxr    %w0, %3\n"    //ldaxr指令和ldrex指令功能一样,都是独占访问指令,lockval = *lock
"    add    %w1, %w0, %w5\n"  // newval = lockval+1<<16,相当于newval=lockval.next++
"    stxr    %w2, %w1, %3\n"  //stxr指令功能和strex指令一样,*lock = newval,相当于执行了lock->next++
"    cbnz    %w2, 1b\n",    //如果独占访问不成功则跳转到第一步继续执行
    /* LSE atomics */
"    mov    %w2, %w5\n"
"    ldadda    %w2, %w0, %3\n"
    __nops(3)
    )

    /* Did we get the lock? */
"    eor    %w1, %w0, %w0, ror #16\n"  //判断lockval.next和lockval.ower是否相等
"    cbz    %w1, 3f\n"            //如果相等则表示加锁成功,跳转到步骤3,结束加锁过程
    /*
     * No: spin on the owner. Send a local event to avoid missing an
     * unlock before the exclusive load.
     */
"    sevl\n"                  //指令的功能是发送一个本地事件,避免错过其他处理器释放自旋锁时发送的事件。
"2:    wfe\n"                //加锁不成功则进入休眠等待
"    ldaxrh    %w2, %4\n"         //其它cpu解锁,唤醒当前cpu,重新加载lock->owner值,tmp = lock->owner
"    eor    %w1, %w2, %w0, lsr #16\n"  //再次判断next和ower是否相等
"    cbnz    %w1, 2b\n"          //如果不相等表示未获得锁,跳到步骤2继续休眠,否则成功获得锁,结束加锁过程
    /* 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");
}

解锁源码分析:

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
    unsigned long tmp;

    asm volatile(ARM64_LSE_ATOMIC_INSN(
    /* LL/SC */
    "    ldrh    %w1, %0\n"    //tmp = lock->owner
    "    add    %w1, %w1, #1\n"  //tmp += 1
    "    stlrh    %w1, %0",    //lock->owner = tmp ,实现lock->owner++功能
    /* LSE atomics */
    "    mov    %w1, #1\n"
    "    staddlh    %w1, %0\n"
    __nops(1))
    : "=Q" (lock->owner), "=&r" (tmp)
    :
    : "memory");
}

关于在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的指令。

内核资料直通车:Linux内核源码技术学习路线+视频教程代码资料

学习直通车:Linux内核源码/内存调优/文件系统/进程管理/设备驱动/网络协议栈

原文作者:极致Linux内核

原文地址:Linux内核:进程管理——自旋锁 - 知乎(版权归原文作者所有,侵权留言联系删除)

猜你喜欢

转载自blog.csdn.net/m0_74282605/article/details/130172367