第一次作业(2.加锁)

1.什么是锁

为了保护共享数据,需要一些同步机制,如自旋锁(spinlock),读写锁(rwlock),它们使用起来非常简单,而且是一种很有效的同步机制,在UNIX系统和Linux系统中得到了广泛的使用。

2.linux下的锁

自旋锁、读写锁、顺序锁、RCU

(1)锁的由来和发展

自从各种任务不再顺序执行的那一天起,自从多道程序设计开始上线的那天,进程就戴上了脚镣。古老的操作系统的变体当然也接过了父亲的狼牙棒,信号量杯证明是一种有效的互斥方式,可是它却存在很多弊端。其实unix不喜欢混乱,因此unix创造了进程这个可被操纵系统内核强制管理的执行绪概念,unix几乎给了所有可以执行的东西一个进程上下文,然后管理这些进程,unix的进程管理是很强大的,可是总有管不了的,就是中断,中断处理机制被视为一个硬件的无序性和软件进程管理的有序性之间的协调接口,因此中断处理程序并不属于任何进程的上下文,因此在中断中就不能像在别的进程上下文可以请求信号量,因为中断处理中根本就没有地方放床导致任意进程上下文下没有地方睡眠。中断处理不能睡眠是不能在中断处理用信号量的原因,因此必须提供一个中断处理中的互斥方案,另外,即使在进程上下文,如果需要的信号量很多,那么势必会造成进程频繁睡眠/被唤醒,这样进程调度的开销就过大,因此自旋锁就出来了,不用睡眠,只是自旋,很简单,在内核中很高效。

可是自旋锁是个平均的权衡结果,没有考虑到数据的客观特殊类型以及操作类型,自旋锁太机制化了,没有一点策略,其实想想便知道,读请求根本就不用锁也就是不用互斥,而写才需要互斥,在这种特殊需求下,linux提出了读写锁的概念。

从本质上说,读写锁是用自旋锁的思想实现的,自旋锁就是不断判断一个数字的大小,如果小于0就说明得不到锁,如果为1就得到了锁,释放锁就是将锁设置为1就可以,近来又实现了ticket自旋锁,使得锁变得有序化了,照顾到了硬件缓存的缓存结果。自旋锁就是不断判断一个数的大小,不管谁想得到锁都要经过竞争,也就是自旋,得不到锁就是因为将那个数减1后,它小于0了,如果让它不小于0不就可以得到锁了吗?我们的需求是读之间可以随意,但是写之间必须互斥,另外读写之间也要互斥,而在自旋锁中,无论读写都要一样的动作,那么我们只需要将读写分开成为不同的动作就可以了,目的就是要体现出读和写的不对称性,先看看自旋锁的实现,只要请求锁都要将锁变量减去1,只要释放都要将锁设置为1,那么读写锁中我们将读和写减去的数设置为不同就可以了,设置写锁请求时要减去一个很大的数N,而读锁只需要减去1,只要大于0就可以得到锁,如果不是就忙等待,这样的话,如果初始化的时候将锁初始化为N,那么只要有一个读,那么所就是N-1,如果再读,就是N-2,仍然大于0,如果写,就是-2,小于0,忙等,等待两个读者释放了锁,就是0了,那么写可以进行,写完后释放锁,就是将锁加上N,于是锁恢复N,其实这个实现和信号量有点类似,不同的是等不到锁时不睡眠而是忙等,此实现给了读很多的N个可用信号量,因为对于读,信号量的跨度是1,而对于写,只有1个信号量,写的跨度是N,这可以说是信号量的升级版,一个信号量支持两个不同的跨度,不过在等待的意义上,它又像是自旋锁,总之,linux内核的实现很是艺术吧。

说完了读写锁,那么看看顺序锁,顺序锁更具有创意,它基于一个事实,在读写锁中,如果读的时候恰好有一个写者在写,那么读者就要忙等,忙等是因为害怕读到不一致的数据,反过来如果写者发现有读者在读,它也要忙等,忙等是因为怕读者读到不一致的数据,这种互相照顾必然会影响效率,不管怎样我们都可以赌一把,这种赌博需要很小的赌注就值得,没有必要每次读的时候都要请求锁,而是随时都可以读,一旦读到不一致的数据,那么大不了再读一次,这个设想应该是很不错的,读写随时进行,谁也不管谁,自己注点意就可以了,比如以写者为主,读者自己进行数据一致性的判断,这样效率会提高不少的。

