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:到底怎么选?

这个问题,我建议你从三个维度来考虑:

  1. 性能要求:如果你的驱动需要频繁操作硬件,或者对延迟敏感,选KMDF。比如显卡驱动、网卡驱动、存储控制器驱动。
  2. 稳定性要求:如果驱动崩溃不能影响整个系统,选UMDF。比如打印机驱动、扫描仪驱动。UMDF驱动崩溃了,只是那个进程挂了,系统没事。
  3. 开发效率: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式驱动 vs WDF框架 架构对比 NT式驱动(传统WDM) I/O管理器 IRP(手动管理) DriverEntry / Dispatch函数 硬件操作(手动同步) ⚠ 需要手动处理所有IRP WDF框架(KMDF/UMDF) I/O管理器 WDF框架(自动管理IRP) EvtXxx回调函数 硬件操作(框架同步) ✅ 框架自动处理IRP和同步

避坑指南:我曾经在一个项目中,为了省事直接拿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