內核同步對象(下)

內核互斥對象

互斥(mutex)就是互相排斥(mutual exclusion)的簡寫。內核互斥對象爲多個競爭線程串行化訪問共享資源提供了一種方法(不一定是最好的方法)。如果互斥對象不被某線程所擁有,則它是信號態,反之則是非信號態。當線程爲了獲得互斥對象的控制權而調用KeWaitXxx例程時,內核同時也做了一些工作以幫助避免可能的死鎖。同樣,互斥對象也需要與KeWaitForSingleObject類似的附加動作。內核可以確保線程不被換出,並且阻止所有APC的提交,內核專用APC(如IoCompleteRequest用以完成I/O請求的APC)除外。

通常我們應該使用executive部件輸出的快速互斥對象而不是內核互斥對象。這兩者的主要不同是,內核互斥可以被遞歸獲取,而executive快速互斥則不能。即內核互斥的所有者可以調用KeWaitXxx並指定所擁有的互斥對象從而使等待立即被滿足。如果一個線程真的這樣做,它必須也要以同樣的次數釋放該互斥對象,否則該互斥對象不被認爲是空閒的。

如果你需要長時間串行化訪問一個對象,你應該首先考慮使用互斥(而不是依賴提升的IRQL和自旋鎖)。利用互斥對象控制資源的訪問,可以使其它線程分佈到多處理器平臺上的其它CPU中運行,還允許導致頁故障的代碼仍能鎖定資源而不被其它線程訪問。表4-4列出了互斥對象的服務函數。

表4-4. 互斥對象服務函數

服務函數
描述

KeInitializeMutex
初始化互斥對象

KeReadStateMutex
取互斥對象的當前狀態

KeReleaseMutex
設置互斥對象爲信號態

爲了創建一個互斥對象,你需要爲KMUTEX對象保留一塊非分頁內存,然後象下面這樣初始化:

ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); KeInitializeMutex(mutex, level);

mutex是KMUTEX對象的地址,level參數最初是用於輔助避免多互斥對象帶來的死鎖。但現在,內核忽略level參數。

互斥對象的初始狀態爲信號態,即未被任何線程擁有。KeWaitXxx調用將使調用者接管互斥對象的控制並使其進入非信號態。

利用下面函數可以獲取互斥對象的當前狀態:

ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); LONG signalled = KeReadStateMutex(mutex);

返回值0表示互斥對象已被佔用,非0表示未被佔用。

下面函數可以使所有者放棄其佔有的互斥對象並使其進入信號態:

ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); LONG wassignalled = KeReleaseMutex(mutex, wait);

wait參數與KeSetEvent函數中的含義相同。該函數返回值總是0,表示該互斥對象曾被佔用過,如果不是這種情況(所有者釋放的不是它自己的對象),KeReleaseMutex將產生bug check。

出於完整性的考慮,我想提一下KeWaitForMutexObject函數,它是DDK中的宏(見WDM.H)。其定義如下:

#define KeWaitForMutexObject KeWaitForSingleObject

內核定時器

內核還提供了一種定時器對象,該對象可以在指定的絕對時間或間隔時間後自動從非信號態變爲信號態。它還可以週期性地進入信號態。我們可以用它來安排一個定期執行的DPC回調函數。表4-5列出了用於定時器對象的服務函數。

表4-5. 內核定時器對象的服務函數

服務函數
描述

KeCancelTimer
取消一個活動的定時器

KeInitializeTimer
初始化一次性的通知定時器

KeInitializeTimerEx
初始化一次性的或重複通知的或同步的定時器

KeReadStateTimer
獲取定時器的當前狀態

KeSetTimer
爲通知定時器設定時間

KeSetTimerEx
爲定時器設定時間和其它屬性

通知定時器用起來象事件

在這一段中,我們將創建一個通知定時器對象並等到它達到預定時間。首先,我們在非分頁內存中分配一個KTIMER對象。然後,我們在低於或等於DISPATCH_LEVEL級上初始化這個定時器對象:

