为什么使用等待队列的时候要用lockosspinlock 自旋锁锁

第三方登录:23217人阅读
linux(99)
POSIX threads(简称Pthreads)是在多核平台上进行并行编程的一套常用的API。线程同步(Thread Synchronization)是并行编程中非常重要的通讯手段,其中最典型的应用就是用Pthreads提供的锁机制(lock)来对多个线程之间共 享的临界区(Critical Section)进行保护(另一种常用的同步机制是barrier)。
Pthreads提供了多种锁机制:
(1) Mutex(互斥量):pthread_mutex_***
(2) Spin lock(自旋锁):pthread_spin_***
(3) Condition Variable(条件变量):pthread_con_***
(4) Read/Write lock(读写锁):pthread_rwlock_***
Pthreads提供的Mutex锁操作相关的API主要有:
pthread_mutex_lock (pthread_mutex_t *mutex);
pthread_mutex_trylock (pthread_mutex_t *mutex);
pthread_mutex_unlock (pthread_mutex_t *mutex);
Pthreads提供的与Spin Lock锁操作相关的API主要有:
pthread_spin_lock (pthread_spinlock_t *lock);
pthread_spin_trylock (pthread_spinlock_t *lock);
pthread_spin_unlock (pthread_spinlock_t *lock);
从 实现原理上来讲,Mutex属于sleep-waiting类型的锁。例如在一个双核的机器上有两个线程(线程A和线程B),它们分别运行在Core0和 Core1上。假设线程A想要通过pthread_mutex_lock操作去得到一个临界区的锁,而此时这个锁正被线程B所持有,那么线程A就会被阻塞 (blocking),Core0 会在此时进行上下文切换(Context Switch)将线程A置于等待队列中,此时Core0就可以运行其他的任务(例如另一个线程C)而不必进行忙等待。而Spin lock则不然,它属于busy-waiting类型的锁,如果线程A是使用pthread_spin_lock操作去请求锁,那么线程A就会一直在
Core0上进行忙等待并不停的进行锁请求,直到得到这个锁为止。
所以,自旋锁一般用用多核的服务器。&
自旋锁(Spin lock)
自旋锁与互斥锁有点类似,只是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是 否该自旋锁的保持者已经释放了锁,&自旋&一词就是因此而得名。其作用是为了解决某项资源的互斥使用。因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远 高于互斥锁。虽然它的效率比互斥锁高,但是它也有些不足之处:
&&& 1、自旋锁一直占用CPU,他在未获得锁的情况下,一直运行--自旋,所以占用着CPU,如果不能在很短的时 间内获得锁,这无疑会使CPU效率降低。
&&& 2、在用自旋锁时有可能造成死锁,当递归调用时有可能造成死锁,调用有些其他函数也可能造成死锁,如 copy_to_user()、copy_from_user()、kmalloc()等。
因此我们要慎重使用自旋锁,自旋锁只有在内核可抢占式或SMP的情况下才真正需要,在单CPU且不可抢占式的内核下,自旋锁的操作为空操作。自旋锁适用于锁使用者保持锁时间比较短的情况下。
&&&&&&&自旋锁的用法如下:
&& 首先定义:spinlock_
&然后初始化:spin_lock_init(spinlock_t *x);&& //自旋锁在真正使用前必须先初始化
&在2.6.11内核中将定义和初始化合并为一个宏:DEFINE_SPINLOCK(x)
& 获得自旋锁:spin_lock(x);&& //只有在获得锁的情况下才返回,否则一直“自旋”
&&& &&& &&& & & & & & & && spin_trylock(x);&&//如立即获得锁则返回真,否则立即返回假
&&&&& 释放锁:spin_unlock(x);
结合以上有以下代码段:
&&&&spinlock_&&& &&& //定义一个自旋锁
&&& spin_lock_init(&lock);
&&& spin_lock(&lock);&&&&
&&& .......&&& &&& //临界区
&&& spin_unlock(&lock);&& //释放锁
&&& 还有一些其他用法:
spin_is_locked(x)
&&& //  该宏用于判断自旋锁x是否已经被某执行单元保持(即被锁),如果是,&& 返回真,否则返回假。
spin_unlock_wait(x)
&&& //  该宏用于等待自旋锁x变得没有被任何执行单元保持,如果没有任何执行单元保持该自旋锁,该宏立即返回,否
&&& //将循环&&& 在那里,直到该自旋锁被保持者释放。
spin_lock_irqsave(lock, flags)
&&& //  该宏获得自旋锁的同时把标志寄存器的值保存到变量flags中并失效本地中//断。相当于:spin_lock()+local_irq_save()
spin_unlock_irqrestore(lock, flags)
&&& //  该宏释放自旋锁lock的同时,也恢复标志寄存器的值为变量flags保存的//值。它与spin_lock_irqsave配对使用。
&&& //相当于:spin_unlock()+local_irq_restore()
spin_lock_irq(lock)
 &&& //该宏类似于spin_lock_irqsave,只是该宏不保存标志寄存器的值。相当&&&&&&&& //于:spin_lock()+local_irq_disable()
