spin_lock

1.概述

1.1.自旋锁的由来

自旋锁最初是为了SMP系统设计的,实现了在多处理器情况下保护临界区,所以在SMP系统中,自旋锁的实现是完整的,实现了真正的自旋(忙)等待。但是对于UP(单处理器)系统,自旋锁并没有自旋,而是通过关闭系统抢占来保护临界区。

1.2.自旋锁的目的

自旋锁的实现是为了保护一段短小的临界区操作代码,保证这个临界区的操作是原子的,从而避免并发的竞争风险。在内核中,可以看到许多内核数据结构中都嵌入了类型为spinlock_t的变量,这些变量用于保证访问共享数据结构的操作是原子的,即将临界区的并发访问串行化。
在SMP系统中,如果内核控制路径发现可以获取自旋锁,就获取锁访问临界区。相反,如果内核控制路径发现锁已被获取,自旋锁会在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被释放。获取自旋锁的过程是一个忙等待的过程,所以自旋锁保护的临界区必须小,且操作过程必须短。而在UP系统中,CPU通过关闭内核抢占来获取自旋锁,释放自旋锁时打开内核抢占,没有忙等待的过程,当然UP系统中的临界区也是越短越好,不然内核抢占关闭的时间过长将导致系统的实时性下降。

1.3.自旋锁使用场景

自旋锁用于将临界区的并发访问串行化,因此有必要梳理一下产生并发操作的场景。
(1)中断
假如线程正在访问临界区,这时中断产生了,中断例程被执行,如果在中断例程中访问了同一个临界区,临界区的原子性就被打破。如果某个临界区都能被线程和中断例程访问,那么就必须用自旋锁保护。不同的中断类型(硬件中断和软件中断)对应于不同版本的自旋锁实现,其中包含了中断禁用和开启的代码。但如果没有中断例程访问临界区,可不考虑中断带来的并发风险。
(2)内核抢占
在2.6以后的内核中,支持内核抢占,并且是可配置的。这使UP系统和SMP类似,会出现内核态下的并发。这种情况下进入临界区就需要避免因抢占造成的并发,所以解决的方法是获取自旋锁的时候禁用内核抢占(preempt_disable),在释放自旋锁时开启内核抢占(preempt_enable,注意此时会执行一次抢占调度)。内核不支持抢占时,自旋锁退化为空操作。
(3)SMP系统
在SMP系统中,多个物理处理器同时工作,存在多个CPU同时访问临界区的可能。这样就需要在内存中加一个标志,每个需要进入临界区的代码都必须检查这个标志,看是否有CPU已经在临界区中。检查标志和设置标志的代码也必须保证原子性,这通常和体系结构相关,由具体的硬件实现,如arm V7指令集的STREX LDREX CLREX,可实现内存的独占性访问。

2.代码阅读

Linux内核和自旋锁相关的源码文件如下:
(1)include/linux/spinlock_types.h
包含了spinlock通用数据类型及spinlock初始化函数。
(2)include/linux/spinlock_types_up.h
包含了up系统上自旋锁的数据类型及初始化操作接口。如果没有定义SMP宏定义CONFIG_SMP,则将spinlock_types_up.h头文件包含到spinlock_types.h文件中,即up系统中使用spinlock_types_up.h头文件。内核中其他模块不应该包含此头文件。
(3)include/linux/spinlock.h
包含了通用的spinlock操作接口函数,如spin_lockspin_unlock等,内核中需要使用自旋锁的模块可以直接包含此头文件,无需关心具体实现及结构。
(4)kernel/locking/spinlock.c
SMP系统中的自旋锁底层实现。
(5)include/linux/spinlock_up.h
up系统中调试版本的自旋锁(UP-debug version of spinlocks)。如果没有定义SMP宏定义CONFIG_SMP,则将spinlock_up.h头文件包含到spinlock.h文件中。内核中其他模块不应该包含此头文件。
(6)include/linux/spinlock_api_up.h
up系统中的自旋锁,如果没有定义CONFIG_SMPCONFIG_DEBUG_SPINLOCK,则将spinlock_api_up.h头文件包含到spinlock.h文件中。内核中其他模块不应该包含此头文件。
(7)linux/spinlock_api_smp.h
SMP系统中使用的自旋锁,如果定义CONFIG_SMPCONFIG_DEBUG_SPINLOCK,则将spinlock_api_smp.h头文件包含到spinlock.h文件中。内核中其他模块不应该包含此头文件。可以看出,当配置自旋锁调试选项后,自旋锁将被编译成SMP版本

