- 分層驅動程序概念
分層的目的是將功能復雜的驅動程序分解成多個簡單的驅動程序。一般來說,他們是指兩個或兩個 以上的驅動程序,它們分別創建設備對象,並且形成一個由高到低的設備對象棧。IRP請求一般會被傳送到設備棧的最頂層的設備對象,頂層的設備對象可以選擇 直接結束IRP請求,也可以選擇將IRP請求向下層的設備對象轉發。如果是向下層設備對象轉發IRP請求,當IRP請求結束時,IRP會順着設備棧的反方 向原路返回。當得知下層驅動程序已經結束IRP請求時,本層設備對象可以選擇繼續將IRP向上返回,或者選擇重新將IRP再次傳遞給底層設備驅動。
分層驅動程序對應多個驅動程序,每個驅動程序創建一個設備對象,然后設備對象會一層一層地“掛載”在其它設備對象智商。這里所謂的掛載,就是指設備對象中的有個指針指向了別的設備對象。
設備對象的數據結構如下:
1 typedef struct _DEVICE_OBJECT { 2 CSHORT Type; 3 USHORT Size; 4 LONG ReferenceCount; 5 struct _DRIVER_OBJECT *DriverObject; 6 struct _DEVICE_OBJECT *NextDevice; 7 struct _DEVICE_OBJECT *AttachedDevice; 8 struct _IRP *CurrentIrp; 9 PIO_TIMER Timer; 10 ULONG Flags; 11 ULONG Characteristics; 12 __volatile PVPB Vpb; 13 PVOID DeviceExtension; 14 DEVICE_TYPE DeviceType; 15 CCHAR StackSize; 16 union { 17 LIST_ENTRY ListEntry; 18 WAIT_CONTEXT_BLOCK Wcb; 19 } Queue; 20 ULONG AlignmentRequirement; 21 KDEVICE_QUEUE DeviceQueue; 22 KDPC Dpc; 23 ULONG ActiveThreadCount; 24 PSECURITY_DESCRIPTOR SecurityDescriptor; 25 KEVENT DeviceLock; 26 USHORT SectorSize; 27 USHORT Spare1; 28 struct _DEVOBJ_EXTENSION * DeviceObjectExtension; 29 PVOID Reserved; 30 } DEVICE_OBJECT, *PDEVICE_OBJECT;
分層驅動程序使程序設計變得模塊化。
分層驅動程序可以對已經存在的驅動程序的功能進行修正。例如,某設備提供讀寫功能,而讀寫的大小沒有閑置。為了優化這個驅動程序的讀寫性能,可以在該驅動程序上掛載一層新的驅動程序,這個驅動程序將讀寫請求分成大小相等的讀寫請求。
分層驅動程序還可以監視某個設備的操作情況。
設備堆棧與掛載:
實現掛載的函數是IoAttachDeviceToDeviceStack,而從設備棧彈出的內核函數是IoDetachDevice。
IoAttachDeviceToDeviceStack的MSDN相關解釋如下:
The IoAttachDeviceToDeviceStack routine attaches the caller's device object to the highest device object in the chain and returns a pointer to the previously highest device object.
Parameters
- SourceDevice [in]
-
Pointer to the caller-created device object.
- TargetDevice [in]
-
Pointer to another driver's device object, such as a pointer returned by a preceding call to IoGetDeviceObjectPointer.
Return value
IoAttachDeviceToDeviceStack returns a pointer to the device object to which the SourceDevice was attached. The returned device object pointer can differ from TargetDevice if TargetDevice had additional drivers layered on top of it.
IoAttachDeviceToDeviceStack returns NULL if it could not attach the device object because, for example, the target device was being unloaded.
Remarks
IoAttachDeviceToDeviceStack establishes layering between drivers so that the same IRPs are sent to each driver in the chain.
An intermediate driver can use this routine during initialization to attach its own device object to another driver's device object. Subsequent I/O requests sent to TargetDevice are sent first to the intermediate driver.
This routine sets the AlignmentRequirement in SourceDevice to the value in the next-lower device object and sets the StackSize to the value in the next-lower-object plus one.
A driver writer must take care to call this routine before any drivers that must layer on top of their driver. IoAttachDeviceToDeviceStack attachesSourceDevice to the highest device object currently layered in the chain and has no way to determine whether drivers are being layered in the correct order.
A driver that acquired a pointer to the target device by calling IoGetDeviceObjectPointer should call ObDereferenceObject with the file object pointer that was returned by IoGetDeviceObjectPointer to release its reference to the file object before it detaches its own device object, for example, when such a higher-level driver is unloaded.
參考:https://msdn.microsoft.com/zh-cn/library/windows/hardware/ff548300
I/O堆棧:
MSDN中相關解釋如下:
The I/O manager gives each driver in a chain of layered drivers an I/O stack location for every IRP that it sets up. Each I/O stack location consists of anIO_STACK_LOCATION structure.
The I/O manager creates an array of I/O stack locations for each IRP, with an array element corresponding to each driver in a chain of layered drivers. Each driver owns one of the stack locations in the packet and calls IoGetCurrentIrpStackLocation to obtain driver-specific information about the I/O operation.
相關參考見:https://msdn.microsoft.com/en-us/library/ff551821
I/O堆棧用IO_STACK_LOCATION數據結構表示。它和設備堆棧緊密聯合。IRP一般會由應用程序的ReadFile或WriteFile創建,然后發送到設備堆棧的頂層。如果最上層的設備不處理IRP,就會將IRP轉發到下一層設備。每一層設備堆棧都有可能處理IRP。
在IRP的數據結構中,存儲着一個IO_STACK_LOCATION數組的指針。調用IoAllocateIrp內核函數創建IRP時,有一個StackSize參數,該參數就是IO_STACK_LOCATION數組的大小。IRP每穿越一層設備堆棧,就會用IO_STACK_LOCATION記錄下本次操作的某些屬性。
當頂層驅動設備對象收到IRP請求並進入派遣函數后,有多種方式處理IRP:
(1)直接處理該IRP,即調用IoCompleteReuest內核函數。
(2)調用StartIO,操作系統會將IRP請求串行化。除了當前運行的IRP,其它的IRP請求進入IRP隊列。
(3)選擇讓底層驅動完成IRP。
向下轉發IRP涉及設備堆棧和I/O堆棧。一個設備堆棧對應着一個I/O堆棧。IRP內部有個指針指向當前正在使用的IO_STACK_LOCATION,可以使用內核宏IoGetCurrentIrpStackLocation獲得當前I/O堆棧。每次調用IoCallDriver時,內核函數都會將IRP的當前指針下移,指向下一個IO_STACK_LOCATION。但是有的時候,當前設備堆棧不對IRP做任何處理。因此,當前設備就不需要對應I/O堆棧。但是IoCallDriver已經將當前I/O堆棧向下移動了一個單位,所以,DDK提供了內核宏IoSkipCurrentIrpStackLocation,它的作用就是講當前I/O堆棧又往回(上)移動一個單位。這樣IoCalDriver和IoSkipCurrentIrpStackLocation就對設備堆棧的移動就實現了平衡。
示例代碼:
附加DriverB到DriverA:
1 NTSTATUS DriverEntry( 2 IN PDRIVER_OBJECT pDriverObject, 3 IN PUNICODE_STRING pRegistryPath) 4 { 5 DbgPrint("DriverB loaded!\n"); 6 UNREFERENCED_PARAMETER(pDriverObject); 7 UNREFERENCED_PARAMETER(pRegistryPath); 8 pDriverObject->DriverUnload = DriverUnload; 9 pDriverObject->MajorFunction[IRP_MJ_CREATE] = HelloDDKCreate; 10 pDriverObject->MajorFunction[IRP_MJ_READ] = HelloDDKRead_SentToDriverA; 11 // 12 UNICODE_STRING DeviceName; 13 RtlInitUnicodeString(&DeviceName, L"\\Device\\MyDDKDevice"); 14 PDEVICE_OBJECT pDeviceObject = NULL; 15 PFILE_OBJECT pFileObject = NULL; 16 NTSTATUS status = IoGetDeviceObjectPointer(&DeviceName, FILE_ALL_ACCESS, &pFileObject, &pDeviceObject); 17 if (!NT_SUCCESS(status)){ 18 19 return status; 20 } 21 status = CreateDevice(pDriverObject); 22 if (!NT_SUCCESS(status)){ 23 ObDereferenceObject(pFileObject); 24 return status; 25 } 26 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDriverObject->DeviceObject->DeviceExtension; 27 PDEVICE_OBJECT FileterDeviceObject = pdx->pDevice; 28 PDEVICE_OBJECT TargetDevice = IoAttachDeviceToDeviceStack(FileterDeviceObject, pDeviceObject); 29 pdx->pTargetDevice = TargetDevice; 30 if (!TargetDevice){ 31 ObDereferenceObject(pFileObject); 32 IoDeleteDevice(TargetDevice); 33 return STATUS_INSUFFICIENT_RESOURCES; 34 } 35 FileterDeviceObject->DeviceType = TargetDevice->DeviceType; 36 FileterDeviceObject->Characteristics = TargetDevice->Characteristics; 37 FileterDeviceObject->Flags &= ~DO_DEVICE_INITIALIZING; 38 FileterDeviceObject->Flags |= (TargetDevice->Flags &(DO_DIRECT_IO | DO_BUFFERED_IO)); 39 ObDereferenceObject(pFileObject); 40 DbgPrint("Attach DriverA successfully!\n"); 41 return STATUS_SUCCESS; 42 }
向下轉發IRP的代碼如下:
1 NTSTATUS HelloDDKRead_SentToDriverA(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 DbgPrint("Enter HelloDDKRead_SentToDriverA!\n"); 3 NTSTATUS status = STATUS_SUCCESS; 4 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; 5 IoSkipCurrentIrpStackLocation(pIrp); 6 status = IoCallDriver(pdx->pTargetDevice, pIrp); 7 DbgPrint("Leave HelloDDKRead_SentToDriverA!\n"); 8 return status; 9 }
運行結果如下:
- 完成例程
在將IRP發送給底層驅動或者其他驅動前,可以對IRP設置一個完成例程。一旦底層驅動將IRP完成后,IRP完成例程立刻被觸發。通過設置完成例程可以方便地是程序員了解其他程序對IRP進行的處理。
不管是調用自己的底層驅動或者是調用其它驅動,都使用內核函數IoCallDriver。當IoCallDriver將IRP的控制權交給被動驅動時,有兩種情況。第一種,即調用的設備是同步完成這個IRP的,從IoCallDriver返回的時刻,即代表此IRP已經完成。第二中情況,就是調用的設備是異步操作,IoCallDriver會立刻返回IoCallDriver,但此時並沒有真正的完成IRP。第二種情況下,調用IoCallDriver前,先對IRP注冊一個完成例程,當底層驅動或者其他驅動完成此IRP時,此完成例程立刻被調用。注冊IRP的完成例程就是在當前的堆棧中CompletionRoutine子域。IRP完成后,一層層堆棧向上彈出,如果遇到IO_STACK_LOCATION的CompletionRoutine非空,則調用這個函數。如果使用完成例程,就不能使用內核宏IoSkipCurrentIrpStackLocation,即不能將本層IRP作為下層I/O堆棧。而必須使用IoCopyCurrentIrpStackLocationToNext,將本層I/O堆棧拷貝到下一層的I/O堆棧中。
當調用IoCallDriver后,當前的驅動就失去了對IRP的控制,如果這時候設置IRP的屬性,會引起系統崩潰。完成例程只有兩種返回的可能,一種是STATUS_SUCCESS,這種情況下驅動不會再得到IRP的控制。另一種情況是完成例程返回STATUS_MORE_PROCESSING_REQUIRED,這時候本層設備堆棧會重新獲得IRP的控制權,並且設備棧不會向上彈出,也就是向上“回卷”設備棧停止。此時可以選擇在此向底層發送IRP。
傳播Pending位——暫時沒看懂、沒理解
- 完成例程返回STATUS_SUCCESS:
當IRP被IoCompleteRequest完成時,IRP就會沿着一層層的設備堆棧向上回卷。如果途經遇到某設備堆棧的完成例程,則進入該完成例程。完成例程如果返回STATUS_SUCCESS,則繼續向上回卷。
這里DriverB的完成例程代碼如下:
1 NTSTATUS Complete_SUCC(PDEVICE_OBJECT pDeviceObject,PIRP pIrp,PVOID pContext){ 2 UNREFERENCED_PARAMETER(pContext); 3 UNREFERENCED_PARAMETER(pDeviceObject); 4 DbgPrint("Enter Complete_SUCC!\n"); 5 if (pIrp->PendingReturned){ 6 IoMarkIrpPending(pIrp); 7 } 8 DbgPrint("Leave Complete_SUCC!\n"); 9 return STATUS_SUCCESS; 10 }
其派遣函數代碼如下:
1 NTSTATUS HelloDDKRead_ComRoutine_SUCC(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 DbgPrint("Enter HelloDDKRead_ComRoutine_SUCC!\n"); 3 NTSTATUS status = STATUS_SUCCESS; 4 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; 5 IoCopyCurrentIrpStackLocationToNext(pIrp); 6 IoSetCompletionRoutine(pIrp, Complete_SUCC, NULL, TRUE, TRUE, TRUE); 7 status = IoCallDriver(pdx->pTargetDevice, pIrp); 8 if (status == STATUS_PENDING){ 9 DbgPrint("Return Status_Pending!\n"); 10 } 11 status = STATUS_PENDING; 12 DbgPrint("Leave HelloDDKRead_ComRoutine_SUCC!\n"); 13 return status; 14 }
它下層的DriverA中的代碼如下:
1 NTSTATUS HelloDDKRead_Timeout(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 DbgPrint("Enter HelloDDKRead_Timeout!\n"); 3 PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) 4 pDevObj->DeviceExtension; 5 IoMarkIrpPending(pIrp); 6 pDevExt->currentPendingIRP = pIrp; 7 ULONG ulMicroSecond = 7000000; 8 LARGE_INTEGER timeout = RtlConvertLongToLargeInteger(-10 * ulMicroSecond); 9 KeSetTimer(&pDevExt->pollingTimer,//設置完就開始計時,本示例設置的超時時間是3s 10 timeout, 11 &pDevExt->pollingDPC); 12 DbgPrint("Leave HelloDDKRead_Timeout!\n"); 13 return STATUS_PENDING; 14 }
定時器例程代碼如下:
1 VOID TimerOutDPC(PKDPC pDpc, PVOID pContext, PVOID SysArg1, PVOID SysArg2){ 2 UNREFERENCED_PARAMETER(pDpc); 3 UNREFERENCED_PARAMETER(SysArg1); 4 UNREFERENCED_PARAMETER(SysArg2); 5 6 DbgPrint("Enter TimerOutDPC!\n"); 7 PDEVICE_OBJECT pDevObj = (PDEVICE_OBJECT)pContext; 8 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; 9 PIRP currentPendingIRP = pdx->currentPendingIRP; 10 currentPendingIRP->IoStatus.Status = STATUS_CANCELLED; 11 currentPendingIRP->IoStatus.Information = 0; 12 IoCompleteRequest(currentPendingIRP, IO_NO_INCREMENT); 13 DbgPrint("Leave TimerOutDPC!\n"); 14 }
運行后輸出結果如下:
這段代碼的邏輯是這樣的,DriverB收到IRP下發給DriverA,然后進入DriverA的派遣函數HelloDDKRead_TimeOut中去處理。HelloDDKRead_TimeOut返回的只是掛起的IRP並沒有結束IRP,但是這個HelloDDKRead_TimeOut依然是返回了的,DriverB的HelloDDKRead_ComRoutine_SUCC繼續IoCallDriver后邊的代碼執行,直到HelloDDKRead_ComRoutine_SUCC結束並返回。然后,再等到進入DriverA的定時器,並完成這個IRP后,就立即執行DriverB的完成例程。
如果我們取消掉DriverA的定時器例程,則沒有完成底層的IRP而是DriverA的派遣函數直接返回:
則輸出結果如下:
一方面我們沒有看到輸出“Return Status_Pending”,說明DriverA派遣函數返回的STATUS_SUCCESS就是DriverB的IoCallDirver的返回值。另一方面這個IRP沒有被結束掉而卡在DriverA而沒有“上傳”,僅僅是派遣函數的逐層return。
- 完成例程返回STATUS_MORE_PROCESSING_REQUIRED:
當IRP被IoCompleteRequest完成時,IRP就會沿着一層層的設備堆棧向上回卷。如果途經遇到某設備堆棧的完成例程,則進入該完成例程。完成例程如果返回STATUS_MORE_PROCESSING_REQUIRED,則停止向上回卷。這時本層堆棧又重新獲得IRP的控制,並且該IRP從完成狀態有變成了未完成的狀態,需要再次完成,即需要再次執行IoCompleteRequest。重新獲得IRP可以再次發往底層驅動,也可以自己標志完成,即調用IoCompleteRequest。
完成例程:
1 NTSTATUS Complete_REQ(PDEVICE_OBJECT pDeviceObject, PIRP pIrp, PVOID pContext){ 2 UNREFERENCED_PARAMETER(pDeviceObject); 3 if (pIrp->PendingReturned == TRUE){ 4 KeSetEvent((PKEVENT)pContext, IO_NO_INCREMENT, FALSE); 5 } 6 return STATUS_MORE_PROCESSING_REQUIRED; 7 }
派遣函數:
1 NTSTATUS HelloDDKRead_ComRoutine_REQ(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 2 DbgPrint("Enter HelloDDKRead_ComRoutine_REQ!\n"); 3 NTSTATUS status = STATUS_SUCCESS; 4 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; 5 IoCopyCurrentIrpStackLocationToNext(pIrp); 6 KEVENT event; 7 KeInitializeEvent(&event, NotificationEvent, FALSE); 8 IoSetCompletionRoutine(pIrp, Complete_REQ, &event, TRUE, TRUE, TRUE); 9 status = IoCallDriver(pdx->pTargetDevice, pIrp); 10 if (status == STATUS_PENDING){ 11 DbgPrint("Event waiting...\n"); 12 KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL); 13 status = pIrp->IoStatus.Status; 14 } 15 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 16 DbgPrint("Leave HelloDDKRead_ComRoutine_REQ!\n"); 17 return status; 18 }
輸出結果:
- 將IRP分成多個IRP
IRP可以分解成多個小IRP,例如,需要對某個設備讀寫大量的數據,但是設備所支持的一次物理讀寫只有很小的字節數,這樣對設備的讀寫操作就可以分解成多個IRP,每個IRP所讀寫的字節數都在設備允許的范圍內。
在驅動編寫中,經常會遇到這種需求,就是將IRP請求分成多個IRP請求。例如,DriverA實現了讀取功能,但是對讀取的字節只能在1024字節以內,不支持更多的字節讀取。這時候,應用程序每次的讀請求只能是1024個字節。如果此時編寫一個中間驅動DriverB,可以用以解決上述問題。DriverB的操作如下:
(1)如果讀取字節數N是1024字節以內,就直接轉發IRP給DriverA。
(2)如果讀取字節數N是大於1024字節,則將當前IRP讀取字節數設置為1024,並設置一個完成例程,將IRP轉發到DriverA。
(3)一旦進入完成例程,就代表完成一個1024個字節的讀取。這時候繼續利用IRP並重新轉發IRP給DriverA。由於IRP還需要繼續轉發,所以完成例程退出時返回STATUS_MORE_PROCESSING_REQUIRED。
(4)重復(2)、(3),直到所有字節數都傳送完畢,這時候IRP操作才算真正完成。
DriverB的派遣函數和DriverB的完成例程會反復多次調用DriverA,並多次調用DriverA的派遣例程。應用程序的讀請求首先會來到DriverB的派遣函數,由於讀取的字節超過1024字節,所以先向DriverA請求1024字節的讀請求,並將剩下的字節作為參數傳遞給DriverB的完成例程。