PKTIMER timer; // someone gives you this ASSERT(KeGetCurrentIrql() &lt;= DISPATCH_LEVEL); KeInitializeTimer(timer);

在此,定時器處於非信號狀態,它還沒有開始倒計時,在這樣的定時器上等待的線程永遠得不到喚醒。爲了啓動定時器倒計時,我們調用KeSetTimer函數:

ASSERT(KeGetCurrentIrql() &lt;= DISPATCH_LEVEL); LARGE_INTEGER duetime; BOOLEAN wascounting = KeSetTimer(timer, duetime, NULL);

duetime是一個64位的時間值,單位爲100納秒。如果該值爲正,則表示一個從1601年1月1日算起的絕對時間。如果該值爲負,則它是相對於當前時間的一段時間間隔。

返回值如果爲TRUE,則表明定時器已經啓動。(在這種情況下,如果我們再調用KeSetTimer函數,則定時器放棄原來的時間重新開始倒計時)

下面語句讀取定時器的當前狀態:

ASSERT(KeGetCurrentIrql() &lt;= DISPATCH_LEVEL); BOOLEAN counting = KeReadStateTimer(timer);

KeInitializeTimer和KeSetTimer實際上是舊的服務函數,它們已經被新函數取代。我們可以用下面調用初始化定時器:

ASSERT(KeGetCurrentIqrl() &lt;= DISPATCH_LEVEL); KeInitializeTimerEx(timer, NotificationTimer);

定時器設置函數也有擴展版本,KeSetTimerEx

ASSERT(KeGetCurrentIrql() &lt;= DISPATCH_LEVEL); LARGE_INTEGER duetime; BOOLEAN wascounting = KeSetTimerEx(timer, duetime, 0, NULL);

我將在本章後面解釋該函數擴展版本的新參數。

即使定時器開始倒計時,它仍處於非信號態,直到到達指定的時間。在那個時刻,該定時器對象自動變爲信號態,所有等待的線程都被釋放。

在這一小節中,我們想讓定時器去觸發一個DPC例程。使用這種方法,不論你的線程有什麼優先級都會響應超時事件。(因爲線程只能在PASSIVE_LEVEL級上等待,而定時器到時間後,獲取CPU控制權的線程是隨機的。然而,DPC例程執行在提升的IRQL級上,它可以有效地搶先所有線程)

我們用同樣的方法初始化定時器對象。另外我們還再初始化一個KDPC對象,該對象應該在非分頁內存中分配。如下面代碼:

PKDPC dpc; // points to KDPC you've allocated ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); KeInitializeTimer(timer); KeInitializeDpc(dpc, DpcRoutine, context);

用KeInitializeTimer或KeInitializeTimerEx初始化定時器對象。DpcRoutine是一個DPC(推遲過程調用)例程的地址,這個例程必須存在於非分頁內存中。context參數是一個任意的32位值(類型爲PVOID),它將作爲參數傳遞給DPC例程。dpc參數是一個指向KDPC對象的指針(該對象必須在非分頁內存中。例如,在你的設備擴展中)。

當開始啓動定時器的倒計時,我們把DPC對象指定爲KeSetTimer或KeSetTimerEx函數的一個參數:

ASSERT(KeGetCurrentIrql() &lt;= DISPATCH_LEVEL); LARGE_INTEGER duetime; BOOLEAN wascounting = KeSetTimer(timer, duetime, dpc);

這個KeSetTimer調用與上一段中的調用的不同之處是,我們在最後一個參數中指定了一個DPC對象地址。當定時器時間到時,系統將把該DPC排入隊列,並且只要條件允許就立即執行它。在最差的情況下,它也與在PASSIVE_LEVEL級上喚醒線程一樣快。DPC函數的定義如下:

VOID DpcRoutine(PKDPC dpc, PVOID context, PVOID junk1, PVOID junk2) { ... }

即使你爲KeSetTimer或KeSetTimerEx提供了DPC參數,你仍可以調用KeWaitXxx函數使自己在PASSIVE_LEVEL級上等待。在單CPU的系統上,DPC將在等待完成前執行,因爲它執行在更高的IRQL上。

 

 

