首页 > Windows驱动开发 > IO_STACK_LOCATION与IRP的一点笔记

IO_STACK_LOCATION与IRP的一点笔记

IO_STACK_LOCATION和IRP算是驱动中两个很基础的东西,为了理解这两个东西,找了一点资料。

1. IRP可以看成是Win32窗口程序中的消息(Message),DEVICE_OBJECT可以看成是Win32窗口程序中的窗口(Window)

2. 任何内核模式程序在创建一个IRP时,同时还创建了一个与之关联的IO_STACK_LOCATION结构数组:数组中的每个堆栈单元都对应一个将处理该IRP的驱动程序。
IRP的头部有一个当前IO_STACK_LOCATION的数组索引,同时也有一个指向该IO_STACK_LOCATION的指针。索引是从1开始,没有0。当驱动程序准备向次低层驱动程序传递IRP时可以调用IoCallDriver例程,它其中的一个工作是递减当前IO_STACK_LOCATION的索引,使之与下一层的驱动程序匹配。但该索引不会设置成0,如果设置成0,系统将会崩溃。就是说,最底层的驱动程序不会调用IoCallDriver例程。

3. IO_STACK_LOCATION中有一个PIO_COMPLETION_ROUTINE类型的成员CompletionRoutine,这是一个I/O完成例程的地址,该地址是由与这个堆栈单元对应的驱动程序的更上一层驱动程序设置的。你绝对不要直接设置这个域,应该调用IoSetCompletionRoutine函数,该函数知道如何参考下一层驱动程序的堆栈单元。设备堆栈的最低一级驱动程序并不需要完成例程,因为它们必须直接完成请求。然而,请求的发起者有时确实需要一个完成例程,但通常没有自己的堆栈单元这就是为什么每一级驱动程序都使用下一级驱动程序的堆栈单元保存自己完成例程指针的原因

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
VOID
IoSetCompletionRoutine(
    __in PIRP Irp,
    __in_opt PIO_COMPLETION_ROUTINE CompletionRoutine,
    __in_opt __drv_aliasesMem PVOID Context,
    __in BOOLEAN InvokeOnSuccess,
    __in BOOLEAN InvokeOnError,
    __in BOOLEAN InvokeOnCancel
    )
{
    PIO_STACK_LOCATION irpSp;
    ASSERT( (InvokeOnSuccess || InvokeOnError || InvokeOnCancel) ? (CompletionRoutine != NULL) : TRUE );
    irpSp = IoGetNextIrpStackLocation(Irp);
    irpSp->CompletionRoutine = CompletionRoutine;
    irpSp->Context = Context;
    irpSp->Control = 0;
 
    if (InvokeOnSuccess) {
        irpSp->Control = SL_INVOKE_ON_SUCCESS;
    }
 
    if (InvokeOnError) {
        irpSp->Control |= SL_INVOKE_ON_ERROR;
    }
 
    if (InvokeOnCancel) {
        irpSp->Control |= SL_INVOKE_ON_CANCEL;
    }
}

总算解释了一下为什么IoSetCompletionRoutine中把完成例程设置在下一个IO堆栈之中。最底层的驱动程序不应该安装一个完成例程。

4. 完成例程的框架

1
2
3
4
5
6
7
8
9
10
NTSTATUS CompletionRoutine(PDEVICE_OBJECT device, PIRP Irp, PVOID context)
{
    if (Irp->PendingReturned)
    {
        IoMarkIrpPending(Irp);
    }
    // ...
 
    return STATUS_SUCCESS /* or some other status code */ ;
}

如果Irp->PendingReturned为TRUE,那么任何不返回STATUS_MORE_PROCESSING_REQUIRED的完成例程都应该调用IoMarkIrpPending,这几乎完全是对的,但仍有例外。如果驱动程序分配了IRP,安装了完成例程,然后在未改变堆栈指针的情况下调用IoCallDriver,那么完成例程就不应该包含这两行代码,因为没有堆栈单元与你的驱动程序关联。(下划线部分理解不太清楚,可以先不做理解,实际情况注意下就是了。有些书甚至都没有提及这一点)

5. 如果你的驱动程序不用关心IRP传递到下层驱动程序之后的事情,没有必要花费处理器时间(调用IoCopyCurrentIrpStackLocationToNext)去把你的堆栈单元内容复制到下一个堆栈单元,因为那个堆栈单元已经含有下一层驱动程序要得到的参数,以及自己上一层驱动程序可能给出的任何完成例程指针。因此可以用下面的代码:

1
2
3
4
5
6
NTSTATUS ForwardAndForget(PDEVICE_OBJECT fdo, PIRP Irp)
{
    PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
    IoSkipCurrentIrpStackLocation(Irp);
    return IoCallDriver(pdx->LowerDeviceObject, Irp);
}

这样的代码是不是在过滤驱动中经常见到呢?

6. IoCopyCurrentIrpStackLocationToNext和IoSkipCurrentIrpStackLocation
IoCopyCurrentIrpStackLocationToNext复制IO堆栈除了完成例程以及完成例程参数以为的内容。