2.1.自旋锁数据类型

自旋锁的基本数据类型为spinlock_t,其内部又包含了跟配置相关的代码。raw_spinlock_t中的数据类型arch_spinlock_t分为SMP系统版本和UP系统版本。

    include/linux/spinlock_types.h
    typedef struct spinlock {
        union {
            struct raw_spinlock rlock;
            // 删除一些不重要的东西
        };
    } spinlock_t;
    typedef struct raw_spinlock {
        arch_spinlock_t raw_lock;    // 跟具体配置相关
        // 删除一些不重要的东西
    } raw_spinlock_t;   

arch_spinlock_tSMP系统版本如下,内部定义了一个u32整数slock,分为两个u16整数,获取自旋锁时next1,释放自旋锁时owner1。将slock分为两个u16的整数,和自旋锁的公平竞争有关,后续会讲到。

    include/linux/spinlock_types.h  
    typedef struct {
        union {
            u32 slock;
            struct __raw_tickets {
    #ifdef __ARMEB__         // arm大端
                u16 next;
                u16 owner;
    #else                    // arm小端
                u16 owner;
                u16 next;
    #endif
            } tickets;
        };
    } arch_spinlock_t;

arch_spinlock_t UP系统中的实现分为调试版本和非调试版本,非调试版本定义为空。

    include/linux/spinlock_types_up.h  
    #ifdef CONFIG_DEBUG_SPINLOCK
    typedef struct {
        volatile unsigned int slock;
    } arch_spinlock_t;  // 调试版本
    #else
    typedef struct { } arch_spinlock_t;  // 非调试版本
    #endif
2.2.自旋锁基本操作函数分析

自旋锁操作函数有好几种类型,但其都是在基本的操作函数上进行扩展得到的。自旋锁基本的操作函数为spin_lockspin_unlock,在SMP系统和UP系统中内部实现又有所不通。spin_lock用于获取锁,spin_unlock用于释放锁。下面重点分析一下SMP系统和UP系统中spin_lockspin_unlock的源码实现。

2.2.1.SMP系统中的实现

首先分析一下spin_lock的实现,spin_lock的调用流程如下所示,在获取锁之前会禁止内核抢占,底层调用和体系结构相关的函数arch_spin_lock获取锁。

spin_lock
  raw_spin_lock    // 宏定义,会被替换为_raw_spin_lock
  ->_raw_spin_lock    // 定义在spinlock.c中的函数
    ->__raw_spin_lock
      preempt_disable    // 禁止内核抢占
      ->spin_acquire       // 和自旋锁检查相关的函数,可忽略
      LOCK_CONTENDED       // 宏定义
      ->do_raw_spin_lock
        ->__acquire
        ->arch_spin_lock   // 最底层的执行函数

