大话Linux内核中锁机制之原子操作、自旋锁【转】

转自:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html

多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实是由于操作系统中存在多进程对共享资源的并发访问,从而引起了进程间的竞态。这其中包括了我们所熟知的SMP系统,多核间的相互竞争资源,单CPU之间的相互竞争,中断和进程间的相互抢占等诸多问题。

通常情况下,如图1所示,对于一段程序,我们的理想是总是美好的,希望它能够这样执行:进程1先对临界区完成操作,然后进程2再去操作临界区。但是往往现实总是残酷的,进程1在执行过程中,进程2很可能在此插入一脚,导致两个进程同时对临界区进行读写访问,读是没有问题,但写的话问题就大了。这样的话,得到的结果往往不是我们想要的。

 

图1  一个简单的例子

因此,我们需要一些解决方法,在Linux内核中它提供了如下几种锁机制,供用户在针对不同情况分别或配合使用,包括:原子操作、自旋锁、内存屏障、读写自旋锁、顺序锁、信号量、读写信号量、完成量、RCU机制、BKL(大内核锁)等等,下面笔者将分五篇博文一一讨论这些锁机制。另外,本文所涉及的关于Linux内核源码采用版本为:Linux 3.3.1。OK,让我们首先讨论有关原子操作和自旋锁的相关内容吧。

一、原子操作

所谓的原子操作即是保证指令以原子的方式执行,它在执行过程中不被打断。它包括了原子整数操作和原子位操作,在内核中分别定义于include\linux\types.h和arch\x86\include\asm\bitops.h。通常了解一个东西,我们是先了解它怎么用的,因此,我们先来看看内核提供给用户的一些接口函数。对于整数原子操作函数,如下图1.1所示,下述有关加法的操作在内核中均有相应的减法操作。

 

图1.1      内核中的整数原子操作函数

如图1.2展示的是内核中提供的一些主要位原子操作函数。同时内核还提供了一组与上述操作对应的非原子位操作函数,名字前多两下划线。由于不保证原子性,因此速度可能执行更快。

 

图1.2      内核中的位原子操作函数

下面笔者展示一个关于原子操作的具体例子,请注意加粗部分的内容,它的作用是实现了设备只能被一个进程打开。配合注释的内容,应该不难理解,如图1.3所示。

 

图1.3      原子操作示例程序

下面给出笔者在讨论关于原子操作的认为的一些比较重要的内容:1、原子操作在不同体系架构实现的方法不同,基本采用汇编实现;2、上述的整数原子函数集仅针对32位,内核中关于64位有另一套函数。3、对于SMP系统,内核还提供了local_t数据类型,实现对单个CPU的整数原子操作,接口函数仅将atomic_替换成local_即可,具体的定义可参见arch/x86/include/asm/local.h中定义。

接下来看它的实现核心,如图1.4所示。鉴于篇幅的限制,中间关于SMP的汇编操作在这里省去,感兴趣的读者可参见具体的内核源码。

 

图1.4      原子操作的实现核心

可以看到对于SMP系统,它的实现核心是lock指令,而对于单CPU系统来说,则退化为空操作,因为对于单CPU来说,在某程序执行期间,不可能有其它CPU来中断它的执行,因此,实际上,非SMP系统中的原子操作是没有必要存在的。下面讨论SMP系统。讨论前,先了解x86中的lock指令。lock指令是一种前缀,它可与其他指令联合,用来维持总线的锁存信号直到与其联合的指令执行完为止。当CPU与其他处理机协同工作时,该指令可避免破坏有用信息。它对中断没有任何影响,因为中断只能在指令之间产生。lock前缀的真正作用是保持对系统总线的控制,直到整条指令执行完毕。

了解完lock指令的作用后,对于原子操作采用lock指令的原因就已经很明显。但需注意:lock指令只是针对自身CPU进行处理。lock指令在执行中占用CPU资源,从硬件上考虑,多核之间要负责相互通信,要让某个核的修改被其他核发现,因此lock指令的过多使用必然降低系统的性能。

