一、函數(shù)多線程的安全問題
函數(shù)多線程安全指的是,當(dāng)一個函數(shù)在被調(diào)用但尚未返回時,如果被其他線程再次調(diào)用,其執(zhí)行結(jié)果仍然是可靠的。
在用戶層編寫多線程程序時,我們通常會關(guān)注同步問題,以確保線程安全。同樣地,在內(nèi)核中也需要注意以下幾點:
-
可能在多線程環(huán)境中運行的函數(shù)必須保證線程安全。如果運行在單線程環(huán)境中,則不需要考慮線程安全性,因為沒有其他線程操作。
-
如果函數(shù)A調(diào)用函數(shù)B,函數(shù)B調(diào)用函數(shù)C,且A和B都運行在同一單線程中,那么C也應(yīng)保證運行在單線程中。
-
如果函數(shù)A調(diào)用函數(shù)B,函數(shù)B調(diào)用函數(shù)C,且B和C可能在多線程環(huán)境中運行,那么A也可能在多線程環(huán)境中運行。因為A會調(diào)用B和C,所以B和C在多線程環(huán)境中運行。
-
如果函數(shù)A調(diào)用函數(shù)B,函數(shù)B調(diào)用函數(shù)C,且B運行在多線程環(huán)境中,那么需要采取多線程序列化為單線程的強制措施,使得函數(shù)B在單線程中運行。
內(nèi)核中的“多線程序列化為單線程的強制措施”包括互斥體和自旋鎖。
-
只使用函數(shù)內(nèi)部資源,不使用全局變量、靜態(tài)變量或其他全局性資源的函數(shù)是多線程安全的。
-
如果使用全局變量、靜態(tài)變量等,那么需要使用同步函數(shù)來進(jìn)行同步。
二、中斷級別
這篇博客主要討論了理論和需要注意的問題。在內(nèi)核中,Kernel API有中斷級別之分。
在用戶層,代碼都是同級別運行,因此不需要關(guān)心中斷級別,但在Kernel內(nèi)核中,必須關(guān)注中斷級別,否則可能會導(dǎo)致持續(xù)的問題。
目前,中斷級別有兩個:
- Passive級別。
- Dispatch級別。
關(guān)于這兩種級別,內(nèi)核博客的前兩課有簡單介紹其本質(zhì)。我們只需注意這兩種級別的區(qū)別即可。
簡單的函數(shù)運行在Dispatch級別中。因此,在調(diào)用任何Kernel API之前,都需要查詢一下中斷級別。
Dispatch級別比Passive級別高。
如何判斷中斷級別?
- 調(diào)用路徑:A -> B -> C,那么C的調(diào)用路徑就是A和B,因為是一條線。
- 調(diào)用源:A -> B -> C,那么一次遞推到A,那么C的調(diào)用源就是A。
判斷規(guī)則:
- 如果調(diào)用路徑上沒有特殊情況(導(dǎo)致中斷級別提高或降低的情況),那么函數(shù)執(zhí)行時的中斷級別與其調(diào)用源級別相同。
- 如果調(diào)用路徑上有獲取自旋鎖,則中斷級別隨之提高;如果有釋放自旋鎖,則中斷級別降低。
內(nèi)核中的中斷級別:
調(diào)用源 | 級別 |
---|---|
DriverEntry | Passive級別 |
DriverUnload | Passive級別 |
各種分發(fā)函數(shù) | Passive級別 |
完成函數(shù) | Dispatch級別 |
各種NDIS回調(diào)函數(shù) | Dispatch級別 |
查詢Kernel API時,會發(fā)現(xiàn)有說明該API在什么級別使用。
例如:
可以看到中斷級別是…
疑問?
如果當(dāng)前代碼運行在DISPATCH級別,但必須調(diào)用PASSIVE級別的API,該怎么辦?可以利用內(nèi)核API降低當(dāng)前中斷級別嗎?
答:
不可以。Windows代碼都是在規(guī)范的級別上運行的,任意降低或提高都會影響代碼的執(zhí)行。因此,在需要調(diào)用這種API時,可以創(chuàng)建一個專門的線程來執(zhí)行PASSIVE級別的代碼。
解決方法很多,可以在網(wǎng)絡(luò)上搜索相關(guān)資料。博主也是自學(xué),所以暫時還沒有接觸到更多方法。
三、內(nèi)核中宏代碼代表的意思
在內(nèi)核中查看API時,可以看到許多宏,這些宏都是空宏,用于說明。
例如:
- IN:表示這個參數(shù)是傳遞進(jìn)去的。
- OUT:表示這個參數(shù)是傳出參數(shù)。
例如:
__in_bcount(StatusBuffsize) IN PVOID statusBuffer;
這里的參數(shù)表示statusBuffer的大小依賴于StatusBuffsize這個參數(shù)來指定。
四、指定函數(shù)位置的預(yù)編譯指令
這個小主題本來可以放在第三個問題中,但單獨說明更重要。
#pragma alloc_text()
這個宏表示我們的函數(shù)的可執(zhí)行代碼編譯出來之后在.sys文件中的位置。
.sys文件本質(zhì)上是PE文件,PE文件有段和節(jié),不同的節(jié)加載到內(nèi)存中會有不同的處理情況。我們需要關(guān)注的有三種:
- INIT節(jié):這個節(jié)的特點是初始化完畢后就會釋放,不再占用內(nèi)存空間。
- PAGE節(jié):這個節(jié)的特點是可以進(jìn)行分頁交換的內(nèi)存空間,即內(nèi)存不夠時可以臨時放到磁盤上。
- PAGELK節(jié):這是默認(rèn)的節(jié),如果不指定代碼放在哪里,就會放在這個節(jié)中。特點是不能進(jìn)行內(nèi)存交換,即不可以和磁盤交互。
我們可以指定我們的代碼放到哪個節(jié)中。
例如,入口函數(shù)只會調(diào)用一次,可以放到INIT節(jié)中,這樣初始化完后就不占用內(nèi)核內(nèi)存,因為內(nèi)核內(nèi)存是共享的,用完就沒了。
例如:
#pragma alloc_text(INIT,DriverEntry)
注意的問題:
如果將函數(shù)放入PAGE節(jié)中,代表該函數(shù)運行中可以放到磁盤上,這樣會產(chǎn)生缺頁中斷。因此,放到PAGE節(jié)中的函數(shù)不能調(diào)用DISPATCH級別的函數(shù)。