4、EvtIoRead派遣函数:读取请求处理流程、缓冲区管理、完成例程注册

在KMDF框架中,EvtIoRead是处理IRP_MJ_READ请求的核心派遣回调。驱动程序通过注册该回调,接收来自应用程序或内核其他组件的读取请求。本章深入剖析读取请求的完整处理流程、三种缓冲区管理模型的选择与实现,以及完成例程的注册机制与执行上下文。

4.1 读取请求处理流程

当应用程序调用ReadFile或内核组件发送IRP_MJ_READ时,KMDF框架自动调用驱动注册的EvtIoRead回调。整个处理流程可分为四个阶段:请求接收、参数验证、数据传输、请求完成。

关键原则: EvtIoRead回调运行在任意线程上下文(通常为发送请求的线程),且IRQL级别为PASSIVE_LEVEL或DISPATCH_LEVEL。驱动必须避免在此回调中执行长时间阻塞操作,否则会降低系统响应性。
EvtIoRead 读取请求处理流程 阶段1:请求接收 阶段2:参数验证 阶段3:数据传输 阶段4:请求完成 各阶段核心操作: • 请求接收:WdfRequestRetrieveInputBuffer / WdfRequestRetrieveOutputBuffer 获取缓冲区 • 参数验证:检查 Length、Offset、IoControlCode 等参数合法性 • 数据传输:通过 DMA、PIO 或内存拷贝完成数据交换 • 请求完成:WdfRequestComplete / WdfRequestCompleteWithInformation 返回状态和字节数

4.2 缓冲区管理模型

KMDF提供三种缓冲区管理模型,驱动在创建队列时通过WDF_IO_QUEUE_CONFIG结构体的PowerManaged和DefaultQueue等字段配合WdfDeviceInitSetIoType指定。选择正确的模型直接影响性能与兼容性。

模型 描述 适用场景 IRQL限制
缓冲I/O (Buffered) 系统分配中间缓冲区,KMDF自动在用户与内核空间之间拷贝数据 小数据量(<4KB)、对安全性要求高、驱动逻辑简单 PASSIVE_LEVEL
直接I/O (Direct) 系统锁定用户内存页,KMDF提供MDL描述物理内存 大数据量(>4KB)、DMA传输、高性能要求 DISPATCH_LEVEL
两者皆可 (Neither) KMDF不处理缓冲区,驱动直接访问用户空间指针(需自行探测地址有效性) 特殊协议驱动、需要零拷贝的场景 PASSIVE_LEVEL
最佳实践: 对于大多数PCIe/SPI/I2C设备驱动,推荐使用直接I/O模型。它避免了缓冲I/O的额外拷贝开销,同时由系统保证内存锁定,降低驱动复杂度。使用WdfDeviceInitSetIoType在初始化时设置首选I/O类型。
三种缓冲区管理模型对比 缓冲I/O (Buffered) 用户缓冲区 ↓ 系统拷贝 内核中间缓冲区 ↓ 驱动访问 WdfRequestRetrieveOutputBuffer 优点:安全、简单 缺点:额外拷贝开销 直接I/O (Direct) 用户缓冲区 ↓ 锁定内存页 MDL + 物理地址 ↓ DMA / 直接访问 WdfRequestRetrieveOutputMemory 优点:高性能、零拷贝 缺点:需处理分页 两者皆可 (Neither) 用户缓冲区 ↓ 直接指针访问 用户空间虚拟地址 ↓ 需ProbeForRead/Write Irp->UserBuffer / SystemBuffer 优点:零拷贝、灵活 缺点:安全性风险高

4.3 缓冲区获取与数据访问

在EvtIoRead中,驱动通过KMDF提供的API获取缓冲区指针或内存对象。不同I/O模型对应的API不同:

  • 缓冲I/O: 使用 WdfRequestRetrieveOutputBuffer 获取系统分配的中间缓冲区。
  • 直接I/O: 使用 WdfRequestRetrieveOutputMemory 获取WDFMEMORY对象,再通过 WdfMemoryGetBuffer 获取虚拟地址。
  • 两者皆可: 直接访问 WdfRequestWdmGetIrp 返回的IRP中的 UserBuffer 或 SystemBuffer。
// 示例:在EvtIoRead中使用缓冲I/O获取输出缓冲区
VOID
EvtIoRead(
    _In_ WDFQUEUE Queue,
    _In_ WDFREQUEST Request,
    _In_ size_t Length
    )
{
    NTSTATUS status;
    PVOID buffer;
    size_t bufferLength;

    // 获取输出缓冲区(缓冲I/O模型)
    status = WdfRequestRetrieveOutputBuffer(
        Request,
        Length,           // 请求的最小字节数
        &buffer,
        &bufferLength
        );

    if (!NT_SUCCESS(status)) {
        WdfRequestComplete(Request, status);
        return;
    }

    // 执行硬件读取操作,将数据填入buffer
    // ...

    // 完成请求,返回实际读取的字节数
    WdfRequestCompleteWithInformation(
        Request,
        STATUS_SUCCESS,
        bufferLength
        );
}
常见错误: 在直接I/O模型中,驱动不能长时间持有MDL锁定的内存。如果读取操作需要异步完成,必须在完成例程中释放内存引用,否则会导致系统内存压力。使用WdfRequestForwardToIoQueue将请求转发到专用队列进行异步处理。