至此,关于原子操作的内容基本上讨论到这里。总结一下,对于原子操作,它的优点就是简单,但它的缺点也很明了,即是只能作计数操作,保护的东西太少,从它所提供的接口函数即可看出。

二、自旋锁

接下来笔者将讨论关于自旋锁的内容。它的定义可表述如下:某个进程在试图加锁的时候,若当前锁已经处于“锁定”状态,试图加锁进程就进行不断的“旋转”,用一个死循环测试锁的状态,直到成功的获得锁。它在内核include\linux\spinlock_types.h中定义,核心的结构体及成员如图2.1所示。

图2.1      自旋锁核心结构体及成员

下面首先看下自旋锁提供了哪些函数,依次定义include\linux\spinlock.h文件中,部分函数如图2.2所示。

 

图2.2      自旋锁提供的部分接口函数

同样配合的看个例子,这个例子其实就是对device_count变量的保护,例子如图2.3所示,同样需注意加粗部分。仔细研究这个例子对于后续了解顺序锁有很大帮助,到时读者便会发现它其实是顺序锁的核心实现理念。

 

图2.3      自旋锁示例程序

上面关于自旋锁的例子应该不难理解,下面让我们深入自旋锁加解锁的核心源码,进一步来看下它到底是怎么实现的。首先,对于单CPU来说,它的机制实际上就是禁止和使能抢占,下图展示的是自旋锁加锁和解锁在内核中层层迭代的源码,特别注意加粗部分内容。深入琢磨下去,实际上这里牵扯到一个“引用计数器”的概念。它是内存管理的一个技巧,可以看做C/C++中的一种垃圾回收机制,具体的内容读者可以去了解,这里不再细说。

 

图2.4      自旋锁单CPU的加锁函数实现核心

以上展示的是一个内核加锁函数的源码实现的过程,实际上对于解锁也是这么一个过程。如图2.5所示。

 

图2.5      自旋锁针对单CPU的实现源码

总结来说,其实对于单CPU来说,其实就是很简单的内容,对于CPU存在内核抢占机制的,将禁止内核抢占,否则,退化为空操作。

对于SMP系统来说,它除了简单的禁止或使能本CPU的抢占机制外,还做了一些另外的操作。通过源码的搜索,我们可以发现它的实现核心其实是图2.6中所展示的两个函数,采用AT&T汇编实现。看的挺复杂,但实际上分析起来还是很简单的。

 

图2.6      自旋锁针对SMP系统的实现源码

在这里,我们可以看到它真正实现了对于多进程的之间某个进程“自旋的情况”,看源码前先记住几条指令:”xaddw”, ”cmpb”, “movb”, “incb”,其中xaddw表示先交换源操作数和目的操作数的数值,然后两个操作数再按字求和,最终结果保存在目的寄存器中;”cmpb”, “movb”, “incb”较为简单,后续的b(byte)后缀表示按字节执行这条指令。同时注意在Linux内核中,采用的AT&T汇编格式,指令操作数的顺序是先源后目的,而不是x86汇编中的先目的后源,如图2.6中的“xaddw”汇编指令则是%1所代表的寄存器为目的寄存器,即lock->slock变量。下面我们看下具体的执行过程,其中P1,P2表示系统中的两个不同的进程,如图2.7所描述。

 

图2.7      自旋锁内核的执行过程

到这里,读者应该明白了到底自旋锁是怎么实现自旋的。注意:对于“xaddw”它实际上完成了三条指令的事,为了防止被这个过程被打断,所以加了LOCK_PREFIX宏,在前面的原子操作我们也看到了LOCK_PREFIX宏实际上是针对lock指令的包装,当然是针对SMP系统。