分析到这里可以想到,有两个点要注意,一个就是读开始,一个就是读结束,这两个点将时间分为了三个部分,读前,读中,读后,读前写完成和读后写完成都不会影响读操作,那么只有三类写操作会污染读者读到的数据,一个就是写在读前开始在读中结束,一个是在读中开始在读中结束,一个是在读中开始,在读后结束,于是我们只需要监控写开始和写结束即可,按照最简单的数学原理,如果设定一个变量,在写开始和写结束时都对它递增,我们就可以知道写的状态,这个数只要是偶数就说明没有在写中或者相反,取决于初始设置,那么读的时候可以尽量等到没有在写中的时候进行,然后在读结束的时候判断这个变量,如果被增加了就说明开始了一次写已经结束或者开始了一次写还没有结束,总之我们的数据被污染了,那么就需要再读一次,即使再读一次也不能保证没有写进行,但是我们可以再赌一把,这实际上是一个循环。这个顺序锁最起码解放了写操作,写操作不必关心读的情况,对于读操作纯粹是赌博,但是读写锁对读操作是有好处的,因为大多数情况下只要没有写者,读者都可以随意,总而言之,顺序锁还是比读写锁好的。

RCU锁基本没有利用什么锁,可是除了真正的锁之外,其它的特性它都用到了,比如RCU用到的就是cpu调度的周期,因为RCU确信自己不会破坏自己,只有别人可能破坏自己,如果不想自己被破坏,那么就不让别人运行,于是传统的RCU锁就是禁用抢占,而最新的RCU锁允许了抢占,但是Rcu的机制保护着rcu应该保护的数据。

(2)自旋锁和RCU

自旋锁是专为多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)  

自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种

锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。

事实上,自旋锁的初衷就是:在短期间内进行轻量级的锁定。一个被争用的自旋锁使得请求它的线程在等待锁重新可用的期间进行自旋(特别浪费处理器时间),所以自旋锁不应该被持有时间过长。如果需要长时间锁定的话最好使用信号量。

自旋锁在同一时刻只能被最多一个内核任务持有,所以一个时刻只有一个线程允许存在于临界区中。这点很好地满足了对称多处理机器需要的锁定服务。

在单处理器上,自旋锁仅仅当作一个设置内核抢占的开关。如果内核抢占也不存在,那么自旋锁会在编译时被完全剔除出内核。简单的说,自旋锁在内核中主要用来防止多处理器中并发访问临界区,防止内核抢占造成的竞争。另外自旋锁不允许任务睡眠(持有自旋锁的任务睡眠会造成自死锁——因为睡眠有可能造成持有锁的内核任务被重新调度,而再次申请自己已持有的锁),它能够在中断上下文中使用。

死锁:假设有一个或多个内核任务和一个或多个资源,每个内核都在等待其中的一个资源,但所有的资源都已经被占用了。这便会发生所有内核任务都在相互等待,但它们永远不会释放已经占有的资源,于是任何内核任务都无法获得所需要的资源,无法继续运行,这便意味着死锁发生了。自死琐是说自己占有了某个资源,然后自己又申请自己已占有的资源,显然不可能再获得该资源,因此就自缚手脚了。

 

RCU(Read-Copy Update),顾名思义就是读-拷贝修改,RCU是2.6内核引入的新的锁机制,在绝大部分为读而只有极少部分为写的情况下,它是非常高效的,因此在路由表维护、系统调用审计、SELinux的AVC、dcache和IPC等代码部分中,使用它来取代rwlock来获得更高的性能。但是,它也有缺点,延后的删除或释放将占用一些内存,尤其是对嵌入式系统,这可能是非常昂贵的内存开销。此外,写者的开销比较大,尤其是对于那些无法容忍旧数据的情况以及不只一个写者的情况,写者需要spinlock或其他的锁机制来与其他写者同步。对于被RCU保护的共享数据结构,读者不需要获得任何锁就可以访问它,但写者在访问它时首先拷贝一个副本,然后对副本进行修改,最后使用一个回调(callback)机制在适当的时机把指向原来数据的指针重新指向新的被修改的数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作。

