粒子物理学里有关于宇宙的“标准模型”,WDM也是这样。图5-5显示了一个典型的IRP在各个处理阶段的所有权流程。并不是每种IRP都经过这些步骤,由于设备类型和IRP种类的不同某些步骤会改变或根本不存在。尽管这个过程可能有各种变化形式,但这个图为我们将要展开的讨论提供了一个很好的起点。
比你想象的更复杂...
当你第一次遇到IRP处理标准模型这个概念时,你也许认为这是个比较复杂的概念。但不幸的是,这个概念还不能满足所有问题,比如热拔插设备、动态资源再分配,和电源管理等等。在后面的章节中,我将描述处理这些额外问题的其它IRP排队和取消方法。标准模型仅作为你阅读时清晰的参考模型!
抛开这些特殊问题,许多设备仍能利用这个标准模型。如果你的设备在系统运行时不能被删除或重配置,并且在低电源状态下拒绝I/O请求,那么你可以使用这个标准模型。
IRP开始于某个实体调用I/O管理器函数创建它。在上图中,我使用术语“I/O管理器”来描述这个实体,尽管系统中确实有一个单独的系统部件用于创建IRP。事实上,更精确地说,应该是某个实体创建了IRP,并不是操作系统的某个例程创建了IRP。例如,你的驱动程序有时会创建IRP,而此时出现在图中第一个方框中的实体就应该是你的驱动程序。
可以使用下面任何一种函数创建IRP:
前两个函数中的Fsd表明这些函数专用于文件系统驱动程序(FSD)。虽然FSD是这两个函数的主要使用者,但其它驱动程序也可以调用这些函数。DDK还公开了一个IoMakeAssociatedIrp函数,该函数用于创建某些IRP的从属IRP。WDM驱动程序不应该使用这个函数。
决定该调用哪一个函数,和决定对IRP执行什么额外的初始化是更复杂的问题,我将在本章的结尾再回到这个问题上。
创建完IRP后,你可以调用IoGetNextIrpStackLocation函数获得该IRP第一个堆栈单元的指针。然后初始化这个堆栈单元。在初始化过程的最后,你需要填充MajorFunction代码。堆栈单元初始化完成后,就可以调用IoCallDriver函数把IRP发送到设备驱动程序:
PDEVICE_OBJECT DeviceObject; //something gives you this PIO_STACK_LOCATION stack = IoGetNextIrpStackLocation(Irp); stack->MajorFunction = IRP_MJ_Xxx; NTSTATUS status = IoCallDriver(DeviceObject, Irp);
IoCallDriver函数的第一个参数是你在某处获得的设备对象的地址。我将在本章的结尾处描述获得设备对象指针的两个常用方法。在这里,我们先假设你已经有了这个指针。
IRP中的第一个堆栈单元指针被初始化成指向该堆栈单元之前的堆栈单元,因为I/O堆栈实际上是IO_STACK_LOCATION结构数组,你可以认为这个指针被初始化为指向一个不存在的“-1”元素,因此当我们要初始化第一个堆栈单元时我们实际需要的是“下一个”堆栈单元。IoCallDriver将沿着这个堆栈指针找到第0个表项,并提取我们放在那里的主功能代码,在上例中为IRP_MJ_Xxx。然后IoCallDriver函数将利用DriverObject指针找到设备对象中的MajorFunction表。IoCallDriver将使用主功能代码索引这个表,最后调用找到的地址(派遣函数)。
你可以把IoCallDriver函数想象为下面代码:
NTSTATUS IoCallDriver(PDEVICE_OBJECT device, PIRP Irp) { IoSetNextIrpStackLocation(Irp); PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); stack->DeviceObject = device; ULONG fcn = stack->MajorFunction; PDRIVER_OBJECT driver = device->DriverObject; return (*driver->MajorFunction[fcn])(device, Irp); }
IRP派遣例程的原型看起来像下面这样:
NTSTATUS DispatchXxx(PDEVICE_OBJECT device, PIRP Irp) { PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); <--1 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension; <--2 ... return STATUS_Xxx; <--3 }
在本书中,我使用DispatchXxx(如DispatchRead、DispatchPnp,等等)来代表例子驱动程序中的派遣例程。其它人可能会使用另外的约定,但Microsoft推荐用这样的方法,例如,如果你的驱动程序名为RANDOM.SYS,那么你应该命名IRP_MJ_READ派遣函数为RandomDispatchRead。这个方法使驱动程序调试跟踪起来更容易,但它同时也需要你输入更多的文字。由于这些名称在驱动程序的名空间之外是不可见的,所以由你自己决定是使用Microsoft推荐的命名方案,还是使用你认为更有意义的命名方法。
在上面派遣函数原型中省略号的地方,是派遣函数必须做出决定的地方,有三种选择:
我将在本章详细讨论这三种选择,但在这里我仅讨论排队的可能性,因为这个过程就是IRP处理标准模型所描述的。你知道,当有大量读写请求进入设备时,通常需要把这些请求放入一个队列中,以便使硬件访问串行化。
每个设备对象都自带一个请求队列对象,下面是使用这个队列的标准方法:
NTSTATUS DispatchXxx(...) { ... IoMarkIrpPending(Irp); <--1 IoStartPacket(device, Irp, NULL, NULL); <--2 return STATUS_PENDING; <--3 }
注意,一旦我们调用了IoStartPacket函数,就不要再碰IRP。因为在该函数返回之前,IRP可能已经被完成并且其占用的内存可能被释放,而我们拥有的该IRP的指针也许是无效的。
每处理一个IRP,I/O管理器就调用一次StartIo例程:
VOID StartIo(PDEVICE_OBJECT device, PIRP Irp) { PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(Irp); PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) device->DeviceExtension; ... }
StartIo例程在DISPATCH_LEVEL级上获得控制,这意味着该函数不能生成任何页故障。另外,设备对象的CurrentIrp域和Irp参数都指向I/O管理器送来的IRP。
StartIo的工作是就着手处理IRP。如何做要完全取决于你的设备。通常你需要访问硬件寄存器,但可能有其它例程,如你的中断服务例程,或者是驱动程序中的其它例程也需要访问这些寄存器。实际上,有时着手一个新操作的最容易的方式是在设备扩展中保存某些状态信息,然后伪造一个中断。由于这些方法的执行都需要在一个自旋锁的保护之下,而这个自旋锁与保护你的ISR所使用的是同一个自旋锁,所以正确的方法是调用KeSynchronizeExecution函数。例如:
VOID StartIo(...) { ... KeSynchronizeExecution(pdx->InterruptObject, TransferFirst, (PVOID) pdx); } BOOLEAN TransferFirst(PVOID context) { PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) context; ... return TRUE; }
这里的TransferFirst例程是同步关键段(SynchCritSection)的一个例子,之所以这样做是因为StartIo需要与ISR同步。我将在第七章中详细讨论同步关键段(SynchCritSection)的概念。
一旦StartIo使设备忙于处理新请求,它就立即返回。当设备完成传输并发出中断时你将看到下一个请求。
当设备完成数据传输后,它将以硬件中断形式发出通知。在第七章中,我将讲述如何用IoConnectInterrupt函数“钩住”一个中断,该函数的一个参数就是ISR的地址。因此当中断发生时,硬件抽象层(HAL)就调用你的ISR。ISR运行在DIRQL上,并由ISR专用的自旋锁保护。ISR的函数原型如下:
BOOLEAN OnInterrupt(PKINTERRUPT InterruptObject, PVOID context) { ... }
ISR的第一个参数是中断对象的地址,中断对象由IoConnectInterrupt函数创建,但是你不太可能用到这个参数。第二个参数是在调用IoConnectInterrupt时你指定的任意上下文值;它可能是设备对象或设备扩展的地址,完全由你决定。
我将在第七章中详细讨论ISR的职责。为了继续标准模型的讨论,我要告诉你一点,一个ISR最可能做的事就是调度DPC例程(推迟过程调用)。而DPC的目的就是让你做某些事情,如调用IoCompleteRequest,而该调用不可能运行在ISR运行的DIRQL级上。所以,你的ISR中将有下面一行语句(device是指向设备对象的指针):
IoRequestDpc(device, device->CurrentIrp, NULL);
那么下一次你将在DPC例程中看到这个IRP,这个DPC例程是你在AddDevice函数中用IoInitializeDpcRequest寄存的。DPC例程的传统名称为DpcForIsr,因为它是由ISR请求的。
DpcForIsr例程在DISPATCH_LEVEL级上获得控制。通常,它的工作就是完成IRP(导致最近的中断发生)。但一般情况下,它通过调用IoCompleteRequest函数把剩余的工作交给完成例程来做。
VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT device, PIRP Irp, PDEVICE_EXTENSION pdx) { ... IoStartNextPacket(device, FALSE); <--1 IoCompleteRequest(Irp, boost); <--2 }
调用IoCompleteRequest例程是处理I/O请求的标准结束方式。在这个调用之后,I/O管理器(或者是任何在开始处创建该IRP的实体)将再次拥有该IRP。最后该IRP被这个实体销毁并解除等侍线程的阻塞状态。
有些设备的操作需要多个请求队列。一个常见的例子就是串行口,它可以同时地并且分开地处理输入输出请求流。IoStartPacket和IoStartNextPacket函数(以及其它含有键排序功能的等价函数)都使用设备对象自带的队列。创建与标准队列有相同工作方式的附加队列要相对容易一些。
为了使我们更容易讨论问题,让我们假设你需要一个单独的队列来管理IRP_MJ_SPECIAL(并不存在这个主功能码,使用它是为了使问题更具体一些)请求。你将写两个与StartIo和DpcForIsr例程功能类似的,但专用于处理这些假想IRP的辅助例程:
你还需要在你的设备扩展中创建一个KDEVICE_QUEUE对象,并在AddDevice例程中初始化这个队列对象:
NTSTATUS AddDevice(...) { ... KeInitializeDeviceQueue(&pdx->dqSpecial); ... }
dqSpecial就是KDEVICE_OBJECT对象的名字,用于排队IRP_MJ_SPECIAL请求。设备队列对象是一种三态对象(见图5-6)。这三种状态反映了设备队列例程是如何操作设备队列的:
在下面代码中,我们在派遣例程和DPC例程中使用了这些支持例程和我们专用的设备队列:
NTSTATUS DispatchSpecial(PDEVICE_OBJECT fdo, PIRP Irp) { IoMarkIrpPending(Irp); <--1 KIRQL oldirql; KeRaiseIrql(DISPATCH_LEVEL, &oldirql); <--2 PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension; if (!KeInsertDeviceQueue(&pdx->dqSpecial, &Irp->Tail.Overlay.DeviceQueueEntry)) <--3 StartIoSpecial(fdo, Irp); KeLowerIrql(oldirql); return STATUS_PENDING; } VOID DpcSpecial(...) { ... PKDEVICE_QUEUE_ENTRY qep = KeRemoveDeviceQueue(&pdx->dqSpecial); <--4 if (qep) StartIoSpecial(fdo, CONTAINING_RECORD(qep, IRP, Tail.Overlay.DeviceQueueEntry)); ... }
我以前描述的StartPacket和StartNextPacket函数使用一个名为DeviceQueue的KDEVICE_QUEUE对象。该对象是设备对象中的一个不透明域,其工作原理与管理私有设备队列相同。