前言:
在复习休眠的过程中,我想验证自旋锁中不可休眠,所以编写了一个在自旋锁中休眠的模块。但是在我的ARMv7的单核CPU(TI的A8芯片)中测试的时候,不会锁死,并且自旋锁可以多次获取。实验现象和我对自旋锁和休眠的理解有出路。

我后来我将这个模块放到自己的PC上测试,成功锁死了,说明我的模块原理上没有问题。但是为什么在ARM上会这样呢???后来我将模块给了我的两个同事测试,在Omap3530中一样不会锁死,但是在S3C6410中成功的锁死了。这是怎么回事??

我觉得应该是内核配置的问题,便让同事将他的6410的内核配置给我对比一下,发现对于配置上的不同:6410在spinlock上不过就是多了CONFIG_DEBUG_SPINLOCK的自旋锁调试功能。于是我将自己板子的内核也加了这个配置,并让同事Omap3530的内核也加了这个配置进行测试,结果正常了:锁死!!一个调试选项怎么会影响到自旋锁的基本功能?这说明我对自旋锁的理解不正确。这种时候RTFSC就是最好的解决办法。

我通过阅读内核的自旋锁源码发现:如果内核配置为SMP系统,自旋锁就按SMP系统上的要求来实现真正的自旋等待,但是对于UP系统,自旋锁仅做抢占和中断操作,没有实现真正的“自旋”。如果配置了CONFIG_DEBUG_SPINLOCK,那么自旋锁按照SMP系统来编译。

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


  • 自旋锁spinlock的由来

众所周知,自旋锁最初就是为了SMP系统设计的,实现在多处理器情况下保护临界区。所以在SMP系统中,自旋锁的实现是完整的本来面目。但是对于UP系统,自旋锁可以说是SMP版本的阉割版。因为只有在SMP系统中的自旋锁才需要真正“自旋”。

  • 自旋锁的目的

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

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

  • 自旋锁需要做的工作

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

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构架指令集开始(V6、V7),采用LDREX和STREX指令实现真正的自旋等待。

  • 自旋锁操作组成 
根据上的介绍,我们很容易知道自旋锁的组成:
中断控制(仅在中断代码可能访问临界区时需要)
抢占控制(仅存在于可抢占内核中需要)
自旋锁标志控制  (仅SMP系统需要)
中断控制是按代码访问临界区的不同而在编程时选用不同的变体,有些API中有,有些没有。而抢占控制和自旋锁标志控制依据内核配置(是否支持内核抢占)和硬件平台(是否为SMP)的不同而在编译时确定。如果不需要,相应的控制代码就编译为空函数。 

对于非抢占式内核,由自旋锁所保护的每个临界区都有禁止内核抢占的API,但是为空操作。

由于UP系统不存在物理上的并行,所以可以阉割掉自旋的部分,剩下抢占和中断操作部分即可。

到这里其实就可以解释为什么我开始的实验现象和预想的完全不同了:
由于UP系统(在不配置CONFIG_DEBUG_SPINLOCK的情况下),根本就没有自旋锁控制的部分,多次获得自旋锁是可能的(这种编程本来就是错误的,只是我想看错误的现象而已)。对于其中的一点疑惑:
在有禁用中断的版本中,既然已经禁用了中断,在本处理器上就不会被打断,禁用抢占是否多余?
     1、禁用了中断可以避免因为中断引起的抢占调度,但是如果在自旋锁保护的临界区中存在 preempt_disable()
               和 preempt_enable()对。这样在preempt_enable();就会引发抢占调度。
     2、避免SMP系统中别的处理器执行调度程序使得本处理器的进程会被调度出去。?????
              对于这个问题我不是很确定,还有深入研究调度系统后才会有准确的答案。
  • 自旋锁变体的使用规则

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

不会在任何中断例程中操作临界区:

static inline void spin_lock(spinlock_t *lock)
static inline void spin_unlock(spinlock_t *lock)

如果在软件中断中操作临界区: 
static inline void spin_lock_bh(spinlock_t *lock)

static inline void spin_unlock_bh(spinlock_t *lock)
bh代表bottom half,也就是中断中的底半部,因内核中断的底半部一般通过软件中断(tasklet等)来处理而得名。

如果在硬件中断中操作临界区:
static inline void spin_lock_irq(spinlock_t *lock)
static inline void spin_unlock_irq(spinlock_t *lock)

如果在控制硬件中断的时候需要同时保存中断状态:
spin_lock_irqsave(lock, flags)
static inline void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)

这些情况描诉似乎有点简单,我在网上找到了一篇使用规则,非常详细。我稍作修改,转载如下:

获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。

