3. UMDF驱动基本结构:DriverEntry、DeviceAdd、IO目标、设备接口、驱动卸载
UMDF(User-Mode Driver Framework)驱动的基本结构由一组固定的回调函数和对象模型组成。理解这些核心组件是开发稳定、高效驱动程序的基础。本章将详细剖析UMDF驱动的生命周期,从入口点 DriverEntry 到卸载过程,并重点介绍设备初始化、I/O目标以及设备接口的注册机制。
3.1 驱动入口点:DriverEntry
DriverEntry 是UMDF驱动的第一个执行函数,类似于应用程序的 main 函数。当驱动被加载到用户模式宿主进程(WudfHost.exe)中时,框架会调用此函数。其主要职责是初始化驱动对象并注册事件回调。
关键职责:
- 创建并初始化
IWDFDriver对象。 - 设置驱动注册表路径和配置信息。
- 注册
IDriverEntry接口,特别是OnDeviceAdd回调。 - 执行任何一次性的全局初始化(如日志系统、全局锁)。
以下是一个典型的 DriverEntry 实现示例:
HRESULT CMyDriver::OnDriverInitialize(
__in IWDFDriver *pDriver,
__in IWDFDeviceInitialize *pDeviceInit
)
{
UNREFERENCED_PARAMETER(pDeviceInit);
// 保存驱动对象指针
m_pDriver = pDriver;
// 注册设备添加回调
pDriver->SetDriverEntry(this);
return S_OK;
}
// 标准DriverEntry入口
HRESULT STDAPICALLTYPE DllGetClassObject(
__in REFCLSID rclsid,
__in REFIID riid,
__out LPVOID *ppv
)
{
// COM类工厂实现
return CMyDriver::CreateInstance(rclsid, riid, ppv);
}
注意:UMDF驱动本质上是COM对象,因此
DriverEntry 实际上是通过COM类工厂机制实现的。驱动必须导出 DllGetClassObject 和 DllCanUnloadNow 函数。
3.2 设备添加回调:OnDeviceAdd
OnDeviceAdd 是UMDF驱动中最重要的回调函数。当PnP管理器检测到新设备并确定由该驱动控制时,框架会调用此函数。在此函数中,驱动需要完成设备对象的创建、初始化以及I/O处理程序的注册。
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 创建设备对象 | 调用 pDriver->CreateDevice() 创建 IWDFDevice 实例 |
| 2 | 配置设备属性 | 设置设备名称、访问掩码、独占模式等 |
| 3 | 创建I/O队列 | 调用 IWDFDevice::CreateIoQueue() 创建默认或自定义队列 |
| 4 | 注册I/O回调 | 设置 IQueueCallbackRead、IQueueCallbackWrite 等接口 |
| 5 | 创建设备接口 | 调用 IWDFDevice::CreateDeviceInterface() 暴露给应用程序 |
HRESULT CMyDevice::OnDeviceAdd(
__in IWDFDriver *pDriver,
__in IWDFDeviceInitialize *pDeviceInit
)
{
HRESULT hr;
CComPtr<IWDFDevice> pDevice;
// 1. 创建设备对象
hr = pDriver->CreateDevice(pDeviceInit, this, &pDevice);
if (FAILED(hr)) return hr;
// 2. 创建默认I/O队列(并行队列)
CComPtr<IWDFIoQueue> pDefaultQueue;
hr = pDevice->CreateIoQueue(
this, // 回调接口
TRUE, // 默认队列
WdfIoQueueDispatchParallel, // 分发模式
TRUE, // 电源管理
FALSE, // 允许零长度缓冲区
&pDefaultQueue
);
if (FAILED(hr)) return hr;
// 3. 注册设备接口 GUID
hr = pDevice->CreateDeviceInterface(
&GUID_DEVINTERFACE_MYDRIVER,
NULL
);
return hr;
}
常见错误:在
OnDeviceAdd 中执行耗时操作(如等待硬件就绪)会导致系统认为驱动无响应。建议将硬件初始化逻辑放在 IPnpCallback::OnD0Entry 中处理。
3.3 I/O目标(I/O Target)
I/O目标是UMDF中用于发送I/O请求到内核模式或其它驱动组件的抽象对象。UMDF驱动通常不能直接访问硬件,而是通过I/O目标将请求转发到内核模式驱动程序堆栈。
I/O目标分为两种主要类型:
- 本地I/O目标:指向当前设备栈的下一个内核模式驱动(通常是KMDF驱动或端口驱动)。
- 远程I/O目标:指向系统中任意其他设备栈,用于跨驱动通信。
| 属性 | 本地I/O目标 | 远程I/O目标 |
|---|---|---|
| 创建方式 | IWDFDevice::GetDefaultIoTarget() |
IWDFDevice::CreateRemoteTarget() |
| 目标对象 | 内核模式驱动栈 | 其他设备接口 |
| 典型用途 | 发送DMA请求、读写硬件寄存器 | 与传感器、过滤器等协同工作 |
| 生命周期 | 与设备对象绑定 | 由驱动管理 |
// 获取默认I/O目标并发送请求
HRESULT CMyDevice::SendIoRequest(
__in IWDFDevice *pDevice,
__in IWDFMemory *pMemory
)
{
CComPtr<IWDFIoTarget> pIoTarget;
CComPtr<IWDFIoRequest> pRequest;
HRESULT hr;
// 获取默认I/O目标
pIoTarget = pDevice->GetDefaultIoTarget();
if (!pIoTarget) return E_FAIL;
// 创建I/O请求
hr = pDevice->CreateIoRequest(NULL, &pRequest);
if (FAILED(hr)) return hr;
// 格式化请求为写操作
hr = pRequest->FormatWrite(
pMemory, // 数据缓冲区
NULL, // 偏移量
NULL // 完成回调
);
if (FAILED(hr)) return hr;
// 发送请求到I/O目标
hr = pRequest->Send(pIoTarget);
return hr;
}
性能提示:对于频繁的I/O操作,建议使用
IWDFIoRequest::SendAndForget 方法,它不需要等待完成回调,减少了用户模式与内核模式之间的上下文切换开销。
3.4 设备接口(Device Interface)
设备接口是UMDF驱动暴露给用户模式应用程序的通信端点。通过注册一个全局唯一的GUID,应用程序可以使用Win32 API(如 CreateFile)打开设备并发送IOCTL请求。
设备接口的注册流程:
- 在驱动中定义一个GUID(例如
GUID_DEVINTERFACE_MYDRIVER)。 - 在
OnDeviceAdd中调用CreateDeviceInterface注册。 - 应用程序通过
SetupDiGetClassDevs枚举设备接口实例。 - 应用程序调用
CreateFile打开设备路径。 - 驱动通过I/O队列接收来自应用程序的请求。
// 定义设备接口GUID
// {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
DEFINE_GUID(GUID_DEVINTERFACE_MYDRIVER,
0xa1b2c3d4, 0xe5f6, 0x7890,
0xab, 0xcd, 0xef, 0x12, 0x34, 0x56, 0x78, 0x90);
// 在OnDeviceAdd中注册
hr = pDevice->CreateDeviceInterface(
&GUID_DEVINTERFACE_MYDRIVER,
NULL // 参考字符串,可选
);
// 应用程序端打开设备
HANDLE hDevice = CreateFile(
L"\\\\.\\GLOBALROOT\\Device\\UMDF\\{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL
);
最佳实践:
- 使用
SetupDiGetClassDevs和SetupDiEnumDeviceInfo动态获取设备路径,避免硬编码。 - 设备接口GUID应在驱动头文件中定义,并随驱动安装包一起发布。
- 支持多个设备实例时,每个实例都会注册独立的设备接口。
3.5 驱动卸载(Driver Unload)
当驱动被卸载或系统关闭时,UMDF框架会调用驱动的清理回调。驱动必须在此阶段释放所有已分配的资源,包括内存、句柄、同步对象等。
UMDF驱动的卸载流程:
| 阶段 | 回调函数 | 操作 |
|---|---|---|
| 1 | IPnpCallback::OnD0Exit |
设备进入D0(工作状态)退出,停止硬件访问 |
| 2 | IPnpCallback::OnSurpriseRemoval |
处理意外移除(如USB热插拔) |
| 3 | IDriverEntry::OnDriverUnload |
驱动卸载前清理全局资源 |
| 4 | COM卸载 | DllCanUnloadNow 返回S_OK,宿主进程释放DLL |
// 驱动卸载回调
HRESULT CMyDriver::OnDriverUnload(
__in IWDFDriver *pDriver
)
{
UNREFERENCED_PARAMETER(pDriver);
// 释放全局资源
if (m_pGlobalLock) {
delete m_pGlobalLock;
m_pGlobalLock = NULL;
}
// 关闭全局日志句柄
if (m_hLogFile != INVALID_HANDLE_VALUE) {
CloseHandle(m_hLogFile);
m_hLogFile = INVALID_HANDLE_VALUE;
}
return S_OK;
}
// 设备清理回调
HRESULT CMyDevice::OnD0Exit(
__in IWDFDevice *pDevice,
__in WDF_POWER_DEVICE_STATE previousState
)
{
UNREFERENCED_PARAMETER(pDevice);
UNREFERENCED_PARAMETER(previousState);
// 停止所有待处理的I/O请求
AbortPendingRequests();
// 释放设备级资源
ReleaseHardwareResources();
return S_OK;
}
重要:UMDF驱动卸载时,框架会自动清理I/O队列和设备对象。但驱动必须确保所有异步操作(如待处理的DMA传输)在
OnD0Exit 中完成或取消,否则可能导致内存泄漏或系统崩溃。
3.6 本章小结
UMDF驱动的基本结构围绕COM对象模型和事件回调机制构建。核心要点总结如下:
- DriverEntry:通过COM类工厂实现,注册
OnDeviceAdd回调并执行全局初始化。 - OnDeviceAdd:创建设备对象、I/O队列和设备接口,是驱动初始化的核心。
- I/O目标:封装了与内核模式通信的细节,支持本地和远程两种模式。
- 设备接口:通过GUID暴露给应用程序,实现用户模式与驱动的IOCTL通信。
- 驱动卸载:必须按顺序清理资源,确保所有异步操作完成。
学习建议:建议读者使用Visual Studio的UMDF模板创建空驱动项目,逐步添加上述组件,并通过WinDbg观察驱动加载和卸载的日志输出,加深对驱动生命周期的理解。