3. IRP与分发函数:深入理解Windows驱动核心机制

说实话,IRP(I/O Request Packet)是Windows驱动开发里最绕不开的东西。我刚开始学驱动时,看到IRP那一大堆字段就头大。但后来我发现,只要抓住几个关键点,这东西其实没那么复杂。

3.1 IRP结构详解:别被吓到,核心就这几个字段

IRP说白了就是一个数据结构,用来描述一次I/O请求。每次应用程序调用ReadFile、WriteFile,或者DeviceIoControl,I/O管理器就会创建一个IRP,然后发给你的驱动。

我习惯把IRP想象成一个快递包裹。包裹上有发件人信息、收件人地址、包裹内容。IRP也一样,它包含了这次请求的所有信息。

IRP关键字段 说明 我常用的场景
IoStatus 记录操作结果,状态码和信息字节数 完成请求前必须设置,否则蓝屏
UserBuffer 用户模式下的缓冲区地址 用于读写操作的数据传输
AssociatedIrp 包含SystemBuffer或MasterIrp 根据IOCTL的METHOD决定用哪个
MdlAddress 内存描述符链表,用于直接I/O 大数据传输时用MDL效率更高
PendingReturned 标记是否返回STATUS_PENDING 异步处理时必须检查这个
CancelRoutine 取消例程,用于处理请求取消 用户按取消按钮时触发

嗯,这里要注意的是IoStatus。我见过不少新手驱动写完忘了设置这个字段,结果系统直接蓝屏。每次完成IRP前,一定记得设置Irp->IoStatus.Status和Irp->IoStatus.Information。

3.2 MajorFunction注册:你的驱动入口在哪里

驱动加载后,系统怎么知道该调用你的哪个函数来处理读写请求?答案就在DriverObject->MajorFunction数组里。

这个数组有IRP_MJ_MAXIMUM_FUNCTION + 1个元素,每个元素对应一种IRP类型。你只需要把对应的处理函数注册进去就行。

// 典型的DriverEntry注册代码
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;
}

我个人习惯把所有的分发函数都先写一个默认处理,只返回STATUS_NOT_SUPPORTED。然后根据需求逐个实现。这样能避免漏掉某个IRP导致系统异常。

小技巧: 如果你不确定某个IRP_MJ_xxx是否必须处理,可以查WDK文档。但说实话,IRP_MJ_CREATE和IRP_MJ_CLOSE基本是必选的,否则应用层打不开设备。

3.3 完成例程(CompletionRoutine):IRP的"善后工作"

完成例程是IRP处理流程中一个很巧妙的设计。当你的驱动把IRP传递给下层驱动后,下层处理完会回调你的完成例程。这就像你委托别人办事,办完后对方给你回个电话。

为什么要用完成例程?举个例子。我在项目中做过一个USB驱动,需要把数据发给USB设备。驱动本身不直接操作硬件,而是把IRP传给USB总线驱动。总线驱动处理完后,通过完成例程通知我结果。

// 设置完成例程的典型代码
NTSTATUS DispatchRead(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    // 设置完成例程
    IoSetCompletionRoutine(Irp, 
                           ReadCompletionRoutine,  // 完成函数
                           DeviceObject,           // 上下文
                           TRUE,                   // 成功时调用
                           TRUE,                   // 失败时调用
                           TRUE);                  // 取消时调用
    
    // 把IRP传递给下层驱动
    return IoCallDriver(NextDeviceObject, Irp);
}

// 完成例程实现
NTSTATUS ReadCompletionRoutine(PDEVICE_OBJECT DeviceObject, 
                                PIRP Irp, 
                                PVOID Context)
{
    // 检查下层驱动处理结果
    if (Irp->IoStatus.Status == STATUS_SUCCESS) {
        // 处理数据...
    }
    
    // 返回STATUS_MORE_PROCESSING_REQUIRED表示我们还要继续处理
    return STATUS_MORE_PROCESSING_REQUIRED;
}
避坑指南: 我曾经在完成例程里忘记返回STATUS_MORE_PROCESSING_REQUIRED,结果IRP被系统自动释放了,后续代码访问Irp指针直接蓝屏。记住:如果你在完成例程里还要访问IRP,一定要返回这个状态码。

3.4 同步与异步IO处理:两种模式,两种思维

同步IO和异步IO,说白了就是"等不等"的问题。

同步IO: 应用层调用ReadFile,驱动处理完才返回。这期间应用线程被阻塞。简单,但效率低。

异步IO: 应用层调用ReadFile,驱动立即返回STATUS_PENDING。应用线程继续干别的事。等驱动处理完了,再通知应用层。复杂,但效率高。

我刚开始做驱动时,所有操作都用同步方式。后来做一个高速数据采集卡驱动,发现同步方式根本跑不起来——采集卡每秒产生几万个数据包,同步处理CPU占用率直接100%。改成异步后,问题迎刃而解。

实现异步IO的关键点:

  • 在分发函数中返回STATUS_PENDING
  • 调用IoMarkIrpPending标记IRP为pending状态
  • 在硬件中断或DPC中完成IRP
  • 应用层使用OVERLAPPED结构发起异步请求
// 异步读的实现框架
NTSTATUS DispatchReadAsync(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
    // 标记IRP为pending
    IoMarkIrpPending(Irp);
    
    // 启动硬件操作(比如DMA传输)
    StartHardwareOperation(Irp);
    
    // 立即返回pending
    return STATUS_PENDING;
}

// 硬件中断处理中完成IRP
VOID DpcForIsr(PKDPC Dpc, PDEVICE_OBJECT DeviceObject, PIRP Irp, PVOID Context)
{
    // 设置操作结果
    Irp->IoStatus.Status = STATUS_SUCCESS;
    Irp->IoStatus.Information = bytesTransferred;
    
    // 完成IRP,唤醒等待的应用线程
    IoCompleteRequest(Irp, IO_NO_INCREMENT);
}

核心要点: 同步还是异步,取决于你的硬件特性和性能要求。如果操作很快(微秒级),同步就够了。如果操作耗时(毫秒级以上),必须用异步。我个人的经验是:拿不准的时候,先实现同步,再根据性能测试结果决定是否改异步。

3.5 本章知识体系总览

下面这张图是我自己总结的IRP处理流程,希望能帮你理清思路:

IRP处理流程与分发函数架构 应用层 (User Mode) CreateFile/ReadFile/WriteFile I/O管理器 创建IRP,调用MajorFunction 驱动分发函数 (MajorFunction) DispatchCreate / DispatchRead / DispatchWrite / DispatchIoControl 同步? 是 同步处理 直接完成IRP,返回结果 否 异步处理 返回STATUS_PENDING 完成例程 (CompletionRoutine) 善后处理

这张图把IRP从应用层到驱动层的完整路径画出来了。你可以看到,同步和异步在分发函数处就分叉了。同步直接完成,异步则先返回pending,等硬件处理完再通过完成例程收尾。

好了,IRP和分发函数的核心内容就这些。记住:IRP是驱动和应用沟通的桥梁,MajorFunction是驱动处理请求的入口,完成例程是IRP生命周期的最后一步。把这三点吃透了,驱动开发就算入门了。

可可精品资料库