深入分析Linux自旋锁【转】
转自:http://blog.chinaunix.net/uid-20543672-id-3252604.html
前言:
在复习休眠的过程中,我想验证自旋锁中不可休眠,所以编写了一个在自旋锁中休眠的模块。但是在我的ARMv7的单核CPU(TI的A8芯片)中测试的时候,不会锁死,并且自旋锁可以多次获取。实验现象和我对自旋锁和休眠的理解有出路。
在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的时间。
三、自旋锁需要做的工作
从保证临界区访问原子性的目的来考虑,自旋锁应该阻止在代码运行过程中出现的任何并发干扰。这些“干扰”包括:
、中断,包括硬件中断和软件中断 (仅在中断代码可能访问临界区时需要)
这种干扰存在于任何系统中,一个中断的到来导致了中断例程的执行,如果在中断例程中访问了临界区,原子性就被打破了。所以如果在某种中断例程中存在访问某个临界区的代码,那么就必须用spinlock保护。对于不同的中断类型(硬件中断和软件中断)对应于不同版本的自旋锁实现,其中包含了中断禁用和开启的代码。但是如果你保证没有中断代码会访问临界区,那么使用不带中断禁用的自旋锁API即可。
2、内核抢占(仅存在于可抢占内核中)
在2.6以后的内核中,支持内核抢占,并且是可配置的。这使UP系统和SMP类似,会出现内核态下的并发。这种情况下进入临界区就需要避免因抢占造成的并发,所以解决的方法就是在加锁时禁用抢占(preempt_disable(); ),在开锁时开启抢占(preempt_enable();注意此时会执行一次抢占调度) 。
、 其他处理器对同一临界区的访问 (仅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、在有禁用中断的版本中,既然已经禁用了中断,在本处理器上就不会被打断,禁用抢占是否多余?
- (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)
这些情况描诉似乎有点简单,我在网上找到了一篇使用规则((转)自旋锁(spinlock)
解释得经典,透彻),非常详细。我稍作修改,转载如下:
- 获得自旋锁和释放自旋锁有好几个版本,因此让读者知道在什么样的情况下使用什么版本的获得和释放锁的宏是非常必要的。如果被保护的共享资源只在进程上下文访问和软中断(包括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平台上的实现
4.2.12. LDREX 和 STREX
深入分析Linux自旋锁【转】的更多相关文章
- 深入分析Linux自旋锁
原创 2016-08-12 tekkamanninja CU技术社区 作者| tekkamanninja本文版权由tekkamanninja所有,如需转载,请联系本公众号获取授权!在复习休眠的过程 ...
- linux 自旋锁
一.概述: 自旋锁是SMP架构中的一种low-level的同步机制.当线程A想要获取一把自旋锁而该锁又被其它线程锁持有时,线程A会在一个循环中自旋以检测锁是不是已经可用了.对于自选锁需要注意: 由于自 ...
- linux自旋锁、互斥锁、信号量
为了避免并发,防止竞争.内核提供了一组同步方法来提供对共享数据的保护. 我们的重点不是介绍这些方法的详细用法,而是强调为什么使用这些方法和它们之间的差别. Linux 使用的同步机制可以说从2.0到2 ...
- linux 自旋锁和信号量【转】
转自:http://blog.csdn.net/xu_guo/article/details/6072823 版权声明:本文为博主原创文章,未经博主允许不得转载. 自旋锁最多只能被一个可执行线程持有( ...
- (linux)自旋锁及其衍生锁
自旋锁 毫秒以下. 自旋锁用于多个CPU系统中,在单处理器系统中,自旋锁不起锁的作用,只是禁止或启用内核抢占.在自旋锁忙等待期间,内核抢占机制还是有效的,等待自旋锁释放的线程可能被更高优先级的线程 ...
- linux 自旋锁 API 简介
自旋锁原语要求的包含文件是 <linux/spinlock.h>. 一个实际的锁有类型 spinlock_t. 象任何其他数据结构, 一个 自旋锁必须初始化. 这个初始化可以在编译时完成, ...
- linux自旋锁函数
我们已经看到 2 个函数, spin_lock 和 spin_unlock, 可以操作自旋锁. 有其他几个函 数, 然而, 有类似的名子和用途. 我们现在会展示全套. 这个讨论将带我们到一个我们无 法 ...
- Linux 自旋锁,互斥量(互斥锁),读写锁
自旋锁(Spin Lock) 自旋锁类似于互斥量,不过自旋锁不是通过休眠阻塞进程,而是在取得锁之前一直处于忙等待的阻塞状态.这个忙等的阻塞状态,也叫做自旋. 自旋锁通常作为底层原语实现其他类型的锁. ...
- linux自旋锁
一.前言 在linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机 ...
随机推荐
- 自学Aruba7.4-Aruba安全认证-MAC认证(web页面配置)
点击返回:自学Aruba之路 自学Aruba7.4-Aruba安全认证-MAC认证(web页面配置) 由于前三节已经讲述了3种如何web页面配置安全认证,MAC认证就不过多讲解重复的步骤. 步骤1 ...
- emwin之CHECKBOX控件的通知代码的响应规则
@2018-08-28 [小记] 在 case WM_INIT_DIALOG: 中使用 CHECKBOX_SetState()函数改变了复选框状态,就会产生 WM_NOTIFICATION_VALUE ...
- 【转】IAR for STM8介绍、下载、安装与注册
Ⅰ.写在前面 本文讲述的内容是IAR for STM8(EWSTM8)的介绍.下载.安装与注册,其安装.注册过程和IAR for ARM类似,如果需要了解IAR for ARM相关的文章,可以到我博客 ...
- python测试webservice接口
1.下载库:https://pypi.python.org/pypi/suds-jurko 2.解压后,进入到解压目录,安装库:python3 setup.py install 3.测试获取手机归属地 ...
- luogu1345 奶牛的电信 (最小割)
虽然割点不好搞,但是可以变成割边呀 拆点,拆出来的边权给1,原图中的边权给inf,然后跑dinic就行了 #include<bits/stdc++.h> #define pa pair&l ...
- FFMEPG -- A ffmpeg and SDL Tutorial : tutorial05
修改了同步播放ffmpeg问题.并且增加可以放大视频. // tutorial05.c // A pedagogical video player that really works! // // C ...
- 洛谷P2680 运输计划
大概就是二分+树上差分... 题意:给你树上m条路径,你要把一条边权变为0,使最长的路径最短. 最大的最小,看出二分(事实上我并没有看出来...) 然后二分k,对于所有大于k的边,树上差分求出最长公共 ...
- pthread_cleanup_push()/pthread_cleanup_pop()的详解
一般来说,Posix的线程终止有两种情况:正常终止和非正常终止.线程主动调用pthread_exit()或者从线程函数中return都将使线程正常退出,这是可预见的退出方式:非正常终止是线程在其他线程 ...
- SuperArray
package com.lovo.array; public class SuperIntArray { //属性 public int[] array; private int index;//代表 ...
- MySQL邮件使用情况统计方法
邮件使用情况统计方法如下: ) AS domain, COUNT(DISTINCT [column_name]) AS mail_count, COUNT([column_name]) AS mail ...