大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新Idea2022.1教程亲测有效,一键激活。
Jetbrains全系列IDE使用 1年只要46元 售后保障 童叟无欺
一 什么是自旋锁
自旋锁(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(
_In_ PKSPIN_LOCK SpinLock,
_Out_ PKIRQL OldIrql
);
SpinLock:指向经过KeInitializeSpinLock的结构体
OldIrql:用于保存当前的中断请求级
注意:
当使用全局变量存储 OldIrql时,不同的锁最好不要共用一个全局块,否则很容易引起竞争问题(race condition)。
3.3 释放自旋锁
___________________
VOID KeReleaseSpinLock(
_Inout_ PKSPIN_LOCK SpinLock,
_In_ KIRQL NewIrql
);
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
VOID
NTAPI
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
KIRQL
KeAcquireSpinLockRaiseToDpc (
__inout PKSPIN_LOCK SpinLock
)
{
KIRQL OldIrql;
//
// Raise IRQL to DISPATCH_LEVEL and acquire the specified spin lock.
//
OldIrql = KfRaiseIrql(DISPATCH_LEVEL); //提升IRQL
KxAcquireSpinLock(SpinLock); //获取自旋锁
return OldIrql;
}
其中用于获取自旋锁的KxAcquireSpinLock函数:
__forceinline
VOID
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位版本如下:
BOOLEAN
FORCEINLINE
InterlockedBitTestAndSet (
IN LONG *Base,
IN LONG Bit
)
{
__asm {
mov eax, Bit
mov ecx, Base
lock bts [ecx], eax
setc al
};
}
关键就在bts指令,是一个进行位测试并置位的指令。这里在进行关键的操作时有lock前缀,保证了多处理器安全。
4.4 KxReleaseSpinLock
___________________
__forceinline
VOID
KxReleaseSpinLock (
__inout PKSPIN_LOCK SpinLock
)
{
InterlockedAnd64((LONG64 *)SpinLock, 0);//释放时进行与操作设置其为0
}
4.5 真实系统上的实现
___________________
好了,对于自旋锁的初始化、获取、释放,都有了了解。但是只是谈谈原理,看看WRK,似乎有种纸上谈兵的感觉?那就实战一下,看看真实系统中是如何实现的。以双核系统中XP SP2下内核中关于SpinLock的实现细节为例:
用IDA分析双核系统的内核文件ntkrnlpa.exe,关于自旋锁操作的两个基本函数是KiAcquireSpinLock和KiReleaseSpinLock,其它几个类似。
.text:004689C0 KiAcquireSpinLock proc near ; CODE XREF:
sub_416FEE+2D p
.text:004689C0 ; sub_4206C0+5 j ...
.text:004689C0 lock bts dword ptr [ecx], 0
.text:004689C5 jb short loc_4689C8
.text:004689C7 retn
.text:004689C8 ; ---------------------------------------------------------------------------
.text:004689C8
.text:004689C8 loc_4689C8: ; CODE XREF: KiAcquireSpinLock+5 j
.text:004689C8 ; KiAcquireSpinLock+12 j
.text:004689C8 test dword ptr [ecx], 1
.text:004689CE jz short KiAcquireSpinLock
.text:004689D0 pause
.text:004689D2 jmp short loc_4689C8
.text:004689D2 KiAcquireSpinLock endp
代码比较简单,还原成源码是这样子的:
void __fastcall KiAcquireSpinLock(int _ECX)
{
while ( 1 )
{
__asm { lock bts dword ptr [ecx], 0 }
if ( !_CF )
break;
while ( *(_DWORD *)_ECX & 1 )
__asm { pause }//应是rep nop,IDA将其翻译成pause
}
}
fastcall方式调用,参数KSPIN_LOCK在ECX中,可以看到是一个死循环,先测试其是否置位,若否,则CF将置0,并将ECX置位,即获取锁的操作成功;若是,即锁已被占有,则一直对其进行测试并进入空转状态,这和前面分析的完全一致,只是代码似乎更精炼了一点,毕竟是实用的玩意嘛。
再来看看释放时:
.text:004689E0 public KiReleaseSpinLock
.text:004689E0 KiReleaseSpinLock proc near ; CODE XREF: sub_41702E+E p
.text:004689E0 ; sub_4206D0+5 j ...
.text:004689E0 mov byte ptr [ecx], 0
.text:004689E3 retn
.text:004689E3 KiReleaseSpinLock endp
这个再清楚不过了,直接设置为0就代表了将其释放,此时那些如虎狼般疯狂空转的其它处理器将马上获知这一信息,于是,下一个获取、释放的过程开始了。这就是最基本的自旋锁,其它一些自旋锁形式是对这种基本形式的扩充。比如排队自旋锁,是为了解决多处理器竞争时的无序状态等等,不多说了。
现在对自旋锁可谓真的是明明白白了,之前我犯的错误就是以为用了自旋锁就能保证多核同步,其实不是的,用自旋锁来保证多核同步的前提是大家都要用这个锁。若当前处理器已占有自旋锁,只有别的处理器也来请求这个锁时,才会进入空转,不进行别的操作,这时你的操作将不会受到干扰。
参考链接:
【原创】明明白白自旋锁
Linux 内核的排队自旋锁(FIFO Ticket Spinlock)
Linux 内核的同步机制,第 1 部分
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/166478.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...