内核同步之自旋锁与读写自旋锁

用在多个CPU系统中的锁机制,当一个CPU正访问自旋锁保护的临界区时,临界区将被锁上,其他需要访问此临界区的CPU只能忙等待,直到前面的CPU已访问完临界区,将临界区开锁。自旋锁上锁后让等待线程进行忙等待而不是睡眠阻塞,而信号量是让等待线程睡眠阻塞。自旋锁的忙等待浪费了处理器的时间,但时间通常很短,在1毫秒以下。

自旋锁用于多个CPU系统中,在单处理器系统中,自旋锁不起锁的作用,只是禁止或启用内核抢占。在自旋锁忙等待期间,内核抢占机制还是有效的,等待自旋锁释放的线程可能被更高优先级的线程抢占CPU。

自旋锁基于共享变量。一个线程通过给共享变量设置一个值来获取锁,其他等待线程查询共享变量是否为0来确定锁现是否可用,然后在忙等待的循环中"自旋"直到锁可用为止。

由于互斥的特点,使用自旋锁的代码毫无线程并发性可言,多处理器系统的性能受到限制。通过观察线程在临界区的访问行为,我们发现有些线程只是 简单地读取信息,并不修改任何东西,那么允许它们同时进入临界区不会有任何危险,反而能大大提高系统的并发性。这种将线程区分为读者和写者、多个读者允许 同时访问共享资源、申请线程在等待期内依然使用忙等待方式的锁,我们称之为读写自旋锁(Reader-Writer Spinlock)。

读写自旋锁的属性

上面提及的共享资源可以是简单的单一变量或多个变量,也可以是像文件这样的复杂数据结构。为了防止错误地使用读写自旋锁而引发的 bug,我们假定每个共享资源关联一把唯一的读写自旋锁,线程只允许按照类似大象装冰箱的方式访问共享资源:

  1. 申请锁。
  2. 获得锁后,读写共享资源。
  3. 释放锁。

有些用户态实现的读写锁支持线程在持有锁的情况下继续申请相同类型的锁,以及读者在持有锁的情况下变换身份成写者。这 2 个特性对于适用于短小临界区的读写自旋锁而言并无实际意义,因此本文不作讨论。

对于线程的执行,我们假设:

  1. 系统存在一个全局时钟,我们讨论的时间是离散的,不是连续的、数学意义上的时间。
  2. 任意时刻,系统中活跃线程的总数目是 有限的。
  3. 线程的执行不会因为调度、缺页异常等原因无限期地被延迟。理论上,线程的执行可以被系统无限期地延迟,因此任何互斥算法都有死锁的危险。我们希望排除系统的干扰,集中关注算法及具体实现本身。
  4. 线程对共享资源的访问在有限步骤内结束。
  5. 当线程释放锁时,我们 希望:线程在有限步骤内释放锁。
  6. 因为每个程序步骤花费有限时间,所以如果满足上述 5 个条件,那么:获得锁的线程必然在有限时间内将锁释放掉。

我们说某个读写自旋锁算法是正确的,是指该锁满足如下三个属性:

1. 互斥。任意时刻读者和写者不能同时访问共享资源(即获得锁);任意时刻只能有至多一个写者访问共享资源。

2. 读者并发。在满足“互斥”的前提下,多个读者可以同时访问共享资源。

3. 无死锁(Freedom from Deadlock)。如果线程 A 试图获取锁,那么某个线程必将获得锁,这个线程可能是 A 自己;如果线程 A 试图但是却永远没有获得锁,那么某个或某些线程必定无限次地获得锁。

读写自旋锁主要用于比较短小的代码片段,线程等待期间不应该进入睡眠状态,因为睡眠 / 唤醒操作相当耗时,大大延长了获得锁的等待时间,所以我们要求:

4. 忙等待。申请锁的线程必须不断地查询是否发生退出等待的事件,不能进入睡眠状态。这个要求只是描述线程执行锁申请操作未成功时的行为,并不涉及锁自身的正确性。

