1. 程式人生 > >同步下的資源互斥:停運保護(Run-Down Protection)機制

同步下的資源互斥:停運保護(Run-Down Protection)機制

簡單 調試 windows系統 tro 深入 瓶頸 eas 需要 計算資源

背景

近期在學習ProcessHacker的源碼,Process Hacker是一個免費的、功能強大的任務管理器,可用於監聽系統資源的使用情況,調試軟件以及檢測惡意程序。使用中你會發現其可以與Sysinternals開發的Process Explorer相媲美。最重要的它是開源的,源碼均可以在Github上查看,這使得我們有機會深入了解其實現原理和窺探一些重要的Windows系統接口。我的計劃是結合《深入解析windows操作系統》這本書籍學習一些Windows系統原理的相關知識。

關於停運保護(Run-Down Protection)機制

關於停運保護(暫且這樣翻譯)的介紹,發哥我翻找了官方的資料,邊理解邊做了部分的翻譯,如有錯誤或模棱兩可之處,還請你高擡貴手幫忙指出:

WindowsXP開始,內核驅動就支持停運保護機制。驅動通過停運機制可以安全地訪問在系統內存中的對象,通常這些對象是由其他內核驅動創建和銷毀的。

當對一個對象的所有訪問操作已經完成並且不再允許其他新的操作請求,那麽就可以將這個對象視為停運的。比如說一個共享對象可能需要被停運,這樣的話它就可以被清理然後用新的對象替換它。

擁有共享對象的驅動允許其他驅動對該對象請求並實施停運保護機制。當停運保護生效時,除對象的所有者外,其他驅動可以訪問該對象而不用擔心在訪問結束前該對象會被其所有者刪除。在訪問開始之前,要訪問的驅動會提出對目標對象實施停運保護的請求。對於一個存活周期較長的對象來說,這類請求幾乎都是被允許的。當訪問結束時,執行訪問的驅動會卸除之前對對象實施的停運保護。

常規的停運保護流程

要想共享一個對象,擁有該對象的驅動要調用ExInitializeRundownProtection函數以初始化停運保護機制,在這之後,其他要訪問此對象的驅動就可以對其實施和撤銷停運保護功能。

要訪問共享對象的驅動通過調用ExAcquireRundownProtection函數來請求對該對象的停運保護,當訪問結束後,驅動通過調用 ExReleaseRundownProtection 來取消停運保護。

如果對象擁有者打算刪除共享對象,它將調用ExWaitForRundownProtectionRelease來等待對象停運。在這期間,驅動調用線程會被阻塞,該函數會一直等待直至在之前被允許的所有停運保護被釋放,同時拒絕新的停運保護請求。直到最後一次的訪問結束並且所有停運保護被釋放後,ExWaitForRundownProtectionRelease

方才返回,這時對象的擁有者就可以安全地刪除該對象了。為了防止等待阻塞過長時間,訪問對象的驅動線程在實施停運保護的過程中應避免出現延緩的情況。

適合使用場景

停運機制很適合用於那些經常有效可用但不知何時會突然被刪除或替換的共享對象,訪問共享對象數據的驅動或者是調用線程在對象被刪除後需確保不再嘗試訪問該對象,否則這些非法訪問可能會造成無法預料的行為後果比如數據損壞,更嚴重點甚至會出現系統崩潰。

舉個例子,典型的病毒防禦驅動在操作系統運行時需要長時間加載到內存中。運行期間,其他驅動會發送IO請求到防禦驅動以訪問驅動中的數據和函數,但有時驅動需要被卸載和更新,為避免驅動還在處理IO請求時過早地被卸載,在發送IO請求之前,一個內核組件如文件系統過濾管理器,可以請求停運保護,當IO請求完成後,停運保護被釋放,這時再卸載和更新就安全了。

停運保護不支持串行訪問共享對象,如果兩個或兩個以上的驅動同時對同一對象實施停運保護並且要求必須要串行訪問的話,那麽一些其他的防護措施比如說互斥鎖就需要派上用場了。

相對於鎖

停運保護是眾多用於保證安全訪問共享對象的方式之一,而另外一種方式是使用互斥軟件鎖。如果一個驅動需要訪問一個已被其他驅動上鎖的對象,那麽前者必須要等待後者釋放鎖才可以對其進行訪問。然而,請求和釋放鎖會造成性能上的瓶頸,並且會消耗大量的內存。如果使用不正確,鎖可能還會對同時進行資源競爭的驅動造成死鎖的局面,但為檢測和避免死鎖,往往也需要耗費大量的計算資源。

原文翻譯自:MSDN官方原文鏈接

實現細則

需要一個結構EX_RUNDOWN_REF用於追蹤共享對象停運保護的狀態,該結構內容是不透明的(也就是不對外開放的),停運保護機制的相關接口都以指向該結構的指針類型作為傳入參數類型,該結構記錄當前在共享對象上實施的停運保護的次數。

  1. 擁有者調用ExInitializeRundownProtection將共享對象綁定到EX_RUNDOWN_REF結構;
  2. 其他要訪問的驅動使用EX_RUNDOWN_REF結構值調用 ExAcquireRundownProtectionExReleaseRundownProtection 來請求和釋放針對該對象的停運保護;
  3. 擁有者調用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;
        }
    }
}

總結

看別人的代碼就像是在遊歷一個世界,閱讀讓批判思維和共情能力顯得如此重要。這段代碼看得出編碼的人是花了心思進行多番重構的,可借鑒的點:

  1. 同一變量存儲的值的意義切換;
  2. 原子操作Interlocked系列函數的使用;
  3. 看似簡單的奇偶位標識。

通俗的講,停運保護的機制就比如:一座博物館,平日敞開大門供遊客參觀,現在突然說要裝修,然後把大門關了,只準出不許入,而博物館的人不能驅逐裏面的遊客遊客,只能等著,直到所有在裏面的遊客都出去了,然後才能開始裝修

同步下的資源互斥:停運保護(Run-Down Protection)機制