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

背景

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

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

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

WindowsXP開始,內核驅動就支持停運保護機制。驅動經過停運機制能夠安全地訪問在系統內存中的對象,一般這些對象是由其餘內核驅動建立和銷燬的。github

當對一個對象的全部訪問操做已經完成而且再也不容許其餘新的操做請求,那麼就能夠將這個對象視爲停運的。好比說一個共享對象可能須要被停運,這樣的話它就能夠被清理而後用新的對象替換它。windows

擁有共享對象的驅動容許其餘驅動對該對象請求並實施停運保護機制。當停運保護生效時,除對象的全部者外,其餘驅動能夠訪問該對象而不用擔憂在訪問結束前該對象會被其全部者刪除。在訪問開始以前,要訪問的驅動會提出對目標對象實施停運保護的請求。對於一個存活週期較長的對象來講,這類請求幾乎都是被容許的。當訪問結束時,執行訪問的驅動會卸除以前對對象實施的停運保護。安全

常規的停運保護流程

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

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

若是對象擁有者打算刪除共享對象,它將調用ExWaitForRundownProtectionRelease來等待對象停運。在這期間,驅動調用線程會被阻塞,該函數會一直等待直至在以前被容許的全部停運保護被釋放,同時拒絕新的停運保護請求。直到最後一次的訪問結束而且全部停運保護被釋放後,ExWaitForRundownProtectionRelease方纔返回,這時對象的擁有者就能夠安全地刪除該對象了。爲了防止等待阻塞過長時間,訪問對象的驅動線程在實施停運保護的過程當中應避免出現延緩的狀況。性能

適合使用場景

停運機制很適合用於那些常常有效可用但不知什麼時候會忽然被刪除或替換的共享對象,訪問共享對象數據的驅動或者是調用線程在對象被刪除後需確保再也不嘗試訪問該對象,不然這些非法訪問可能會形成沒法預料的行爲後果好比數據損壞,更嚴重點甚至會出現系統崩潰。學習

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

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

相對於鎖

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

原文翻譯自: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. 看似簡單的奇偶位標識。

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

相關文章
相關標籤/搜索