因此RCU实际上是一种改进的rwlock,读者几乎没有什么同步开销,它不需要锁,不使用原子指令,而且在除alpha的所有架构上也不需要内存栅(Memory Barrier),因此不会导致锁竞争,内存延迟以及流水线停滞。不需要锁也使得使用更容易,因为死锁问题就不需要考虑了。写者的同步开销比较大,它需要延迟数据结构的释放,复制被修改的数据结构,它也必须使用某种锁机制同步并行的其它写者的修改操作。读者必须提供一个信号给写者以便写者能够确定数据可以被安全地释放或修改的时机。有一个专门的垃圾收集器来探测读者的信号,一旦所有的读者都已经发送信号告知它们都不在使用被RCU保护的数据结构,垃圾收集器就调用回调函数完成最后的数据释放或修改操作。 RCU与rwlock的不同之处是:它既允许多个读者同时访问被保护的数据,又允许多个读者和多个写者同时访问被保护的数据(注意:是否可以有多个写者并行访问取决于写者之间使用的同步机制),读者没有任何同步开销,而写者的同步开销则取决于使用的写者间同步机制。但RCU不能替代rwlock,因为如果写比较多时,对读者的性能提高不能弥补写者导致的损失。

读者在访问被RCU保护的共享数据期间不能被阻塞,这是RCU机制得以实现的一个基本前提,也就说当读者在引用被RCU保护的共享数据期间,读者所在的CPU不能发生上下文切换,spinlock和rwlock都需要这样的前提。写者在访问被RCU保护的共享数据时不需要和读者竞争任何锁,只有在有多于一个写者的情况下需要获得某种锁以与其他写者同步。写者修改数据前首先拷贝一个被修改元素的副本,然后在副本上进行修改,修改完毕后它向垃圾回收器注册一个回调函数以便在适当的时机执行真正的修改操作。等待适当时机的这一时期称为grace period,而CPU发生了上下文切换称为经历一个quiescent state,grace period就是所有CPU都经历一次quiescent state所需要的等待的时间。垃圾收集器就是在grace period之后调用写者注册的回调函数来完成真正的数据修改或数据释放操作的。

RCU是2.6内核引入的新的锁机制,在绝大部分为读而只有极少部分为写的情况下,它是非常高效的,因此在路由表维护、系统调用审计、SELinux的AVC、dcache和IPC等代码部分中,使用它来取代rwlock来获得更高的性能。但是,它也有缺点,延后的删除或释放将占用一些内存,尤其是对嵌入式系统,这可能是非常昂贵的内存开销。此外,写者的开销比较大,尤其是对于那些无法容忍旧数据的情况以及不只一个写者的情况,写者需要spinlock或其他的锁机制来与其他写者同步。


   

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
项目:使用AngularJs编写的简单 益智游戏(附源代码)  这是一个简单的 javascript 项目。这是一个拼图游戏,也包含一个填字游戏。这个游戏玩起来很棒。有两个不同的版本可以玩这个游戏。你也可以玩填字游戏。 关于游戏 这款游戏的玩法很简单。如上所述,它包含拼图和填字游戏。您可以通过移动图像来玩滑动拼图。您还可以选择要在滑动面板拥有的列数和网格数。 另一个是填字游戏。在这里你只需要找到浏览器左侧提到的那些单词。 要运行此游戏,您需要在系统上安装浏览器。下载并在代码编辑器打开此项目。然后有一个 index.html 文件可供您修改。在命令提示符运行该文件,或者您可以直接运行索引文件。使用 Google Chrome 或 FireFox 可获得更好的用户体验。此外,这是一款多人游戏,双方玩家都是人类。 这个游戏包含很多 JavaScript 验证。这个游戏很有趣,如果你能用一点 CSS 修改它,那就更好了。 总的来说,这个项目使用了很多 javascript 和 javascript 库。如果你可以添加一些具有不同颜色选项的级别,那么你一定可以利用其库来提高你的 javascript 技能。 演示: 该项目为国外大神项目,可以作为毕业设计的项目,也可以作为大作业项目,不用担心代码重复,设计重复等,如果需要对项目进行修改,需要具备一定基础知识。 注意:如果装有360等杀毒软件,可能会出现误报的情况,源码本身并无病毒,使用源码时可以关闭360,或者添加信任。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值