Win32 核心 DPC 設計思想和實現思路淺析

http://flier_lu.blogone.net/?id=1397656


    x86架構設計在上是基於中斷思想的,因而從DOS到Win32,操作系統中大量使用中斷的概念來表達異步操作的行爲。但與DOS下獨佔的情況不同,Win32下需要由系統對多任務進行調度,因此中斷響應代碼必須儘可能地簡單,並且儘快的將控制權交還給系統。雖然這樣一來系統調度的響應速度和實現過程方便了,但還是有很多功能需要在中斷響應中完成。爲此,Win32核心提供了DPC(Deferred Procedure Call)和APC(Asynchronous Procedure Call)兩個IRQL特殊的軟件中斷級別,用於實現延遲和異步的過程調用。
    從IRQL分層來說,DPC和APC是介於較高級別的設備中斷和最低級別的Passive中斷之間,由操作系統用於完成特殊方法調用的中斷級別。與處理硬件操作的設備中斷和更高級別的時鐘、處理器中斷不同,這兩級中斷純粹是爲了實現功能調用異步性而設計實現的,因此操作系統本身也對它們具有很強的依賴型。APC這裏暫且不討論,以後有機會再寫篇文章專門討論 :)

    DPC在功能上可以理解爲ISR(Interrupt Service Routine)的一部分。只是因爲ISR爲了儘量簡單和返回控制權給操作系統,而將一部分功能剝離出來放入相應DPC中,延遲調用。因爲DPC的IRQL僅在APC和Passive中斷之上,所以系統可以從容地處理完高級別的中斷後,再在DPC一級慢慢處理積累起來的相對並不那麼緊急功能。
    DPC在使用上可以理解爲一個回調函數的封裝對象。系統本身或者設備驅動程序,在合適的地方如設備驅動程序的AddDevice函數或DispatchPnP函數處理IRP_MN_START_DEVICE請求時,初始化一個DPC對象;在ISR中判斷是否需要進一步處理中斷,是則請求將DPC對象插入到系統DPC隊列中;系統處理完高IRQL後,會在IRQL DISPATCH_LEVEL級別慢慢處理DPC隊列中的DPC對象;每個DPC對象封裝的回調函數,會使用同時封裝的調用參數,被系統調用,完成在ISR中來不及完成的工作;如果需要進一步的工作,還可以繼續請求插入DPC對象到DPC隊列中。

    DPC對象從最終用戶角度有兩種:DpcForIsr和CustomDPC。前者是與設備驅動對象(Device Object)綁定的;後者則由驅動自行維護。但從實現上來說,只有一種DPC對象存在,DpcForIsr所涉及的維護函數,實際上都是對CustomDPC的一個封裝而已。

    我們首先來看看初始化DPC對象的實現。KeInitializeDpc函數(ntos/ke/dpcobj.c:39)完成具體的DPC對象的初始化,實際上就是填充一個內存結構KDPC(ntos/inc/ntosdef.h:331)。

以下爲引用:

//
// Deferred Procedure Call (DPC) object
//

typedef struct _KDPC {
    CSHORT Type;
    UCHAR Number;
    UCHAR Importance;
    LIST_ENTRY DpcListEntry;
    PKDEFERRED_ROUTINE DeferredRoutine;
    PVOID DeferredContext;
    PVOID SystemArgument1;
    PVOID SystemArgument2;
    PULONG_PTR Lock;
} KDPC, *PKDPC, *RESTRICTED_POINTER PRKDPC;



    Type 表示此內核對象的類型,在KOBJECTS枚舉類型(ntos/inc/ke.h:122)中定義,缺省爲 DpcObject = 0x13。此外WinXP/2003新增了一種ThreadedDpcObject = 0x18
    Number 在多處理器環境下用於指定此DPC對象加入到哪個處理器的DPC隊列中,我們等會討論多處理器時詳細描述。缺省爲 0
    Importance 表示此DPC對象的重要性,在KDPC_IMPORTANCE枚舉類型(ntos/inc/ntosdef.h:321)中定義,缺省爲 MediumImportance = 1
    DpcListEntry 是用於維護DPC隊列的鏈表指針
    DeferredRoutine 是此DPC對象綁定的回調函數,後面DeferredContext、SystemArgument1和SystemArgument2分別是此回調函數被調用時的參數。如ISR中調用IoRequestDpc時,後面兩個參數就用於傳遞Irp和Context參數給DPC的回調函數。
    Lock 保存此DPC對象所在DPC隊列的自旋鎖,用於鎖定DPC隊列,同時也用於判斷此DPC對象是否被加入到一個DPC隊列中。

    瞭解了KDPC對象的結構,實際上維護代碼就非常簡單了。KeInitializeDpc函數將KDPC對象結構初始化爲初值;IoInitializeDpcRequest函數則只是對KeInitializeDpc函數的一個簡單包裝,如下

