同步篇——臨界區與自旋鎖


寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的復雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。


🔒 華麗的分割線 🔒


並發與同步

  並發是指多個線程在同時執行。單核是分時執行,不是真正的同時,而多核是在某一個時刻,會同時有多個線程再執行。同步則是保證在並發執行的環境中各個線程可以有序的執行。但是這個定義是不太准確,我們給幾個示例請判斷如下代碼是否是並發,先看如下代碼:

void proc1()
{
    int x = 5;
    printf("%d",x);
}

void proc2()
{
    int x = 6;
    printf("%d",x);
}

  請問上面這兩個函數同時執行,存在不存在並發問題呢?其實並不存在,因為局部變量是在棧中分配的,你用你的,我用我的,互不影響。如果是下面的代碼:

int x = 6;

void proc1()
{
    printf("%d",x);
}

void proc2()
{
    printf("%x",x);
}

  這個也是不存在並發問題的,因為這兩個函數雖然都是用到的是同一個變量,但是,它們並沒有修改此變量,這兩個函數怎么執行都不會互相影響,故也不存在並發問題。但是,代碼這樣一改就不行了:

int x = 6;

void proc1()
{
    x--;
}

void proc2()
{
    ++x;
}

  因為這兩個函數執行都會修改全局變量,它們的執行會影響結果,故存在並發問題。如果其他操作用到這個值,將會影響判斷。

臨界區

  在學習臨界區之前,先看看如下代碼:

int x = 6;

void proc1()
{
    x++;
}

  請問這一行x++代碼,是不是線程安全的?
  答案是不是,盡管我們的C代碼是只有一句,但是翻譯成匯編,它就不是一句了。我們假設全局變量x經過編譯器編譯后的地址為0x12345678,那么匯編就翻譯成如下幾句匯編:

mov eax,[0x12345678]
add eax,1
mov [0x12345678],eax

  如果任何一處匯編執行的時候被時鍾中斷進行切換線程,大量的線程執行此函數的結果是不一樣的。比如線程1執行到add eax,1完成被切換走了,線程2執行完此流程,那么,最終這兩個線程執行的結果x的值為2,這就是典型的線程安全問題。如果我改成用INC DWORD PTR DS:[0x12345678]這個匯編來實現此函數功能,這代碼安全嗎?
  對於單核,這個是沒問題的。但是對於多核這是有問題的。就和同時執行兩個線程來修改同一個變量的原因是一樣的,CPU實現肯定是讀取地址獲取數值,然后使用加法器進行加一,然后放回去。但是如何實現多核下的線程安全呢?
  如下匯編就解決了這個問題:

LOCK INC DWORD PTR DS:[0x12345678]

  對,前面加一個LOCK,這個也是一條匯編指令。它是一個鎖,鎖的是你要執行指令的地址,而不是匯編執行的執行。也就是說0x12345678在同一時刻只能由一個核進行訪問修改。這就解決了多核下的線程安全,這種操作也被稱之為原子操作,雖然原子可以再分,但是意思就是不能再分割的操作。
  Windows為了方便我們應用原子操作,也封裝好了幾個函數:InterlockedIncrementInterlockedExchangeAddInterlockedDecrementInterlockedFlushSListInterlockedExchangeInterlockedPopEntrySListInterlockedCompareExchangeInterlockedPushEntrySList。由於怎樣用函數不是我們教程講解的重點,我們研究的是怎樣實現,所以這塊地方就不贅述了。我們來看看微軟是怎么實現原子操作加的:

; int __fastcall InterlockedIncrement(LPLONG lpAddend)
                public InterlockedIncrement
InterlockedIncrement proc near
                mov     eax, 1
                lock xadd [ecx], eax
                inc     eax
                retn
InterlockedIncrement endp

  這幾行就是實現原子操作的。由於是調用約定是快速調用,這個lpAddend參數是由ecx傳遞的。xadd指令就是實現先交換,再相加放到目的操作數,如下是白皮書描述:

Description
Exchanges the first operand (destination operand) with the second operand (source operand), then loads the sum of the two values into the destination operand. The destination operand can be a register or a memory location; the source operand is a register.

Operation
TEMP ← SRC + DEST;
SRC ← DEST;
DEST ← TEMP;

  此原子加法前面加了鎖而ecx指向的變量是多個線程可能訪問的,故線程安全。
  如果我寫了balabala一個代碼塊,想要解決線程安全問題,我想使用LOCK方式解決可以嗎?比如下面這樣:

LOCK INC DWORD PTR DS:[0x12345678]
LOCK INC DWORD PTR DS:[0x12345678]
LOCK INC DWORD PTR DS:[0x12345678]
LOCK INC DWORD PTR DS:[0x12345678]
LOCK INC DWORD PTR DS:[0x12345678]

  這樣雖然每一行都保證只能一個線程占有資源,但是保證這些代碼只能由一個線程運行,還是不能實現的,所以不能實現線程安全。
  好了,鋪墊了這么多,我們來講講臨界區是啥。一次只允許一個線程進入直到離開,這樣的東西就是臨界區,打比方一堆人排隊上一個單間廁所,一次只能由一個人進一個人出,如果人是線程,坑是變量等資源,那么這個廁所就是所謂的臨界區。用代碼演示一下:

DWORD dwFlag = 0;   //實現臨界區的方式就是加鎖
                    //鎖:全局變量  進去加一 出去減一

if(dwFlag == 0)        //進入臨界區
{   
    dwFlag = 1;
    //.......
    //一堆代碼
    //.......
    dwFlag = 0;   //離開臨界區
}

  當然這個代碼是有問題的,不能夠實現臨界區,只是思想展示的示例。如果真正的實現臨界區,就必須用匯編,如下是實現進入臨界區的代碼:

Lab:
    mov eax,1
    lock xadd [Flag],eax
    cmp eax,0
    jz endLab
    dec [Flag]
    //調用線程等待Sleep ……
endLab:
    ret

  其中Flag是上面的全局變量,也就是所謂的鎖。我們再看看如何退出臨界區:

lock dec [Flag]

  為什么加匯編加lock我就不贅述了。不過上面的實現,性能比較差,因為一旦兩個線程同時執行,一個線程正在跑着,另一個就去睡大覺了。對於臨界區,就介紹這么多。

自旋鎖

  自旋鎖也是用來解決同步問題的,為什么這個名字,我們首先看看微軟是如何實現自旋鎖這個東西的,故先定位到如下函數:

; void __stdcall KeAcquireSpinLockAtDpcLevel(PKSPIN_LOCK SpinLock)
                public KeAcquireSpinLockAtDpcLevel
KeAcquireSpinLockAtDpcLevel proc near

SpinLock        = dword ptr  4

                mov     ecx, [esp+SpinLock]

loc_469998:                             ; CODE XREF: KeAcquireSpinLockAtDpcLevel+14↓j
                lock bts dword ptr [ecx], 0
                jb      short loc_4699A2
                retn    4
; ---------------------------------------------------------------------------

loc_4699A2:                             ; CODE XREF: KeAcquireSpinLockAtDpcLevel+9↑j
                                        ; KeAcquireSpinLockAtDpcLevel+18↓j
                test    dword ptr [ecx], 1
                jz      short loc_469998
                pause
                jmp     short loc_4699A2
KeAcquireSpinLockAtDpcLevel endp

  lock bts dword ptr [ecx], 0這行代碼的作用是將ECX指向數據的第0位置1,如果[ECX]原來的值為0,那么CF = 0,否則CF = 1lock保證了只能單核處理。
  如果CF = 1,也就是說這個已經上鎖了,就跳轉到loc_4699A2這個地方,如果鎖上着,就繼續往下走,pause會讓CPU暫停一會,然后死循環,轉起圈,直至鎖被釋放,這就是所謂的自旋鎖。
  自旋鎖只對於多核才有意義,如果是單核反而會造成大量的性能損失。自旋鎖與臨界區、事件、互斥體一樣,都是一種同步機制,都可以讓當前線程處於等待狀態,區別在於自旋鎖不用切換線程,有關自旋鎖的知識就介紹這么多。

本節練習

本節的答案將會在下一節的正文給出,務必把本節練習做完后看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。

  俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到后面,不做練習的話容易夾生了,開始還明白,后來就真的一點都不明白了。本節練習不多,請保質保量的完成,答案解析將在正文展示。

1️⃣ 自己實現一個臨界區,不得使用本篇章介紹的實現。
2️⃣ 多核情況下,實現在高並發的內核函數內部進行Hook,而不能出錯。

下一篇

  同步篇——事件等待與喚醒


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM