内存缓冲区生命周期

内存缓冲区的生命周期跨越从创建缓冲区到删除缓冲区的时间。 本主题介绍缓冲区使用方案及其在删除缓冲区时的影响。

在 KMDF) (内核模式驱动程序框架中,请求对象表示 I/O 请求。 每个请求对象都与一个或多个内存对象相关联,每个内存对象表示用于请求中的输入或输出的缓冲区。

当框架创建表示传入 I/O 请求的请求和内存对象时,它会将请求对象设置为关联内存对象的父对象。 因此,内存对象的保留期不能超过请求对象的生存期。 当基于框架的驱动程序完成 I/O 请求时,框架将删除请求对象和内存对象,使这两个对象的句柄变得无效。

但是,基础缓冲区是不同的。 根据创建缓冲区的组件及其创建缓冲区的方式,缓冲区可能具有引用计数,并且可能由内存对象拥有,也可能不具有引用计数。 如果内存对象拥有缓冲区,则缓冲区具有引用计数,并且其生存期限制为内存对象的生存期。 如果其他组件创建了缓冲区,则缓冲区的生存期与内存对象无关。

基于框架的驱动程序还可以创建自己的请求对象以发送到 I/O 目标。 驱动程序创建的请求可以重复使用驱动程序在 I/O 请求中收到的现有内存对象。 经常向 I/O 目标发送请求的驱动程序可以重复使用它创建 的请求对象

了解请求对象、内存对象和基础缓冲区的生存期对于确保驱动程序不会尝试引用无效句柄或缓冲区指针非常重要。

请考虑以下使用场景:

方案 1:驱动程序从 KMDF 接收 I/O 请求,对其进行处理并完成。

在最简单的方案中,KMDF 将请求调度到驱动程序,该驱动程序执行 I/O 并完成请求。 在这种情况下,基础缓冲区可能是由用户模式应用程序、其他驱动程序或操作系统本身创建的。 有关如何访问缓冲区的信息,请参阅 访问 Framework-Based 驱动程序中的数据缓冲区

当驱动程序 完成请求时,框架将删除内存对象。 缓冲区指针随后无效。

方案 2:驱动程序从 KMDF 接收 I/O 请求并将其转发到 I/O 目标。

在此方案中,驱动程序 将请求转发 到 I/O 目标。 以下示例代码演示驱动程序如何从传入请求对象检索内存对象的句柄、设置要发送到 I/O 目标的请求的格式,以及发送请求:

VOID
EvtIoRead(
    IN WDFQUEUE Queue,
    IN WDFREQUEST Request,
    IN size_t Length
    )
{
    NTSTATUS status;
    WDFMEMORY memory;
    WDFIOTARGET ioTarget;
    BOOLEAN ret;
    ioTarget = WdfDeviceGetIoTarget(WdfIoQueueGetDevice(Queue));

    status = WdfRequestRetrieveOutputMemory(Request, &memory);
    if (!NT_SUCCESS(status)) {
        goto End;
    }

    status = WdfIoTargetFormatRequestForRead(ioTarget,
                                    Request,
                                    memory,
                                    NULL,
                                    NULL);
    if (!NT_SUCCESS(status)) {
        goto End;
    }

    WdfRequestSetCompletionRoutine(Request,
                                    RequestCompletionRoutine,
                                    WDF_NO_CONTEXT);

    ret = WdfRequestSend (Request, ioTarget, WDF_NO_SEND_OPTIONS);
    if (!ret) {
        status = WdfRequestGetStatus (Request);
        goto End;
    }

    return;

End:
    WdfRequestComplete(Request, status);
    return;

}

当 I/O 目标完成请求时,框架将调用驱动程序为请求设置的完成回调。 以下代码显示了一个简单的完成回调:

VOID
RequestCompletionRoutine(
    IN WDFREQUEST                  Request,
    IN WDFIOTARGET                 Target,
    PWDF_REQUEST_COMPLETION_PARAMS CompletionParams,
    IN WDFCONTEXT                  Context
    )
{
    UNREFERENCED_PARAMETER(Target);
    UNREFERENCED_PARAMETER(Context);

    WdfRequestComplete(Request, CompletionParams->IoStatus.Status);

    return;

}

当驱动程序从其完成回调调用 WdfRequestComplete 时,框架将删除内存对象。 驱动程序检索的内存对象句柄现在无效。

方案 3:驱动程序发出使用现有内存对象的 I/O 请求。

某些驱动程序发出自己的 I/O 请求并将其发送到 I/O 目标,这些目标由 I/O 目标对象表示。 驱动程序可以创建自己的请求对象,也可以 重复使用框架创建的请求对象。 使用任一技术,驱动程序都可以重复使用来自上一个请求的内存对象。 驱动程序不得更改基础缓冲区,但在设置新 I/O 请求的格式时,它可以传递缓冲区偏移量。

有关如何格式化使用现有内存对象的新 I/O 请求的信息,请参阅 向常规 I/O 目标发送 I/O 请求

当框架格式化要发送到 I/O 目标的请求时,它将代表 I/O 目标对象对回收的内存对象进行引用。 I/O 目标对象将保留此引用,直到执行以下操作之一:

  • 请求已完成。
  • 驱动程序通过调用 WdfIoTargetFormatRequestXxxWdfIoTargetSendXxxSynchronously 方法之一再次重新设置请求对象的格式。 有关这些方法的详细信息,请参阅 框架 I/O 目标对象方法
  • 驱动程序调用 WdfRequestReuse

新的 I/O 请求完成后,框架将调用驱动程序为此请求设置的 I/O 完成回调。 此时,I/O 目标对象仍保留对内存对象的引用。 因此,在 I/O 完成回调中,驱动程序必须先对驱动程序创建的请求对象调用 WdfRequestReuse ,然后才能完成从中检索内存对象的原始请求。 如果驱动程序不调用 WdfRequestReuse,则由于额外的引用,检查会出现 bug。

方案 4:驱动程序发出使用新内存对象的 I/O 请求。

框架为驱动程序提供了三种方法来创建新的内存对象,具体取决于基础缓冲区的源。 有关详细信息,请参阅 使用内存缓冲区

如果缓冲区由框架分配或从驱动程序创建的 lookaside 列表中分配,则内存对象拥有该缓冲区,因此只要内存对象存在,缓冲区指针就保持有效。 发出异步 I/O 请求的驱动程序应始终使用内存对象拥有的缓冲区,以便框架可以确保缓冲区一直存在,直到 I/O 请求完成发证驱动程序。

如果驱动程序通过调用 WdfMemoryCreatePreallocated 将以前分配的缓冲区分配给新的内存对象,则内存对象不拥有该缓冲区。 在这种情况下,内存对象的生存期与基础缓冲区的生存期无关。 驱动程序必须管理缓冲区的生存期,并且不得尝试使用无效的缓冲区指针。

方案 5:驱动程序重复使用它创建的请求对象。

驱动程序可以重用其创建的请求对象,但在每次重用之前,它必须通过调用 WdfRequestReuse 来重新初始化每个此类对象。 有关详细信息,请参阅 重用框架请求对象

有关重新初始化请求对象的示例代码,请参阅 KMDF 版本随附的 ToasterNdisEdge 示例。