当然,上述给出的源码是最大只支持256个处理器的情况,对于操作256个处理器的时候,内核中还有一套函数去处理,感兴趣的可以去研究下。可能分析完源码后有人会提出这个的疑问:如果P2和P3都在等待自旋锁,Linux系统如何保证能够正确的顺序执行呢?其实,这个在源码中已经体现出来了,实际上,考虑slcok的值,我们可以观察到它实际上已经保证了后续在等待自旋锁的进程的顺序执行性,比如上述分析过程中我们得到P2的slcok=0x0201,假如P1还未释放,P3又来申请自旋锁,这时候,内核经过计算得到P3的slcok=0x0301。进而继续分析源码,我们可知P3要想执行,必须得到P2执行完毕后(此时slcok=0x0302),方可有条件(slcok经过低8位加1后等于0x0303)申请到自旋锁,从而无形中保证了申请自旋锁进程的顺序执行性。由于slock只有高8位用于保证顺序性,所以这段源码最大只支持256个处理器同时申请自旋锁。

另外说到自旋锁,不得不提自旋锁和中断之间的关系,首先看一个双重请求的例子,假如某一进程在临界区正在执行,然而这时候,突然有一个中断来打断了它,于是,在临界区触发了中断处理程序,若中断处理程序里面也有包含申请自旋锁的操作,这将造成一个大问题,即所谓的双重请求的例子。如下图2.8所示。

 

图2.8      双重请求图例

当然内核当然考虑了此种情况,于是在自旋锁中就有了关于关闭中断的函数:spin_lock_irq()和spin_unlock_irq()。如图2.9代码所示的函数,但正如图中所提的那样,这两个函数的使用是有条件的,它要求中断在加锁前必须是激活的。假如现在有一个进程,它的中断本来就是关闭的,但是你通过这个锁的过程后中断变成开了,这不就又造成问题了吗。考虑此种情况,内核中提出了如图2.10所展示的自旋锁函数:spin_lock_irqsave()和spin_unlock_irqrestore()。可能读者在此会有些许疑惑,既然flags是作为spin_lock_irqsave()的输出参数,理论上应当要有”&”符号才对,这里却没有。事实上,spin_lock_irqsave()中flags不用“&”是因为这个函数是以宏形式定义的,一直嵌套,最终到arch\x86\include\asm\irqflags.h下,感兴趣的搜索到本源。一般来说,若是输出参数不带”&”符号的函数,几乎都是采用宏形式的定义的,这点需注意。

       图2.9  spin_lock_irq()的使用实例                            图2.10  spin_lock_irqsave()的使用实例

关于中断下半部的问题,还有几点需要说明。首先如果存在中断下半部和某个进程之间存在数据共享的问题,那就需要注意一下,因为中断下半部可抢占进程上下文,因此下半部和进程之间存在临界区时除了加锁,还需要禁止下半部执行。内核中提供的包括函数spin_lock_bh() 和spin_unlock_bh()即是实现禁止或使能下半部。同时关于自旋锁中的中断还有两点要说明:① 若中断处理程序与中断下半部共享数据,则对数据区加锁的同时也要禁止中断,因为中断也是可抢占中断下半部。② 若数据被软中断共享,也需要加锁,因为在不同处理器上存在软中断同时执行问题。

OK!上述讨论了自旋锁多方面的内容,下面是对自旋锁一番总结。首先从前面的实现机制上,读者可以看到自旋锁主要针对SMP系统,而且它们存在抢占情况。对于单CPU系统,自旋锁的实现则退化为空操作。其次自旋锁是忙等待,要求临界区执行时间短。再次自旋锁可能引发死锁,引发的情况有:自旋锁的递归调用(双重请求)或获得自旋锁后不释放,最终将导致系统不可用。最后一点则是若自旋锁在锁定期间调用可能引发睡眠的函数,如kmalloc()等,从此“一睡不醒”(因为这时候“无人”负责唤醒,主要原因是连中断都被关闭了,从此在无人唤醒,除非重启系统)。这一点需特别注意。