4.4 完成例程注册

完成例程(CompletionRoutine)是KMDF驱动中处理异步I/O操作完成通知的关键机制。当驱动将请求提交到硬件或转发到其他队列后,通过注册完成例程来接收操作完成的通知。

完成例程的注册通过WdfRequestSetCompletionRoutine实现,其函数原型如下:

VOID
WdfRequestSetCompletionRoutine(
    _In_ WDFREQUEST Request,
    _In_ PFN_WDF_REQUEST_COMPLETION_ROUTINE CompletionRoutine,
    _In_opt_ WDFCONTEXT Context
    );

完成例程回调函数必须遵循以下签名:

VOID
EvtRequestCompletionRoutine(
    _In_ WDFREQUEST Request,
    _In_ WDFIOTARGET Target,
    _In_ PWDF_REQUEST_COMPLETION_PARAMS Params,
    _In_ WDFCONTEXT Context
    );
参数 说明
Request 已完成的请求句柄
Target 请求发送到的I/O目标
Params 包含完成状态、传输字节数等信息的结构体
Context 注册时传入的驱动定义上下文
完成例程注册与执行流程 EvtIoRead WdfRequestSet CompletionRoutine WdfRequestSend 硬件完成 EvtRequest CompletionRoutine 注册完成回调 异步完成通知

4.5 完成例程中的关键操作

在完成例程中,驱动通常需要执行以下操作:

  1. 检查完成状态: 通过 Params->IoStatus.Status 判断操作是否成功。
  2. 获取传输字节数: 通过 Params->IoStatus.Information 获取实际传输的字节数。
  3. 处理错误: 如果硬件返回错误,驱动可能需要重试或记录错误日志。
  4. 完成原始请求: 调用 WdfRequestComplete 或 WdfRequestCompleteWithInformation 通知上层请求已完成。
// 完成例程实现示例
VOID
EvtReadCompletionRoutine(
    _In_ WDFREQUEST Request,
    _In_ WDFIOTARGET Target,
    _In_ PWDF_REQUEST_COMPLETION_PARAMS Params,
    _In_ WDFCONTEXT Context
    )
{
    NTSTATUS status = Params->IoStatus.Status;
    size_t information = (size_t)Params->IoStatus.Information;

    if (!NT_SUCCESS(status)) {
        // 硬件操作失败,记录错误
        TraceEvents(TRACE_LEVEL_ERROR, DBG_READ,
                    "Read failed with status 0x%X\n", status);
        WdfRequestComplete(Request, status);
        return;
    }

    // 成功:将实际读取的字节数返回给上层
    WdfRequestCompleteWithInformation(
        Request,
        STATUS_SUCCESS,
        information
        );
}
调试提示: 在完成例程中设置断点时,注意IRQL可能为DISPATCH_LEVEL。使用!wdfkd.wdfrequest命令可以查看请求的当前状态和完成例程信息。如果完成例程未被调用,检查WdfRequestSetCompletionRoutine是否在WdfRequestSend之前调用。

4.6 异步读取与请求转发

对于需要长时间等待硬件响应的读取操作,驱动不应在EvtIoRead中同步等待。正确的做法是将请求转发到专用队列,由DpcForIsr或专用线程完成操作。

// 将读取请求转发到手动队列进行异步处理
VOID
EvtIoRead(
    _In_ WDFQUEUE Queue,
    _In_ WDFREQUEST Request,
    _In_ size_t Length
    )
{
    NTSTATUS status;
    WDFQUEUE manualQueue;

    // 获取驱动定义的异步处理队列
    manualQueue = GetDeviceContext(Queue)->AsyncReadQueue;

    // 转发请求到手动队列
    status = WdfRequestForwardToIoQueue(
        Request,
        manualQueue
        );

    if (!NT_SUCCESS(status)) {
        WdfRequestComplete(Request, status);
    }
    // 请求已转发,EvtIoRead返回
}

在异步队列中,驱动可以注册EvtIoDefault回调来处理转发的请求,并在硬件中断的DpcForIsr中完成请求。

异步读取请求转发与完成路径 默认队列 EvtIoRead 手动队列 EvtIoDefault DpcForIsr 请求完成 转发 硬件中断 DPC回调

4.7 常见故障排查清单

故障现象 可能原因 排查方法
读取请求返回0字节 缓冲区长度参数错误;硬件未就绪 检查Length参数;使用WinDbg查看硬件状态寄存器
完成例程未被调用 未注册完成例程;请求未发送到I/O目标 使用!wdfkd.wdfrequest检查请求状态
蓝屏:IRQL_NOT_LESS_OR_EQUAL 在DISPATCH_LEVEL访问分页内存 检查缓冲区访问是否在正确的IRQL下进行
读取数据损坏 缓冲区模型选择错误;DMA对齐问题 验证I/O类型配置;检查DMA缓冲区对齐要求
核心要点回顾:
  • EvtIoRead运行在任意线程上下文,避免长时间阻塞。
  • 根据数据量和性能需求选择缓冲I/O、直接I/O或两者皆可模型。
  • 完成例程是异步I/O的核心,必须在请求发送前注册。
  • 对于长时间操作,使用请求转发+DPC模式避免阻塞调用线程。