明明白白自旋鎖

明明白白自旋鎖

自某日看了iceboy和MJ0011關於多處理器同步的一些討論,才發現原來我對自旋鎖的理解也有一定錯誤,還好現在明白了~~爲了加深理解,就深入分析了一下自旋鎖的實現,一篇小小的自旋鎖分析文章,獻給大家。寫得比較碎,歡迎各位大牛小牛指出錯誤~
一、自旋鎖是什麼?
先進行下簡單科普,自旋鎖是一種輕量級的多處理器間的同步機制。因此,自旋鎖對於單處理器是沒有實際意義的。它要求持有鎖的處理器所佔用的時間儘可能短,因爲此時別的處理器正在高速運轉並等待鎖的釋放,所以不能長時間佔有。
曾經有個經典的例子來比喻自旋鎖:A,B兩個人合租一套房子,共用一個廁所,那麼這個廁所就是共享資源,且在任一時刻最多只能有一個人在使用。當廁所閒置時,誰來了都可以使用,當A使用時,就會關上廁所門,而B也要使用,但是急啊,就得在門外焦急地等待,急得團團轉,是爲“自旋”,呵呵。這個比喻還算恰當吧,大家也明白爲什麼要求鎖的持有時間儘量短了吧!尤其b4佔着茅坑不拉屎的行爲~~
二、操作系統如何實現自旋鎖?
在Linux和Windows中都實現了自旋鎖,下面我們就來看一看Windows下是如何實現的吧。
自旋鎖的結構:
KSPIN_LOCK SpinLock;
KSPIN_LOCK實際是一個操作系統相關的無符號整數,32位系統上是32位的unsigned long,64位系統則定義爲unsigned __int64。
在初始化時,其值被設置爲0,爲空閒狀態。
參見WRK:
Copy code
FORCEINLINE
VOID
NTAPI
KeInitializeSpinLock (
    __out PKSPIN_LOCK SpinLock
    )
{
    *SpinLock = 0;
}

關於自旋鎖的兩個基本操作:獲取和釋放
VOID
KeAcquireSpinLock(
    IN PKSPIN_LOCK  SpinLock,
    OUT PKIRQL  OldIrql
    );
VOID
  KeReleaseSpinLock(
    IN PKSPIN_LOCK  SpinLock,
    IN KIRQL  NewIrql
    );
獲取時做了哪些工作呢?
Ntddk.h中是這樣定義的:
#define KeAcquireSpinLock(SpinLock, OldIrql) /
*(OldIrql) = KeAcquireSpinLockRaiseToDpc(SpinLock)
很明顯,核心的操作對象是SpinLock,同時也與IRQL有關。
再翻翻WRK,找到KeAcquireSpinLockRaiseToDpc的定義:
Copy code
__forceinline
KIRQL
KeAcquireSpinLockRaiseToDpc (
    __inout PKSPIN_LOCK SpinLock
    )
{

    KIRQL OldIrql;
    //
    // Raise IRQL to DISPATCH_LEVEL and acquire the specified spin lock.
    //
    OldIrql = KfRaiseIrql(DISPATCH_LEVEL);
    KxAcquireSpinLock(SpinLock);
    return OldIrql;
}

首先會提升IRQL到DISPATCH_LEVEL,然後調用KxAcquireSpinLock()。(若當前IRQL就是DISPATCH_LEVEL,那麼就調用KeAcquireSpinLockAtDpcLevel,省去提升IRQL一步)。因爲線程調度也是發生在DISPATCH_LEVEL,所以提升IRQL之後當前處理器上就不會發生線程切換。單處理器時,當前只能有一個線程被執行,而這個線程提升IRQL至DISPATCH_LEVEL之後又不會因爲調度被切換出去,自然也可以實現我們想要的互斥“效果”,其實只操作IRQL即可,無需SpinLock。實際上單核系統的內核文件ntosknl.exe中導出的有關SpinLock的函數都只有一句話,就是return,呵呵。
而多處理器呢?提升IRQL只會影響到當前處理器,保證當前處理器的當前線程不被切換,那還得考慮其它處理器啊,繼續看 KxAcquireSpinLock()函數吧。在WRK中找到的KxAcquireSpinLock()函數是Amd64位處理器上的代碼(位於(/inc/private/ntos/inc/Amd64.h)中),32位x86的沒找到。不過原理相通,一樣可以參考
Copy code
__forceinline
VOID
KxAcquireSpinLock (
    __inout PKSPIN_LOCK SpinLock
    )
{
    if (InterlockedBitTestAndSet64((LONG64 *)SpinLock, 0))//64位函數
    {

        KxWaitForSpinLockAndAcquire(SpinLock);    //只有聲明沒有定義的函數,應該是做了測試,等待的工作
    }
}

