自旋鎖(Spinlock)是一種普遍運用的底層同步機制。自旋鎖是一個互斥設備,它只有兩個值:「鎖定」和「解鎖」。它一般實現爲某個整數值中的某個位。但願得到某個特定鎖得代碼測試相關的位。若是鎖可用,則「鎖定」被設置,而代碼繼續進入臨界區;相反,若是鎖被其餘人得到,則代碼進入忙循環(而不是休眠,這也是自旋鎖和通常鎖的區別)並重複檢查這個鎖,直到該鎖可用爲止,這就是自旋的過程。「測試並設置位」的操做必須是原子的,這樣,即便多個線程在給定時間自旋,也只有一個線程可得到該鎖。php
自旋鎖對於SMP和單處理器可搶佔內核都適用。能夠想象,當一個處理器處於自旋狀態時,它作不了任何有用的工做,所以自旋鎖對於單處理器不可搶佔內核沒有意義,實際上,非搶佔式的單處理器系統上自旋鎖被實現爲空操做,不作任何事情。html
曾經有個經典的例子來比喻自旋鎖:A,B兩我的合租一套房子,共用一個廁所,那麼這個廁所就是共享資源,且在任一時刻最多隻能有一我的在使用。當廁所閒置時,誰來了均可以使用,當A使用時,就會關上廁所門,而B也要使用,可是急啊,就得在門外焦急地等待,急得團團轉,是爲「自旋」,這也是要求鎖的持有時間儘可能短的緣由!linux
自旋鎖有如下特色:
___________________算法
補充:
___________________windows
臨界區和互斥:對於某些全局資源,多個併發執行的線程在訪問這些資源時,操做系統可能會交錯執行多個併發線程的訪問指令,一個錯誤的指令順序可能會致使最終的結果錯誤。多個線程對共享的資源的訪問指令構成了一個臨界區(critical section),這個臨界區不該該和其餘線程的交替執行,確保每一個線程執行臨界區時能對臨界區裏的共享資源互斥的訪問。安全
___________________併發
互斥鎖得不到鎖時,線程會進入休眠,這類同步機制都有一個共性就是 一旦資源被佔用都會產生任務切換,任務切換涉及不少東西的(保存原來的上下文,按調度算法選擇新的任務,恢復新任務的上下文,還有就是要修改cr3寄存器會致使cache失效)這些都是須要大量時間的,所以用互斥之類來同步一旦涉及到阻塞代價是十分昂貴的。函數
一個互斥鎖來控制2行代碼的原子操做,這個時候一個CPU正在執行這個代碼,另外一個CPU也要進入, 另外一個CPU就會產生任務切換。爲了短短的兩行代碼 就進行任務切換執行大量的代碼,對系統性能不利,另外一個CPU還不如直接有條件的死循環,等待那個CPU把那兩行代碼執行完。post
___________________性能
當鎖被其餘線程佔有時,獲取鎖的線程便會進入自旋,不斷檢測自旋鎖的狀態。一旦自旋鎖被釋放,線程便結束自旋,獲得自旋鎖的線程即可以執行臨界區的代碼。對於臨界區的代碼必須短小,不然其餘線程會一直受到阻塞,這也是要求鎖的持有時間儘可能短的緣由!
___________________
在windows下,自旋鎖用一個名爲KSPIN_LOCK的結構體進行表示。
VOID KeInitializeSpinLock( _Out_ PKSPIN_LOCK SpinLock );
注意:
存儲KSPIN_LOCK變量必須是常駐在內存的,通常能夠放在設備對象的設備擴展結構體中,控制對象的控制擴展中,或者調用者申請的非分頁內存池中。
可運行在任意IRQL中。
___________________
VOID KeAcquireSpinLock( _In_ PKSPIN_LOCK SpinLock, _Out_ PKIRQL OldIrql );
SpinLock:指向通過KeInitializeSpinLock的結構體
OldIrql:用於保存當前的中斷請求級
注意:
當使用全局變量存儲 OldIrql時,不一樣的鎖最好不要共用一個全局塊,不然很容易引發競爭問題(race condition)。
___________________
VOID KeReleaseSpinLock( _Inout_ PKSPIN_LOCK SpinLock, _In_ KIRQL NewIrql );
SpinLock:指向通過KeInitializeSpinLock的結構體
NewIrql :KeAcquireSpinLock保存當前的中斷請求級
注意
運行的IRQL = DISPATCH_LEVEL
___________________
KSPIN_LOCK實際是一個操做系統相關的無符號整數,32位系統上是32位的unsigned long,64位系統則定義爲unsigned __int64。
在初始化時,其值被設置爲0,爲空閒狀態。
___________________
FORCEINLINE VOID NTAPI KeInitializeSpinLock ( __out PKSPIN_LOCK SpinLock ) { *SpinLock = 0; //將SpinLock初始化爲0,表示鎖的狀態爲空閒狀態 }
___________________
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。
而多處理器呢?提高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前綴,保證了多處理器安全。
___________________
__forceinline VOID KxReleaseSpinLock ( __inout PKSPIN_LOCK SpinLock ) { InterlockedAnd64((LONG64 *)SpinLock, 0);//釋放時進行與操做設置其爲0 }
___________________
好了,對於自旋鎖的初始化、獲取、釋放,都有了瞭解。可是隻是談談原理,看看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 部分