spin_unlock_irq(lock)
&&& //该宏释放自旋锁lock的同时,也使能本地中断。它与spin_lock_irq配对应用。相当于:&spin_unlock()+local_irq+enable()
spin_lock_bh(lock)
&&& //  该宏在得到自旋锁的同时失效本地软中断。相当于:&&//spin_lock()+local_bh_disable()
spin_unlock_bh(lock)
&&&&& //该宏释放自旋锁lock的同时,也使能本地的软中断。它与spin_lock_bh配对//使用。相当于:spin_unlock()+local_bh_enable()
spin_trylock_irqsave(lock, flags)
&&& //该宏如果获得自旋锁lock,它也将保存标志寄存器的值到变量flags中,并且失//效本地中断,如果没有获得锁,它什么也不做。因此如果能够立即 获得锁,它等//同于spin_lock_irqsave,如果不能获得锁,它等同于spin_trylock。如果该宏//获得自旋锁lock,那需要 使用spin_unlock_irqrestore来释放。
spin_trylock_irq(lock)
&&& //该宏类似于spin_trylock_irqsave,只是该宏不保存标志寄存器。如果该宏获得自旋锁lock,需要使用spin_unlock_irq来释放。
spin_trylock_bh(lock)
&&& //  该宏如果获得了自旋锁,它也将失效本地软中断。如果得不到锁,它什么//也不做。因此,如果得到了锁,它等同于spin_lock_bh,如果得 不到锁,它等同//于spin_trylock。如果该宏得到了自旋锁,需要使用spin_unlock_bh来释放。
spin_can_lock(lock)
&&& //  该宏用于判断自旋锁lock是否能够被锁,它实际是spin_is_locked取反。//如果lock没有被锁,它返回真,否则,返回 假。该宏在2.6.11中第一次被定义,在//先前的内核中并没有该宏。
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:1189416次
积分:15482
积分:15482
排名:第414名
原创:302篇
转载:686篇
评论:122条
(3)(2)(1)(2)(4)(3)(1)(3)(2)(4)(15)(1)(3)(5)(11)(7)(6)(12)(5)(1)(8)(15)(13)(31)(27)(30)(43)(6)(13)(9)(14)(11)(18)(41)(41)(94)(100)(81)(52)(47)(32)(77)(54)(32)(10)【原创+整理】详解自旋锁 - 推酷
【原创+整理】详解自旋锁
一 什么是自旋锁
自旋锁(Spinlock)是一种广泛运用的底层
机制。自旋锁是一个
设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试相关的位。如果锁可用,则“锁定”被设置,而代码继续进入
;相反,如果锁被其他人获得,则代码进入
(而不是休眠,这也是自旋锁和一般锁的区别)并重复检查这个锁,直到该锁可用为止,这就是自旋的过程。“测试并设置位”的操作必须是原子的,这样,即使多个线程在给定时间自旋,也只有一个线程可获得该锁。
自旋锁对于SMP和单处理器可抢占内核都适用。可以想象,当一个处理器处于自旋状态时,它做不了任何有用的工作,因此自旋锁对于单处理器不可抢占内核没有意义,实际上,非抢占式的单处理器系统上自旋锁被实现为空操作,不做任何事情。
曾经有个经典的例子来比喻自旋锁:A,B两个人合租一套房子,共用一个厕所,那么这个厕所就是共享资源,且在任一时刻最多只能有一个人在使用。当厕所闲置时,谁来了都可以使用,当A使用时,就会关上厕所门,而B也要使用,但是急啊,就得在门外焦急地等待,急得团团转,是为“自旋”,这也是要求锁的持有时间尽量短的原因!
自旋锁有以下特点:___________________
用于临界区互斥
在任何时刻最多只能有一个执行单元获得锁
要求持有锁的处理器所占用的时间尽可能短
等待锁的线程进入忙循环
补充:___________________
临界区和互斥:对于某些全局资源,多个并发执行的线程在访问这些资源时,操作系统可能会交错执行多个并发线程的访问指令,一个错误的指令顺序可能会导致最终的结果错误。多个线程对共享的资源的访问指令构成了一个临界区(critical section),这个临界区不应该和其他线程的交替执行,确保每个线程执行临界区时能对临界区里的共享资源互斥的访问。
二 自旋锁较互斥锁之类同步机制的优势
2.1 休眠与忙循环
___________________
互斥锁得不到锁时,线程会进入休眠,这类同步机制都有一个共性就是 一旦资源被占用都会产生任务切换,任务切换涉及很多东西的(保存原来的上下文,按调度算法选择新的任务,恢复新任务的上下文,还有就是要修改cr3寄存器会导致cache失效)这些都是需要大量时间的,因此用互斥之类来同步一旦涉及到阻塞代价是十分昂贵的。
一个互斥锁来控制2行代码的原子操作,这个时候一个CPU正在执行这个代码,另一个CPU也要进入, 另一个CPU就会产生任务切换。为了短短的两行代码 就进行任务切换执行大量的代码,对系统性能不利,另一个CPU还不如直接有条件的死循环,等待那个CPU把那两行代码执行完。
2.2 自旋过程
___________________
当锁被其他线程占有时,获取锁的线程便会进入自旋,不断检测自旋锁的状态。一旦自旋锁被释放,线程便结束自旋,得到自旋锁的线程便可以执行临界区的代码。对于临界区的代码必须短小,否则其他线程会一直受到阻塞,这也是要求锁的持有时间尽量短的原因!
三 windows驱动程序中自旋锁的使用
3.1 初始化自旋锁
___________________
在windows下,自旋锁用一个名为
KSPIN_LOCK
的结构体进行表示。
VOID KeInitializeSpinLock(
_Out_ PKSPIN_LOCK SpinLock
KSPIN_LOCK
变量必须是常驻在内存的,一般可以放在设备对象的设备扩展结构体中,控制对象的控制扩展中,或者调用者申请的非分页内存池中。
可运行在任意IRQL中。
3.2 申请自旋锁
___________________
VOID KeAcquireSpinLock(
PKSPIN_LOCK SpinLock,
_Out_ PKIRQL
SpinLock:指向经过KeInitializeSpinLock的结构体OldIrql:用于保存当前的中断请求级
当使用全局变量存储 OldIrql时,不同的锁最好不要共用一个全局块,否则很容易引起竞争问题(race condition)。
3.3 释放自旋锁
___________________
VOID KeReleaseSpinLock(
_Inout_ PKSPIN_LOCK SpinLock,
SpinLock:指向经过KeInitializeSpinLock的结构体NewIrql :KeAcquireSpinLock保存当前的中断请求级
运行的IRQL = DISPATCH_LEVEL
四 windows下自旋锁的实现
4.1 KSPIN_LOCK结构体
___________________
KSPIN_LOCK实际是一个操作系统相关的无符号整数,32位系统上是32位的unsigned long,64位系统则定义为unsigned __int64。在初始化时,其值被设置为0,为空闲状态。
4.2 KeInitializeSpinLock
___________________
FORCEINLINE
KeInitializeSpinLock (
__out PKSPIN_LOCK SpinLock
*SpinLock = 0;
//将SpinLock初始化为0,表示锁的状态为空闲状态
4.3 KeAcquireSpinLock
___________________
4.3.1 单处理器
wdm.h中是这样定义的:
#define KeAcquireSpinLock(SpinLock, OldIrql) \
*(OldIrql) = KeAcquireSpinLockRaiseToDpc(SpinLock)
很明显,核心的操作对象是SpinLock,同时也与IRQL有关 。
如果当前的IRQL为PASSIVEL_LEVEL,那么首先会提升IRQL到DISPATCH_LEVEL,然后调用KxAcquireSpinLock()。
如果当前的IRQL为DISPATCH_LEVEL,那么就调用KeAcquireSpinLockAtDpcLevel,省去提升IRQL一步。
因为线程调度也是发生在DISPATCH_LEVEL,所以提升IRQL之后当前处理器上就不会发生线程切换。单处理器时,当前只能有一个线程被执行,而这个线程提升IRQL至DISPATCH_LEVEL之后又不会因为调度被切换出去,自然也可以实现我们想要的互斥“效果”,其实只操作IRQL即可,无需SpinLock。实际上单核系统的内核文件ntosknl.exe中导出的有关SpinLock的函数都只有一句话,就是return。
4.3.2 多处理器
而多处理器呢?提升IRQL只会影响到当前处理器,保证当前处理器的当前线程不被切换。
__forceinline
KeAcquireSpinLockRaiseToDpc (
__inout PKSPIN_LOCK SpinLock
KIRQL OldI
// Raise IRQL to DISPATCH_LEVEL and acquire the specified spin lock.
OldIrql = KfRaiseIrql(DISPATCH_LEVEL);
//提升IRQL
KxAcquireSpinLock(SpinLock); //获取自旋锁
return OldI
其中用于获取自旋锁的KxAcquireSpinLock函数:
__forceinline
KxAcquireSpinLock (
__inout PKSPIN_LOCK SpinLock
if (InterlockedBitTestAndSet64((LONG64 *)SpinLock, 0))//64位函数
KxWaitForSpinLockAndAcquire(SpinLock);
//CPU空转进行等待
KxAcquireSpinLock()函数先测试锁的状态。若锁空闲,则
SpinLock为0,那么InterlockedBitTestAndSet()将返回0,并使
SpinLock置位,不再为0。这样KxAcquireSpinLock()就成功得到了锁,并设置锁为占用状态(*SpinLock不为0),函数返回。若锁已被占用呢?InterlockedBitTestAndSet()将返回1,此时将调用KxWaitForSpinLockAndAcquire()等待并获取这个锁。这表明,SPIN_LOCK为0则锁空闲,非0则已被占有。
InterlockedBitTestAndSet64()函数的32位版本如下:
FORCEINLINE
InterlockedBitTestAndSet (
IN LONG *Base,
IN LONG Bit
mov eax, Bit
mov ecx, Base
lock bts [ecx], eax
关键就在bts指令,是一个进行位测试并置位的指令。这里在进行关键的操作时有lock前缀,保证了多处理器安全。
4.4 KxReleaseSpinLock
___________________
__forceinline
KxReleaseSpinLock (
__inout PKSPIN_LOCK SpinLock
InterlockedAnd64((LONG64 *)SpinLock, 0);//释放时进行与操作设置其为0
4.5 真实系统上的实现
___________________
好了,对于自旋锁的初始化、获取、释放,都有了了解。但是只是谈谈原理,看看WRK,似乎有种纸上谈兵的感觉?那就实战一下,看看真实系统中是如何实现的。以双核系统中XP SP2下内核中关于SpinLock的实现细节为例:用IDA分析双核系统的内核文件ntkrnlpa.exe,关于自旋锁操作的两个基本函数是KiAcquireSpinLock和KiReleaseSpinLock,其它几个类似。
.text: KiAcquireSpinLock pr CODE XREF:
sub_416FEE+2D p
sub_ j ...
lock bts dword ptr [ecx], 0
short loc_4689C8
.text: ; ---------------------------------------------------------------------------
.text: loc_4689C8:
CODE XREF: KiAcquireSpinLock+5 j
KiAcquireSpinLock+12 j
dword ptr [ecx], 1
.text:004689CE
short KiAcquireSpinLock
short loc_4689C8
.text: KiAcquireSpinLock endp
代码比较简单,还原成源码是这样子的:
void __fastcall KiAcquireSpinLock(int _ECX)
while ( 1 )
__asm { lock bts dword ptr [ecx], 0 }
if ( !_CF )
while ( *(_DWORD *)_ECX & 1 )
__asm { pause }//应是rep nop,IDA将其翻译成pause
fastcall方式调用,参数KSPIN_LOCK在ECX中,可以看到是一个死循环,先测试其是否置位,若否,则CF将置0,并将ECX置位,即获取锁的操作成功;若是,即锁已被占有,则一直对其进行测试并进入空转状态,这和前面分析的完全一致,只是代码似乎更精炼了一点,毕竟是实用的玩意嘛。再来看看释放时:
public KiReleaseSpinLock
.text: KiReleaseSpinLock pr CODE XREF: sub_41702E+E p
sub_ j ...
byte ptr [ecx], 0
.text: KiReleaseSpinLock endp
这个再清楚不过了,直接设置为0就代表了将其释放,此时那些如虎狼般疯狂空转的其它处理器将马上获知这一信息,于是,下一个获取、释放的过程开始了。这就是最基本的自旋锁,其它一些自旋锁形式是对这种基本形式的扩充。比如排队自旋锁,是为了解决多处理器竞争时的无序状态等等,不多说了。现在对自旋锁可谓真的是明明白白了,之前我犯的错误就是以为用了自旋锁就能保证多核同步,其实不是的,用自旋锁来保证多核同步的前提是大家都要用这个锁。若当前处理器已占有自旋锁,只有别的处理器也来请求这个锁时,才会进入空转,不进行别的操作,这时你的操作将不会受到干扰。
参考链接:
已发表评论数()
已收藏到推刊!
请填写推刊名
描述不能大于100个字符!
权限设置: 公开
仅自己可见
正文不准确
标题不准确
排版有问题
没有分页内容
图片无法显示
视频无法显示
与原文不一致

我要回帖

更多关于 osspinlock 自旋锁 的文章

 

随机推荐