linux内核--自旋锁的理解

linux内核--自旋锁的理解

http://blog.chinaunix.net/uid-20543672-id-3252604.html
css

自旋锁:若是内核配置为SMP系统,自旋锁就按SMP系统上的要求来实现真正的自旋等待,可是对于UP系统,自旋锁仅作抢占和中断操做,没有实现真正的“自旋”。若是配置了CONFIG_DEBUG_SPINLOCK,那么自旋锁按照SMP系统来编译。html

    可是为何在UP系统中不须要真正的“带有自旋的”自旋锁呢?其实在理解了自旋锁的概念和由来,这个问题就迎刃而解了。因此我从新查找了关于自旋锁的资料,认真研究了自旋锁的实现和相关内容。linux


  • 1、自旋锁spinlock的由来程序员

   众所周知,自旋锁最初就是为了SMP系统设计的,实如今多处理器状况下保护临界区。因此SMP系统中,自旋锁的实现是完整的原本面目。可是对于UP系统,自旋锁能够说是SMP版本的阉割版。由于只有SMP系统中的自旋锁才须要真正“自旋”。编程

  • 2、自旋锁的目的缓存

    自旋锁的实现是为了保护一段短小的临界区操做代码,保证这个临界区的操做是原子的,从而避免并发的竞争冒险。在Linux内核中,自旋锁一般用于包含内核数据结构的操做,你能够看到在许多内核数据结构中都嵌入有spinlock,这些大部分就是用于保证它自身被操做的原子性,在操做这样的结构体时都经历这样的过程:上锁-操做-解锁。数据结构

      若是内核控制路径发现自旋锁“开着”(能够获取),就获取锁并继续本身的执行。相反,若是内核控制路径发现锁由运行在另外一个CPU上的内核控制路径“锁着”,就在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被释放。 自旋锁是循环检测“忙等”,即等待时内核无事可作(除了浪费时间),进程在CPU上保持运行,因此它保护的临界区必须小,且操做过程必须短。不过,自旋锁一般很是方便,由于不少内核资源只锁1毫秒的时间片断,因此等待自旋锁的释放不会消耗太多CPU的时间。并发

  • 3、自旋锁须要作的工做jsp

     从保证临界区访问原子性的目的来考虑,自旋锁应该阻止在代码运行过程当中出现的任何并发干扰。这些“干扰”包括:函数

     1、中断,包括硬件中断和软件中断(仅在中断代码可能访问临界区时须要)

       这种干扰存在于任何系统中,一个中断的到来致使了中断例程的执行,若是在中断例程中访问了临界区,原子性就被打破了。因此若是在某种中断例程中存在访问某个临界区的代码,那么就必须用spinlock保护。对于不一样的中断类型(硬件中断和软件中断)对应于不一样版本的自旋锁实现,其中包含了中断禁用和开启的代码。可是若是你保证没有中断代码会访问临界区,那么使用不带中断禁用的自旋锁API便可。 

    2、内核抢占(仅存在于可抢占内核中)

       在2.6之后的内核中,支持内核抢占,而且是可配置的。这使UP系统和SMP相似,会出现内核态下的并发。这种状况下进入临界区就须要避免因抢占形成的并发,因此解决的方法就是在加锁时禁用抢占(preempt_disable(); ),在开锁时开启抢占(preempt_enable();注意此时会执行一次抢占调度)。 

    3、其余处理器对同一临界区的访问(仅SMP系统) 

      SMP系统中,多个物理处理器同时工做,致使可能有多个进程物理上的并发。这样就须要在内存加一个标志,每一个须要进入临界区的代码都必须检查这个标志,看是否有进程已经在这个临界区中。这种状况下检查标志的代码也必须保证原子和快速,这就要求必须精细地实现,正常状况下每一个构架都有本身的汇编实现方案,保证检查的原子性。

      

有些人会觉得自旋锁的自旋检测能够用for实现,这种想法“Too young, too simple, sometimes naive”!你能够在理论上用C去解释,可是若是用for,起码会有以下两个问题:

(1)你如何保证在SMP下其余处理器不会同时访问同一个的标志呢?(也就是标志的独占访问)

(2)必须保证每一个处理器都不会去读取高速缓存而是真正的内存中的标志(能够实现,编程上能够用volitale

       要根本解决这个问题,须要在芯片底层实现物理上的内存地址独占访问,而且在实现上使用特殊的汇编指令访问。请看参考资料中对于自旋锁的实现分析。以arm为例,从存在SMP的ARM构架指令集开始(V六、V7),采用LDREX和STREX指令实现真正的自旋等待。

  •  
    4、自旋锁操做组成

     根据上的介绍,咱们很容易知道自旋锁的组成:

  • 中断控制(仅在中断代码可能访问临界区时须要)
  • 抢占控制(仅存在于可抢占内核中须要)
  • 自旋锁标志控制  (仅SMP系统须要)

    中断控制是按代码访问临界区的不一样而在编程时选用不一样的变体,有些API中有,有些没有。

    而抢占控制和自旋锁标志控制依据内核配置(是否支持内核抢占)和硬件平台(是否为SMP)的不一样而在编译时肯定。若是不须要,相应的控制代码就编译为空函数。 对于非抢占式内核,由自旋锁所保护的每一个临界区都有禁止内核抢占的API,可是为空操做。因为UP系统不存在物理上的并行,因此能够阉割掉自旋的部分,剩下抢占和中断操做部分便可。 

 

  1.    到这里其实就能够解释为何我开始的实验现象和预想的彻底不一样了:
  2.    因为UP系统(在不配置CONFIG_DEBUG_SPINLOCK的状况下),根本就没有自旋锁控制的部分,屡次得到自旋锁是可能的(这种编程原本就是错误的,只是我想看错误的现象而已)。



  1. 对于其中的一点疑惑:
  2.  
  3. 一、在有禁用中断的版本中,既然已经禁用了中断,在本处理器上就不会被打断,禁用抢占是否多余?
  4.  
  5. (1)禁用了中断能够避免由于中断引发的抢占调度,可是若是在自旋锁保护的临界区中存在 preempt_disable();和 preempt_enable();对。这样在preempt_enable();就会引起抢占调度。
  6.  
  7. (2)避免SMP系统中别的处理器执行调度程序使得本处理器的进程会被调度出去。?????
  8.  
  9. 对于这个问题我不是很肯定,还有深刻研究调度系统后才会有准确的答案。

 

  • 5、自旋锁变体的使用规则

      不管是抢占式UP、非抢占式UP仍是SMP系统,只要在某类中断代码可能访问临界区,就须要控制中断,保证操做的原子性。因此这个和模块代码中临界区的访问还有关系,是否可能在中断中操做临界区,只有程序员才知道。因此自旋锁API中有针对不一样中断类型的自旋锁变体:

 

  1. 不会在任何中断例程中操做临界区:
  2.  
  3. static inline void spin_lock(spinlock_t*lock)
  4.  
  5.  static inline void spin_unlock(spinlock_t*lock)
  6.  
  7. 若是在软件中断中操做临界区:
  8.  
  9.  static inline void spin_lock_bh(spinlock_t*lock)
  10.  
  11.  static inline void spin_unlock_bh(spinlock_t*lock)
  12.  
  13. bh表明bottom half,也就是中断中的底半部,因内核中断的底半部通常经过软件中断(tasklet等)来处理而得名。
  14.  
  15. 若是在硬件中断中操做临界区:
  16.  
  17. static inline void spin_lock_irq(spinlock_t*lock)
  18.  
  19. static inline void spin_unlock_irq(spinlock_t*lock)
  20.  
  21. 若是在控制硬件中断的时候须要同时保存中断状态:
  22.  
  23. spin_lock_irqsave(lock, flags)
  24.  
  25. static inline void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags)

       这些状况描诉彷佛有点简单,我在网上找到了一篇使用规则((转)自旋锁(spinlock)解释得经典,透彻),很是详细。我稍做修改,转载以下:

  1.     得到自旋锁和释放自旋锁有好几个版本,所以让读者知道在什么样的状况下使用什么版本的得到和释放锁的宏是很是必要的。
      若是被保护的共享资源只在进程上下文访问和软中断(包括tasklet、timer)上下文访问,那么当在进程上下文访问共享资源时,可能被软中断打断,从而可能进入软中断上下文来对被保护的共享资源访问,所以对于这种状况,对共享资源的访问必须使用spin_lock_bh和spin_unlock_bh来保护。固然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也能够,它们失效了本地硬中断,失效硬中断隐式地也失效了软中断。可是使用spin_lock_bh和spin_unlock_bh是最恰当的,它比其余两个快。

      若是被保护的共享资源只在两个或多个tasklet或timer上下文访问,那么对共享资源的访问仅须要用spin_lock和spin_unlock来保护,没必要使用_bh版本,由于当tasklet或timer运行时,不可能有其余tasklet或timer在当前CPU上运行。
        若是被保护的共享资源只在一个tasklet或timer上下文访问,那么不须要任何自旋锁保护,由于同一个tasklet或timer只能在一个CPU上运行,即便是在SMP环境下也是如此。实际上tasklet在调用tasklet_schedule标记其须要被调度时已经把该tasklet绑定到当前CPU,所以同一个tasklet决不可能同时在其余CPU上运行。timer也是在其被使用add_timer添加到timer队列中时已经被帮定到当前CPU,因此同一个timer毫不可能运行在其余CPU上。固然同一个tasklet有两个实例同时运行在同一个CPU就更不可能了。
        若是被保护的共享资源只在一个软中断(tasklet和timer除外)上下文访问,那么这个共享资源须要用spin_lock和spin_unlock来保护,由于一样的软中断能够同时在不一样的CPU上运行。
        若是被保护的共享资源在两个或多个软中断上下文访问,那么这个共享资源固然更须要用spin_lock和spin_unlock来保护,不一样的软中断可以同时在不一样的CPU上运行。
        若是被保护的共享资源在软中断(包括tasklet和timer)或进程上下文和硬中断上下文访问,那么在软中断或进程上下文访问期间,可能被硬中断打断,从而进入硬中断上下文对共享资源进行访问,所以,在进程或软中断上下文须要使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。
        而在中断处理句柄中使用什么版本,需依状况而定,若是只有一个中断处理句柄访问该共享资源,那么在中断处理句柄中仅须要spin_lock和spin_unlock来保护对共享资源的访问就能够了。由于在执行中断处理句柄期间,不可能被同一CPU上的软中断或进程打断。
        可是若是有不一样的中断处理句柄访问该共享资源,那么须要在中断处理句柄中使用spin_lock_irq和spin_unlock_irq来保护对共享资源的访问。
        在使用spin_lock_irq和spin_unlock_irq的状况下,彻底能够用spin_lock_irqsave和spin_unlock_irqrestore取代,那具体应该使用哪个也须要依状况而定,若是能够确信在对共享资源访问前中断是使能的,那么使用spin_lock_irq更好一些。由于它比spin_lock_irqsave要快一些,可是若是你不能肯定是否中断使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,由于它将恢复访问共享资源前的中断标志而不是直接使能中断。
      固然,有些状况下须要在访问共享资源时必须中断失效,而访问完后必须中断使能,这样的情形使用spin_lock_irq和spin_unlock_irq最好。
        spin_lock用于阻止在不一样CPU上的执行单元对共享资源的同时访问以及不一样进程上下文互相抢占致使的对共享资源的非同步访问,而中断失效和软中断失效倒是为了阻止在同一CPU上软中断或中断对共享资源的非同步访问。
        以上是我对自旋锁的理解和使用上的总结,对与自旋锁的实现,其实网上已经有之类文章了,我不废话。因为自旋锁涉及到内核抢占,全部最好仍是学习如下抢占的相关知识。参考资料以下:

分析Linux中Spinlock在ARM及X86平台上的实现

ARM的SWP和LDREX STREX指令

4.2.12. LDREX 和 STREX

相关文章
相关标签/搜索