大话Linux内核中锁机制之内存屏障、读写自旋锁及顺序锁

大话Linux内核中锁机制之内存屏障、读写自旋锁及顺序锁

在上一篇博文中笔者讨论了关于原子操作和自旋锁的相关内容,本篇博文将继续锁机制的讨论,包括内存屏障、读写自旋锁以及顺序锁的相关内容。下面首先讨论内存屏障的相关内容。

三、内存屏障

不知读者是是否记得在笔者讨论自旋锁的禁止或使能的时候,提到过一个内存屏障函数。OK,接下来,笔者将讨论内存屏障的具体细节内容。我们首先来看下它的概念,Memory Barrier是指编译器和处理器对代码进行优化(对读写指令进行重新排序)后,导致对内存的写入操作不能及时的反应到读操作中(锁机制无法保证时序正确)。可能读起来有点绕口,没关系,我们继续讨论,内容完后,读者再回来阅读的时候便会有深层次的体会。在arch\x86\include\asm\system.h中定义。首先针对barrier函数(即在自旋锁部分先前看到的那个),它主要针对编译器的优化屏障,但它却阻止不了CPU重排指令时序。由于CPU存在重排时序等问题,因此仅仅有一个barrier是不够的,因此,内核还提供了如下几种机制,如下图3.1所展示的那部分函数。

图3.1      内核中的内存屏障函数接口

针对上述内容,值得一提的是,使用这些内存屏障,相当于停用了处理器或编译器提供的优化机制,显然这样的结果必然影响程序的性能。

那么这些函数如何能做到上述注释中提到的内容呢?下面我们来看一下它们到底是如何实现的。实现的核心其实是依靠如下指令:① lock指令(已在第一部分中提及);② lfence指令:停止相关流水线,直到lfence之前对内存进行读取操作的指令全部完成;③ sfence指令:停止相关流水线,直到sfence之前对内存进行写入操作的指令全部完成;④ mfence指令:停止相关流水线,直到mfence之前对内存进行读取和写入操作的指令全部完成。了解完它们的核心指令后,再看其在内核中的源码实现,如图3.2所示。

图3.2      部分内核屏障函数的核心源码

图3.2只展示部分函数,但关于smp_rmb()等函数的实现比较复杂,添加一些了针对smp系统特有的保护代码,但其根本还是调用这些如rmb()等函数。至此,关于内存屏障部分的内容即讨论到这里,虽然内容较少,但其重要性不可忽略,同时,读者若再回头去看,去理解内存屏障的概念,是否会有新的收获呢。

四、读写自旋锁

接下来讨论读写自旋锁的实现原理,它其实是自旋锁的升级版。同样,我们先看看它能实现的功能:针对写:最多只有一个写进程;针对读:可以同时有多个读写单元。但读和写不能同时进行。它于include\linux\rwlock.h下定义,而它提供的函数形式和自旋锁类似,仅将“spin_”替换成“read_”或“write_”。包括read_lock(lock)和read_unlock(lock)、write_lock(lock)和write_unlock(lock)等。接下来我们来看下它该是如何使用的。而关于它的使用其实很简单,如图4.1所示,很好理解,即是在临界区前后分别加上加解锁函数即可,这里便不在细说。

图4.1      读写自旋锁的使用示例

然后我们来讨论它的实现核心。事实上,它的实现讨论起来也挺简单,就是有点绕。实现流程与spin_lock几乎完全一致,唯一不同的是最后调用体系结构相关的函数是arch_read_lock而不是arch_spin_lock(自旋锁的arch_spin_lock最终调用__ticket_spin_lock())等。

下面分析它的源码实现。假如现在有进程P1向操作系统申请了读写自旋锁,设读写锁变量A已经被定义,它的初值为RW_LOCK_UNLOCKED(0x01000000)。配合图4.2,图4.3所示的源码内容,若此时进程P1对它申请读锁操作,则read_lock()对读写锁变量A减1,如果相减后A的值结果为负,则说明此时系统中已经有某个进程(设为P2)对这个读写锁变量使用写锁函数write_lock()上锁,这时候系统便会持续等待P2对写锁的释放,持续等待过程的源码实现如图4.5所示。假如进程P1读锁申请成功,若P1在使用读锁过程中,存在另一个进程P3申请写锁操作。此时write_lock()对读写锁变量A减0x01000000并判断,如果结果非零,则说明此时锁变量A已被write_lock()函数上锁或用read_lock()函数上锁,进而便跳转到图4.6所示的写锁失败的汇编函数,持续等待锁变量A被进程P1释放。

图4.2  读锁函数的内核源码                                          图4.3  写锁函数的内核源码

下面给出对应的解锁函数。如图4.4所示。

图4.4      解锁函数的内核源码

