21
1月
2016

7、深入理解IRP概念、传递流程与完成函数

摘要:

    本文讲述了IRP的概念与实质,并分析了IRP的传递流程与完成函数。对常见的函数进行了详细讲解。部分片段与概念节选自公开发行物与翻译作品。

关键字:IRP概念,IRP实质,IRP传递流程,完成函数的设置,IRP堆栈IO_STACK_LOCATION,完成例程返回值等等。

一:IRP的概念与实质

   在微软的Windows 操作系统家族中,都通过发送I/O 请求包(IRP, I/o Request Packets)来进行 和驱动程序的通讯。用来封装IRP 的数据结构不仅仅用来描述一个I/O 操作的请求本身的内容还要用来维护这一请求在一系列驱动程序中传递的过程中的相关状态信息。实现这一数 据结构其实是为了两重目的,也就是说IRP 可以被定义[理解]为:

  1. 一个放置I/O 请求的容器
  2. 一个与线程无关的调用堆栈

   所有的IRP 都包含的两个部分:

  • 一个用来描述主要I/O 请求的头部
  • 一组用来描述下级请求的参数(堆栈)

任何IRP头部的数据尺寸是固定,而下级请求数组堆栈的多少根据驱动程序的层数决定。

每个IRP 的头部信息,都各自包含着每个要操作这个IRP 的驱动程序会要使用的数据。当一个给定的驱动正在处理一个IRP时,这个驱动就被定义为此IRP 的当前拥有者。

  • 每一个IRP 的头部,都包含有以下的指针(Pointer):

            1,针对这一IRP读取输入以及写回输出的缓冲区。

            2,当前拥有这一IRP 的驱动程序的内存区域。

            3, 一个由当前拥有这一IRP 的驱动提供的例程。

            4,这一例程将由系统在IRP 被取消时[后]调用。

            5, 指向当前的子请求的参数 其它数据,描述请求的状态和其它内在信息(微硬未开放的信息)。

  • IO_STACK_LOCATION 结构,会包含有以下内容:

            1,特定IRP 的主要以及次要的方法代码。

            2,针对这些代码的参数。

            3,指向相应驱动的设备对象(device object)的指针。

            4,指向IoCompletion 例程的指针,如果驱动有过设定。

            5,指向和这一请求相关联的文件对象的指针。

            6,一些可变标志以及上下文[关联]区域。

IRP 作为一个线程无关的调用栈:由于驱动可以异步的处理请求

左侧,线程栈表示了驱动程序A,B 和C 的参数和返回值地址是如何在调用栈(callstack)中被组织的;

右侧,图表展示了这些参数和返回值地址是如何在一个IRP 中对应到I/O 栈位置以及IoCompletion 例程上的。

二:IRP的传递流程

驱动向下调用IRP栈,当前驱动必须为下层驱动设置好栈(IO_STACK_LOCATION)完成函数(IoSetCompletionRoutine)(如果有)。下层驱动获取由当前驱动建立的栈后进行一系列运算并使用IoCompletion函数完成例程,在下层驱动完成时调用由上层驱动设置好的完成函数,即IoSetCompletionRoutine设置的函数。

  • 建立下一个I/O 栈位置的参数。驱动程序可以采取以下措施

           1,调用IoGetNextIrpStackLocation 例程来得到一个指针指向下一个I/O 栈位置,然后将请求参数组复制到那个得到的位置。

           2,调用IoCopyCurrentIrpStackLocationToNext例程(如果驱动按第二步设置了IoCompletion 例程),或者IoSkioCurrentIrpStackLocation 例程(如果没有在第二步设置IoCompletion 例程)来传递当前位置所使用的同样的参数组。