同步定時器

與事件對象類似,定時器對象也有兩種形式:通知方式和同步方式。通知定時器允許有任意數量的等待線程。同步定時器正相反,它只允許有一個等待線程。一旦有線程在這種定時器上等待,定時器就自動進入非信號態。爲了創建同步定時器,你必須使用擴展形式的初始化函數:

ASSERT(KeGetCurrentIrql() &lt;= DISPATCH_LEVEL); KeInitializeTimerEx(timer, SynchronizationTimer);

SynchronizationTimer是枚舉類型TIMER_TYPE的一個枚舉值。另一個枚舉值是NotificationTimer

如果你在同步定時器上使用DPC例程,可以把排隊DPC看成是定時器到期時發生的額外事情。即定時器到期時,系統把定時器置成信號態,並把DPC對象插入DPC隊列。定時器進入信號態將使阻塞的線程得以釋放。

週期性定時器

到現在爲止,我們討論過的定時器僅能定時一次。通過使用定時器的擴展設置函數,你可以請求一個週期性的超時:

ASSERT(KeGetCurrentIrql() &lt;= DISPATCH_LEVEL); LARGE_INTEGER duetime; BOOLEAN wascounting = KeSetTimerEx(timer, duetime, period, dpc);

這裏,period是週期超時值,單位爲毫秒(ms),dpc是一個可選的指向KDPC對象的指針。這種定時器在第一次倒計時時使用duetime時間,到期後再使用period值重複倒計時。爲了準確地完成周期定時,應該把duetime時間指定爲與週期間隔參數一樣的相對時間。指定爲0的duetime參數將使定時器立即完成第一次倒計時,然後開始週期性倒計時。由於不用重複等待超時通知,所以週期性定時器常常與DPC對象聯用。

取消一個週期性定時器

在定時器對象超出定義範圍之外前,一定要調用KeCancelTimer取消任何已創建的週期性定時器。如果這個週期性定時器帶有一個DPC,則還需要在取消該定時器之後調用KeRemoveQueueDpc。甚至即使你做了這兩件事,還可能出現一個無法解決的問題。如果你在DriverUnload例程中取消這種定時器,可能會出現一種罕見的情形:你的驅動程序已被卸載,但那個DPC例程的實例卻仍運行在另一個CPU上。這個問題只有等待未來版本的操作系統來解決。你可以儘早地取消這種定時器以便減少該問題出現的可能性,比如在IRP_MN_REMOVE_DEVICE的處理程序中。

一個例子

內核定時器的一個用處是爲定期檢測設備活動的系統線程提供循環定時。今天很少有設備需要循檢服務,但你可能遇到例外情況。我將在第九章中討論這個主題。隨書光盤中有一個例子(POLLING)演示了這個概念。這個例子的部分代碼以固定的間隔時間循檢設備。這個循環可以被設置的kill事件所打破,所以程序使用了KeWaitForMultipleObjects函數。實際的代碼要比下面的例子更復雜一些,下面代碼片段主要側重於定時器的使用:

