寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看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
為了方便我們應用原子操作,也封裝好了幾個函式:InterlockedIncrement
、InterlockedExchangeAdd
、InterlockedDecrement
、InterlockedFlushSList
、InterlockedExchange
、InterlockedPopEntrySList
、InterlockedCompareExchange
、InterlockedPushEntrySList
。由於怎樣用函式不是我們教程講解的重點,我們研究的是怎樣實現,所以這塊地方就不贅述了。我們來看看微軟是怎麼實現原子操作加的:
; 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 = 1
,否則CF = 0
,lock
保證了只能單核處理。
如果CF = 1
,也就是說這個已經上鎖了,就跳轉到loc_4699A2
這個地方,如果鎖上著,就繼續往下走,pause
會讓CPU
暫停一會,然後死迴圈,轉起圈,直至鎖被釋放,這就是所謂的自旋鎖。
自旋鎖只對於多核才有意義,如果是單核反而會造成大量的效能損失。自旋鎖與臨界區、事件、互斥體一樣,都是一種同步機制,都可以讓當前執行緒處於等待狀態,區別在於自旋鎖不用切換執行緒,有關自旋鎖的知識就介紹這麼多。
本節練習
本節的答案將會在下一節的正文給出,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習不多,請保質保量的完成。
1️⃣ 自己實現一個臨界區,不得使用本篇章介紹的實現。
2️⃣ 多核情況下,實現在高併發的核心函式內部進行Hook
,而不能出錯。
下一篇