InterlockedBitTestAndSet64()函數的32位版本如下:
ps:我彙編功底不太好,見諒~
Copy code
BOOLEAN
FORCEINLINE
InterlockedBitTestAndSet (
    IN LONG *Base,
    IN LONG Bit
    )
{
   
__asm {
          mov eax, Bit
          mov ecx, Base
          lock bts [ecx], eax
          setc al
    };
}

關鍵就在bts指令,是一個進行位測試並置位的指令。,這裏在進行關鍵的操作時有lock前綴,保證了多處理器安全。InterLockedXXX函數都有這個特點。顯然,KxAcquireSpinLock()函數先測試鎖的狀態。若鎖空閒,則*SpinLock爲0,那麼InterlockedBitTestAndSet()將返回0,並使*SpinLock置位,不再爲0。這樣KxAcquireSpinLock()就成功得到了鎖,並設置鎖爲佔用狀態(*SpinLock不爲0),函數返回。若鎖已被佔用呢?InterlockedBitTestAndSet()將返回1,此時將調用KxWaitForSpinLockAndAcquire()等待並獲取這個鎖。這表明,SPIN_LOCK爲0則鎖空閒,非0則已被佔有。
由於WRK中僅有KxWaitForSpinLockAndAcquire()的聲明而無定義,我們只能從名字猜測其做了什麼。在WRK中看到了這兩個函數:
Copy code
__forceinline
BOOLEAN
KxTryToAcquireSpinLock (
    __inout PKSPIN_LOCK SpinLock
    )
{
if (*(volatile LONG64 *)SpinLock == 0)
  {
    return !InterlockedBitTestAndSet64((LONG64 *)SpinLock, 0);
  }
else
{
        KeYieldProcessor();
        return FALSE;
}
}

從名字看應該是試圖獲取自旋鎖,先判斷鎖是否被佔有。若空閒,則設置其爲佔用狀態,這就成功地搶佔了。若已被佔用,則調用KeYieldProcessor(),這個函數其實只是一個宏:
#define KeYieldProcessor()    __asm { rep nop } //空轉
都知道nop幹啥的,CPU就是在空轉進行等待而已。
下面這個函數則是僅測試自旋鎖的狀態:
Copy code
__forceinline
BOOLEAN
KeTestSpinLock (
    __in PKSPIN_LOCK SpinLock
    )
{
    KeMemoryBarrierWithoutFence();//這個函數我也不知道幹啥的
    if (*SpinLock != 0) {
        KeYieldProcessor();//若被佔用,則空轉
        return FALSE;

    } else {
        return TRUE;
    }
}

好,看了獲取部分,再看看釋放鎖的時候做了什麼。
Copy code
__forceinline
VOID
KeReleaseSpinLock (
    __inout PKSPIN_LOCK SpinLock,
    __in KIRQL OldIrql
    )
{
    KxReleaseSpinLock(SpinLock);//先釋放鎖
  KeLowerIrql(OldIrql);//恢復至原IRQL
    return;
}

繼續看KxReleaseSpinLock()
Copy code
__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
代碼比較簡單,還原成源碼是這樣子的(偷懶用了F5):
Copy code
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就代表了將其釋放,此時那些如虎狼般瘋狂空轉的其它處理器將馬上獲知這一信息,於是,下一個獲取、釋放的過程開始了。這就是最基本的自旋鎖,其它一些自旋鎖形式是對這種基本形式的擴充。比如排隊自旋鎖,是爲了解決多處理器競爭時的無序狀態等等,不多說了。
現在對自旋鎖可謂真的是明明白白了,之前我犯的錯誤就是以爲用了自旋鎖就能保證多核同步,其實不是的,用自旋鎖來保證多核同步的前提是大家都要用這個鎖。若當前處理器已佔有自旋鎖,只有別的處理器也來請求這個鎖時,纔會進入空轉,不進行別的操作,這時你的操作將不會受到干擾。但是假如某個需要互斥的操作只有你這個線程才做而別人根本不去做(以iceboy的安全實現Inline Hook爲例,請求鎖、修改代碼、釋放鎖的過程只有這個線程纔會做,別的處理器上的
線程如果要執行這裏還是照樣執行,人家又不用修改),所以人家不請求鎖時還是該幹嘛幹嘛啊,自己在那兒自旋只是一廂情願。所以MJ說“別人都不跟你旋,你自己旋個頭啊”,經典經典…….
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章