注意:驱动程序不能够使用RtlCopyMemory 例程来复制当前的参数组。这个 例程把指针复制到当前驱动的IoCompletion 例程, 而且这样会导致IoCompletion例程被不止一次调用。
  • 如果需要的话,调用IoSetCompletionRoutine例程,为后期处理(post-porcessing)设置一个IoCompletion 例程。如果驱动设置了IpCompletion 例程,那么他在上一步(第 一步)中必须使用IoCopyCurrentIrpStackLocationToNext。
  • 通过调用IoCallDriver 例程将请求传递到下一个驱动。这个例程会自动通告IRP 栈 指针,并且调用下一个驱动的分发例程。

     在驱动程序将IRP 传递个下一个驱动之后,就不再拥有这个IRP,并且不能试图再去访问它。否则会导致系统崩溃。那个IRP 会被其它的驱动或者线程释放或完成。如果驱动需要访问一个已经在栈里传下去的IRP,这个驱动必须实现(设置)IoCompletion 例程。当I/O管理器(I/O Manager)调用IoCompletion 例程时,这个驱动就能够在IoCompletion 例程执行期间重新获得对这一IRP 的所有权。因此,IoCompletion 例程就能够访问IRP 中的域。

     若是驱动的分发例程也还须在IRP 被后面的驱动处理成之后再处理它,这个IoCompletion例程必须返回STATUS_MORE_PROCESSING_REQUIRED,以将IRP 的所有权返回给分发例程。如此一来,I/O 管理器会停止IRP 的处理,将最终完成IRP 的任务留给分发例程。分 发例程能够在之后调用ICompleteRequest 来完成这个IRP,或者还能将这个IRP 标记为等候进一步处理。

  • 完成一个IRP

    当输入、输出操作(I/O)完成时,完成这个I/O 操作的驱动会调用完成函数IoCompleteRequest。这个例程将IRP 栈指针移到指向IRP 栈中的前一个(更上面)的位置,如图所示。

     上图表示出了在驱动C 调用IoCompleteRequest 后的当前栈位置。左侧的实线箭头表示出栈指针现在指向了驱动B 的参数组和回调函数。点虚线箭头表示出前一个栈位置。右侧的中空箭头表示出IoCompletion 例程被调用的顺序。

注意:为了解释上的方便,本文把IRP 中I/O 栈位置的顺序表示成“倒置”的,就是说,是从A 到C 的反向顺序,而不是从C 到A。用这样的倒置图表是为了更直观用“向下”将调用在栈中沿向下的方向进行表现出来。

如果一个驱动在设备栈中向下传递IRP 时设定了IoCompletion 例程,I/O 管理器就会在IRP 栈指针再次指向这一驱动的这个I/O 栈位置时调用此例程。如此一来,IoCompletion 例程就 表现成为,当IRP 在设备栈中传递时,操作IRP 的那些驱动的返回地址。

一个IoCompletion 例程能够返回两个状态值中的任一个:

  1. STATUS_CONTINUE_COMPLETION 。
  2. STATUS_MORE_PROCESSING_REQUIRED。

      STATUS_CONTINUE_COMPLETION : 继续向上完成特定IRP。I/O 管理器提升IRP 栈指针,并且调用上一个驱动的IoCompletion 例程。

      STATUS_MORE_PROCESSING_REQUIRED : 中断向上完成特定IRP 的过程,并且把IRP 栈指针留在当前位置。返回这一状态的驱动通常会在过后调用IoCompleteRequest 例程来重新开始向上完成特定IRP 的过程。

注意:如果当前驱动想要重新获取控制权则必须在完成函数中设置返回STATUS_MORE_PROCESSING_REQUIRED,这样在下层驱动返回IRP后会中断并将完成当前驱动想要的IRP过程(比如拦截、过滤等等),其堆栈仍保存在当前驱动位置(即本层驱动位置,而不是下层驱动位置)。在本层最终调用IoCompleteRequest例程后重新向上返回IRP。

当每一个驱动都完成了它对应的子请求,I/O 请求就完成了。I/O 管理器从Irp->IoStatus.Status 域取回请求的状态信息,并且从Irp->IoStatus.Information 域取回传输的字节数。

Categories: 驱动开发

Overall Rating (0)

0 out of 5 stars

Leave your comments

Post comment as a guest

0 Character restriction
Your text should be more than 3 characters

People in this conversation