1. 程式人生 > 實用技巧 >關於臨界區CRITICAL_SECTION

關於臨界區CRITICAL_SECTION

有時候遇到執行緒鎖的問題,那麼瞭解CRITICAL_SECTION就顯得比較重要了

( 更詳細的資料可以查閱:https://www.cnblogs.com/crj8812/p/4499058.html)

  在臨界區未被使用的理想情況中,對 EnterCriticalSection 的呼叫非常快速,因為它只是讀取和修改使用者模式記憶體中的記憶體位置。否則(在後文將會遇到一種例外情況),阻止於臨界區的執行緒有效地完成這一工作,而不需要消耗額外的 CPU 週期。所阻止的執行緒以核心模式等待,在該臨界區的所有者將其釋放之前,不能對這些執行緒進行排程。如果有多個執行緒被阻止於一個臨界區中,當另一執行緒釋放該臨界區時,只有一個執行緒獲得該臨界區。

深入研究: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 欄位(此欄位被錯誤地定義於 WINNT.H 中 — 應當是 DWORD 而不是 HANDLE)包含了擁有此臨界區的執行緒 ID。此欄位與 (RecursionCount -1) 數值之間的差值表示有多少個其他執行緒在等待獲得該臨界區。

RecursionCount此欄位包含所有者執行緒已經獲得該臨界區的次數。如果該數值為零,下一個嘗試獲取該臨界區的執行緒將會成功。

OwningThread此欄位包含當前佔用此臨界區的執行緒的執行緒識別符號。此執行緒 ID 與 GetCurrentThreadId 之類的 API 所返回的 ID 相同。

LockSemaphore此欄位的命名不恰當,它實際上是一個自復位事件,而不是一個訊號。它是一個核心物件控制代碼,用於通知作業系統:該臨界區現在空閒。作業系統在一個執行緒第一次嘗試獲得該臨界區,但被另一個已經擁有該臨界區的執行緒所阻止時,自動建立這樣一個控制代碼。應當呼叫 DeleteCriticalSection(它將發出一個呼叫該事件的 CloseHandle 呼叫,並在必要時釋放該除錯結構),否則將會發生資源洩漏。

SpinCount僅用於多處理器系統。MSDN 文件對此欄位進行如下說明:“在多處理器系統中,如果該臨界區不可用,呼叫執行緒將在對與該臨界區相關的訊號執行等待操作之前,旋轉 dwSpinCount 次。如果該臨界區在旋轉操作期間變為可用,該呼叫執行緒就避免了等待操作。”旋轉計數可以在多處理器計算機上提供更佳效能,其原因在於在一個迴圈中旋轉通常要快於進入核心模式等待狀態。此欄位預設值為零,但可以用 InitializeCriticalSectionAndSpinCount API 將其設定為一個不同值。

RTL_CRITICAL_SECTION_DEBUG 結構

前面我們注意到,在 RTL_CRITICAL_SECTION 結構內,DebugInfo 欄位指向一個 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 和事件鏈中其他參與者之間的關係。

ProcessLocksListLIST_ENTRY 是用於表示雙向連結串列中節點的標準 Windows 資料結構。RTL_CRITICAL_SECTION_DEBUG 包含了連結串列的一部分,允許向前和向後遍歷該臨界區。本文後面給出的實用工具說明如何使用 Flink(前向連結)和 Blink(後向連結)欄位在連結串列中的成員之間移動。任何從事過裝置驅動程式或者研究過 Windows 核心的人都會非常熟悉這一資料結構。

EntryCount/ContentionCount這些欄位在相同的時間、出於相同的原因被遞增。這是那些因為不能馬上獲得臨界區而進入等待狀態的執行緒的數目。與 LockCount 和 RecursionCount 欄位不同,這些欄位永遠都不會遞減。

Spares這兩個欄位未使用,甚至未被初始化(儘管在刪除臨界區結構時將這些欄位進行了清零)。後面將會說明,可以用這些未被使用的欄位來儲存有用的診斷值。

即使 RTL_CRITICAL_SECTION_DEBUG 中包含多個欄位,它也是常規臨界區結構的必要成分。事實上,如果系統恰巧不能由程序堆中獲得這一結構的儲存區,InitializeCriticalSection 將返回為 STATUS_NO_MEMORY 的 LastError 結果,然後返回處於不完整狀態的臨界區結構。