VOID PollingThreadRoutine(PDEVICE_EXTENSION pdx) { NTSTATUS status; KTIMER timer; KeInitializeTimerEx(&timer, SynchronizationTimer); &lt;--1 PVOID pollevents[] = { &lt;--2 (PVOID) &pdx->evKill, (PVOID) &timer, }; ASSERT(arraysize(pollevents) <= THREAD_WAIT_OBJECTS); LARGE_INTEGER duetime = {0}; #define POLLING_INTERVAL 500 KeSetTimerEx(&timer, duetime, POLLING_INTERVAL, NULL); &lt;--3 while (TRUE) { status = KeWaitForMultipleObjects(arraysize(pollevents), &lt;--4 pollevents, WaitAny, Executive, KernelMode, FALSE, NULL, NULL); if (status == STATUS_WAIT_0) break; if (&lt;device needs attention>) <--5 &lt;do something>; } KeCancelTimer(&timer); PsTerminateSystemThread(STATUS_SUCCESS); }

  1. 在此,我們把一個內核定時器初始化成同步方式。它只能用於一個線程,本線程。
  2. 我們需要爲KeWaitForMultipleObjects函數提供一個同步對象指針數組。第一個數組元素是kill事件對象,驅動程序的其它部分可能在系統線程需要退出時設置這個對象,以終止循環。第二個數組元素就是定時器對象。
  3. KeSetTimerEx語句啓動週期定時器。由於duetime參數是0,所以定時器立即進入信號態。然後每隔500毫秒觸發一次。
  4. 在循檢循環內,我們等待定時器到期或kill事件發生。如果等待由於kill事件而結束,我們退出循環,並做一些清理工作,最後終止這個系統線程。如果等待是由定時器到期而結束,我們就前進到下一步處理。
  5. 在這裏,設備驅動程序可以做一些與硬件有關的操作。

定時函數

除了使用內核定時器對象外,你還可以使用另外兩個定時函數,它們也許更適合你。第一函數是KeDelayExecutionThread,你可以在PASSIVE_LEVEL級上調用該函數並給出一個時間間隔。該函數省去了使用定時器時的麻煩操作,如創建,初始化,設置,等待操作。

ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); LARGE_INTEGER duetime; NSTATUS status = KeDelayExecutionThread(WaitMode, Alertable, &duetime);

在這裏,WaitModeAlertable,和函數返回代碼與KeWaitXxx中的對應部分有相同的含義。duetime也是內核定時器中使用的同一種時間表達類型。

如果你需要延遲一段非常短的時間(少於50毫秒),可以調用KeStallExecutionProcessor,在任何IRQL級上:

KeStallExecutionProcessor(nMicroSeconds);

這個延遲的目的是允許硬件在程序繼續執行前有時間爲下一次操作做準備。實際的延遲時間可能大大超過你請求的時間,因爲KeStallExecutionProcessor可以被其它運行在更高IRQL級上的活動搶先,但不能被同一IRQL級上的活動搶先。

內核線程同步

操作系統的進程結構部件(Process Structure)提供了一些例程,WDM驅動程序可以使用這些例程創建和控制內核線程,這些例程可以幫助驅動程序週期性循檢設備,我將在第九章中討論這些例程。出於完整性考慮,我在這裏先提一下。如果在KeWaitXxx調用中指定一個內核線程對象,那麼你的線程將被阻塞直到那個內核線程結束運行。那個內核線程通過調用PsTerminateSystemThread函數終止自身。

爲了等待某內核線程結束,你首先應獲得一個KTHREAD對象(不透明對象)的指針,在內部,該對象用於代表內核線程,但這裏還有一點問題,當你運行在某線程的上下文中時,你可以容易地獲取當前線程的KTHREAD指針:

ASSERT(KeGetCurrentIrql() &lt;= DISPATCH_LEVEL); PKTHREAD thread = KeGetCurrentThread();

不幸的是,當你調用PsCreateSystemThread創建新內核線程時,你僅能獲取該線程的不透明句柄。爲了獲得KTHREAD對象指針,你必須使用對象管理器服務函數:

HANDLE hthread; PKTHREAD thread; PsCreateSystemThread(&hthread, ...); ObReferenceObjectByHandle(hthread, THREAD_ALL_ACCESS, NULL, KernelMode, (PVOID*) &thread, NULL); ZwClose(hthread);

ObReferenceObjectByHandle函數把你提供的句柄轉換成一個指向下層內核對象的指針。一旦有了這個指針,你就可以調用ZwClose關閉那個句柄。在某些地方,你還需要調用ObDereferenceObject函數釋放對該線程對象的引用。

ObDereferenceObject(thread);

線程警惕和APC

在內部,Windows NT內核有時使用線程警惕(thread alert)來喚醒線程。這種方法使用APC(異步過程調用)來喚醒線程去執行某些特殊例程。用於生成警惕和APC的支持例程沒有輸出給WDM驅動程序開發者使用。但是,由於DDK文檔和頭文件中有大量地方引用了這個概念,所以我想在這裏談一下。

