IRP处理模型

IRP处理的“标准模型”

 

IRP处理模型_第1张图片

图5-5. IRP处理的“标准模型”

创建IRP

IRP开始于某个实体调用I/O管理器函数创建它。在上图中,我使用术语“I/O管理器”来描述这个实体,尽管系统中确实有一个单独的系统部件用于创建IRP。事实上,更精确地说,应该是某个实体创建了IRP,并不是操作系统的某个例程创建了IRP。例如,你的驱动程序有时会创建IRP,而此时出现在图中第一个方框中的实体就应该是你的驱动程序。

可以使用下面任何一种函数创建IRP:

  • IoBuildAsynchronousFsdRequest 创建异步IRP(不需要等待其完成)。该函数和下一个函数仅适用于创建某些类型的IRP。
  • IoBuildSynchronousFsdRequest 创建同步IRP(需要等待其完成)。
  • IoBuildDeviceIoControlRequest 创建一个同步IRP_MJ_DEVICE_CONTROL或IRP_MJ_INTERNAL_DEVICE_CONTROL请求。
  • IoAllocateIrp 创建上面三个函数不支持的其它种类的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;
<other initialization of "stack">
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
}

  1. 你通常需要访问当前堆栈单元以确定参数或副功能码。
  2. 你可能还需要访问你创建的设备扩展。
  3. 你将向IoCallDriver函数返回某个NTSTATUS代码,而IoCallDriver函数将把这个状态码返回给它的调用者。

在上面派遣函数原型中省略号的地方,是派遣函数必须做出决定的地方,有三种选择:

  • 派遣函数立即完成该IRP。
  • 把该IRP传递到处于同一堆栈的下层驱动程序。
  • 排队该IRP以便由这个驱动程序中的其它例程来处理。

当有大量读写请求进入设备时,通常需要把这些请求放入一个队列中,以便使硬件访问串行化。

每个设备对象都自带一个请求队列对象,下面是使用这个队列的标准方法:

NTSTATUS DispatchXxx(...)
{
  ...
  IoMarkIrpPending(Irp);								<--1
  IoStartPacket(device, Irp, NULL, NULL);						<--2
  return STATUS_PENDING;								<--3
}

  1. 无论何时,当你的派遣例程返回STATUS_PENDING状态代码时,你应该先调用这个IoMarkIrpPending函数,以帮助I/O管理器避免内部竞争。我们必须在放弃IRP所有权之前做这一点。
  2. 如果设备正忙,IoStartPacket就把请求放到队列中。如果设备空闲,IoStartPacket将把设备置成忙并调用StartIo例程。IoStartPacket的第三个参数是用于排序队列的键(ULONG)的地址,例如磁盘驱动程序将在这里指定一个柱面地址以提供顺序搜索的排队。如过你在这里指定一个NULL,则该请求被加到队列的尾部。最后一个参数是取消例程的地址。我将在本章的后面讨论取消例程,这种例程比较复杂。
  3. 返回STATUS_PENDING以通知调用者我们没有完成这个IRP。

注意,一旦我们调用了IoStartPacket函数,就不要再碰IRP。因为在该函数返回之前,IRP可能已经被完成并且其占用的内存可能被释放,而我们拥有的该IRP的指针也许是无效的。

StartIo例程

每处理一个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请求的。

DPC例程

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
}

  1. IoStartNextPacket 取出设备队列中的下一个IRP并发送到StartIo。FALSE参数指出该IRP不能以通常方式取消。
  2. IoCompleteRequest 完成第一个参数指定的IRP。第二参数是等待线程的优先级提高值。注意在调用IoCompleteRequest之前你还要填充IRP中的IoStatus块。

调用IoCompleteRequest例程是处理I/O请求的标准结束方式。在这个调用之后,I/O管理器(或者是任何在开始处创建该IRP的实体)将再次拥有该IRP。最后该IRP被这个实体销毁并解除等侍线程的阻塞状态。

定制队列

有些设备的操作需要多个请求队列。一个常见的例子就是串行口,它可以同时地并且分开地处理输入输出请求流。IoStartPacket和IoStartNextPacket函数(以及其它含有键排序功能的等价函数)都使用设备对象自带的队列。创建与标准队列有相同工作方式的附加队列要相对容易一些。

 

IRP结构

图5-1显示了IRP的数据结构,阴影部分代表不透明域。下面是该结构中重要域的简要描述。

MdlAddress(PMDL)域指向一个内存描述符表(MDL),该表描述了一个与该请求关联的用户模式缓冲区。如果顶级设备对象的Flags域为DO_DIRECT_IO,则I/O管理器为IRP_MJ_READ或IRP_MJ_WRITE请求创建这个MDL。如果一个IRP_MJ_DEVICE_CONTROL请求的控制代码指定METHOD_IN_DIRECT或METHOD_OUT_DIRECT操作方式,则I/O管理器为该请求使用的输出缓冲区创建一个MDL。MDL本身用于描述用户模式虚拟缓冲区,但它同时也含有该缓冲区锁定内存页的物理地址。为了访问用户模式缓冲区,驱动程序必须做一点额外工作。

 

IRP处理模型_第2张图片

图5-1. I/O请求包数据结构

I/O堆栈

任何内核模式程序在创建一个IRP时,同时还创建了一个与之关联的IO_STACK_LOCATION结构数组:数组中的每个堆栈单元都对应一个将处理该IRP的驱动程序,另外还有一个堆栈单元供IRP的创建者使用(见图5-3)。堆栈单元中包含该IRP的类型代码和参数信息以及完成函数的地址。图5-4显示了堆栈单元的结构。

 

IRP处理模型_第3张图片

图5-3. 驱动程序和I/O堆栈之间的平行关系

 

IRP处理模型_第4张图片

图5-4. I/O堆栈单元数据结构

你可能感兴趣的:(数据结构,工作,object,null,extension,initialization)