arch_spin_lock内部嵌入了汇编代码,与体系结构相关。ldrexstrex为arm架构特有汇编指令,具体意义可以参考《ARM Cortex-A Series Programmers Guide》中的18.8节,LDREXSTREXCLREX三条汇编指令可以实现独占性的内存访问,每条指令的含义如下:
LDREX (Load Exclusive) performs a load of memory, but also tags the physical address to
be monitored for exclusive access by that core.
STREX (Store Exclusive) performs a conditional store to memory, succeeding only if the
target location is tagged as being monitored for exclusive access by that core. This
instruction returns the value of 1 in a general purpose register if the store does not take
place, and a value of 0 if the store is successful.
CLREX (Clear Exclusive) clears any exclusive access tag for that core.
执行LDREX的CPU会将访问的内存地址标记为独占访问,随后执行STREX时会检查这个标记,如果标记存在,说明没有CPU访问这个内存地址,则会将数据写到内存并返回0,表示更新成功,如果标记不存在,说明此内存地址的值被修改过,则不会将数据写入内存,此时返回1,表示更新失败,需要重新执行LDREXSTREX写入数据。
C语言的内联汇编语法可参考《Linux内核源代码情景分析》1.5节。内联汇编代码可分为4部分,第一部分为指令部,表示执行的汇编指令,arch_spin_lock函数执行了ldrexaddstrexteqbne五条汇编指令。第二部分为输出部,表示指令部输出的数据应该保存在那个变量中,按顺序进行标记,arch_spin_lock函数中%0对应lockval变量,%1对应newval变量,%2对应tmp变量。第三部分为输入部,表示指令部应该从那个变量中读取数据,接着输出部的顺序进行标记,arch_spin_lock函数中%3对应slock变量的地址,%4对应常数1 << TICKET_SHIFT。第四部分为损坏部,用于告诉C语言编译器汇编代码改变了那个寄存器的值,让C语言编译器做出相应的处理。下面将对arch_spin_lock函数中重要的语句添加注释进行解释,同时说明其执行过程。
(1)首先执行ldrex指令,将自旋锁的数据slock加载到局部变量lockval
(2)执行add指令,将slocknext字段加1,并将结果保存到局部变量newval中,表示获取自旋锁的线程数量增加了一个,注意lockval的值并没有变化。
(3)执行strex指令,将newval的值更新到自旋锁的数据slock中,更新的结果保存在tmp
(4)执行teq指令判断tmp是否等于0
(5)执行bnq,如果tmp不等于0,则说明strex更新自旋锁数据失败,则跳转到标号1处,继续更新
(6)如果更新成功,则进入到while循环中,判断nextowner是否相等,如相等,则说明可以获取自旋锁,如不相等,则说明锁已被占用,需要等待锁被释放。获取锁时next加1,释放锁时owner加1,锁被占用时,next大于owner,最先等待自旋锁的CPU其next字段的值越接近owner字段,因此也越容易获取自旋锁,保证了自旋锁的公平竞争,等待的越久越容易获取自旋锁。
(7)nextowner不相等,则需要循环读取owner的值,并与next进行比较,判断是否能获取自旋锁

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

        prefetchw(&lock->slock);  // 数据预取,加快访问速度
        __asm__ __volatile__(     // 告诉编译器后续为汇编代码,不要优化
        @ 指令部开始
        "1:	ldrex	%0, [%3]\n"  @ 将slock的值加载到变量lockval中
        "	add	%1, %0, %4\n"  @ 将lockval中的next字段加1并将结果保存到newval中,此时lockval的值没变,和slock相等
        "	strex	%2, %1, [%3]\n"  @ 将newval的值更新到slock中,更新结果保存在tmp中
        "	teq	%2, #0\n"    @ 判断tmp的值是否为0
        "	bne	1b"          @ 不为0,表示更新失败,则跳转到标号1处继续执行
        @ 输出部开始
        : "=&r" (lockval), "=&r" (newval), "=&r" (tmp)  @ 输出部 %0(lockval) %1(newval) %2(tmp)
        @ 输入部开始
        : "r" (&lock->slock), "I" (1 << TICKET_SHIFT)   @ 输入部 "I"表示常数,1左移16位对应next字段
        @ 损坏部开始
        : "cc");    @ 损坏部
        // 判断next和owner是否相等,相等则跳过while循环,表示获取锁成功,开始访问临界区
        while (lockval.tickets.next != lockval.tickets.owner) {
            // 不相等则执行wfe指令,此时CPU处于挂起状态,等待事件发生。wfe是arm架构特有指令,目的是降低功耗。
            wfe();
            // 事件发生后被唤醒,获取owner的值,继续执行while循环判断next和owner是否相等
            lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
        }
        /* smp_mb是SMP系统中的内存屏障,用来同步多个CPU,防止CPU把smp_mb附近的指令重新排序,进行乱序执行。确保执行完smp_mb()之前的所有指令,才能访问临界区 */
        smp_mb();
    }

接着分析一下spin_unlock的实现,spin_unlock实现较简单,底层调用和体系结构相关的函数arch_spin_unlock来释放锁。

spin_unlock
  raw_spin_unlock      // 宏定义,会被替换为_raw_spin_unlock
  ->_raw_spin_unlock     // 定义在spinlock.c中的函数
    ->__raw_spin_unlock
      ->spin_release     // 和自旋锁检查相关的函数,可忽略
      ->do_raw_spin_unlock
        ->arch_spin_unlock  // 最底层的执行函数
      preempt_enable      // 使能内核抢占