如果被保护的共享资源只在进程上下文访问和软中断(包括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上运行。

如果被保护的共享资源在软中断(包括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

spinlock与linux内核调度的关系

深入分析Linux自旋锁相关推荐

  1. LINUX自旋锁详解2

    对于互斥, 旗标是一个有用的工具, 但是它们不是内核提供的唯一这样的工具. 相反, 大部分加锁是由一种称为自旋锁的机制来实现. 不象旗标, 自旋锁可用在不能睡眠的代码中, 例如中断处理. 当正确地使用 ...

  2. LINUX自旋锁详解

    加锁(locking)是一种广泛应用的同步技术.当内核控制路径必须访问共享数据结构或进入临界区时,就需要为自己获取一把"锁".由锁机制保护的资源非常类似于限制于房间内的资源,当某人 ...

  3. SMP并行编程为什么这么难-从Linux自旋锁到图灵机

    周末,聊点轻松的话题. 本文通过一个不甚成功的优化自旋锁的半吊子尝试,聊一下操作系统并行的难点,中间的部分,说说图灵机和冯诺伊曼体系. 和往常一样,我的文章对概念的阐述非常少,更多的是一些需要思考的东 ...

  4. 深入理解Linux自旋锁(1.0)

    学习方法论 写作原则 标题括号中的数字代表完成度与完善度 0.0-1.0 代表完成度,1.1-1.5 代表完善度 0.0 :还没开始写 0.1 :写了一个简介 0.3 :写了一小部分内容 0.5 :写 ...

  5. Linux 自旋锁 使用

    自旋锁的作用 解决并发访问临界资源时,竞争资源问题.如有两个线程A,B同时访问一块内存区域,就可以用一个自旋锁来对这块内存区域进行保护,确保同一时间点只有一个线程访问内存.线程在访问内存前都要先获取锁 ...

  6. spin_lock 自旋锁

    自旋锁 Spin lock 的实现是为了保护一段短小的临界区操作代码,保证这个临界区的操作是原子的,从而避免并发的竞争冒险.在Linux内核中,自旋锁通常用于包含内核数据结构的操作,你可以看到在许多内 ...

  7. Linux内核自旋锁使用笔记

    Reference: spin_lock_bh()与spin_unlock_bh() Linux内核自旋锁 Linux自旋锁 Spinlock - Wikipedia, the free encycl ...

  8. linux内核锁机制学习

    在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问.尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上 ...

  9. linux并发控制之自旋锁

    自旋锁是一种对临界资源进行互斥访问的典型手段,其名来源于它的工作方式. 通俗的讲,自旋锁就是一个变量,该变量把一个临界区标记为"我当前在运行,请等待"或者标记为"我当前不 ...

  10. linux内核 task_struct 中自旋锁的应用

    自旋锁是用于保护短的代码片段,其中只包含少量C语句,因此会很快执行完毕.大多数内核数据结构都有自身的自旋锁,在处理结构中的关键成员时,必须获得相应的自旋锁. 定义自旋锁 struct task_str ...

最新文章

  1. 从头认识Spring-2.3 注解装配-@autowired(5)-限定器@Qualifier(1)
  2. python 作用域 前缀_Python面试题:杂项内容
  3. 数据结构学习笔记4.1--查找节点
  4. 数学学习--最小二乘法案例剖析
  5. oem718d 基准站设置_RTK电台、网络模式作业设置流程
  6. 用python画玫瑰花脚本-python画一个玫瑰和一个爱心
  7. Android 开发之ViewPage官方文档学习笔记
  8. Android 仿QQ消息界面
  9. flink Sql查询异常NoResourceAvailableException: Could not acquire the minimum required resources
  10. SAP系统的模块分类
  11. window安装python27和配置环境变量
  12. H5手写签名,适用于手机网页、电脑网页(IE9+)
  13. spring security自定义登录失败返回错误信息
  14. 腾讯云Ubuntu18.04.4上安装brat文本标注工具(2020年9月)
  15. java设计模式 之适配器模式
  16. Fortify扫描漏洞解决方案
  17. 塑料壳上下扣合的卡扣设计_一种组合玩具的塑料卡扣的制作方法
  18. matlab中的covsfa什么意思,MALSAR1.1 多种多任务学习算法的工具箱 有代码有使 册 matlab版 238万源代码下载- www.pudn.com...
  19. a全球及中国电子手环行业销售现状及竞争趋势预测报告2022-2027年
  20. Spacy en_core_web_sm

热门文章

  1. 用maven编译spark2.1.0
  2. python爬虫:爬取医药数据库drugbank
  3. hdu 3065 AC自动机模版题
  4. php’s explode() 函数
  5. python的条件(三元)运算符——实现b? x: y
  6. 利用 Google API 调用谷歌地图 演示1
  7. Java并发容器,底层原理深入分析
  8. WEB前端性能优化及应用服务器性能优化和存储性能优化
  9. 如何查看mysql 的端口号?
  10. css 的块级元素和行内元素