可以看出解锁函数中的实现其实简单的加1和减1操作。下面重点关注加锁函数中的两个申请锁失败时的两个的链接函数,如图4.5,图4.6所示。采用纯汇编实现,由于图4.5所示的源码和图4.6所示的源码内容实现机理实际上均是相同,故这里仅重点分析图4.5源码。图4.5的源码搞懂了,图4.6的源码自然也不是问题。

在分析源码前,可能读者对于源码中的rep指令等有些许疑问。事实上,rep重复串操作直到cx寄存器的内容为0为止。结合的指令有限,只有MOVS、STOS、LODS、INS和OUTS,如rep stosb;等。而rep; nop是一个混合指令,被翻译成pause指令。分号在此非注释,AT&T中表示指令的有效定界符。pause指令会向处理器提供一种提示:告诉处理器所执行的代码序列是一个自旋等待状态,处理器会根据这个提示而避开内存序列冲突(Intel手册说明)。

当read_lock()函数申请读锁失败后,首先将rw变量加1(注:rdi实际上代表的是rw,它们之间是通过寄存器来传递的,未加1前rw的值为-1)。紧接着,在执行cmpl指令前rdi的值为0,于是同立即数1进行比较。如果不相等,则继续跳转到“1”标志位处继续循环比较,直到相等,因为相等的时候,说明系统执行了读锁的释放函数,将rw变量加1了,具体可参看read_unlock()函数实现(图4.4所示)。此时在退出__read_lock_failed函数之前还需将rdi变量即rw变量减1,表示另一进程申请读锁成功,从而保证后续申请读写锁的进程的正确性。至此,已将__read_lock_failed函数源码分析完毕,至于__writed_lock_failed也是类似的思想,读者可自己类比推理即可理解。

图4.5  读锁函数中的跳转函数                                          图4.6  写锁函数中的跳转函数

至此,关于读写自旋锁的内容基本讨论完毕。此时,读者若再回头去看,去理解读写自旋锁的功能,是否有更多新的收获呢。

五、顺序锁

紧接着笔者将讨论顺序锁的内容。它的定义如下:读执行单元不会被写单元阻塞,允许读写同时进行,但写单元之间仍然互斥。但是简单的从定义上看,我们很容易就能发现一个问题:读操作期间,发生写操作,怎么办?不用担心,内核必然考虑到了这一点,解决方法是:读执行单元重新读取数据,确保得到的数据是完整的。但需注意的是:被保护的资源不能含指针,否则重读的时候可能出问题(如指针被释放)。顺序锁在include\linux\seqlock.h文件中定义和实现。首先看下它的结构体实现,如图5.1所示。

图5.1      顺序锁的结构体定义

特别注意结构体中的sequence变量,这个变量是顺序锁核心内容,后续将会解释原因。然后我们讨论一下它的使用示例,如图5.2所示。示例中的do-while循环实现的就是本节一开始提到的那个问题,若读操作期间发生了写操作,则实现重读,而实现重读的关键就是对sequence变量的有效利用。

图5.2      顺序锁的使用示例

下面我们来看一下顺序锁的源码实现,如图5.3,图5.4,图5.5所示。首先针对写操作函数,图5.3所示,可以看到它实际上就是利用自旋锁是对sequence变量的保护,进程在申请写锁的时候,只是简单的将sequence变量的值增1,这和我们一开始关于自旋锁的例子特别的类似(读者可回头看看)。

图5.3      写操作函数的内核源码

对于读操作函数,依据图5.4,图5.5所示。可以看到在申请读锁的时候,首先将当前的所的顺序,也就是sequence变量的值保存下来(这时候已在do-while循环中),然后在read_seqretry函数中作判断,检测当前的读锁变量是否发生变化,若发生变化(调用写锁函数,sequence变量的值增1),则重新读,正如图5.2所给出的示例一般;若不发生变化,则表示申请读锁成功。至于源码中的unlikely()函数,那是内核中提供了关于变量检测的函数,它的检测速度比一般的if-else更快,至于为何快,将又是一套较复杂的内容,感兴趣的可以看查相关方面的资料,这里不再细说。

图5.4      读操作的内核源码

图5.5      读操作的内核源码

通过阅读源码,可看到它实现了可允许同时读写,但写单元之间仍然互斥。其实,可以回头看一下先前提到的那个关于顺序锁的示例就会明白的更加透彻。

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

大话Linux内核中锁机制之内存屏障、读写自旋锁及顺序锁

时间: 2024-12-26 08:41:40

大话Linux内核中锁机制之内存屏障、读写自旋锁及顺序锁的相关文章

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

转至:http://blog.sina.com.cn/s/blog_6d7fa49b01014q7p.html 很多人会问这样的问题,Linux内核中提供了各式各样的同步锁机制到底有何作用?追根到底其实是由于操作系统中存在多进程对共享资源的并发访问,从而引起了进程间的竞态.这其中包括了我们所熟知的SMP系统,多核间的相互竞争资源,单CPU之间的相互竞争,中断和进程间的相互抢占等诸多问题. 通常情况下,如图1所示,对于一段程序,我们的理想是总是美好的,希望它能够这样执行:进程1先对临界区完成操作,

