代码来源及参考:wdk 7,路径:C:\WinDDK\7600.16385.1\src\general\event
这个例子演示了两种关于当硬件事件发生时,驱动如何通知应用程序的方法。一种是基于event的,一种是基于IRP。但是样例驱动程序实际上没有与任何硬件通信,所以就通过 timer DPC 来模拟硬件事件。应用程序告诉驱动是通过设置event 信号还是完成 pending IRP 来通知自己,并给出一个相对的超时时限。
两种方法的处理流程:
1,基于event的方法
应用程序通过CreateEvent()创建一个事件对象,然后通过自定义的 ioctl IOCTL_REGISTER_EVENT 发个驱动程序。由于驱动是单一的(monolithic),顶层的(toplevel),导致它的IRP 处理例程运行在与应用程序进程相同的环境中(process context 一样)----这一点有点神奇啊!,所以event 的句柄在驱动中也是有效的。驱动访问 event 对象句柄并保存器对象指针以备后用,然后将 自定义的 timer DPC 入队列。当DPC 超时,驱动通过KeSetEvent(),在 DISPATCH_LEVEL 将event 设置为有信号并删除 event 对象的访问。使用这种方法必须保证驱动是单一的,顶层的,如果不是这样,就无法保证进程环境与应用程序一致。
2,基于 pending IRP 方法
应用程序发送同步的 ioctl (IOCTL_REGISTER_EVENT)请求。驱动将 IRP 的状态标志为 pending并入队一个timer DPC 。当timer 超时(模拟的硬件事件),驱动完成对IRP的处理再通知应用程序。
下面看一下各函数的生命周期:
首先,驱动被加载后最先被调用的应该是DriverEntry()。在DriverEntry()中,先创建设备对象,接着设置IRP 处理例程,最后初始化设备扩展结构体里面的数据。完成这些准备工作之后,DriverEntry()也就不会再被调用。剩下的就交给IRP 处理函数了。
{
PDEVICE_OBJECT deviceObject;
PDEVICE_EXTENSION deviceExtension;
UNICODE_STRING ntDeviceName;
UNICODE_STRING symbolicLinkName;
NTSTATUS status;
UNREFERENCED_PARAMETER(RegistryPath);
DebugPrint(("==>DriverEntry\n"));
//
// Create the device object
//
RtlInitUnicodeString(&ntDeviceName, NTDEVICE_NAME_STRING);
status = IoCreateDevice(DriverObject, // DriverObject
sizeof(DEVICE_EXTENSION), // DeviceExtensionSize
&ntDeviceName, // DeviceName
FILE_DEVICE_UNKNOWN, // DeviceType
FILE_DEVICE_SECURE_OPEN, // DeviceCharacteristics
FALSE, // Not Exclusive
&deviceObject // DeviceObject
);
if (!NT_SUCCESS(status)) {
DebugPrint(("\tIoCreateDevice returned 0x%x\n", status));
return(status);
}
//
// Set up dispatch entry points for the driver.
//
DriverObject->MajorFunction[IRP_MJ_CREATE] = EventCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = EventCreateClose;
DriverObject->MajorFunction[IRP_MJ_CLEANUP] = EventCleanup;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = EventDispatchIoControl;
DriverObject->DriverUnload = EventUnload;
//
// Create a symbolic link for userapp to interact with the driver.
//
RtlInitUnicodeString(&symbolicLinkName, SYMBOLIC_NAME_STRING);
status = IoCreateSymbolicLink(&symbolicLinkName, &ntDeviceName);
if (!NT_SUCCESS(status)) {
IoDeleteDevice(deviceObject);
DebugPrint(("\tIoCreateSymbolicLink returned 0x%x\n", status));
return(status);
}
//
// Initialize the device extension.
//
deviceExtension = deviceObject->DeviceExtension;
InitializeListHead(&deviceExtension->EventQueueHead);
KeInitializeSpinLock(&deviceExtension->QueueLock);
deviceExtension->Self = deviceObject;
//
// Establish user-buffer access method.
//
deviceObject->Flags |= DO_BUFFERED_IO;
DebugPrint(("<==DriverEntry\n"));
ASSERT(NT_SUCCESS(status));
return status;
}
通过调试信息看一下这些IRP 处理函数的调用顺序。命令行运行 应用程序: event.exe 10 0 ,延迟时限为10秒,通知方式是 基于 pending IRP 的。
从上图可以看出,DriverEntry()退出后;首先,调用的 IRP_MJ_CREATE的处理函数(对应应用层的CreateFile())进行一些资源申请,为后面处理IRP_MJ_DEVICE_CONTROL(对应应用层的DeviceIoControl())通信中的数据;接着,应用程序就可以和驱动通信。当应用程序结束通信并停止服务,这个时候IRP_MJ_CLEANUP的处理函数被调用,来清理现场;接着,IRP_MJ_CLOSE的处理函数(对应应用层的CloseHandle())进一步释放资源。最后,调用驱动卸载函数,整个驱动就结束运行。简化为下图所示:
ring3 ring0
CreateFile() ---> IRP_MJ_CREATE
DeviceIoControl() <---> IRP_MJ_DEVICE_CONTROL
CloseHandle ---> IRP_MJ_CLEANUP,IRP_MJ_CLOSE
下面分别看看这3个阶段的处理流程:
相关结构体:
typedef struct _DEVICE_EXTENSION {
PDEVICE_OBJECT Self;
LIST_ENTRY EventQueueHead; // where all the user notification requests are queued
KSPIN_LOCK QueueLock;
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
typedef struct _NOTIFY_RECORD{
NOTIFY_TYPE Type;
LIST_ENTRY ListEntry;
union {
PKEVENT Event;
PIRP PendingIrp;
} Message;
KDPC Dpc;
KTIMER Timer;
PFILE_OBJECT FileObject;
PDEVICE_EXTENSION DeviceExtension;
BOOLEAN CancelRoutineFreeMemory;
} NOTIFY_RECORD, *PNOTIFY_RECORD;
typedef struct _FILE_CONTEXT{
//
// Lock to rundown threads that are dispatching I/Os on a file handle
// while the cleanup for that handle is in progress.
//
IO_REMOVE_LOCK FileRundownLock;
} FILE_CONTEXT, *PFILE_CONTEXT;
IRP_MJ_CREATE/IRP_MJ_CLOSE对应的处理函数EventCreateClose()主要片段,当应用程序调用CreateFile()打开设备,与驱动建立会话;case IRP_MJ_CREATE 部分被调用。初始化 FILE_CONTEXT 结构体,里面就一个项目FileRundownLock,用来管理当前会话中的多次通信。
case IRP_MJ_CREATE:
DebugPrint(("IRP_MJ_CREATE\n"));
fileContext = ExAllocatePoolWithQuotaTag(NonPagedPool,
sizeof(FILE_CONTEXT),
TAG);
if (NULL == fileContext) {
status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
IoInitializeRemoveLock(&fileContext->FileRundownLock, TAG, 0, 0);
//
// Make sure nobody is using the FsContext scratch area.
//
ASSERT(irpStack->FileObject->FsContext == NULL);
//
// Store the context in the FileObject's scratch area.
//
irpStack->FileObject->FsContext = (PVOID) fileContext;
status = STATUS_SUCCESS;
break;
基于 pending IRP 的处理流程,在函数RegisterIrpBasedNotification()中通过设置 timer DPC 来模拟硬件事件,将 irp 设为pending 状态;当timer 超时,对应的DPC 函数被调用完成irp并通知应用程序。这是正常的情况,当应用程序提前申请结束与驱动程序的会话,而这个时候在驱动中有的 irp 还处于 pending 状态,无法等到timer 超时再处理 irp;现在只能中断irp 的处理流程,将无法正常完成的irp 状态设为 STATUS_CANCELED,并完成请求。
{
PDEVICE_EXTENSION deviceExtension;
PNOTIFY_RECORD notifyRecord;
PIO_STACK_LOCATION irpStack;
KIRQL oldIrql;
PREGISTER_EVENT registerEvent;
DebugPrint(("\tRegisterIrpBasedNotification\n"));
irpStack = IoGetCurrentIrpStackLocation(Irp);
deviceExtension = DeviceObject->DeviceExtension;
registerEvent = (PREGISTER_EVENT)Irp->AssociatedIrp.SystemBuffer;//获取应用程序传送的数据
//
// Allocate a record and save all the event context.
//申请一个 记录用的结构体
notifyRecord = ExAllocatePoolWithQuotaTag(NonPagedPool,
sizeof(NOTIFY_RECORD),
TAG);
if (NULL == notifyRecord) {
return STATUS_INSUFFICIENT_RESOURCES;
}
InitializeListHead(¬ifyRecord->ListEntry);
//往结构体里面填充数据
notifyRecord->FileObject = irpStack->FileObject;
notifyRecord->DeviceExtension = deviceExtension;
notifyRecord->Type = IRP_BASED;
notifyRecord->Message.PendingIrp = Irp;
//
// Start the timer to run the CustomTimerDPC in DueTime seconds to
// simulate an interrupt (which would queue a DPC).
// The user's event object is signaled or the IRP is completed in the DPC to
// notify the hardware event.
//
// ensure relative time for this sample
if (registerEvent->DueTime.QuadPart > 0) {
registerEvent->DueTime.QuadPart = -(registerEvent->DueTime.QuadPart);
}
KeInitializeDpc(¬ifyRecord->Dpc, // Dpc
CustomTimerDPC, // DeferredRoutine
notifyRecord // DeferredContext
);
KeInitializeTimer(¬ifyRecord->Timer);
//
// We will set the cancel routine and TimerDpc within the
// lock so that they don't modify the list before we are
// completely done.
//
KeAcquireSpinLock(&deviceExtension->QueueLock, &oldIrql);
//
// Set the cancel routine. This is required if the app decides to
// exit or cancel the event prematurely.
//
IoSetCancelRoutine (Irp, EventCancelRoutine);
//
// Before we queue the IRP, we must check to see if it's cancelled.
//
if (Irp->Cancel) {
//
// Clear the cancel-routine automically and check the return value.
// We will complete the IRP here if we succeed in clearing it. If
// we fail then we will let the cancel-routine complete it.
//
if (IoSetCancelRoutine (Irp, NULL) != NULL) {
//
// We are able to successfully clear the routine. Either the
// the IRP is cancelled before we set the cancel-routine or
// we won the race with I/O manager in clearing the routine.
// Return STATUS_CANCELLED so that the caller can complete
// the request.
KeReleaseSpinLock(&deviceExtension->QueueLock, oldIrql);
ExFreePoolWithTag(notifyRecord, TAG);
return STATUS_CANCELLED;
} else {
//
// The IRP got cancelled after we set the cancel-routine and the
// I/O manager won the race in clearing it and called the cancel
// routine. So queue the request so that cancel-routine can dequeue
// and complete it. Note the cancel-routine cannot run until we
// drop the queue lock.
//
}
}
IoMarkIrpPending(Irp);
InsertTailList(&deviceExtension->EventQueueHead,
¬ifyRecord->ListEntry);
notifyRecord->CancelRoutineFreeMemory = FALSE;
//
// We will save the record pointer in the IRP so that we can get to
// it directly in the CancelRoutine.
//
Irp->Tail.Overlay.DriverContext[3] = notifyRecord;
KeSetTimer(¬ifyRecord->Timer, // Timer
registerEvent->DueTime, // DueTime
¬ifyRecord->Dpc // Dpc
);
KeReleaseSpinLock(&deviceExtension->QueueLock, oldIrql);
//
// We will return pending as we have marked the IRP pending.
//
return STATUS_PENDING;;
}
关于自旋锁(SpinLock)的使用
在上面例子中,为了管理pending irp会使用到链表(使用链表保存当前会话的通知记录);由于会话中的每次通信相当于多线程,所以这里存在同步的问题。当一个线程准备对链表进行插入或删除操作时,必须保证对链表的独占(此时其他线程无法操作链表),这里就是通过自旋锁来实现这一点的。
自旋锁的使用,避免了对链表(某种资源或数据)进行操作时发送不必要的冲突,解决多线程之间同步问题。通俗的来讲就是,大家约定上厕所要关门。
应用程序1--->DeviceIoControl
------>
------->
-----> event.sys-------->LIST_ENTRY(只有一个链表来 保存 所有的pending IRP)
------>
------ >
应用程序2--->DeviceIoControl
详情请参考 wdk 文档。