同步下的資源互斥:停運保護(Run-Down Protection)機制
背景
近期在學習ofollow,noindex" target="_blank"> ProcessHacker 的原始碼,Process Hacker 是一個免費的、功能強大的 “工作管理員 ” ,可用於監聽系統資源的使用情況,除錯軟體以及檢測惡意程式。使用中你會發現其可以與 Sysinternals 開發的Process Explorer 相媲美。最重要的它是開源的,原始碼均可以在Github 上檢視,這使得我們有機會深入瞭解其實現原理和窺探一些重要的Windows系統介面。我的計劃是結合《深入解析windows作業系統 》這本書籍學習一些Windows 系統原理的相關知識。
關於停運保護(Run-Down Protection)機制
關於停運保護(暫且這樣翻譯)的介紹,發哥我翻找了官方的資料,邊理解邊做了部分的翻譯,如有錯誤或模稜兩可之處,還請你高抬貴手幫忙指出:
從WindowsXP 開始,核心驅動就支援停運保護機制。驅動通過停運機制可以安全地 訪問在系統記憶體中的物件,通常這些物件是由其他核心驅動建立和銷燬的。
當對一個物件的所有訪問操作已經完成並且不再允許其他新的操作請求,那麼就可以將這個物件視為停運的。比如說一個共享物件可能需要被停運,這樣的話它就可以被清理然後用新的物件替換它。
擁有共享物件的驅動允許其他驅動對該物件請求並實施停運保護機制。當停運保護生效時,除物件的所有者外,其他驅動可以訪問該物件而不用擔心在訪問結束前該物件會被其所有者刪除。在訪問開始之前,要訪問的驅動會提出對目標物件實施停運保護的請求。對於一個存活週期較長的物件來說,這類請求幾乎都是被允許的。當訪問結束時,執行訪問的驅動會卸除之前對物件實施的停運保護。
常規的停運保護流程
要想共享一個物件,擁有該物件的驅動要呼叫 ExInitializeRundownProtection 函式以初始化停運保護機制,在這之後,其他要訪問此物件的驅動就可以對其實施和撤銷停運保護功能。
要訪問共享物件的驅動通過呼叫 ExAcquireRundownProtection 函式來請求對該物件的停運保護,當訪問結束後,驅動通過呼叫 ExReleaseRundownProtection 來取消停運保護。
如果物件擁有者打算刪除共享物件,它將呼叫 ExWaitForRundownProtectionRelease 來等待物件停運。在這期間,驅動呼叫執行緒會被阻塞,該函式會一直等待直至在之前被允許的所有停運保護被釋放,同時拒絕新的停運保護請求。直到最後一次的訪問結束並且所有停運保護被釋放後,ExWaitForRundownProtectionRelease 方才返回,這時物件的擁有者就可以安全地刪除該物件了。為了防止等待阻塞過長時間,訪問物件的驅動執行緒在實施停運保護的過程中應避免出現延緩的情況。
適合使用場景
停運機制很適合用於那些經常有效可用但不知何時會突然被刪除或替換的共享物件,訪問共享物件資料的驅動或者是呼叫執行緒在物件被刪除後需確保不再嘗試訪問該物件,否則這些非法訪問可能會造成無法預料的行為後果比如資料損壞,更嚴重點甚至會出現系統崩潰。
舉個例子,典型的病毒防禦驅動在作業系統執行時需要長時間載入到記憶體中。執行期間,其他驅動會發送IO請求到防禦驅動以訪問驅動中的資料和函式,但有時驅動需要被解除安裝和更新,為避免驅動還在處理IO請求時過早地被解除安裝,在傳送IO請求之前,一個核心元件如檔案系統過濾管理器,可以請求停運保護,當IO請求完成後,停運保護被釋放,這時再解除安裝和更新就安全了。
停運保護不支援序列訪問共享物件,如果兩個或兩個以上的驅動同時對同一物件實施停運保護並且要求必須要序列訪問的話,那麼一些其他的防護措施比如說互斥鎖 就需要派上用場了。
相對於鎖
停運保護是眾多用於保證安全訪問共享物件的方式之一,而另外一種方式是使用互斥軟體鎖。如果一個驅動需要訪問一個已被其他驅動上鎖的物件,那麼前者必須要等待後者釋放鎖才可以對其進行訪問。然而,請求和釋放鎖會造成效能上的瓶頸,並且會消耗大量的記憶體。如果使用不正確,鎖可能還會對同時進行資源競爭的驅動造成死鎖的局面,但為檢測和避免死鎖,往往也需要耗費大量的計算資源。
實現細則
需要一個結構 EX_RUNDOWN_REF 用於追蹤共享物件停運保護的狀態,該結構內容是不透明的(也就是不對外開放的),停運保護機制的相關介面都以指向該結構的指標型別作為傳入引數型別,該結構記錄當前在共享物件上實施的停運保護的次數。
- 擁有者呼叫ExInitializeRundownProtection 將共享物件繫結到EX_RUNDOWN_REF 結構;
- 其他要訪問的驅動使用EX_RUNDOWN_REF 結構值呼叫ExAcquireRundownProtection 和ExReleaseRundownProtection 來請求和釋放針對該物件的停運保護;
- 擁有者呼叫ExWaitForRundownProtectionRelease 來等待物件被釋放以此確保物件可以被安全地刪除。
程式碼解析
摘自 phlib\include\phbasesup.h 檔案
#define PH_RUNDOWN_ACTIVE 0x1 #define PH_RUNDOWN_REF_SHIFT 1 #define PH_RUNDOWN_REF_INC 0x2 typedef struct _PH_RUNDOWN_PROTECT { /* 1. 儲存PH_RUNDOWN_WAIT_BLOCK型別變數的地址; 2. 停運保護是否啟用的標誌位 */ ULONG_PTR Value; } PH_RUNDOWN_PROTECT, *PPH_RUNDOWN_PROTECT; #define PH_RUNDOWN_PROTECT_INIT { 0 } typedef struct _PH_RUNDOWN_WAIT_BLOCK { /*共享物件的請求此處,表明共享物件正在被訪問*/ ULONG_PTR Count; /* 事件丟擲表明所有對共享物件的訪問已結束, 所有者發起的等待函式將返回,意味著接下來可以對共享物件進行刪除或替換 */ PH_EVENT WakeEvent; } PH_RUNDOWN_WAIT_BLOCK, *PPH_RUNDOWN_WAIT_BLOCK;
摘自 phlib\sync.c 檔案
VOID FASTCALL PhfInitializeRundownProtection( _Out_ PPH_RUNDOWN_PROTECT Protection ) { Protection->Value = 0; } BOOLEAN FASTCALL PhfAcquireRundownProtection( _Inout_ PPH_RUNDOWN_PROTECT Protection ) { ULONG_PTR value; // Increment the reference count only if rundown has not started. while (TRUE) { value = Protection->Value; if (value & PH_RUNDOWN_ACTIVE) return FALSE; /*原子操作:對比後滿足相等條件則進行賦值,函式返回目標引數的原有值*/ if ((ULONG_PTR)_InterlockedCompareExchangePointer( (PVOID *)&Protection->Value, /*每次請求物件共享則增加引用計數,每次都加2(PH_RUNDOWN_REF_INC)*/ (PVOID)(value + PH_RUNDOWN_REF_INC), (PVOID)value ) == value) return TRUE; } } VOID FASTCALL PhfReleaseRundownProtection( _Inout_ PPH_RUNDOWN_PROTECT Protection ) { ULONG_PTR value; while (TRUE) { value = Protection->Value; /*如果停運保護沒被啟用,value不可能為奇數,PH_RUNDOWN_ACTIVE的值為1*/ if (value & PH_RUNDOWN_ACTIVE) {/*停運保護已被啟用*/ PPH_RUNDOWN_WAIT_BLOCK waitBlock; // Since rundown is active, the reference count has been moved to the waiter's wait // block. If we are the last user, we must wake up the waiter. /*一旦停運保護啟用後,Protection->Value將改變原有的意義,現在儲存的是等待塊的地址*/ waitBlock = (PPH_RUNDOWN_WAIT_BLOCK)(value & ~PH_RUNDOWN_ACTIVE); if (_InterlockedDecrementPointer(&waitBlock->Count) == 0) { PhSetEvent(&waitBlock->WakeEvent); } break; } else { // Decrement the reference count normally. if ((ULONG_PTR)_InterlockedCompareExchangePointer( (PVOID *)&Protection->Value, (PVOID)(value - PH_RUNDOWN_REF_INC), (PVOID)value ) == value) break; } } } VOID FASTCALL PhfWaitForRundownProtection( _Inout_ PPH_RUNDOWN_PROTECT Protection ) { ULONG_PTR value; ULONG_PTR count; PH_RUNDOWN_WAIT_BLOCK waitBlock; BOOLEAN waitBlockInitialized; // Fast path. If the reference count is 0 or rundown has already been completed, return. value = (ULONG_PTR)_InterlockedCompareExchangePointer( (PVOID *)&Protection->Value, (PVOID)PH_RUNDOWN_ACTIVE, (PVOID)0 ); if (value == 0 || value == PH_RUNDOWN_ACTIVE) return; waitBlockInitialized = FALSE; while (TRUE) { value = Protection->Value; /* 向右移一位,有兩個作用: 1. 消除 PH_RUNDOWN_ACTIVE 的影響; 2. 之前每次請求共享物件時都是加2,現在右移1位相當於除以2,得到的是真正的引用次數! */ count = value >> PH_RUNDOWN_REF_SHIFT; // Initialize the wait block if necessary. if (count != 0 && !waitBlockInitialized) { PhInitializeEvent(&waitBlock.WakeEvent); waitBlockInitialized = TRUE; } // Save the existing reference count. waitBlock.Count = count; /* 為什麼要不厭其煩地使用原子操作? 因為怕在執行此迴圈的每一條語句時有請求插入,改變Protection->Value的值 */ if ((ULONG_PTR)_InterlockedCompareExchangePointer( (PVOID *)&Protection->Value, (PVOID)((ULONG_PTR)&waitBlock | PH_RUNDOWN_ACTIVE), (PVOID)value ) == value) { /*有共享物件的訪問還沒結束,要等待,觸發事件見 PhfReleaseRundownProtection 函式*/ if (count != 0) PhWaitForEvent(&waitBlock.WakeEvent, NULL); break; } } }
總結
看別人的程式碼就像是在遊歷一個世界,閱讀讓批判思維和共情能力顯得如此重要。這段程式碼看得出編碼的人是花了心思進行多番重構的,可借鑑的點:
- 同一變數儲存的值的意義切換;
- 原子操作Interlocked系列函式的使用;
- 看似簡單的奇偶位標識。
通俗的講,停運保護 的機制就比如:一座博物館,平日敞開大門供遊客參觀,現在突然說要裝修,然後把大門關了,只准出不許入,而博物館的人不能驅逐裡面的遊客遊客,只能等著,直到所有在裡面的遊客都出去了,然後才能開始裝修 。