7、IO请求包(IRP):IRP结构详解、MajorFunction分发、完成例程、取消例程

说实话,IRP 是 Windows 驱动开发里最绕不开的东西。你写驱动,本质上就是在跟 IRP 打交道。我刚开始学驱动那会儿,看到 IRP 那一大堆字段,头都大了。后来踩了几个坑,才慢慢摸清楚门道。

今天咱们就把 IRP 彻底聊透。从结构到分发,再到完成和取消,一条线串下来。

7.1 IRP 结构长什么样?

IRP 的全称是 I/O Request Packet,说白了就是操作系统用来描述一个 I/O 请求的数据结构。每个 IRP 都对应一次 I/O 操作,比如读文件、写设备、控制设备等等。

它的核心结构大致分三块:

  • 头部(Header):包含请求类型、状态、优先级等元信息
  • 栈空间(Stack Locations):每个驱动层都有自己的栈位置,存着该层需要的参数
  • 关联缓冲区(Associated Buffer):存放用户数据或系统缓冲区

我习惯把 IRP 想象成一个快递包裹。头部是快递单号,栈空间是每个中转站的贴条,缓冲区就是包裹里的货物。每一站处理完,就在自己的贴条上写点东西,然后传给下一站。

关键字段速查表:

字段含义我常用的场景
IoStatus.Status请求最终状态完成例程里必设
IoStatus.Information传输字节数读/写完成后赋值
RequestorMode发起者模式(内核/用户)判断是否要 Probe
PendingReturned是否挂起过完成例程里检查
Cancel是否被取消取消例程里置位
CancelRoutine取消例程指针注册取消回调
UserBuffer用户模式缓冲区METHOD_NEITHER 时用
AssociatedIrp关联 IRP(主/从)拆分大请求时用

7.2 MajorFunction 分发机制

驱动收到 IRP 后,怎么知道该调用哪个函数?答案就在 MajorFunction 表里。

每个驱动对象(DRIVER_OBJECT)都有一张函数指针表,叫 MajorFunction。索引是 IRP 的主功能码,比如 IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_DEVICE_CONTROL。你注册驱动时,把对应的处理函数填进去就行。

// 典型的分发函数注册
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
    DriverObject->MajorFunction[IRP_MJ_CREATE]         = DispatchCreate;
    DriverObject->MajorFunction[IRP_MJ_CLOSE]          = DispatchClose;
    DriverObject->MajorFunction[IRP_MJ_READ]           = DispatchRead;
    DriverObject->MajorFunction[IRP_MJ_WRITE]          = DispatchWrite;
    DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoControl;
    
    DriverObject->DriverUnload = DriverUnload;
    return STATUS_SUCCESS;
}

分发函数长这样:

NTSTATUS DispatchRead(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
    
    // 检查设备状态
    if (!devExt->DeviceStarted) {
        Irp->IoStatus.Status = STATUS_DEVICE_NOT_READY;
        Irp->IoStatus.Information = 0;
        IoCompleteRequest(Irp, IO_NO_INCREMENT);
        return STATUS_DEVICE_NOT_READY;
    }
    
    // 把 IRP 挂起,稍后处理
    IoMarkIrpPending(Irp);
    // ... 启动异步操作
    return STATUS_PENDING;
}

这里有个坑:如果你返回 STATUS_PENDING,必须调用 IoMarkIrpPending。否则系统会认为你忘了挂起,直接蓝屏。我曾经因为这个 bug 调试了整整一个下午,最后发现是少调了这个宏。

7.3 完成例程(CompletionRoutine)

完成例程是 IRP 在驱动栈中传递时,每一层都可以注册的回调。当下层驱动完成请求后,会沿着栈向上回调。

注册完成例程用 IoSetCompletionRoutine:

IoSetCompletionRoutine(
    Irp,                    // 要注册的 IRP
    MyCompletionRoutine,    // 完成函数
    Context,                // 上下文指针
    TRUE,                   // InvokeOnSuccess
    TRUE,                   // InvokeOnError
    TRUE                    // InvokeOnCancel
);

完成例程长这样:

NTSTATUS MyCompletionRoutine(
    PDEVICE_OBJECT DeviceObject,
    PIRP Irp,
    PVOID Context)
{
    // 检查下层是否挂起过
    if (Irp->PendingReturned) {
        IoMarkIrpPending(Irp);
    }
    
    // 检查是否被取消
    if (Irp->Cancel) {
        // 清理资源
        ExFreePool(Context);
        return STATUS_MORE_PROCESSING_REQUIRED;
    }
    
    // 正常完成
    return STATUS_SUCCESS;
}

注意返回值:

  • STATUS_SUCCESS:继续向上传递
  • STATUS_MORE_PROCESSING_REQUIRED:停止传递,自己处理

我遇到过一种情况:下层驱动返回了错误,但上层完成例程里没检查 Irp->IoStatus.Status,直接用了缓冲区数据。结果就是读到一堆垃圾。所以我的习惯是:完成例程里第一件事就是检查状态。

7.4 取消例程(CancelRoutine)

用户程序随时可以取消一个 I/O 请求。比如用户按了取消按钮,或者进程被强制终止。这时候系统会调用你注册的取消例程。

取消例程的注册时机很关键:

// 在挂起 IRP 之前注册取消例程
IoAcquireCancelSpinLock(&Irp->CancelIrql);
IoSetCancelRoutine(Irp, MyCancelRoutine);
IoReleaseCancelSpinLock(Irp->CancelIrql);

// 然后挂起
IoMarkIrpPending(Irp);

取消例程的实现:

VOID MyCancelRoutine(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    // 注意:此时已持有取消自旋锁
    
    // 从队列中移除 IRP
    RemoveEntryList(&Irp->Tail.Overlay.ListEntry);
    
    // 释放自旋锁
    IoReleaseCancelSpinLock(Irp->CancelIrql);
    
    // 设置完成状态
    Irp->IoStatus.Status = STATUS_CANCELLED;
    Irp->IoStatus.Information = 0;
    
    // 完成请求
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
}

取消例程的陷阱:

  • 取消例程运行时,系统已经帮你拿到了取消自旋锁。你必须在返回前释放它。
  • 不要在取消例程里做耗时操作,比如等待事件、分配内存。它运行在 DISPATCH_LEVEL。
  • 如果 IRP 已经被完成了,再调用 IoSetCancelRoutine 会覆盖之前的取消例程指针,导致内存泄漏。

7.5 核心流程:一张图说清楚

下面这张图展示了 IRP 从创建到完成的完整生命周期。我画的时候特意把每个阶段的关键动作标出来了。

IRP 生命周期流程图 1. IRP 创建 IoBuildDeviceIoControlRequest 2. MajorFunction 分发 DriverObject->MajorFunction[] 3. 驱动处理 IoMarkIrpPending / 异步 正常路径 取消路径 4a. 完成例程回调 IoSetCompletionRoutine 4b. 取消例程回调 IoSetCancelRoutine 5. IoCompleteRequest 设置 IoStatus,释放 IRP IRP 结束 创建 分发 处理 完成 取消 结束

7.6 避坑指南

我的经验总结:

  • IRP 的栈空间索引从 0 开始,当前层用 IoGetCurrentIrpStackLocation,下一层用 IoGetNextIrpStackLocation。别搞混了,我见过有人把这两个用反,结果参数全错。
  • 完成例程里如果要修改 IRP 的完成状态,记得先检查 IoStatus.Status。不要盲目覆盖下层返回的错误码。
  • 取消例程里不要调用 IoCompleteRequest 之后再去访问 IRP 的字段。IRP 可能已经被释放了。
  • 如果你在完成例程里返回 STATUS_MORE_PROCESSING_REQUIRED,你必须自己负责最终调用 IoCompleteRequest。否则 IRP 永远不会被释放,造成内存泄漏。

嗯,IRP 这块内容确实不少。但只要你把结构、分发、完成、取消这四条线理清楚,写驱动的时候心里就有底了。我当年就是靠这张流程图,把 IRP 的整个流转过程刻在脑子里的。

公众号:蓝海资料掘金营,微信deep3321