设备驱动程序和用户应用程序之间的数据交换






4.61/5 (13投票s)
2004年11月6日
5分钟阅读

143688

2735
描述了如何在设备驱动程序和用户模式应用程序之间交换数据。
引言
尽管这个问题已经被提出过很多很多次,但我还没有找到一个好的、简洁的解释,说明实现用户模式应用程序和内核模式驱动程序之间通信所需的基本技术。大多数第一次尝试都是关于共享事件,并使用 SetEvent
和 WaitForSingleObject
函数来实现通知。虽然这在某些情况下可能有用,但这种技术很慢,而且不是一种好的通信方式。
在本文中,我将描述我的应用程序和驱动程序进行通信的方式。这不是唯一的技巧,但我觉得它方便且易于实现。
示例项目
请注意,示例应用程序不完整,因为我无法模拟硬件 IRQ。尽管如此,它包含了理解发生了什么所需的所有代码。用户模式应用程序也未经测试,并且使用了 IOCP 端口,这使其变得更复杂一些。
假设
我对这个问题做出的假设是:
- 您有一个会产生中断的硬件。
- 当 IRQ 触发时,您想从硬件读取一些数据。
- 您想将这些数据传递回应用程序。
请注意,这个通用描述适用于许多问题,例如从硬盘读取文件,或从自定义数据采集硬件收集数据。
用户模式应用程序的实现
正如我之前所说,我的假设是用户模式应用程序正在等待驱动程序生成某些数据。最有可能的是,您会想要一个包含循环的线程,该线程将从驱动程序读取数据并对其进行处理。因此,第一步显然是打开驱动程序。
1. 打开设备
我在这里不会深入过多细节。您必须使用 CreateFile
函数来打开设备。为 dwCreationDisposition
传递 OPEN_EXISTING
标志,为 dwFlagsAndAttributes
传递 FILE_FLAG_OVERLAPPED
。后者不是必需的,但由于您的驱动程序很可能 100% 异步就绪(是吗?),您无论如何都会与之异步通信。关于打开设备的另一条说明:当您在驱动程序中创建设备对象时,必须调用 IoCreateSymbolicLink
函数,以便能够使用 CreateFile
函数打开设备。
2. 读取数据!
一旦设备打开,您就可以使用 ReadFile
函数从设备驱动程序读取数据。由于我们用重叠标志打开了设备,所以这里必须使用 OVERLAPPED
结构。当您调用 ReadFile
时,它会立即返回 0
,并且 GetLastError
将返回 ERROR_IO_PENDING
。现在,调用其中一个等待函数(例如 WaitForSingleObject
、GetQueuedCompletionStatus
等)来等待数据到达。
当等待函数成功返回时,您传递给 ReadFile
的缓冲区就包含了您迫切想要获取的数据!
内核模式驱动程序的实现
由于我们要从驱动程序读取数据,因此您需要一个分发读取函数。您可以在 DriverEntry
例程中通过设置 DriverObject->MajorFunction[IRP_MJ_READ]
字段来指定它。此函数具有以下签名。
NTSTATUS DispatchRead(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp);
下一步是设置某种队列来存储读取请求。为此,我使用 LIST_ENTRY
结构并使用 InitializeListHead
函数对其进行初始化。您可以在 DriverEntry
例程中执行此操作。不要忘记,此列表必须位于不可分页内存中!最好的方法是将其放入您的设备扩展结构中。如您可能知道的,当您将 IRP 存储在队列中并想确保正确取消这些 IRP 时,队列是一个真正的痛苦。感谢微软,因为它(在新版 DDK 中)提供了 CSQ 例程!它们将完成大部分繁重的工作。(有关完整示例,请参阅 DDK。)
因此,下一步是“安全取消”初始化我们的队列。这是通过调用 IoCsqInitialize
函数完成的。
在此之后,我们实现分发读取函数,使其将传入的 IRP 放入我们的队列(仅为清晰起见:用户模式的 1 个 ReadFile
= 分发读取函数中的 1 个 IRP)。这是通过调用 IoCsqInsertIrp
函数完成的。现在我们有了排队的 IRP,我们只需返回 STATUS_PENDING
,告诉 IO 系统该操作已注册并将得到处理。请注意,当从分发例程返回时,用户模式代码中的 ReadFile
函数也将返回,并且 — 如预期 — GetLastError
返回 ERROR_STATUS_PENDING
。
现在,当 IRQ 到达时,我们必须从设备读取数据。执行方式如下:
- 禁用硬件中的 IRQ,以便我们可以安全地访问硬件内存或端口。
- 通过调用
KeInsertQueueDpc
来排队 DPC 例程。在 IRQ 例程中除了这两个步骤之外不执行任何操作(当然,您可以做任何您想做的事情,但那样您的 Windows 将会极其缓慢)是至关重要的! - 在您的 DPC 例程中,调用
IoCsqRemoveNextIrp
函数来从分发读取函数排队的 IRP 中移除一个。如果返回NULL
,则队列为空。 - 通过执行类似以下的操作来访问用户缓冲区(
Irp
是我们刚刚出队的 IRP)PUCHAR UserBuffer = (PUCHAR)MmGetSystemAddressForMdl(Irp->MdlAddress);
- 从您的硬件读取数据并填充
UserBuffer
。 - 确保所有缓冲区都已刷新。
KeFlushIoBuffers(Irp->MdlAddress, TRUE, FALSE);
- 调用
IoCompleteRequest
并将Irp
传递给它以完成它。 - 重新启用硬件中的 IRQ。
当步骤 7 完成时,用户模式应用程序中的等待函数将返回,并且缓冲区将充满数据!
结论
正如您所看到的,一旦掌握了技术,实现数据交换并不难。在实际应用程序中,您还需要进行其他步骤,例如检查用户缓冲区的尺寸和可访问性。另外,每次 IRQ 到来时通常不必完成一个 IRP。您也可以只填充缓冲区直到它满,然后在缓冲区没有空间时完成它。缓冲区的尺寸在 IRP 的当前堆栈位置中。
PIO_STACK_LOCATION IrpStack = IoGetCurrentIrpStackLocation(Irp); ULONG BufferSize = IrpStack->Parameters.Read.Length;