zoukankan      html  css  js  c++  java
  • Linux中的spinlock机制[四]

    转自:https://zhuanlan.zhihu.com/p/90634198

    Linux中的spinlock机制[四] - API的使用

    Linux中的spinlock机制[四] - API的使用

     

    前面文章介绍的spinlock加锁的实现都是基于的arch_spin_lock()这个函数,但内核编程实际使用的通常是spin_lock(),它们中间还隔了好几层调用关系。先来看最外面的一层(代码位于/include/linux/spinlock.h):

    static __always_inline void spin_lock(spinlock_t *lock)
    {
        raw_spin_lock(&lock->rlock);
    }
    
    #define raw_spin_lock(lock) _raw_spin_lock(lock)
    

    接下来,_raw_spin_lock()的实现将出现分野。

    【关闭调度】

    • SMP实现

    spinlock通常是用于多核系统(SMP)的,但它同样也可以用于单核(UP)的场景。针对SMP,_raw_spin_lock()的实现是这样的(定义在/include/linux/spinlock_api_smp.h):

    #ifdef CONFIG_INLINE_SPIN_LOCK
    #define _raw_spin_lock(lock) __raw_spin_lock(lock)
    #endif
    
    static inline void __raw_spin_lock(raw_spinlock_t *lock)
    {
        preempt_disable();
        ...
        LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
    }
    

    采用inline函数,可以减少函数调用的开销,提高执行速度,但不利于跟踪调试,所以内核提供了"CONFIG_INLINE_SPIN_LOCK"这个配置选项供用户选择。

    越往内层,函数名前面的下划线"_"越多。可以看到,在最内侧的__raw_spin_lock()中,调用了preempt_disable()来关闭调度。也就是说,运行在一个CPU上的代码使用spin_lock()试图加锁之后,基于该CPU的线程调度和抢占就被禁止了,这也体现了spinlock作为"busy loop"形式的锁的语义。

    到了do_raw_spin_lock()这一步,就进入了和架构相关的arch_spin_lock()。

    static inline void do_raw_spin_lock(raw_spinlock_t *lock)
    {
        __acquire(lock);
        arch_spin_lock(&lock->raw_lock);
        ...
    }
    
    • UP实现

    再来看下_raw_spin_lock()针对UP系统的实现(代码位于/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)
    

    在UP的环境中,不再需要防止多个CPU对共享变量的同时访问,所以spin_lock()的作用仅仅是关闭调度,等同于(或者说退化成了)preempt_disable()。

    之所以UP系统也支持使用spinlock相关的函数,是因为这样同一套代码可以同时支持UP和SMP的应用,只需要在配置中选择是否使用"CONFIG_SMP"就可以了。

    【关闭中断】

    spin_lock()可以防止线程调度,但不能防止硬件中断的到来,以及随后的中断处理函数(hardirq)的执行,这会带来什么影响呢?

    试想一下,假设一个CPU上的线程T持有了一个spinlock,发生中断后,该CPU转而执行对应的hardirq。如果该hardirq也试图去持有这个spinlock,那么将无法获取成功,导致hardirq无法退出。在hardirq主动退出之前,线程T是无法继续执行以释放spinlock的,最终将导致该CPU上的代码不能继续向前运行,形成死锁(dead lock)。

    为了防止这种情况的发生,我们需要使用spin_lock_irq()函数,一个spin_lock()和local_irq_disable()的结合体,它可以在spinlock加锁的同时关闭中断。

    因为中断关闭的操作是可以嵌套的,更多的时候我们是使用local_irq_save()来记录关中断的状态,对应地一个更常用的函数就是spin_lock_irqsave()

    static inline unsigned long __raw_spin_lock_irq_save(raw_spinlock_t *lock)
    {
        unsigned long flags;
    	
        local_irq_save(flags);
        __raw_spin_lock(lock);
        return flags;
    }
    

    然而,local_irq_save()只能对本地CPU执行关中断操作,所以即便使用了spin_lock_irqsave(),如果其他CPU上发生了中断,那么这些CPU上的hardirq,也有可能试图去获取一个被本地CPU上运行的线程T占有的spinlock。

    不过没有关系,因为此时hardirq和线程T运行在不同的CPU上,等到线程T继续运行释放了这个spinlock,hardirq就有机会获取到,不至于造成死锁。

    对于UP系统,spin_lock_irqsave()的作用只剩下关闭中断了(中断关闭时不会产生时钟中断,调度自然也是关闭的),也就退化成了local_irq_save()。

    【屏蔽softirq】

    如果hardirq不会和线程共享变量,是不是就可以直接使用spin_lock()呢?非也,因为在切回被打断的线程之前,还可能会执行对应的softirq函数。如果该softirq可能访问和线程共享的变量,那么线程就应该使用spin_lock_bh(),一个spin_lock()加local_bh_disable()的二合一函数,否则也可能会导致dead lock。

    "bh"代表bottom half,而Linux中的bottom half包括softirq, taskletworkqueue三种,由于workqueue是运行在进程上下文,所以这里的"bh"只针对softirq和tasklet。

    【API的选择】

    如果关闭了中断,hardirq不会执行,对应的softirq就更不会执行,可见,使用spin_lock_irqsave()无疑是最安全的,但同时也是开销最大的。

    从程序性能的角度出发,在进程上下文中,对于不会和hardirq/softirq共享的变量,应该尽量使用更轻量级的spin_lock()。只会和softirq共享而不会和hardirq共享的,则应该使用spin_lock_bh()。

    对于hardirq上下文,因为Linux是不支持hardirq嵌套的(参考这篇文章评论区的讨论),在hardirq执行期间,CPU对中断的响应默认是关闭的,所以可直接使用spin_lock()。

    至于softirq上下文,因为有可能被hardirq打断,针对会和hardirq共享的变量,需使用spin_lock_irqsave()。

    总之,在用一个锁之前,你得清楚有可能和你竞争这个锁的对手是谁。

    至此,就可以解答上文留下的那个问题,即为什么一个CPU在一种context下,至多试图获取(或者说竞争)一个spinlock。线程使用spin_lock()试图获取spinlock A,此时发生了中断,如果hardirq获取spinlock B,那么该CPU就同时在试图获取2个spinlock。

    如果hardirq没有试图获取spinlock,执行完后进入了softirq,softirq试图获取spinlock B,然后又被另一个中断打断,新的hardirq在执行过程中又试图获取spinlock C,那么该CPU就同时在试图获取3个spinlock。

    如果再加入nmi,以此类推,一个CPU至多同时试图获取4个spinlock。

    spin_lock_irqsave()/spin_lock_bh()可以防止hardirq/softirq和线程共享变量造成的死锁,但这只是死锁可能出现的一种情况,也可以说是仅依靠选择合适的API就可以避免的死锁,更多死锁的场景和应对办法,将交由下文讨论。

     

    参考:

    字节岛 - 自旋锁spin_lock、spin_lock_irq以及spin_lock_irqsave的区别

     

    原创文章,转载请注明出处。

    【作者】张昺华
    【大饼教你学系列】https://edu.csdn.net/course/detail/10393
    【新浪微博】 张昺华--sky
    【twitter】 @sky2030_
    【微信公众号】 张昺华
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.
  • 相关阅读:
    函数式编程,高阶函数,纯函数,函数柯里化
    JS防抖与节流
    闭包使用场景
    Promise 内部实现
    视图组件
    认证 权限 频率控制 组件
    序列化组件
    Django之中间件及Auth认证模块
    Cookie,Session
    forms组件
  • 原文地址:https://www.cnblogs.com/sky-heaven/p/13602357.html
Copyright © 2011-2022 走看看