當某人通過調用KeWaitXxx例程阻塞一個線程時,需要指定一個布爾參數,該參數表明等待是否是警惕的(alertable)。一個警惕的等待可以提前完成,即不用滿足任何等待條件或超時,僅由於線程警惕。線程警惕起源於用戶模式的native API函數NtAlertThread。如果因爲警惕等待提前終止,則內核返回特殊的狀態值STATUS_ALERTED。

APC機制使操作系統能在特定線程上下文中執行一個函數。APC的異步含義是,系統可以有效地中斷目標線程以執行一個外部例程。APC的動作有點類似於硬件中斷使處理器從任何當前代碼突然跳到ISR的情形,它是不可預見的。

APC來自三種地方:用戶模式、內核模式,和特殊內核模式。用戶模式代碼通過調用Win32 API函數QueueUserAPC請求一個用戶模式APC。內核模式代碼通過調用一個未公開的函數請求一個APC,而且該函數在DDK頭文件中沒有原型。某些逆向工程師可能已經知道該例程的名稱以及如何調用它,但該函數的確是僅用於內部,所以我不在這裏討論它。系統把APC排入一個特殊線程直到和適的執行條件出現。和適的執行條件要取決於APC的類型,如下:

  • 特殊的內核APC被儘可能快地執行,既只要APC_LEVEL級上有可調度的活動。在很多情況下,特殊的內核APC甚至能喚醒阻塞的線程。
  • 普通的內核APC僅在所有特殊APC都被執行完,並且目標線程仍在運行,同時該線程中也沒有其它內核模式APC正執行時才執行。
  • 用戶模式APC在所有內核模式APC執行完後才執行,並且僅在目標線程有警惕屬性時才執行。

如果系統喚醒線程去提交一個APC,則使該線程阻塞的等待原語函數將返回特殊狀態值STATUS_KERNEL_APC或STATUS_USER_APC。

APC與I/O請求

內核使用APC概念有多種目的。由於本書僅討論驅動程序的編寫,所以我僅解釋APC與執行I/O操作之間的關係。在某些場合,當用戶模式程序在一個句柄上執行同步的ReadFile操作時,Win32子系統就調用一個名爲NtReadFile(儘管未公開,但已經被廣泛瞭解)的內核模式例程。該函數創建並提交一個IRP到適當的設備驅動程序,而驅動程序通常返回STATUS_PENDING以指出操作未完成。NtReadFile然後向ReadFile也返回這個狀態代碼,於是ReadFile調用NtWaitForSingleObject函數,這將使應用程序在那個用戶模式句柄指向的文件對象上等待。NtWaitForSingleObject接着調用KeWaitForSingleObject以執行一個非警惕的用戶模式的等待,在文件對象內部的一個事件對象上等待。

當設備驅動程序最後完成了讀操作時,它調用IoCompleteRequest函數,該函數接下來排隊一個特殊的內核模式APC。該APC例程然後調用KeSetEvent函數使文件對象進入信號態,因此應用程序被釋放並得以繼續執行。有時,I/O請求被完成後還需要執行一些其它任務,如緩衝區複製,而這些操作又必須發生在請求線程的地址上下文中,因此會需要其它種類的APC。如果請求線程不處於警惕性的等待狀態,則需要內核模式APC。如果在提交APC時線程並不適合運行,則需要特殊的APC。實際上,APC例程就是用於喚醒線程的機制。

內核模式例程也能調用NtReadFile函數。但驅動程序應該調用ZwReadFile函數替代,它使用與用戶模式程序一樣的系統服務接口到達NtReadFile(注意,NtReadFile函數未公開給設備驅動程序使用)。如果你遵守DDK的限定調用ZwReadFile函數,那麼你向NtReadFile的調用與用戶模式中的調用幾乎沒有什麼不同,僅有兩處不同。第一,ZwReadFile函數更小,並且任何等待都將在內核中完成。另一個不同之處是,如果你調用了ZwCreateFile函數並指定了同步操作,則I/O管理器將自動等待你的讀操作直到完成。這個等待可以是警惕的也可以不是,取決於你在ZwCreateFile調用中指定的實際選項。

