转载自:http://mzf2008.blog.163.com/blog/static/355997862010112412048667/
同步操作设备
如果需要同步操作设备,那么在打开设备的时候就要指定以“同步”的方式打开设备。打开设备用CreateFile函数,其函数声明如下:
HANDLE CreateFile(
LPCTSTR lpFileName, // pointer to name of the file
DWORD dwDesiredAccess, // access (read-write) mode
DWORD dwShareMode, // share mode
LPSECURITY_ATTRIBUTES lpSecurityAttributes,
// pointer to security attributes
DWORD dwCreationDisposition, // how to create
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile // handle to file with attributes to copy
);
CreateFile函数的第六个参数dwFlagsAndAttributes是同步异步操作的关键。如果这个参数中没有设置FILE_FLAG_OVERLAPPED属性,则以后对该设备的操作都是同步的,否则所有操作都是异步的。
对设备操作的Win32 API函数,例如,ReadFile函数,都会提供一个OVERLAPPED结构的参数。如下:
BOOL ReadFile(
HANDLE hFile, // handle of file to read
LPVOID lpBuffer, // pointer to buffer that receives data
DWORD nNumberOfBytesToRead, // number of bytes to read
LPDWORD lpNumberOfBytesRead, // pointer to number of bytes read
LPOVERLAPPED lpOverlapped // pointer to structure for data
);
在同步操作设备时,其lpOverlapped参数设置为NULL.
示例代码:
#include <windows.h>
#include <stdio.h>
int main(void)
{
HANDLE hFile = CreateFile("c:\\test.dat",
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, //此处没有设置FILE_FLAG_OVERLAPPED属性
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("创建文件失败, 错误代码:%d\n", GetLastError());
return -1;
}
char* Buffer = "fuckyou";
DWORD dwRet;
//最后一个参数为NULL,标明采用同步方式写文件
BOOL bRet = WriteFile(hFile, Buffer, strlen(Buffer), &dwRet, NULL);
CloseHandle(hFile);
return 0;
}
异步操作设备(方式一)
异步操作设备时主要需要设置OVERLAPPED结构:
typedef struct _OVERLAPPED { 、
DWORD Internal;
DWORD InternalHigh;
DWORD Offset; //操作设备会指定一个偏移量,从设备的偏移量进行读取。该偏移量是用一个64位整型表示,这个是偏移量的低32位
DWORD OffsetHigh; //偏移量的高32位
HANDLE hEvent; //这个事件用于该操作完成后通知应用程序
} OVERLAPPED;
注意:在使用OVERLAPPED结构前,需要对其内部清零,并为其创建事件。
示例代码:
#include <windows.h>
#include <stdio.h>
int main(void)
{
HANDLE hFile = CreateFile("c:\\test.dat",
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("创建文件失败, 错误代码:%d\n", GetLastError());
return -1;
}
char* Buffer = "fuckyou";
DWORD dwRet;
OVERLAPPED ovlp = {0};
HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
ovlp.hEvent = hEvent;
BOOL bRet = WriteFile(hFile, Buffer, strlen(Buffer), &dwRet, &ovlp);
//…….
//这里可以执行一些其他的操作,这些操作可以和写操作并行
//到某一时刻,等待读操作完成
WaitForSingleObject(hEvent, INFINITE);
//……..
//继续其他操作
CloseHandle(hFile);
CloseHandle(hEvent);
return 0;
}
异步操作设备(方式二)
除了ReadFile和WriteFile函数外,还有两个API函数可以实现异步读写,这就是ReadFileEx和WriteFileEx函数。ReadFile和WriteFile函数既可以支持同步读写操作,又可以支持异步读写操作,而ReadFileEx和WriteFileEx函数是专门用于异步读写操作的。
BOOL ReadFileEx(
HANDLE hFile, // handle of file to read
LPVOID lpBuffer, // pointer to buffer
DWORD nNumberOfBytesToRead, // number of bytes to read
LPOVERLAPPED lpOverlapped, // pointer to offset
LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
// pointer to completion routine
);
注意,这里提供的OVERLAPPED结构不需要提供事件句柄。ReadFileEx函数将读请求发送到驱动程序后立刻返回。驱动程序在结束读请求后,会通过调用ReadFileEx提供的回调函数,既lpCompletionRoutine参数提供的函数地址。这类似一个软中断,也就是当读操作结束后,操作系统立刻调用回调函数。Windows将这种机制称为异步过程调用(APC – Asynchronous Procedure Call)
然而,APC的回调函数调用是有条件的,只有线程处于警惕(Alert)状态时,回调函数才有可能被调用。有多个函数可以使线程进入警惕状态,如SleepEx,WaitForSingleObjectEx,WaitForMultipleObjectEx函数等。这些Win32 API函数中都有一个BOOL类型的参数bAlertable,当设置为TRUE时,线程就进入警惕状态。这时,告诉系统,线程已经运行到了一个点,此时可以调用回调函数。
回调函数会报告本次操作完成状况,比如是成功还是失败,同时会报告本次读取操作实际读取的字节数等。下面是一般回调函数的声明:
VOID CALLBACK FileIOCompletionRoutine(
DWORD dwErrorCode, // completion code
DWORD dwNumberOfBytesTransfered, // number of bytes transferred
LPOVERLAPPED lpOverlapped // pointer to structure with I/O
// information
);
示例代码:
#include <windows.h>
#include <stdio.h>
VOID CALLBACK FuckYou(DWORD dwErrorCode,
DWORD dwNumberOfBytesTransfered,
LPOVERLAPPED lpOverlapped)
{
printf("调用回调函数!\n");
}
int main(void)
{
HANDLE hFile = CreateFile("c:\\test.dat",
FILE_GENERIC_READ | FILE_GENERIC_WRITE,
0, NULL, CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("创建文件失败, 错误代码:%d\n", GetLastError());
return -1;
}
char* Buffer = "FuckYou";
//定义OVERLAPPED结构,这里不需要Event事件
OVERLAPPED ovlp = {0};
BOOL bRet = WriteFileEx(hFile, Buffer, strlen(Buffer), &ovlp, FuckYou);
//....
//其他操作
//运行到这个点,必须等待写操作完成,否则不能继续完成别的任务
SleepEx(0, TRUE);
//....
//其他操作
printf("fuck\n");
CloseHandle(hFile);
return 0;
}

