一、为什么用自旋锁
操作系统锁机制的基本原理,就是在某个锁操作过程中不能与其他锁操作交织执行,以免多个执行路径对内核中某些重要的数据及数据结构进行同时操作而造成混乱。在不同的系统环境中,根据系统特点和操作需要,锁机制可以用多种方式来实现。以Linux为例,其系统内核的锁机制一般通过3 种基本方式来实现,即原语、关中断和总线锁。在单CPU系统中,CPU 的读—修改—写原语可以保证是原子的,即执行过程过中不会被中断,所以CPU 通过关中断的方式,从芯片级保证该操作所存取的数据不能被多个内核控制路径同时访问,避免交叉执行。然而,在对称多处理器 (SMP) 环境中,单CPU 涉及读—修改—写原语不再是原子的,因为,在某个CPU 执行读—修改—写指令时有多次总线操作,其他CPU 竞争总线,可导致对同一存储单元的读—写操作与其他CPU 对这一存储单元交叉,这时我们就需要用一个称为自旋锁(spin lock)的原始对象为CPU 提供锁定总线的方法。
二、自旋锁是什么
自旋锁(spin lock)是一个典型的对临界资源的互斥手段,它的名称来源于它的特性。为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其它CPU不可能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行。如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置(test-and-set)”操作,即开始“自旋”。最后,锁的所有者通过重置该变量释放这个自旋锁,于是,某个等待的test-and-set操作向其调用者报告锁已释放。
理解自旋锁最简单的方法是把它作为一个变量看待,这个变量把一个临界区或者标记为“我当前在另一个CPU上运行,请稍等一会”,或者标记为“我当前不在运行,可以被使用”。如果1号CPU首先进入该例程,它就获取该自旋锁;当2号CPU试图进入同一个例程时,该自旋锁告诉它自己已为1号CPU所持有,需等到1号CPU释放自己后才能进入。一个简单的自旋锁实现结构如下:
/ /定义一个自旋锁
sp inlock_tmylock = SPIN_LOCK_UNLOCKED;
sp in_lock (&mylock) ; / /将临界区锁住
. . .
critical section / /临界区
. . .
sp in_unlock (&mylock) ; / /解锁
这是自旋锁的一种常用方法,注意它没有开关中断,也没有保存状态字,因为开关中断对SMP系统来说,开销是比较大的。
三、关于自旋锁的几个事实
自旋锁实际上是忙等锁,当锁不可用时,CPU一直循环执行“测试并设置(test-and-set)”该锁直到可用而取得该锁,CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。这说明只有在占用锁的时间极短的情况下,使用自旋锁是合理的,因为此时某个CPU可能正在等待这个自旋锁。当临界区较为短小时,如只是为了保证对数据修改的原子性,常用自旋锁;当临界区很大,或有共享设备的时候,需要较长时间占用锁,使用自旋锁就不是一个很好的选择,会降低CPU的效率。
自旋锁也存在死锁(deadlock)问题。引发这个问题最常见的情况是要求递归使用一个自旋锁,即如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁。自旋锁没有与其关联的“使用计数器”或“所有者标识”;锁或者被占用或者空闲。如果你在锁被占用时获取它,你将等待到该锁被释放。如果碰巧你的CPU已经拥有了该锁,那么用于释放锁的代码将得不到运行,因为你使CPU永远处于“测试并设置”某个内存变量的自旋状态。另外,如果进程获得自旋锁之后再阻塞,也有可能导致死锁的发生。由于自旋锁造成的死锁,会使整个系统挂起,影响非常大。
自旋锁一定是由系统内核调用的。不可能在用户程序中由用户请求自旋锁。当一个用户进程拥有自旋锁期间,内核是把代码提升到管态的级别上运行。在内部,内核能获取自旋锁,但任何用户都做不到这一点
四、自旋锁与信号量比较
自旋锁和信号量是解决互斥问题的基本手段,无论是单处理系统还是多处理系统,它们可以不需修改代码地进行移植。那么,这两个手段应该如何选择呢?这就要考虑临界区的性质和系统处理的要求。
从严格意义上说,信号量和自旋锁属于不同层次的互斥手段,前者的实现有赖于后者。
信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争不上,会有上下文切换,进程可以去睡眠,但CPU不会停,会接着运行其他的执行路径。从概念上说,这与单CPU或多CPU没有直接的关系,只是在信号量本身的实现上,为了保证信号量结构存取的原子性,在多CPU中需要自旋锁来互斥。但是值得注意的是上下文切换需要一定时间,并且会使高速缓冲失效,对系统性能影响是很大的。因此,只有当进程占用资源很长时间时,用信号量才是不错的选择。
当所要保护的临界区比较短时,用自旋锁是非常方便的,因为它节省上下文切换的时间。但是CPU得不到自旋锁会在那里空转直到锁成功为止,所以要求锁不能在临界区里停留很长时间,否则会降低系统的效率。
综上,自旋锁是一种保护数据结构或代码片段的原始方式,主要用于SMP中,用于CPU同步,在某个时刻只允许一个进程访问临界区内的代码。它的实现是基于CPU锁定数据总线的指令。为保证系统效率,自旋锁锁定的临界区一般比较短。在单CPU系统中,使用自旋锁的意义不大,还容易因为递归调用自旋锁造成死锁。