3、NT式驱动与WDF驱动:NT式驱动框架、WDF框架对比、KMDF与UMDF选择
说实话,刚入行那会儿,我也被这些驱动框架搞得晕头转向。NT式驱动、WDF、KMDF、UMDF……一堆缩写,看着就头大。但搞了十几年驱动开发后,我慢慢摸清了它们的脾气。今天咱们就好好聊聊这个话题。
我个人习惯把驱动开发分成两个时代:一个是「刀耕火种」的NT式驱动时代,一个是「工业化生产」的WDF时代。你想想看,从NT 4.0到Windows 10,微软自己也在不断进化。
NT式驱动框架:老炮儿的必修课
NT式驱动,也叫传统WDM驱动。它是最底层的驱动模型,直接跟I/O管理器打交道。说白了,你写的DriverEntry、DispatchCreate、DispatchRead这些函数,都是NT式驱动的标配。
核心要点:NT式驱动中,你需要手动处理所有IRP(I/O请求包)。从创建、分发到完成,每一步都得自己来。
我记得刚做第一个项目时,写了个简单的串口驱动。光是处理IRP_MJ_READ和IRP_MJ_WRITE的同步问题,就折腾了我整整三天。为什么会这样?因为NT式驱动里,IRP的完成时机完全取决于你,稍不留神就蓝屏了。
来看个典型的NT式驱动入口代码:
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->DriverUnload = DriverUnload;
return STATUS_SUCCESS;
}
NTSTATUS DispatchRead(PDEVICE_OBJECT DeviceObject, PIRP Irp)
{
// 手动设置完成状态
Irp->IoStatus.Status = STATUS_SUCCESS;
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
看到没?每个IRP都要手动调用IoCompleteRequest。你忘了调?系统就卡死了。你调了两次?直接蓝屏给你看。嗯,这里要注意,NT式驱动对新手特别不友好。
WDF框架:现代化的驱动开发方式
WDF(Windows Driver Framework)是微软在Vista之后主推的驱动模型。它把NT式驱动的那些脏活累活都封装起来了。你想想看,有了WDF,你就不用再手动管理IRP了,框架帮你搞定。
WDF分两种:KMDF(内核模式驱动框架)和UMDF(用户模式驱动框架)。
| 特性 | KMDF | UMDF |
|---|---|---|
| 运行模式 | 内核模式 | 用户模式 |
| 崩溃影响 | 蓝屏 | 仅进程崩溃 |
| 性能 | 高 | 中等(有上下文切换) |
| 调试难度 | 高(需要WinDbg) | 低(可用VS调试) |
| 适用场景 | 高性能设备、硬件直控 | USB、HID、打印机等 |
我曾经在一个项目中,需要开发一个高速数据采集卡的驱动。当时我选择了KMDF,因为对延迟要求极高。如果当时选了UMDF,每次读写都要从用户态切到内核态,那延迟根本扛不住。
KMDF vs UMDF:到底怎么选?
这个问题,我建议你从三个维度来考虑:
- 性能要求:如果你的驱动需要频繁操作硬件,或者对延迟敏感,选KMDF。比如显卡驱动、网卡驱动、存储控制器驱动。
- 稳定性要求:如果驱动崩溃不能影响整个系统,选UMDF。比如打印机驱动、扫描仪驱动。UMDF驱动崩溃了,只是那个进程挂了,系统没事。
- 开发效率:UMDF可以用Visual Studio直接调试,开发效率高很多。KMDF调试必须用WinDbg连双机,麻烦得很。
我的建议:如果你刚开始学驱动开发,先从UMDF入手。等熟悉了WDF的套路,再转KMDF。别一上来就搞KMDF,容易劝退。
来看个KMDF的典型代码,跟刚才的NT式驱动做个对比:
NTSTATUS KmdfDriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
WDF_DRIVER_CONFIG config;
WDFDRIVER driver;
WDF_DRIVER_CONFIG_INIT(&config, EvtDeviceAdd);
return WdfDriverCreate(DriverObject, RegistryPath, WDF_NO_OBJECT_ATTRIBUTES, &config, &driver);
}
NTSTATUS EvtDeviceAdd(WDFDRIVER Driver, PWDFDEVICE_INIT DeviceInit)
{
WDFDEVICE device;
WDF_IO_QUEUE_CONFIG queueConfig;
// 框架自动处理IRP,你只需要配置队列
WDF_IO_QUEUE_CONFIG_INIT_DEFAULT_QUEUE(&queueConfig, WdfIoQueueDispatchSequential);
queueConfig.EvtIoRead = EvtIoRead;
queueConfig.EvtIoWrite = EvtIoWrite;
return WdfDeviceCreate(&DeviceInit, WDF_NO_OBJECT_ATTRIBUTES, &device);
}
看到区别了吗?KMDF里你根本不用碰IRP。框架帮你创建了I/O队列,你只需要实现EvtIoRead和EvtIoWrite回调函数就行。省心多了。
框架对比:一张图看懂
我画了张图,把NT式驱动和WDF框架的架构对比了一下。你看完应该就明白了:
避坑指南:我曾经在一个项目中,为了省事直接拿NT式驱动的代码改成了KMDF。结果发现很多IRP处理逻辑根本用不上,反而引入了不少bug。后来我学乖了:用WDF框架,就老老实实按WDF的套路来,别想着「抄近路」。
实际项目中的选择策略
说了这么多理论,咱们来点实际的。我总结了一个选择策略,你照着这个来基本不会错:
- 硬件直控类驱动(显卡、网卡、存储):选KMDF。性能第一,稳定第二。
- 外设类驱动(USB设备、HID设备、打印机):选UMDF。开发快,调试方便。
- 虚拟设备驱动(虚拟网卡、虚拟磁盘):选KMDF。因为需要在内核态创建对象。
- 过滤驱动(文件过滤、网络过滤):选KMDF。过滤驱动必须在内核态才能拦截到所有请求。
- 纯软件驱动(比如加密解密、数据转换):选UMDF。不需要碰硬件,用户态就够了。
嗯,这里要注意一点:UMDF驱动不能直接访问硬件。如果你的驱动需要读写端口、操作PCI配置空间,那只能选KMDF。这是硬性限制,没得商量。
小技巧:如果你不确定该选哪个,可以先写UMDF原型验证功能。等验证通过了,再根据性能需求决定要不要转KMDF。我在好几个项目里都是这么干的,省了不少时间。
最后说一句:不管选哪个框架,理解NT式驱动的原理都很重要。因为WDF底层还是基于NT式驱动的,只是帮你封装了。你想想看,如果连IRP是什么都不懂,出了问题怎么排查?
好了,关于NT式驱动和WDF框架的对比,今天就聊到这儿。记住一句话:选框架不是选「最好的」,而是选「最适合你当前项目」的。
公众号:蓝海资料掘金营,微信deep3321