至此,关于自旋锁部分的内容到此讨论结束,让我们跳出来观全局,如图2.11所示。其实抽象的看自旋锁的实现机制还是挺简单的,而它提供的关于中断的一系列函数则为自旋锁提供了“安全带”的作用。

 

图2.11    跳出来观全局—自旋锁实现机制

出于文章篇幅的限制,本篇博文到此结束,后续将会给出《大话Linux内核中锁机制之内存屏障、读写自旋锁及顺序锁》,感兴趣的读者可继续阅读后一篇博文。由于笔者水平所限,博文中难免有出错之处,欢迎读者指出,大家相互讨论,共同进步。

转载请注明出处:http://blog.sina.com.cn/huangjiadong19880706

【作者】张昺华
【出处】http://www.cnblogs.com/sky-heaven/
【博客园】 http://www.cnblogs.com/sky-heaven/
【新浪博客】 http://blog.sina.com.cn/u/2049150530
【知乎】 http://www.zhihu.com/people/zhang-bing-hua
【我的作品---旋转倒立摆】 http://v.youku.com/v_show/id_XODM5NDAzNjQw.html?spm=a2hzp.8253869.0.0&from=y1.7-2
【我的作品---自平衡自动循迹车】 http://v.youku.com/v_show/id_XODM5MzYyNTIw.html?spm=a2hzp.8253869.0.0&from=y1.7-2
【新浪微博】 张昺华--sky
【twitter】 @sky2030_
【facebook】 张昺华 zhangbinghua
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/540978.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

hashmap put方法_Java HashMap put()方法与示例

hashmap put方法HashMap类的put()方法 (HashMap Class put() method) put() method is available in java.util package. put()方法在java.util包中可用。 put() method is used to link the given value element with the given key element in this HashMap. put()方法用于在…

java中jdom,java – JDOM中的命名空间(默认)

我正在尝试使用最新的JDOM包生成XML文档.我遇到了根元素和命名空间的问题.我需要生成这个根元素:xmlns"http://www.energystar.gov/manageBldgs/req"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://www.…

java enummap_Java EnumMap get()方法与示例

java enummapEnumMap类的get()方法 (EnumMap Class get() method) get() method is available in java.util package. get()方法在java.util包中可用。 get() method is used to get the value mapped with the given key element (key_ele) otherwise it returns null when no…

java后台json传递,后台json传递

json除了可以用于前台传递,还可用于后台之间传递。它可以传递List,Map,Bean等类型的数据。例如:User u1new User();u1.setUsername("zy");u1.setPassword("123");User u2new User();u2.setUsername("msl");u2.setPassword…

专家呼吁建安全漏洞信息共享机制并强化管控

