《Windows驱动开发技术详解》之分层驱动程序
- 分层驱动程序概念
分层的目的是将功能复杂的驱动程序分解成多个简单的驱动程序。一般来说,他们是指两个或两个 以上的驱动程序,它们分别创建设备对象,并且形成一个由高到低的设备对象栈。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的完成例程。