线程同步和锁_自旋锁的实现

线程同步和锁_自旋锁的实现一什么是自旋锁自旋锁(Spinlock)是一种广泛运用的底层同步机制。自旋锁是一个互斥设备,它只有两个值:“锁定”和“解锁”。它通常实现为某个整数值中的某个位。希望获得某个特定锁得代码测试

大家好,又见面了,我是你们的朋友全栈君。如果您正在找激活码,请点击查看最新教程,关注关注公众号 “全栈程序员社区” 获取激活教程,可能之前旧版本教程已经失效.最新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 部分

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/166478.html原文链接:https://javaforall.net

(0)
上一篇 2022年8月6日 下午2:36
下一篇 2022年8月6日 下午2:36


相关推荐

  • 图的邻接矩阵(C语言)

    图的邻接矩阵(C语言)邻接矩阵无向图和有向图在邻接矩阵中的表示方法 无向图和有向图大同小异 在这里只以无向图为例 代码部分通过简单调整即可对应编译有向图邻接矩阵数据类型定义 defineMaxVer 定义最大容量 typedefstruc 包含权的邻接矩阵的的定义 intVertices MaxVertices 顶点信息的数组 intEdge Max

    2026年3月18日
    2
  • c中怎么将string转换成int

    c中怎么将string转换成int通过 int Parse string 类型变量名 实现 但是使用 int Parse 方法实现转换 通常需要额外捕获并处理转换时发生的异常 2 通过 int TryParse strings outintresult 实现 使用 int TryParse 方法实现转换 不需要处理额外处理转换时发生的异常 3 通过 Convert ToInt32 string 类型变量名 实现 补充一下 Convert ToInt32 int Parse 的区别 1 这两个方法的最大不同是

    2026年3月19日
    3
  • 端口号22是哪个协议_https端口号是多少

    端口号22是哪个协议_https端口号是多少https端口:443  服务项目:Https  网页浏览端口,能提供加密和通过安全端口传输的另一种HTTP,简单来说,就是HTTP安全版,打开的网页中,如果网址前缀为https,则说明该网站开启了  https安全访问。  说明:443端口用于网页浏览,关闭电脑443端口,将会导致https网页无法正常打开。  HTTP:80端口  服务:HTTP  说明:用于网页

    2026年1月15日
    5
  • Delphi XE3 下安装ActiveX控件「建议收藏」

    Delphi XE3 下安装ActiveX控件「建议收藏」http://blog.csdn.net/lee576/article/details/1770744此英文文章来自codegear官方网站,但是是讲在BDS2006 下如何安装,我在delphi2007下用此方法,一样通过,原文如下 Description:HowtoimportanActiveXcontrolinBDS2006?Answer/S

    2022年5月14日
    39
  • 正则表达式判断字符有乱码(正则文法转正则表达式)

    #include<QtCore/QCoreApplication>#include<iostream>#include<string>#include<regex>usingnamespacestd;intmain(intargc,char*argv[]){ QCoreApplicationa(argc,argv); boolfoundmatch=false; try{ std::wregexre(L

    2022年4月11日
    314
  • SpringBoot 整合shiro框架

    SpringBoot 整合shiro框架网上有很多整合 shiro 的博客分享 但是貌似没找到一个完整 并且能够实现的 不是包的问题 就是代码的问题 也可能是自己的问题 或者版本的问题 所以 整理了一版自己已应用的 maven 依赖 lt parent gt lt groupId gt org springframew boot lt groupId gt lt artifactId gt sprin

    2026年3月19日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号