IRP的异步完成
IRP被“异步完成”指的是不在派遣函数中调用IoCompleteRequest内核函数。调用IoCompleteRequest函数意味着IRP请求的结束,也标志着本次对设备操作的结束。
如果IRP是被异步完成,而发起IRP的应用程序会有三种形式发起IRP请求,分别是ReadFile函数同步读取设备,用ReadFile函数异步读取设备,用ReadFileEx异步读取数据。一下分别给出这三种读取形式的异同:
1. ReadFile同步读取:当派遣函数退出时,由于IoCompleteRequest函数没有被调用,IRP请求没有被结束。因此ReadFile函数会一直等待,知道操作被结束。
2. ReadFile异步读取:当派遣函数退出时,由于IoCompleteRequest函数没有被调用,IRP请求没有被结束。但ReadFile会立即返回,返回值为FALSE。通过GetLastError函数会返现错误码是ERROR_IO_PENDING,表明当前操作被“挂起”。这不是真正的操作错误,而是意味着ReadFile并没有真正的完成操作,ReadFile只是异步的返回。当IRPQ请求被真正结束,既调用了IoCompleteRequest,ReadFile函数提供的OVERLAPPED结构体重的Event才会被设置。这个事件可以通知应用程序ReadFile的请求真正的被执行完毕。
3. ReadFileEx异步读取:前面部分与ReadFile相同。当是在IRP呗结束时,既调用了IoCompleteRequest后,ReadFileEx提供的回调函数会被插入到APC队列中。一旦线程进入警惕状态,线程的APC队列会自动出队列,进而ReadFileEx提供的回调函数被调用,这相当于操作系统通知应用程序操作真正的执行完毕。
如果派遣函数不调用IoCompleteRequest函数,则需要告诉操作系统此IRP处于“挂起”状态。这需要调用内核函数IoMarkIrpPending。同时,派遣函数应该返回STATUS_PENDING。
VOID
IoMarkIrpPending(
IN OUT PIRP Irp
);
我们现在假设IRP_MJ_READ的派遣函数仅仅是返回“挂起”。应用程序在关闭设备句柄的时候会产生IRP_MJ_CLEARUP类型的IRP。在IRP_MJ_CLEARUP的派遣函数中结束那些挂起的IRP_MJ_READ;
为了能存储有哪些IRP_MJ_READ 的IRP被挂起,这里我们需要使用一个队列,也就是把每个挂起的IRP_MJ_READ的指针都插入队列,最后在IRP_MJ_CLEARUP的派遣函数将一个个IRP出队,并且调用IoCompleteRequest函数结束他们。
示例代码:
定义队列的数据结构:
typedef struct _MY_IRP_LINKLIST
{
PIRP pIrp;
LIST_ENTRY ListEntry;
}MY_IRP_LIST, *PMY_IRP_LIST;
定义设备扩展:
typedef struct _DEVICE_EXTENSION {
PDEVICE_OBJECT pDevice;
UNICODE_STRING ustrDeviceName; //设备名称
UNICODE_STRING ustrSymLinkName; //符号链接名
PLIST_ENTRY pIRPLinkListHead; //链表头
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
IRP_MJ_READ的派遣函数:
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
PDEVICE_EXTENSION pDevEx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
PMY_IRP_LIST pMyIrpList = (PMY_IRP_LIST)ExAllocatePool(PagedPool, sizeof(MY_IRP_LIST));
pMyIrpList->pIrp = pIrp;
//把挂起的IRP插入队列
InsertHeadList(pDevEx->pIRPLinkListHead, &pMyIrpList->ListEntry);
//使IRP挂起
IoMarkIrpPending(pIrp);
KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
//返回PENDING状态
return STATUS_PENDING;
}
IRP_MJ_CLEARUP的派遣函数
NTSTATUS HelloDDKCleraUp(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
KdPrint((“进入IRP_MJ_CLEARUP派遣函数!\n”));
PDEVICE_EXTENSION pDevEx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
//1.将存在队列中的IRP逐个出对,并处理之
PMY_IRP_LIST myIrpList;
while (!IsListEmpty(pDevEx->pIRPLinkListHead))
{
PLIST_ENTRY pListEntry = RemoveHeadList(pDevEx->pIRPLinkListHead);
myIrpList = CONTAINING_RECORD(pListEntry, MY_IRP_LIST, ListEntry);
myIrpList->pIrp->IoStatus.Information = 0;
myIrpList->pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(myIrpList->pIrp, IO_NO_INCREMENT);
ExFreePool(myIrpList);
}
//处理IRP_MJ_CLEARUP的IRP
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
KdPrint((“离开IRP_MJ_CLEARUP派遣函数!\n”));
return STATUS_SUCCESS;
}
用户层代码:
#include <windows.h>
#include <stdio.h>
int main(void)
{
HANDLE hFile = CreateFile("\\\\.\\HelloDDK", GENERIC_READ|GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("打开设备失败!\n");
return -1;
}
UCHAR readBuffer[10];
DWORD dwRet;
OVERLAPPED ovlp1 = {0};
OVERLAPPED ovlp2 = {0};
//连续异步读取两次,产生两个挂起的IRP_MJ_READ
BOOL bRet = ReadFile(hFile, readBuffer, 10, &dwRet, &ovlp1);
if (!bRet && GetLastError() == ERROR_IO_PENDING)
{
printf("挂起!\n");
}
bRet = ReadFile(hFile, readBuffer, 10, &dwRet, &ovlp2);
if (!bRet && GetLastError() == ERROR_IO_PENDING)
{
printf("挂起!\n");
}
Sleep(10000);
//调用CloseHandle时系统产生IRP_MJ_CLEARUP的IRP
//进而对被挂起的线程执行完成操作
CloseHandle(hFile);
return 0;
}
取消IRP
前面提到了在设备句柄被关闭的时候,将“挂起”的IRP结束。还有另外一个办法将“挂起”的IRP逐个结束,这就是取消IRP请求。内核函数IoSetCancelRoutine可以设置取消IRP请求的回调函数,其声明如下:
PDRIVER_CANCEL
IoSetCancelRoutine(
IN PIRP Irp, //需要取消的IRP
IN PDRIVER_CANCEL CancelRoutine //IRP取消后调用的函数
);
我们可以用IoCancelIrp函数指定取消IRP请求。在IoCancelIrp函数内部,需要进行同步。DDK在IoCancelIrp内部使用一个叫做cancel的自旋锁,用来进行同步。
IoCancelIrp在内部会首先获得该自旋锁,然后会调用刚才提到的IoSetCancelRoutine函数设置的取消回调函数。因此,释放该自旋锁的任务就留给了取消回调函数。获得“取消自旋锁”的函数是IoAcquireCancelSpinLock,释放“取消自旋锁”的函数是IoReleaseCancelSpinLock。
在应用程序中,可以调用Win32 API函数取消IRP操作。在CancelIo的内部会枚举所有没有完成的IRP,然后依次调用IoCancelIrp。另外,如果应用程序没有调用CancelIo函数,应用程序在关闭设备句柄时同样会自动调用CnacelIo。
示例代码:
取消IRP的回调函数:
VOID
CancelIrp( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
KdPrint(("进入取消函数!\n"));
//设置IRP为完成状态
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_CANCELLED;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
//释放cancel自旋锁
IoReleaseCancelSpinLock(Irp->CancelIrql);
KdPrint(("离开取消函数!\n"));
}
IRP_MJ_READ派遣函数:
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
//设置IRP的取消执行函数
IoSetCancelRoutine(pIrp, CancelIrp);
//使IRP挂起
IoMarkIrpPending(pIrp);
KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
return STATUS_PENDING;
}
应用层代码:
#include <windows.h>
#include <stdio.h>
int main(void)
{
HANDLE hFile = CreateFile("\\\\.\\HelloDDK", GENERIC_READ|GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("打开设备失败!\n");
return -1;
}
UCHAR readBuffer[10];
DWORD dwRet;
OVERLAPPED ovlp1 = {0};
OVERLAPPED ovlp2 = {0};
//创建两个异步读取
BOOL bRet = ReadFile(hFile, readBuffer, 10, &dwRet, &ovlp1);
if (!bRet && GetLastError() == ERROR_IO_PENDING)
{
printf("挂起!\n");
}
bRet = ReadFile(hFile, readBuffer, 10, &dwRet, &ovlp2);
if (!bRet && GetLastError() == ERROR_IO_PENDING)
{
printf("挂起!\n");
}
Sleep(10000);
//显示调用CancelIo,其实在关闭设备句柄的时候会自动调用此函数
CancelIo(hFile);
CloseHandle(hFile);
return 0;
}
StartIo例程 – 串行化执行IRP请求
在很多情况下,对设备的操作必须是串行执行,而不能并行执行。例如,对串口的操作,假如有N个线程同时操作串口设备时,必须将这些操作排队,然后一一进行处理。
DDK为了简化程序员的工作,为程序员提供了一个内部队列,并将IRP用StartIo例程串行处理。
操作系统为程序员提供了一个IRP队列来实现串行,这个队列用KDEVICE_QUEUE数据结构表示:
typedef struct _KDEVICE_QUEUE {
CSHORT Type;
CSHORT Size;
LIST_ENTRY devicelisthead;
KSPIN_LOCK Lock;
BOOLEAN Busy;
} KDEVICE_QUEUE, *PKDEVICE_QUEUE, *RESTRICTED_POINTER PRKDEVICE_QUEUE;
这个队列的队列头保存在设备对象的DeviceObject->DeviceQueue子域中。插入和删除队列中的元素都是操作系统负责的。在使用这个队列的时候,需要向操作系统提供一个叫做StartIo的例程,如下:
extern "C" NTSTATUS DriverEntry (
IN PDRIVER_OBJECT pDriverObject,
IN PUNICODE_STRING pRegistryPath )
{
。。。
//设置StartIo例程
pDriverObject->DriverStartIo = HelloDDKStartIo;
。。。
}
这个StartIo例程运行在DISPATCH_LEVEL级别,因此这个例程不会被线程所打断,所以我们在声明时要加上#pragma LOCKEDCODE修饰符。
#pragma LOCKEDCODE
VOID HelloDDKStartIo(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
...
}
派遣函数如果想把IRP串行化,只需要在IRP的派遣函数中调用IoStartPacket函数,即可将IRP插入串行化队列了。IoStartPacket函数首先判断当前设备是“忙”还是“空闲”。如果设备空闲,则提升当前IRQL到DISPATCH_LEVEL级别,并进入StartIo例程“串行”处理该IRP。如果设备“忙”,则将IRP插入串行队列后返回。
另外,在StartIo例程结束前,应该调用IoStartNextPacket函数,其作用是从队列中抽取下一个IRP,并将这个IRP作为参数调用StartIo例程。
示例代码:
IRP_MJ_READ派遣函数:
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
//将IRP设为挂起状态
IoMarkIrpPending(pIrp);
//将IRP插入系统串行化队列
IoStartPacket(pDevObj, pIrp, 0 ,OnCancelIrp);
KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
return STATUS_PENDING;
}
在派遣函数中调用IoStartPacket内核函数指定取消例程:
VOID OnCancelIrp( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp )
{
KdPrint(("进入取消函数!\n"));
if (Irp == DeviceObject->CurrentIrp)
{
//当前IRP正由StartIo处理
KIRQL odlirql = Irp->CancelIrql;
//释放cancel自旋锁
IoReleaseCancelSpinLock(Irp->CancelIrql);
//继续下一个IRP
IoStartNextPacket(DeviceObject, TRUE);
KeLowerIrql(odlirql);
}
else
{
//从设备队列中将该IRP抽取出来
KeRemoveEntryDeviceQueue(&DeviceObject->DeviceQueue,
&Irp->Tail.Overlay.DeviceQueueEntry);
//释放自旋锁
IoReleaseCancelSpinLock(Irp->CancelIrql);
}
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_CANCELLED;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
KdPrint(("离开取消函数!\n"));
}
StartIo例程
#pragma LOCKEDCODE
VOID HelloDDKStartIo(IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
KdPrint(("进入StartIo处理例程!\n"));
KIRQL oldIrp;
//获取cancel自旋锁
IoAcquireCancelSpinLock(&oldIrp);
if (Irp != DeviceObject->CurrentIrp || Irp->Cancel)
{
//如果当前有正在处理的IRP,则简单的入队列,并直接返回
//入队列的工作由系统完成
IoReleaseCancelSpinLock(oldIrp);
KdPrint(("离开StartIo处理例程!\n"));
return;
}
else
{
//由于正在处理该IRP,所以不允许调用取消例程
//因此将此IRP的取消例程设置为NULL
IoSetCancelRoutine(Irp ,NULL);
//释放自旋锁
IoReleaseCancelSpinLock(oldIrp);
}
KEVENT event;
KeInitializeEvent(&event, NotificationEvent, FALSE);
//定义一个三秒钟的延时
LARGE_INTEGER time;
time.QuadPart = -3*1000*1000*10;
//等三秒,模拟IRP操作需要大约三秒钟时间
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &time);
Irp->IoStatus.Information = 0;
Irp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
//在队列中读取下一个IRP,并进行StartIo
IoStartNextPacket(DeviceObject, TRUE);
KdPrint(("离开StartIo处理例程!\n"));
}
应用层代码:
#include <windows.h>
#include <stdio.h>
#include <process.h>
UINT WINAPI ThreadProc(LPVOID lpParam)
{
printf("进入新建的线程!\n");
OVERLAPPED ovlp = {0};
ovlp.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
CHAR readBuffer[10];
DWORD dwRet;
BOOL bRet = ReadFile(*(PHANDLE)lpParam, readBuffer, 10, &dwRet, &ovlp);
//可以试验下取消例程
//CancelIo(*(PHANDLE)lpParam);
WaitForSingleObject(ovlp.hEvent, INFINITE);
return 0;
}
int main(void)
{
HANDLE hFile = CreateFile("\\\\.\\HelloDDK", GENERIC_READ|GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("打开设备失败!\n");
return -1;
}
HANDLE hThread[2];
hThread[0] = (HANDLE)_beginthreadex(NULL, 0 ,ThreadProc, &hFile, 0 , NULL);
hThread[1] = (HANDLE)_beginthreadex(NULL, 0 ,ThreadProc, &hFile, 0 , NULL);
WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
CloseHandle(hFile);
return 0;
}
自定义StartIo
我们发现,系统为我们提供的StartIo虽然可以很方便的将IRP串行化,但是存在一个问题,就是读写操作都被一起串行化。而我们有时候需要将读,写分开串行化。此时,希望有两个队列。一个队列串行IRP_MJ_READ类型的IRP,另一个队列串行IRP_MJ_WRITE类型的IRP。
此时我们需要自定义StartIo,自定义StartIo类似于前面介绍的StartIo例程,不同的是程序要需要自己维护IRP队列。程序员可以灵活的维护多个队列,分别应用于不同的IRP。
DDK提供KDEVICE_QUEUE数据结构存储队列:
typedef struct _KDEVICE_QUEUE {
CSHORT Type;
CSHORT Size;
LIST_ENTRY devicelisthead;
KSPIN_LOCK Lock;
BOOLEAN Busy;
} KDEVICE_QUEUE, *PKDEVICE_QUEUE, *RESTRICTED_POINTER PRKDEVICE_QUEUE;
队列中每个元素用KDEVICE_QUEUE_ENTRY数据结构表示:
typedef struct _KDEVICE_QUEUE _ENTRY{
LIST_ENTRY DeviceListEntry;
ULONG SortKey;
BOOLEAN Inserted;
} KDEVICE_QUEUE_ENTRY, *PKDEVICE_QUEUE_ENTRY, *RESTRICTED_POINTER PRKDEVICE_QUEUE_ENTRY;
在使用队列前,应该初始化队列:
VOID
KeInitializeDeviceQueue(
IN PKDEVICE_QUEUE DeviceQueue
);
队列应该存储在设备扩展中,在初始化设备的时候一起初始化该队列。
插入队列:
BOOLEAN
KeInsertDeviceQueue(
IN PKDEVICE_QUEUE DeviceQueue, //要插入的队列
IN PKDEVICE_QUEUE_ENTRY DeviceQueueEntry //要插入的元素
);
该函数的返回值为BOOL值,如果当前设备不忙,则可以直接处理该IRP,因此这时候不需要插入队列,返回FALSE。如果设备正在处理别的请求,这时候需要将IRP插入队列,这时候返回TRUE。
删除元素:
PKDEVICE_QUEUE_ENTRY
KeRemoveDeviceQueue(
IN PKDEVICE_QUEUE DeviceQueue
);
示例代码:
自定义设备扩展:
typedef struct _DEVICE_EXTENSION {
PDEVICE_OBJECT pDevice;
UNICODE_STRING ustrDeviceName; //设备名称
UNICODE_STRING ustrSymLinkName; //符号链接名
KDEVICE_QUEUE deviceQueue; //支持串行化的队列
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
注意:上面的代码仅是为了演示,所以只定义了一个队列,通常我们会定义多个队列,以此来分别串行处理不同的IRP请求。
IRP_MJ_READ派遣函数
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,IN PIRP pIrp)
{
KdPrint(("进入IRP_MJ_READ派遣函数!\n"));
//获得设备扩展
PDEVICE_EXTENSION pdex = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
//将IRP设置为挂起
IoMarkIrpPending(pIrp);
//设置取消例程
IoSetCancelRoutine(pIrp, OnCancelIrp);
//提升IRP至DISPATCH_LEVEL
KIRQL oldIrql;
KeRaiseIrql(DISPATCH_LENGTH, &oldIrql);
KdPrint(("HelloDDKRead IRP : %x\n", pIrp));
KdPrint(("DeviceQueueEntry : %x\n",&pIrp->Tail.Overlay.DeviceQueueEntry));
//插入设备队列
if (!KeInsertDeviceQueue(&pdex->deviceQueue, &pIrp->Tail.Overlay.DeviceQueueEntry))
{
//如果设备”空闲”,就立即调用MyStartIo例程处理IRP
MyStartIo(pDevObj, pIrp);
}
//将IRP降至原来的IRQL级别
KeLowerIrql(oldIrql);
KdPrint(("离开IRP_MJ_READ派遣函数!\n"));
return STATUS_PENDING;
}
自定义的StartIo函数
#pragma LOCKEDCODE
//让函数运行在非分页内存
VOID MyStartIo(IN PDEVICE_OBJECT DeviceObject, IN PIRP firstIrp)
{
KdPrint(("进入MyStartIo处理例程!\n"));
//获得设备扩展
PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension;
PKDEVICE_QUEUE_ENTRY pDeviceQueueEntry;
PIRP pIrp = firstIrp;
do
{
KEVENT event;
//初始化同步事件
KeInitializeEvent(&event, NotificationEvent, FALSE);
//定义一个三秒钟的延时
LARGE_INTEGER time;
time.QuadPart = -3*1000*1000*10;
//等三秒,模拟IRP操作需要大约三秒钟时间
KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &time);
KdPrint(("Complete a irp:%x\n", pIrp));
pIrp->IoStatus.Information = 0;
pIrp->IoStatus.Status = STATUS_SUCCESS;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
//取出队列中的下一个元素
pDeviceQueueEntry = KeRemoveDeviceQueue(&pdx->deviceQueue);
KdPrint(("pDeviceQueueEntry:%x\n", pDeviceQueueEntry));
if (pDeviceQueueEntry == NULL)
{
break;
}
pIrp = CONTAINING_RECORD(pDeviceQueueEntry, IRP, Tail.Overlay.DeviceQueueEntry);
} while (1);
KdPrint(("离开MyStartIo处理例程!\n"));
}