如何指定Alertable和WaitMode參數

現在你已經有足夠的背景資料瞭解等待原語中的AlertableWaitMode參數。作爲一個通用規則,你絕不要寫同步響應用戶模式請求的代碼,僅能爲確定的I/O控制請求這樣做。一般說來,最好掛起長耗時的操作(從派遣例程中返回STATUS_PENDING代碼)而以異步方式完成。再有,你不要一上來就調用等待原語。線程阻塞僅適合設備驅動程序中的某幾個地方使用。下面幾段介紹了這幾個地方。

內核線程 有時,當你的設備需要週期性循檢時,你需要創建自己的內核模式線程。

處理PnP請求 我將在第六章中討論如何處理PnP管理器發送給你的I/O請求。有幾個PnP請求需要你在驅動程序這邊同步處理。換句話說,你把這些請求傳遞到低級驅動程序並等待它們完成。你將調用KeWaitForSingleObject函數並在內核模式中等待,這是由於PnP管理器是在內核模式線程的上下文中調用你的驅動程序。另外,如果你需要執行作爲處理PnP請求一部分的輔助請求時,例如,與USB設備通信,你應在內核模式中等待。

處理其它I/O請求 當你正在處理其它種類的I/O請求時,並且你知道正運行在一個非任意線程上下文中時,那麼你在行動前必須仔細考慮,如果你確信那個線程可以被阻塞,你應該在調用者所處於的處理器模式中等待。在多數情況下,你可以利用IRP中的RequestorMode域。此外,你還可以調用ExGetPreviousMode來確定前一個處理器模式。如果你在用戶模式中等待,並允許用戶程序調用QueueUserAPC提前終止等待,你應該執行一個警惕性等待。

我最後要提到的情況是,在用戶模式中等待並要允許用戶模式APC打斷,你應使用警惕性等待。

底線是:使用非警惕性等待,除非你知道不這樣做的原因。

 

下面是MFC中的同步對象及其用途,僅供參考

CSemaphore的對象代表一個“信號燈”:一種同步對象,允許一個或多個進程中的有限數量的線程訪問某個資源。一個CSemaphore對象維護着當前正訪問某個資源的進程的個數。信號燈通常用於控制僅能支持有限數量用戶的共享資源的訪問。CSemaphore對象計數器的當前值表示還可以允許多少個用戶使用它保護的共享資源。當這個數到達0時,所有試圖訪問被保護資源的操作都被放入一個系統隊列等待,直到計數數值上升到0以上或等待超時。

CEvent的對象代表一個“事件”:一種同步對象,用於一個線程通知另一個線程某事件發生。事件通常用於線程想知道何時執行其任務。例如,複製數據到某文件的線程需要被通知數據何時準備好。用CEvent對象可以通知複製線程數據已經有效,這樣線程就可以儘快地執行其任務。CEvent對象有兩種類型:手動和自動。手動CEvent對象將停留在SetEvent或ResetEvent設置的狀態,除非你再次設置它。自動CEvent對象在一個線程釋放後自動回到非信號態(無效狀態)。

CMutex的對象代表一個“互斥”:一種同步對象,可以使一個線程排斥性地訪問某個資源。互斥可以用於一次僅允許一個線程訪問的資源。例如,向鏈表添加一個接點的過程就是一次只允許一個線程執行。通過使用CMutex對象控制鏈表,一次只能有一個線程獲得鏈表的訪問權。

CCriticalSection的對象代表一個“關鍵段”:一種同步對象,代表一次只允許一個線程訪問的資源或代碼片段。例如,向鏈表添加一個節點。使用CCriticalSection對象控制的鏈表一次只允許一個線程訪問該鏈表。如果着重於執行速度並且被保護資源不跨進程使用,也可以用關鍵段代替互斥。

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