同步篇——核心物件

寂靜的羽夏發表於2022-02-12

寫在前面

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

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

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


? 華麗的分割線 ?


前言

  之前講過,執行緒在進入臨界區之前會呼叫WaitForSingleObject或者WaitForMultipleObjects,此時如果有訊號,執行緒會從函式中退出並進入臨界區,如果沒有訊號那麼執行緒將自己掛入等待連結串列,然後將自己掛入等待網,最後切換執行緒。注意我們這裡的臨界區是指只允許一個執行緒進入直到退出的一段程式碼,不單指用EnterCriticalSectionLeaveCriticalSection而形成的臨界區。
  其他執行緒在適當的時候,呼叫方法修改被等待物件的SignalState為有訊號(不同的等待物件,會呼叫不同的函式),並將等待該物件的其他執行緒從等待連結串列中摘掉,這樣,當前執行緒便會在WaitForSingleObject或者WaitForMultipleObjects恢復執行(在哪切換在哪開始執行),如果符合喚醒條件,此時會修改SignalState的值,並將自己從等待網上摘下來,此時的執行緒才是真正的喚醒。
  下面我將介紹不同的可等待核心物件之間的不同之處和實現,但是具體細節將會在總結與提升進行。在講解之前我們把關鍵的結構體放到下面:

kd> dt _DISPATCHER_HEADER
ntdll!_DISPATCHER_HEADER
   +0x000 Type             : UChar
   +0x001 Absolute         : UChar
   +0x002 Size             : UChar
   +0x003 Inserted         : UChar
   +0x004 SignalState      : Int4B
   +0x008 WaitListHead     : _LIST_ENTRY

事件

  首先我們看看事件的核心結構體:

kd> dt _KEVENT
ntdll!_KEVENT
   +0x000 Header           : _DISPATCHER_HEADER

  可以看出事件這個核心物件十分簡潔,就一個內嵌的必要的子結構體,沒有啥雜七雜八的東西。我們通常用CreateEvent函式來進行建立使用這個核心物件,下面我們來講解與同步相關的引數,如下是函式原型:

HANDLE WINAPI CreateEventW(
    LPSECURITY_ATTRIBUTES lpEventAttributes,
    BOOL bManualReset,
    BOOL bInitialState,
    LPCWSTR lpName
    );

  我們重點講解中間兩個引數。

bManualReset

  該值型別為布林型,如果為True,則為通知型別物件;反之則為普通的事件同步物件。
  這個引數影響_DISPATCHER_HEADERType值,如果為通知型別物件,它的值為0,否則為1。我們可以做如下實驗進行驗證:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

HANDLE hEvent;

DWORD WINAPI ThreadProc1(LPVOID param)
{
   WaitForSingleObject(hEvent,INFINITE);
   puts("等待執行緒1執行!");
   return 0;
}

DWORD WINAPI ThreadProc2(LPVOID param)
{
   WaitForSingleObject(hEvent,INFINITE);
   puts("等待執行緒2執行!");
   return 0;
}

int main(int argc, char* argv[])
{
   hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
   CloseHandle(CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)ThreadProc1,NULL,NULL,NULL));
   CloseHandle(CreateThread(NULL,NULL,(LPTHREAD_START_ROUTINE)ThreadProc2,NULL,NULL,NULL));
   SetEvent(hEvent);
   system("pause");
   CloseHandle(hEvent);
   return 0;
}

  如上程式碼就是通知型物件,我們看一看效果:

同步篇——核心物件

  可以看到,這兩個執行緒都執行了,如果我把第二個引數改為FALSE,我們再看看效果:

同步篇——核心物件

  從實驗結果我們可以看出,這裡僅執行了一個執行緒,另一個執行緒仍處於等待狀態,為什麼出現這樣的結果還是逆向WaitForSingleObject才能明白。

bInitialState

  這個就是設定_DISPATCHER_HEADERSignalState初始值的,TRUE就是1,反之就是0。我們將上面我們做實驗的程式碼的第三個引數改為TRUE,並註釋掉SetEvent函式,實驗效果如下:

同步篇——核心物件

  關於事件就介紹這麼多,其餘的詳細細節將會在總結與提升進行介紹。

訊號量

  有了事件這一個可等待物件,為什麼要有訊號量這東西。訊號量又是什麼。