“无死锁”属性告诉我们,从全局来看一定会有申请线程获得锁,但对于某个或某些申请线程而言,它们可能永远无法获得锁,这种现象称为饥饿 (Starvation)。一种原因源于计算机体系结构的特点:例如在使用基于单一共享变量的读写自旋锁的多核系统中,如果锁的持有者 A 所处的处理器和等待者 B 所处的处理器相邻(也许还能共享二级缓存),B 更容易获知锁被释放,增大获得锁的几率,而距离较远的处理器上的线程则难与之 PK,导致饥饿的发生。还有一种原因源于设计策略,即读写自旋锁刻意偏好某类角色的线程。

为了提高并发性,读写自旋锁可以选择偏好读者,即读者能够优先获得锁:

1. 读者优先(Reader Preference)。如果锁被读者持有,那么新来的读者可以立即获得锁,无需忙等待。至于当锁被“写者持有”或“未被持有”时,新来的读者是否可以“夹塞”到正在等待的写者之前,依赖于具体实现。

如果读者持续不断地到来,等待的写者很可能永远无法获得锁,导致饥饿。在现实中,写者的数目一般较读者少许多,而且到来的频率很低,因此读写自旋锁可以选择偏好写者来有效地缓解饥饿现象:

2. 写者优先(Writer Preference)。写者必须在后到的读者 / 写者之前获得锁。因为在写者之前到来的等待线程数目是有限的,所以可以保证写者的等待时间有个合理的上界。但是多个读者之间获得锁的顺序不确定,且先到的 读者不一定能在后到的写者之前获得锁。可见,如果写者持续到来,读者仍然可能产生饥饿。

为了彻底消除饥饿现象,完美的读写自旋锁还需满足下面任一属性:

3. 无饥饿(Freedom from Starvation)。如果线程 A 试图获取锁,那么 A 必定能在有限时间内获得锁。当然,这个“有限时间”也许相当漫长。

4. 公平(Fairness)。我们把“锁申请”操作的执行分为两个阶段:准备阶段(Doorway Section),能在有限程序步骤结束;等待阶段(Waiting Section),也许永远无法结束等待阶段一旦结束,线程即获得读写自旋锁。如果线程 A 和 B 同时申请锁,但是 A 的等待阶段完成于 B 之前,那么公平读写自旋锁保证 A 在 B 之前获得锁。如果 A 和 B 的等待阶段在时间上有重叠,那么它们获得锁的顺序是不确定的(在第二章中我们彻底取消“重叠”概念)。

“公平”意味着申请锁的线程必定在有限时间内获得锁。若不然,假设 A 申请一个公平读写自旋锁但是永远不能获得,那么在 A 之后完成准备阶段的线程显然也永远不能获得锁。而在 A 之前或“重叠”地完成等待阶段的申请线程数目是 有限的,可见必然发生了“死锁”,矛盾。同时这也说明释放锁的时间也是有限的。使用公平读写自旋锁杜绝了饥饿现象的发生,如果假定线程访问共享资源和释放锁的时间有一个合理的上界,那么锁申请线程的等待时间只与前面等待的线程数目有关,不依赖其它因素。

时间: 2024-08-29 01:08:31

内核同步之自旋锁与读写自旋锁的相关文章

linux内核同步之信号量、顺序锁、RCU、完成量、关闭中断【转】

转自:http://blog.csdn.net/goodluckwhh/article/details/9006065 版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[-] 一信号量 信号量的概念 信号量的数据结构和相关API 数据结构 初始化 获取和释放信号量 读写信号量的概念 读写信号量的数据结构 二顺序锁 顺序锁的概念 数据结构 写操作 读操作 三Read-Copy Update RCU 写操作 读操作 释放旧的版本 四完成量Completions 完成量的概念 数据结构

[内核同步]浅析Linux内核同步机制

转自:http://blog.csdn.net/fzubbsc/article/details/37736683?utm_source=tuicool&utm_medium=referral 很早之前就接触过同步这个概念了,但是一直都很模糊,没有深入地学习了解过,近期有时间了,就花时间研习了一下<linux内核标准教程>和<深入linux设备驱动程序内核机制>这两本书的相关章节.趁刚看完,就把相关的内容总结一下.为了弄清楚什么事同步机制,必须要弄明白以下三个问题: 什么是互

Linux内核同步机制