以下爲引用:

#define IoInitializeDpcRequest( DeviceObject, DpcRoutine ) (/
    KeInitializeDpc( &(DeviceObject)->Dpc,                  /
                     (PKDEFERRED_ROUTINE) (DpcRoutine),     /
                     (DeviceObject) ) )



    注意WinXP/2003下實際上KeInitializeDpc函數和KeInitializeThreadedDpc函數都是由一個KiInitializeDpc函數完成具體工作的,只是傳遞的最後一個參數定義的對象類型不同。

    KeInsertQueueDpc函數(ntos/ke/dpcobj.c:89)實際上是系統對DPC隊列維護的核心函數,其僞代碼如下:

以下爲引用:

BOOLEAN KeInsertQueueDpc (IN PRKDPC Dpc, IN PVOID SystemArgument1,IN PVOID SystemArgument2)
{
  PKSPIN_LOCK Lock;
  KIRQL OldIrql;

  KeRaiseIrql(HIGH_LEVEL, &OldIrql);  // 提升當前IRQL到最高,屏蔽其它中斷

  PKPRCB = KeGetCurrentPrcb();        // 獲取當前處理器控制塊

  // 通過比較Dpc->Lock是否爲空,來判斷此DPC對象是否已經被加入到DPC隊列;
  // 如果DPC對象可以被加入到隊列,則將當前處理器控制塊的DPC自旋鎖複製到Dpc->Lock中
  if ((Lock = InterlockedCompareExchangePointer(&Dpc->Lock, &Prcb->DpcLock, NULL)) == NULL)
  {
    // 更新當前處理器控制塊的統計信息
    Prcb->DpcCount += 1;
    Prcb->DpcQueueDepth += 1;

    // 更新DPC對象的參數信息
    Dpc->SystemArgument1 = SystemArgument1;
    Dpc->SystemArgument2 = SystemArgument2;

    // 根據DPC對象優先級,決定將之加入到DPC隊列的頭部或尾部
    if (Dpc->Importance == HighImportance)
        InsertHeadList(&Prcb->DpcListHead, &Dpc->DpcListEntry);
    else
        InsertTailList(&Prcb->DpcListHead, &Dpc->DpcListEntry);

    // 如果當前處理器沒有DPC對象活動或DPC中斷請求,則進一步判斷是否發出DPC中斷請求
    if (Prcb->DpcRoutineActive == FALSE && Prcb->DpcInterruptRequested == FALSE)
    {
      // 如果DPC對象優先級爲中高;
      // 或者DPC隊列長度超過閾值MaximumDpcQueueDepth;
      // 或者DPC請求速率小於閾值MinimumDpcRate
      if ((Dpc->Importance != LowImportance) ||
          (Prcb->DpcQueueDepth >= Prcb->MaximumDpcQueueDepth) ||
          (Prcb->DpcRequestRate < Prcb->MinimumDpcRate))
      {
        // 滿足觸發條件,則發出DPC中斷請求
        Prcb->DpcInterruptRequested = TRUE;
        KiRequestSoftwareInterrupt(DISPATCH_LEVEL);
      }
    }
  }
  KeLowerIrql(OldIrql);
  return (Lock == NULL);
}



    這裏的幾個閾值,在KiInitializeKernel函數(ntos/ke/i386/kernlini.c:246)中,根據全局變量KiMaximumDpcQueueDepth、KiMinimumDpcRate和KiAdjustDpcThreshold確定。而這幾個全局變量可以通過註冊表項(HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Session Manager/kernel/)下的DpcQueueDepth、MinimumDpcRate和AdjustDpcThreshold三個鍵值來設置。具體的設置方法,請參考MSDN以及性能計數器的Processor/% DPC Time等動態指數。

    而處理與驅動綁定的DPC對象的IoRequestDpc函數只是KeInsertQueueDpc函數的一個簡單包裝。

以下爲引用:

#define IoRequestDpc( DeviceObject, Irp, Context ) ( /
    KeInsertQueueDpc( &(DeviceObject)->Dpc, (Irp), (Context) ) )



    與KeInsertQueueDpc函數對應的KeRemoveQueueDpc函數(ntos/ke/dpcobj.c:272)實際上只是完成簡單的將DPC對象從DPC隊列中刪除的功能。

    最後對DPC對象屬性進行修改的KeSetImportanceDpc函數(ntos/ke/dpcobj.c:367)和KeSetTargetProcessorDpc函數(ntos/ke/dpcobj.c:401)實際上都是直接修改DPC對象結構的相應域。KDPC::Number大於MAXIMUM_PROCESSORS = 32時,用於指定DPC對象的目標CPU。如調用KeSetTargetProcessorDpc(pKDpc, 2)後,pKDpc = MAXIMUM_PROCESSORS + 2。

    在瞭解了DPC對象和DPC隊列的大致維護函數功能後,我們來看看稍微複雜一些的在多處理器下DPC隊列的維護流程。

    前面提到KDPC::Number指定了DPC對象所用的處理器號,因此在KeInsertQueueDpc函數開始獲取處理器控制塊時,需要判斷Number是否指向一個處理器,並從全局處理器控制塊列表中獲取相應的處理器控制塊,爲代碼如下:

以下爲引用:

if (Dpc->Number >= MAXIMUM_PROCESSORS)  // Number大於MAXIMUM_PROCESSORS時用於指定處理器
{
  Processor = Dpc->Number - MAXIMUM_PROCESSORS;
  Prcb = KiProcessorBlock[Processor];   // 全局唯一的處理器控制塊列表

}
else
{
  Prcb = KeGetCurrentPrcb();
}

KiAcquireSpinLock(&Prcb->DpcLock);      // 使用自旋鎖保護處理器控制塊中的DPC隊列



    而在KeInsertQueueDpc函數中判斷是否發出DPC中斷請求時,也需要做更復雜的邏輯判斷。
    對DPC對象目標處理器就是當前處理器的情況,可以和前面單處理器時一樣處理,直接發送DPC中斷請求;但對於DPC對象目標處理器是其他處理器的情況,就必須使用KiIpiSend函數發送IPI(InterProcessor Interrupt)中斷,通知目標處理器執行動作。此IPI中斷是介於系統掉電中斷(POWER_LEVEL)和時鐘中斷之間的特殊IRQL,專門用於在多處理器情況下協調多個處理器的工作。
    此外就是在多處理器情況下,各種對DPC隊列的操作都需要用此處理器控制塊的DPC隊列自旋鎖保護起來,避免同步問題。

    由此我們可以看到,實際上DPC隊列是每個處理器一個的,我們完全可以將某個DPC對象綁定到某個處理器上,實現類似線程親緣性(Thread Affinity)的效果,優化在多處理器環境下的性能。但這同時也帶來一個問題,就是ISR程序可以和DPC回調函數同時被調用,某種程度上也造成了開發複雜度的增加,具體處理方法請參考DDK中相關文檔。

    Kernel-Mode Driver Architecture/Design Guide/Servicing Interrupts/DPC Objects and DPCs

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章