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导致系统异常。
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;
}
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从应用层到驱动层的完整路径画出来了。你可以看到,同步和异步在分发函数处就分叉了。同步直接完成,异步则先返回pending,等硬件处理完再通过完成例程收尾。
好了,IRP和分发函数的核心内容就这些。记住:IRP是驱动和应用沟通的桥梁,MajorFunction是驱动处理请求的入口,完成例程是IRP生命周期的最后一步。把这三点吃透了,驱动开发就算入门了。