近日,由中国网络空间安全协会主办,中国网络空间安全协会网络空间安全法律与公共政策专业委员会、北京邮电大学互联网治理与法律研究中心、公安部第三研究所网络安全法律研究中心、西安交通大学信息安全法律研究中心承办的《网络安全法(草案二…

java的equals方法_Java Date equals()方法与示例

java的equals方法日期类equals()方法 (Date Class equals() method) equals() method is available in java.util package. equals()方法在java.util包中可用。 equals() method is used to check whether this date and the given object (o) are equals or not. equals()方法…

lnmp解析php,LNMP之 php解析

[rootLNMP ~]# vim /usr/local/nginx/conf/nginx.conf打开以下PHP 相关项且更改 scripts$fastcgi_script_name;> /usrlocal/nginx/html$fastcgi_script_name;location ~ \.php$ {root html;fastcgi_pass 127.0.0.1:9000;fastcgi_index index.php;fastcgi_param…

spring历史背景

1.2004年spring出现第一版本spring frameworl1.0 2.写代码永远是最简单的,后续的运维工作才是让人感到无助的 3.spring boot在运维方面做了很多工作,部署,监控,度量。结合spring cloud还可以实现服务发现,服务降级等功…

计算机网络中的传输协议是_计算机网络中的传输方式

计算机网络中的传输协议是传输方式 (Transmission Modes) The mechanism of transferring data or information between two linked devices connected over a network is referred to as Transmission Modes. 在通过网络连接的两个链接的设备之间传输数据或信息的机制称为传输…

https 密钥 php,https加密方式是什么

Https加密介绍Http直接通过明文在浏览器和服务器之间传递消息,容易被监听抓取到通信内容。Https采用对称加密和非对称加密结合的方式来进行通信。Https不是应用层的新协议,而是Http通信接口用SSL和TLS来加强加密和认证机制。加密方式对称加密&#xff1a…

一个前端框架应该有的一些公共函数

一、防止ie浏览器按backspace回退页面 //防止后退返回页面,如果非文本框、密码框、文本域控件,或控件非可用装填,则禁用后退按键 var uanavigator.userAgent.toLowerCase(); var isIEua.indexOf("msie")>-1; window.document.onkeydown fu…

Kruskal(P)和Prim(K)算法

最小生成树 (Minimum Spanning Tree) An MST is a subset of the edges of the connected, undirected graph that connect all the vertices together, in which there is no forming of a cycle and there should be minimum possible total edge weight. MST是已连接的无向图…

java get post 注解,GET/POST接收或发送数据的问题

在文章开始,先来回忆一下GET、POST这两种请求方式的区别。❈Http定义了与服务器交互的不同方法,最基本的方法有4种,分别是GET,POST,PUT,DELETE。URL全称是资源描述符,我们可以这样认为&#xff…

mybatis中sql语句传入多个参数方法

1 使用map <select id"selectRole" parameterType"map" resultType"RoleMap">SELECT id, roleName, noteFROM roleWHERE roleName LIKE Concat(%,#{roleName},%)and note like Concat(%,#{note},%)</select> 在接口中如下定义 List&…

kotlin半生对象_Kotlin程序| 随播对象特征

kotlin半生对象伴侣对象 (Companion object) If you need a function or a property to be tied to a class rather than to instances of it (similar to static in java), you can declare it inside a companion object: 如果需要将函数或属性绑定到类而不是实例(类似于java…

mysql安装注意步骤,mysql安装步骤

mysql安装步骤1、在官网下载对应的压缩文件&#xff0c;放到本地文件夹下&#xff0c;解压缩。2、配置Path环境变量&#xff1a;新增mysql的bin文件夹路径&#xff0c;C:\software\mysql-8.0.23-winx64\bin。3、在mysql根目录下新增my.ini配置文件。内容如下&#xff0c;basedi…

maven插件介绍之tomcat7-maven-plugin

tomcat7-maven-plugin插件的pom.xml依赖为&#xff1a; <dependency><groupId>org.apache.tomcat.maven</groupId><artifactId>tomcat7-maven-plugin</artifactId><version>2.2</version> </dependency>一&#xff1a;直接执行…

在Python中模拟do-while循环

Python as a language doesnt support the do-while loop. However, we can have a workaround to emulate the do-while loop. Python作为一种语言不支持do-while循环。 但是&#xff0c;我们可以采用一种变通方法来模拟do-while循环 。 The syntax for do-while is as follo…

织梦cms生成首页html的php文件,织梦DedeCMS定时自动生成首页HTML的实现方法

只需要制作一个文件然后在首页模板添加一句代码就可以实现让织梦DedeCMS自动生成首页html&#xff0c;具体方法如下&#xff1a;第一步、需要在首页调用随机文章&#xff0c;这样每次自动更新才会有更新的效果&#xff0c;随机文章调用标签如下&#xff1a;{dede:arclist sortr…

Linux下安装Flume

1 下载Flume Welcome to Apache Flume — Apache Flume 下载1.9.0版本 2 上传服务器并解压安装 3 删除lib目录下的guava-11.0.2.jar &#xff08;如同服务器安装了hadoop&#xff0c;则删除&#xff0c;如没有安装hadoop则保留这个文件&#xff0c;否则无法启动flume&#…