http://blog.csdn.net/bullbat/article/details/7376424 Linux内核同步控制方法有很多,信号量.锁.原子量.RCU等等,不同的实现方法应用于不同的环境来提高操作系统效率.首先,看看我们最熟悉的两种机制——信号量.锁. 一.信号量 首先还是看看内核中是怎么实现的,内核中用struct semaphore数据结构表示信号量(<linux/semphone.h>中): [cpp] view plaincopyprint? struct semaph

Linux内核设计与实现——内核同步

内核同步 同步介绍 同步的概念 临界区:也称为临界段,就是訪问和操作共享数据的代码段. 竞争条件: 2个或2个以上线程在临界区里同一时候运行的时候,就构成了竞争条件. 所谓同步.事实上防止在临界区中形成竞争条件. 假设临界区里是原子操作(即整个操作完毕前不会被打断),那么自然就不会出竞争条件.但在实际应用中.临界区中的代码往往不会那么简单,所以为了保持同步,引入了锁机制.但又会产生一些关于锁的问题. 死锁产生的条件:要有一个或多个运行线程和一个或多个资源,每一个线程都在等待当中的一个资源.但全部

浅析Linux内核同步机制

很早之前就接触过同步这个概念了,但是一直都很模糊,没有深入地学习了解过,近期有时间了,就花时间研习了一下<linux内核标准教程>和<深入linux设备驱动程序内核机制>这两本书的相关章节.趁刚看完,就把相关的内容总结一下.为了弄清楚什么事同步机制,必须要弄明白以下三个问题: 什么是互斥与同步? 为什么需要同步机制? Linux内核提供哪些方法用于实现互斥与同步的机制? 1.什么是互斥与同步?(通俗理解) 互斥与同步机制是计算机系统中,用于控制进程对某些特定资源的访问的机制. 同步

Linux内核剖析 之 内核同步

主要内容 1.内核请求何时以交错(interleave)的方式执行以及交错程度如何. 2.内核所实现的基本同步机制. 3.通常情况下如何使用内核提供的同步机制. 内核如何为不同的请求服务 哪些服务? ====>>> 为了更好地理解内核是如何执行的,我们把内核看做必须满足两种请求的侍者:一种请求来自顾客,另一种请求来自数量有限的几个不同的老板.对于不同的请求,侍者采用如下的策略: 1.老板提出请求时,如果侍者空闲,则侍者开始为老板服务. 2.如果老板提出请求时侍者正在为顾客服务,那么侍者停

Linux内核同步机制--自旋锁【转】

本文转载自:http://www.cppblog.com/aaxron/archive/2013/04/12/199386.html 自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,"自旋"一词就是因此而得名. 由于自旋锁使用者一般保持锁时间非常短,因此选择自旋而不是睡眠是非常必要的,自旋锁的效率远高于互斥锁. 信号量和读写信号量适合于保持时间较长的情况,它们会导致调用者睡眠,因此只能在进

linux内核同步之每CPU变量、原子操作、内存屏障、自旋锁【转】

转自:http://blog.csdn.net/goodluckwhh/article/details/9005585 版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[-] 一每CPU变量 二原子操作 三优化和内存屏障 四自旋锁 自旋锁 自旋锁的数据结构和宏函数 读写自旋锁 读写自旋锁的相关函数 linux内核中的各种“任务”都能看到内核地址空间,因而它们之间也需要同步和互斥.linux内核支持的同步/互斥手段包括: 技术 功能 作用范围 每CPU变量 为每个CPU复制一份数据

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

大话Linux内核中锁机制之内存屏障.读写自旋锁及顺序锁 在上一篇博文中笔者讨论了关于原子操作和自旋锁的相关内容,本篇博文将继续锁机制的讨论,包括内存屏障.读写自旋锁以及顺序锁的相关内容.下面首先讨论内存屏障的相关内容. 三.内存屏障 不知读者是是否记得在笔者讨论自旋锁的禁止或使能的时候,提到过一个内存屏障函数.OK,接下来,笔者将讨论内存屏障的具体细节内容.我们首先来看下它的概念,Memory Barrier是指编译器和处理器对代码进行优化(对读写指令进行重新排序)后,导致对内存的写入操作不能