臨界區:
臨界區是一種輕量級機制,在某一時間內只允許一個線程執行某個給定代碼段。通常在多線程修改全局數據時會使用臨界區。事件、信號量也用於多線程同步,但臨界區與它們不同,並不總是執行向內核模式的切換,這一轉換成本昂貴。要獲得一個未占用臨界區,事實上只需要對內存做出很少的修改,其速度非常快。只有在嘗試獲得已占用臨界區時,它才會跳至內核模式。這一輕量級特性的缺點在於臨界區只能用於對同一進程內的線程進行同步。
臨界區由 WINNT.H 中所定義的 RTL_CRITICAL_SECTION 結構表示。 WINBASE.H 后您會發現:
typedef RTL_CRITICAL_SECTION CRITICAL_SECTION;
操作臨界區的API函數有:
(1)初始化臨界區InitializeCriticalSection
(2)進入臨界區EnterCriticalSection
(3)離開臨界區LeaveCriticalSection
(4)刪除臨界區DeleteCriticalSection
在臨界區未被使用的理想情況中,對 EnterCriticalSection 的調用非常快速,因為它只是讀取和修改用戶模式內存中的內存位置。所阻止的線程以內核模式等待,在該臨界區的所有者將其釋放之前,不能對這些線程進行調度。如果有多個線程被阻止於一個臨界區中,當另一線程釋放該臨界區時,只有一個線程獲得該臨界區。
RTL_CRITICAL_SECTION 結構
一個進程的臨界區是保存於一個鏈表中,並且可以對其進行枚舉。實際上,WINDBG 支持 !locks 命令,這一命令可以列出目標進程中的所有臨界區。
RTL_CRITICAL_SECTION 結構如下:
struct RTL_CRITICAL_SECTION
{
PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
LONG LockCount;
LONG RecursionCount;
HANDLE OwningThread;
HANDLE LockSemaphore;
ULONG_PTR SpinCount;
};
以下各段對每個字段進行說明。
DebugInfo 此字段包含一個指針,指向系統分配的伴隨結構,該結構的類型為 RTL_CRITICAL_SECTION_DEBUG。這一結構中包含更多極有價值的信息,也定義於 WINNT.H 中。LockCount 這是臨界區中最重要的一個字段。它被初始化為數值 -1;此數值等於或大於 0 時,表示此臨界區被占用。當其不等於 -1 時,OwningThread 字段包含了擁有此臨界區的線程 ID。此字段與 (RecursionCount-1) 數值之間的差值表示有多少個其他線程在等待獲得該臨界區。
RecursionCount 此字段包含所有者線程已經獲得該臨界區的次數。如果該數值為零,下一個嘗試獲取該臨界區的線程將會成功。
OwningThread 此字段包含當前占用此臨界區的線程的線程標識符。此線程 ID 與 GetCurrentThreadId 之類的 API 所返回的 ID 相同。
LockSemaphore 它是一個內核對象句柄,用於通知操作系統:該臨界區現在空閑。操作系統在一個線程第一次嘗試獲得該臨界區,但被另一個已經擁有該臨界區的線程所阻止時,自動創建這樣一個句柄。應當調用 DeleteCriticalSection(它將發出一個調用該事件的 CloseHandle 調用,並在必要時釋放該調試結構),否則將會發生資源泄漏。
SpinCount 僅用於多處理器系統。MSDN文檔對此字段進行如下說明:“在多處理器系統中,如果該臨界區不可用,調用線程將在對與該臨界區相關的信號執行等待操作之前,旋轉 dwSpinCount 次。如果該臨界區在旋轉操作期間變為可用,該調用線程就避免了等待操作。”旋轉計數可以在多處理器計算機上提供更佳性能,其原因在於在一個循環中旋轉通常要快於進入內核模式等待狀態。此字段默認值為零,但可以用 InitializeCriticalSectionAndSpinCount API 將其設置為一個不同值。
RTL_CRITICAL_SECTION_DEBUG結構如下:
struct _RTL_CRITICAL_SECTION_DEBUG
{
WORD Type;
WORD CreatorBackTraceIndex;
RTL_CRITICAL_SECTION *CriticalSection;
LIST_ENTRY ProcessLocksList;
DWORD EntryCount;
DWORD ContentionCount;
DWORD Spare[ 2 ];
}
這一結構由InitializeCriticalSection分配和初始化。它既可以由NTDLL內的預分配數組分配,也可以由進程堆分配。RTL_CRITICAL_SECTION的這一伴隨結構包含一組匹配字段,具有迥然不同的角色:有兩個難以理解,隨后兩個提供了理解這一臨界區鏈結構的關鍵,兩個是重復設置的,最后兩個未使用。
下面是對 RTL_CRITICAL_SECTION 字段的說明。
Type 此字段未使用,被初始化為數值 0。
CreatorBackTraceIndex 此字段僅用於診斷情形中。在注冊表項 HKLM\Software\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\YourProgram 之下是 keyfield、GlobalFlag 和 StackTraceDatabaseSizeInMb 值。注意,只有在運行稍后說明的 Gflags 命令時才會顯示這些值。這些注冊表值的設置正確時,CreatorBackTraceIndex 字段將由堆棧跟蹤中所用的一個索引值填充。在 MSDN 中搜索 GFlags 文檔中的短語“create user mode stack trace database”和“enlarging the user-mode stack trace database”,可以找到有關這一內容的更多信息。
CriticalSection 指向與此結構相關的 RTL_CRITICAL_SECTION。圖 1 說明該基礎結構以及 RTL_CRITICAL_SECTION、RTL_CRITICAL_SECTION_DEBUG 和事件鏈中其他參與者之間的關系。
圖 1 臨界區處理流程
ProcessLocksList LIST_ENTRY 是用於表示雙向鏈表中節點的標准 Windows 數據結構。RTL_CRITICAL_SECTION_DEBUG 包含了鏈表的一部分,允許向前和向后遍歷該臨界區。本文后面給出的實用工具說明如何使用 Flink(前向鏈接)和 Blink(后向鏈接)字段在鏈表中的成員之間移動。任何從事過設備驅動程序或者研究過 Windows 內核的人都會非常熟悉這一數據結構。
EntryCount/ContentionCount 這些字段在相同的時間、出於相同的原因被遞增。這是那些因為不能馬上獲得臨界區而進入等待狀態的線程的數目。與 LockCount 和 RecursionCount 字段不同,這些字段永遠都不會遞減。
Spares 這兩個字段未使用,甚至未被初始化(盡管在刪除臨界區結構時將這些字段進行了清零)。后面將會說明,可以用這些未被使用的字段來保存有用的診斷值。
總結:
(1)如果 LockCount 字段有一個不等於 -1 的數值,此臨界區被占用,OwningThread 字段包含擁有該臨界區的線程的線程標識符。
(2)如果 RecursionCount 是一個大於 1 的數值,其告知您所有者線程已經重新獲得該臨界區多少次(也許不必要)。
(3)LockCount 與 RecursionCount 字段中分別包含其初始值 -1 和 0,這一點非常重要。事實上,對於單線程程序,不能僅通過檢查這些字段來判斷是否曾獲得過臨界區。但是,多線程程序留下了一些標記,可以用來判斷是否有兩個或多個線程試圖同時擁有同一臨界區。
(4)在該臨界區未被占用時 LockSemaphore 字段中仍包含一個非零值。這表示:在某一時間,此臨界區阻止了一個或多個線程。事件句柄用於通知該臨界區已被釋放,等待該臨界區的線程之一現在可以獲得該臨界區並繼續執行。因為 OS 在臨界區阻止另一個線程時自動分配事件句柄,所以如果您在不再需要臨界區時忘記將其刪除,LockSemaphore 字段可能會導致程序中發生資源泄漏。
(5)在多線程程序中可能遇到的另一狀態是 EntryCount 和 ContentionCount 字段包含一個大於零的數值。這兩個字段保存有臨界區對一個線程進行阻止的次數。在每次發生這一事件時,這兩個字段被遞增,但在臨界區存在期間不會被遞減。這些字段可用於間接確定程序的執行路徑和特性。例如,EntryCount 非常高時則意味着該臨界區經歷着大量爭用,可能會成為代碼執行過程中的一個潛在瓶頸。
(6)可以通過RTL_CRITICAL_SECTION_DEBUG 中的LIST_ENTRY 遍歷進程中的臨界區,Flink=NULL為表頭,Blink=NULL為表尾。
(7)利用RTL_CRITICAL_SECTION 的 Spare 字段可以區分我們定義的臨界區和系統定義的臨界區。
但是如何知道哪些線程等待某個臨界區呢?