訊號量(Semaphore),有時被稱為訊號燈,是在多執行緒環境下使用的一種設施,是可以用來保證兩個或多個關鍵程式碼段不被併發呼叫。在進入一個關鍵程式碼段之前,執行緒必須獲取一個訊號量;一旦該關鍵程式碼段完成了,那麼該執行緒必須釋放訊號量。

  這個雖然聽起來說了訊號量是啥,但又啥也沒說。我們將通過示例來進行講解。
  我們之前使用事件這個東西,如果有訊號,如果是用於同步的型別,設定訊號只能讓1個執行緒通過WaitForSingleObjectSetEvent組成的程式碼臨界區;如果是通知型別,就讓全部的執行緒通過,示意圖如下:

同步篇——核心物件

  但是有種情況,我讓其有訊號的時候,假設有5個執行緒,但我讓3個執行緒執行,這個是事件方便實現的,於是乎,訊號量應運而生,如下是其示意圖:

同步篇——核心物件

  如果做專案比較多的話,就會遇到生成者執行緒和消費者執行緒問題,如下圖所示:

同步篇——核心物件

  如果是上面第一個情況,這兩種執行緒各一個執行緒,運用事件就可以很好的解決問題。但是對於第二種情況,這就不太適用了。如果我讓這代表全域性變數的三個綠塊都加1,把它們都啟用或者一個一個啟用總是不太方便的,這個問題通過訊號量就能輕鬆解決。我們來看看建立訊號量的函式原型:

HANDLE WINAPI CreateSemaphoreW(
    LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
    LONG lInitialCount,
    LONG lMaximumCount,
    LPCWSTR lpName
    );

  然後看看訊號量的結構體長啥樣子:

kd> dt _KSEMAPHORE
ntdll!_KSEMAPHORE
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 Limit            : Int4B

lInitialCount

  這個引數會影響_DISPATCHER_HEADERSignalState的值。它不會像事件只設定0或者1,它可以設定更大的數,這個就是實現指定數目執行緒執行的關鍵所在。

lMaximumCount

  這個引數會影響KSEMAPHORE結構體中的Limit這個成員,是訊號量物件的最大計數。

ReleaseSemaphore 淺析

  既然有建立訊號量,就有釋放訊號量函式,讓的呼叫流程如下:

graph TD ReleaseSemaphore --> NtReleaseSemaphore --> KeReleaseSemaphore

  有關訊號量的其他具體細節將會在同步與提升進行講解。

互斥體

  前面有事件、訊號量,為什麼還要有互斥體這個東西。肯定一個東西的出現必須解決一些問題。互斥體MUTANT與事件EVENT和訊號量SEMAPHORE一樣,都可以用來進行執行緒的同步控制。但是,這幾個物件都是核心物件,這就意味著,通過這些物件可以進行跨程式的執行緒同步控制。假設有A程式中的X執行緒和B程式中的Y執行緒,它們都在等待核心物件Z。如果B程式的Y執行緒還沒有來得及呼叫修改SignalState的函式,那麼等待物件Z將被遺棄,這也就以為者X執行緒將永遠等下去。
  互斥體還解決臨界區衝入的問題,如下是一段程式碼:

WaitForSingleObject(A)
.....
WaitForMultipleObjects(A,B,C)
.....

  其中ABC都是等待物件。開始程式碼我們等待A,然後又繼續等待ABC,如果它們都有一次訊號,那麼執行緒就會停到WaitForMultipleObjects不動,這就是所謂的死鎖。
  介紹互斥體之前,我們看看其內部結構:

kd> dt _KMUTANT
nt!_KMUTANT
   +0x000 Header           : _DISPATCHER_HEADER
   +0x010 MutantListEntry  : _LIST_ENTRY
   +0x018 OwnerThread      : Ptr32 _KTHREAD
   +0x01c Abandoned        : UChar
   +0x01d ApcDisable       : UChar

  OwnerThread指向的是正在擁有互斥體的執行緒結構體。Abandoned指示是否已經被棄用的標誌。ApcDisable指示是否禁用核心APC
  那麼,互斥體是如何解決等待物件被遺棄問題呢?原因就存在MutantListEntry這個起作用,這個也是用來串糖葫蘆的。線上程結構體的0x10偏移處的MutantListHead成員把擁有互斥體串串。當程式解除安裝時會呼叫MmUnloadSystemImage,最終會呼叫KeReleaseMutant這個函式來釋放互斥體,而這個函式就會用到Abandon這個值。具體細節將會在總結與提升進行分析。
  對於應用層的核心結構Mutant,核心有一個和這個結構一模一樣的Mutex。但是它們的區別就是ApcDisable值不一樣,Mutex的值為1,而Mutant的值為0,可以通過它的初始化程式碼可以看出。其餘的細節將會在下一篇進行講解。

下一篇

  同步篇——總結與提升

相關文章