arch_spin_unlock函数最核心的语句是将owner加1,owner加1表示释放锁。这里没有使用独占性访问指令,是因为任何时刻只有一个CPU获取锁,也即只有一个CPU释放锁,不会产生竞态现象。
dsb_sev()执行了两条和arm架构相关的汇编指令,分别为dsbsevdsb为数据同步隔离指令,表示在这之前的存储器访问操作必须执行完成后才能执行后面的指令。执行sev指令后会给所有CPU发送信号,唤醒执行wfe指令后处于等待事件发生状态的CPU。

    static inline void arch_spin_unlock(arch_spinlock_t *lock)
    {
        smp_mb();    // SMP系统中的内存屏障
        lock->tickets.owner++;
        dsb_sev();   // 执行dsb和sev指令,这两条指令属于arm架构指令
    }

从获取锁和释放锁的过程,可以总结出一下几点:
(1)SMP系统中,自旋锁中的32位数据成员slock被分为两个16位的变量nextowner。获取锁时next字段加1,释放锁时owner加1,这两个变量保证了自旋锁的公平竞争。
(2)获取自旋锁时要更新next,采用arm架构特有的指令实现了独占性访问,确保同一时刻只有一个CPU更新next成功。
(3)获取自旋锁前,禁止了内核抢占,释放自旋锁后开启了内核抢占。

2.2.2.UP系统中的实现

UP系统中实现很简单,获取自旋锁的时候禁止内核抢占,释放自旋锁的时候开启内核抢占,并没有实现真正的自旋。

// 获取自旋锁
spin_lock
  raw_spin_lock    // 宏定义
  _raw_spin_lock   // 宏定义
  __LOCK           // 宏定义
  preempt_disable  // 禁止内核抢占
  ___LOCK
// 释放自旋锁
spin_unlock
  raw_spin_unlock
  _raw_spin_unlock
  __UNLOCK
  preempt_enable    // 开启内核抢占
  ___UNLOCK

2.4.定义和初始化自旋锁

// 定义自旋锁
spinlock_t lock;
// 初始化自旋锁
spin_lock_init(lock);  // 宏定义   

2.5.禁止中断的自旋锁API

前面分析过,当中断、中断下半部和线程存在同时访问临界区的可能时,需要使用禁止中断、中断下半部的自旋锁API保护临界区。

    void spin_lock_irq(spinlock_t *lock)    // 获取自旋锁同时禁止本地中断
    void spin_unlock_irq(spinlock_t *lock)  // 释放自旋锁同时开启本地中断
    spin_lock_irqsave(lock, flags)	        // 获取自旋锁同时禁止本地中断,并保存中断状态
    // 释放自旋锁同时开启本地中断,并恢复中断状态,使中断状态和获取自旋锁之前的中断状态保持一致
    void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
    void spin_lock_bh(spinlock_t *lock)     // 获取自旋锁同时禁止本地中断下半部分
    void spin_unlock_bh(spinlock_t *lock)   // 释放自旋锁同时开启本地中断下半部分

下面分析一下spin_lock_irqspin_unlock_irq的函数调用:

spin_lock_irq
  raw_spin_lock_irq    // 宏定义
  _raw_spin_lock_irq
    /*======UP系统中的调用========*/
    __LOCK_IRQ        
      local_irq_disable    // 禁止本地中断
      __LOCK
        preempt_disable    // 禁止内核抢占
        ___LOCK
    /*======SMP系统中的调用=======*/
    ->__raw_spin_lock_irq
      ->local_irq_disable   // 禁止本地中断
          raw_local_irq_disable
            arch_local_irq_disable
                // cpsid为arm(V6及以上)架构禁止中断的汇编指令
              	asm volatile("cpsid i @ arch_local_irq_disable"
                  : : : "memory", "cc");
      ->preempt_disable    // 禁止抢占
      ->spin_acquire
      ->LOCK_CONTENDED     // 获取自旋锁,和spin_lock的一致,不再分析

spin_unlock_irq
    raw_spin_unlock_irq
    _raw_spin_unlock_irq
    /*======UP系统中的调用========*/
      __UNLOCK_IRQ
        local_irq_enable
        __UNLOCK
          preempt_enable
          ___UNLOCK
    /*======SMP系统中的调用=======*/
      ->__raw_spin_unlock_irq
        ->spin_release
        ->do_raw_spin_unlock
          ->arch_spin_unlock  // 释放自旋锁
          ->__release
        ->local_irq_enable    // 开启中断
          raw_local_irq_enable
            arch_local_irq_enable
                // cpsie为arm(V6及以上)架构开启中断的汇编指令
              	asm volatile("cpsie i @ arch_local_irq_disable"
                  : : : "memory", "cc");                
        ->preempt_enable      // 开启内核抢占
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值