1
2
3
4
5
6
7
8
9
10
11
12
VOID
IoCopyCurrentIrpStackLocationToNext(
    __inout PIRP Irp
)
{
    PIO_STACK_LOCATION irpSp;
    PIO_STACK_LOCATION nextIrpSp;
    irpSp = IoGetCurrentIrpStackLocation(Irp);
    nextIrpSp = IoGetNextIrpStackLocation(Irp);
    RtlCopyMemory( nextIrpSp, irpSp, FIELD_OFFSET(IO_STACK_LOCATION, CompletionRoutine));
    nextIrpSp->Control = 0;
}

IoSkipCurrentIrpStackLocation使堆栈指针少前进一步,而IoCallDriver函数会使堆栈指针向前一步,中和的结果就是堆栈指针不变。当下一个驱动程序的派遣例程调用IoGetCurrentIrpStackLocation时,它将收到与我们正使用的完全相同的IO_STACK_LOCATION指针。

1
2
3
4
5
6
7
8
9
VOID
IoSkipCurrentIrpStackLocation (
    __inout PIRP Irp
)
{
    ASSERT(Irp->CurrentLocation <= Irp->StackCount);
    Irp->CurrentLocation++;
    Irp->Tail.Overlay.CurrentStackLocation++;
}

原来我在这里是有疑问的:一个IRP对应一个IO_STACK_LOCATION,如果使用IoSkipCurrentIrpStackLocation那不是少了一个IO_STACK_LOCATION吗?是的,确实是这样,书上都没有说清楚~~看下面的图吧:
图中显示了这样一种情形:某设备堆栈有三个驱动程序,你的驱动程序(功能设备对象[FDO])和其它两个驱动程序(一个上层过滤器设备对象[FiDO],一个PDO)。在图(a)中,你将看到执行复制堆栈单元的IoCopyCurrentIrpStackLocationToNext函数,堆栈单元、各个参数,和完成例程之间的关系。在图(b)中,你将看到还是这样的关系,但使用的是IoSkipCurrentIrpStackLocation函数,第三个和最后一个堆栈单元被跳过。
复制I/O堆栈单元与跳过I/O堆栈单元

7. IRP完成例程与STATUS_MORE_PROCESSING_REQUIRED
如果某一层的驱动希望下发的IRP在完成之时能够再次获得IRP的控制权,那么可以再完成例程中返回STATUS_MORE_PROCESSING_REQUIRED。代码看起来如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NTSTATUS ForwardAndWait(PDEVICE_OBJECT fdo, PIRP Irp)
{
	KEVENT event;
	KeInitializeEvent(&event, NotificationEvent, FALSE);
	IoCopyCurrentIrpStackLocationToNext(Irp);
	IoSetCompletionRoutine(Irp,
			(PIO_COMPLETION_ROUTINE) OnRequestComplete,
			(PVOID) &event,
			TRUE,
			TRUE,
			TRUE);
	PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION) fdo->DeviceExtension;
	IoCallDriver(pdx->LowerDeviceObject, Irp);
	KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, NULL);
	return Irp->IoStatus.Status;
}
 
NTSTATUS OnRequestComplete(PDEVICE_OBJECT fdo, PIRP Irp, PKEVENT pev)
{
	KeSetEvent(pev, 0, FALSE);
	return STATUS_MORE_PROCESSING_REQUIRED;
}

一旦我们调用了IoCallDriver,我们就放弃了IRP的控制权,直到某些运行在任意线程上下文中的代码调用IoCompleteRequest通知该IRP完成,IoCompleteRequest将调用我们的完成例程。通过在完成例程中返回STATUS_MORE_PROCESSING_REQUIRED,我们停止了I/O堆栈的回卷处理。此时,上层过滤器驱动程序安装的任何完成例程都得不到调用,并且I/O管理器将停止在该IRP上的工作。这种情形就象根本没有调用过IoCompleteRequest一样,当然,某些已经调用过的低级完成例程除外。在这一时刻,该IRP将处于一个中间状态,但我们的ForwardAndWait例程将再次获得该IRP的所有权。

为什么要重新控制IRP呢?有一种情况是:这个IRP是当前的驱动动态分配的,转发给下层驱动之后,这个IRP总要在下面的驱动处理完之后把IRP分配用的空间回收吧?

Reference:《Programming the Microsoft Windows Driver Model


觉得文章还不错?点击此处对作者进行打赏!


本文地址: 程序人生 >> IO_STACK_LOCATION与IRP的一点笔记
作者:代码疯子(Wins0n) 本站内容如无声明均属原创,转载请保留作者信息与原文链接,谢谢!


更多



  1. 2013年11月30日10:59 | #1

    科长,增加个友情链接吧。本屌已添加
    http://www.cnblogs.com/LittleHann/

    [回复]

    代码疯子 回复:

    @LittleHann, [em022] 加了

    [回复]