大话Linux内核中锁机制之信号量、读写信号量

大话Linux内核中锁机制之信号量.读写信号量 在上一篇博文中笔者分析了关于内存屏障.读写自旋锁以及顺序锁的相关内容,本篇博文将着重讨论有关信号量.读写信号量的内容. 六.信号量 关于信号量的内容,实际上它是与自旋锁类似的概念,只有得到信号量的进程才能执行临界区的代码:不同的是获取不到信号量时,进程不会原地打转而是进入休眠等待状态.它的定义是include\linux\semaphore.h文件中,结构体如图6.1所示.其中的count变量是计数作用,通过使用lock变量实现对count变量的保

大话Linux内核中锁机制之RCU、大内核锁

大话Linux内核中锁机制之RCU.大内核锁 在上篇博文中笔者分析了关于完成量和互斥量的使用以及一些经典的问题,下面笔者将在本篇博文中重点分析有关RCU机制的相关内容以及介绍目前已被淘汰出内核的大内核锁(BKL).文章的最后对<大话Linux内核中锁机制>系列博文进行了总结,并提出关于目前Linux内核中提供的锁机制的一些基本使用观点. 十.RCU机制 本节将讨论另一种重要锁机制:RCU锁机制.首先我们从概念上理解下什么叫RCU,其中读(Read):读者不需要获得任何锁就可访问RCU保护的临界

大话Linux内核中锁机制之完成量、互斥量

大话Linux内核中锁机制之完成量.互斥量 在上一篇博文中笔者分析了关于信号量.读写信号量的使用及源码实现,接下来本篇博文将讨论有关完成量和互斥量的使用和一些经典问题. 八.完成量 下面讨论完成量的内容,首先需明确完成量表示为一个执行单元需要等待另一个执行单元完成某事后方可执行,它是一种轻量级机制.事实上,它即是为了完成进程间的同步而设计的,故而仅仅提供了代替同步信号量的一种解决方法,初值被初始化为0.它在include\linux\completion.h定义. 如图8.1所示,对于执行单元A

Linux内核中的机制学习总结

一.驱动中的poll机制 1.简介:select()和poll()系统调用的本质一样,前者在 BSD UNIX 中引入的,后者在 System V 中引入的. 应用程序使用 select() 或 poll() 调用设备驱动程序的 file_operations 的 poll() 函数. 2.实现(1).初始化一个等待队列头init_waitqueue_head(&dev->rx_wait/&dev->tx_wait); (2).实现驱动中的poll()方法 static uns

Linux 内核的同步机制,第 1 部分 + 第二部分(转)

http://blog.csdn.net/jk198310/article/details/9264721  原文地址: Linux 内核的同步机制,第 1 部分 一. 引言 在现代操作系统里,同一时间可能有多个内核执行流在执行,因此内核其实象多进程多线程编程一样也需要一些同步机制来同步各执行单元对共享数据的访问.尤其是在多处理器系统上,更需要一些同步机制来同步不同处理器上的执行单元对共享的数据的访问.在主流的Linux内核中包含了几乎所有现代的操作系统具有的同步机制,这些同步机制包括:原子操作

Linux 内核中的 Device Mapper 机制

http://www.68idc.cn/help/server/linux/20141127133367.html 结合具体代码对 Linux 内核中的 device mapper 映射机制进行了介绍.Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物 简介: 本文结合具体代码对 Linux 内核中的 device mapper 映射机制进行了介绍.Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机制下,

[转] Linux 内核中的 Device Mapper 机制

本文结合具体代码对 Linux 内核中的 device mapper 映射机制进行了介绍.Device mapper 是 Linux 2.6 内核中提供的一种从逻辑设备到物理设备的映射框架机制,在该机制下,用户可以很方便的根据自己的需要制定实现存储资源的管理策略,当前比较流行的 Linux 下的逻辑卷管理器如 LVM2(Linux Volume Manager 2 version).EVMS(Enterprise Volume Management System).dmraid(Device M

Linux内核的同步机制---自旋锁

自旋锁的思考:http://bbs.chinaunix.net/thread-2333160-1-1.html 近期在看宋宝华的<设备驱动开发具体解释>第二版.看到自旋锁的部分,有些疑惑.所以来请教下大家. 以下是我參考一些网络上的资料得出的一些想法,不知正确与否.记录下来大家讨论下: (1) linux上的自旋锁有三种实现: 1. 在单cpu.不可抢占内核中,自旋锁为空操作. 2. 在单cpu,可抢占内核中,自旋锁实现为"禁止内核抢占".并不实现"自旋"