应用程序中对设备进行同步、异步操作,都必须得到驱动程序的支持。所有对设备的操作都会转化为IRP请求,并传递到相应的派遣函数中。可以有两种方式处理IRP请求,第一种是在派遣函数中直接结束IRP请求,即同步处理。另一种方法是在派遣函数中不结束IRP请求,而是让派遣函数直接返回。IRP在以后的某个时候再进行处理。
1)IRP的同步完成
在应用程序中调用CreateFile win32API函数,这个函数用于打开设备。CreateFile Win32 API函数内部调用了ntdll.dll中的NtcreateFile函数。ntdll.dll中的NtCreateFile函数进入内核模式,然后调用ntoskrnl.exe中的NtCreateFile函数。内核模式中ntoskrnl.exe的NtCreateFile函数创建IRP_MJ_CREATE类型的IRP,然后调用相应驱动程序的派遣函数,并将IRP的指针传递给该派遣函数。派遣函数调用IoCompleteRequest,将IRP请求结束。操作系统按照原路返回,一直退到CrateFile Win32API函数。至此CreateFile函数返回。如果需要读取设备,应用程序应该调用ReadFile Win32 API函数。ReadFile Win32 API函数会调用ntdll.dll中的NTReadFile函数。ntdll.dll中的NtReadFile函数会进入内核模式,调用ntoskrnl.exe中的NtReadFile函数。ntoskrnl.exe中的NtReadFile函数创建IRP_MJ_READ类型的IRP,并将其传入相应的派遣函数中。
对设备进行读取可以有三种方法:1)ReadFile函数同步读取。ReadFile函数内部会创建一个事件,这个事件连同IRP一起被传递到派遣函数中(这个事件是IRP的UserEvent子域)。派遣函数调用IoCompleterequest时,IocompleteRequest内部会设置IRP的userEvent事件。操作系统按照原路一直返回到ReadFile函数,ReadFile函数会等待这个事件。因为该事件已经被设置,所以无须等待。如果在派遣函数中没有调用IoCompleteRequest函数,该事件就没有被设置,ReadFile会一直等IRP被结束。2)如果是用ReadFile函数进行异步读取时,ReadFile内部不会创建事件,但ReadFile函数会接受Overlap参数。overlap参数中会提供一个事件,这个事件被用作同步处理。IoCompleteRequest内部会设置overlap提供的事件。在ReadFile退出前,它不会检测该事件是否被设置,因此可以不等待操作是否真的被完成。当Irp操作被完成后,overlap提供的事件被设置,这个事件会通知应用程序IRP请求被完成。3)如果是用ReadFileEx函数进行异步读取中,ReadFileEx不提供事件,当提供一个回调函数,这个回调函数的地址会作为IRP的参数传递给派遣函数。IoCompleteRequest会将这个完成函数插入APC队列。应用程序只要进入警惕模式,APC队列会自动出队列,完成函数被执行,这相当于通知应用程序操作已经完成。
2)IRP的异步完成
IRP被“异步完成”指的就是不在派遣函数中调用IoCompleteRequest内核函数。调用IoCompleteRequest函数意味着IRP请求的结束,也标志着本次对设备操作的结束。
IRP被异步完成,而发起IRP的应用程序会有三种形式发起IRP请求,分别使用ReadFile函数同步设备,用ReadFile异步读取设备,用ReadFileEx异步读取函数。1)IRP是由ReadFile的同步操作引起的:当派遣函数退出时,由于IoCompleteRequest没有被调用,IRP请求没有被结束。ReadFile会一直等待,直到操作被结束。2)IRP是由ReadFile的异步操作引起的:当派遣函数退出时,由于IoCompleteRequest没有被调用,IRP请求没有被结束。但ReadFile会立刻返回,返回值为失败,但代表操作没有完成。通过调用GetLastError函数,可以得到这时的错误代码是ERROR_IO_PENDING。这不是真正的操作错误,而意味着ReadFile并没有真正完成操作,ReadFile只是异步的返回。当IRP请求被真正的结束,即调用了IoCompleteRequest,ReadFile函数提供的overlap的事件才会被设置。这个事件可以通知应用程序ReadFile的请求真正的被执行完毕。3)IRP是由ReadFileEx的异步操作引起:和ReadFile的异步操作类似,ReadFileEx会立即返回,但返回值是FALSE,说明读操作没有成功。这时候如果调用GetLastError函数,会发现错误码为Error_IO_PENDING,表明当前操作被“挂起”。当IRP被结束后,即调用了IoCompleteRequest后,ReadFileEx提供的回调函数被插入到APC队列中。一旦操作系统进入警惕状态时,线程的APC队列会自动出队列,进而ReadFileEx提供的回调函数被调用,这相当于操作系统通知应用程序操作真正的被执行完毕。
如果派遣函数不调用IoCompleteRequest函数,则需要告诉操作系统此IRP处于挂起状态。这需要调用内核函数IoMarkIrpPending。同事,派遣函数应该返回STATUS_PENDING。例如:
NTSTATUS HelloDDKRead( IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
//……
IoMarkIrpPending(pIrp);
return STATUS_PENDING;
}
为了演示异步处理IRP,假设IRP_MJ_READ的派遣函数仅仅是返回挂起。应用程序关闭设备的时候会产生IRP_MJ_CLEANUP类型的IRP。在IRP_MJ_CLEANUP的派遣函数中结束那些挂起的IRP_MJ_READ。
为了能存储有哪些IRP_MJ_READ IRP被挂起,这里使用一个队列,也就是把每个挂起的IRP_MJ_READ的指针都插入队列,最后IRP_MJ_CLEANUP的派遣函数将一个个IRP出队列,并且调用IoComleteRequest函数将它们结束。
typedef struct _MY_IRP_ENTRY
{
PIRP pIRP;
LIST_ENTRY ListEntry;
} MY_IRP_ENTRY, *PMY_IRP_ENTRY;
在设备扩展中加入“队列”这个变量,这样驱动程序的所有派遣函数都可以使用该队列。在driverEntry中初始化该队列,并在driverunload例程中回收队列。在IRP_MJ_READ的派遣函数中,将IRP插入堆栈,然后返回“挂起”状态。
NTSTATUS HelloDDkRead( IN PDEVICE_OBJECT pDevObj,
IN PIRP pIrp)
{
kdPrint("Enter HelloDDKRead/n");
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
PMY_IRP_ENTRY pIrp_Entry = (PMY_IRP_ENTRY) ExAllocatePool(PagedPool, sizeof(MY_IRP_ENTRY));
pIrp_Entry->pIrp = pIrp;
//插入队列
InsertHeadList(pDevExt->pIRPLinkListHead, &pIrp->entry->listEntry);
//将IRP设置为挂起
IoMarkIrpPending(pIrp);
KdPrint("Leave HelloDDkRead/n");
return STATUS_PENDING;
}
在关闭设备的时候,会产生IRP_MJ_CLEANUP类型的IRP。其派遣函数抽取队列中每一个挂起的IRP,并调用IoCompleteRequest设置完成。
NTSTATUS HelloDDKCleanUp(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
kdPrint("Enter HelloDDKCleanUp/n");
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
//将存在队列中的IRP逐个出队列,并处理
PMY_IRP_ENTRY my_irp_entry;
while(!IsListempty(pDevExt->pIRPLinkListHead))
{
//删除队列元素
PLIST_ENTRY pEntry = RemoveHeadList(pDevExt->pIRPLinkListHead);
//得到元素入口
my_irp_entry = CONTAINING_RECORD(pEntry, MY_IRP_ENTRY, ListEntry);
//设置IRP完成状态
my_irp_entry->pIRP->IoStatus.Status = STATUSS_SUCCESS;
my_irp_entry->pIRP->IoStatus.Information = 0;
IoCompleteRequest( my_irp_entry->pIRP, IO_NO_INCREMENT );
//回收内存
ExFreePool(my_irp_entry);
}
//处理IRP_MJ_CLEANUP的IRP
NTSTATUS status = STATUS_SUCCESS;
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0;
IoCompleteRequest( pIrp, IO_NO_INCREMENT );
KdPrint("Leave HelloDDkCleanUp/n");
return STATUS_SUCCESS;
}
在应用程序中异步操作该设备,先异步读两次,这样会创建两个IRP_MJ_READ,这两个IRP被插入队列。在关闭设备的时候,会导致驱动程序调用IRP_MJ_CLEANUP的派遣函数。
int main()
{
HANDLE hDevice = CreateFile("////.//HelloDDK",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NOMAL|FILE_FLAG_OVERLAPPED,
NULL);
if(hDevice == INVALID_HANDLE_VALUE)
{
printf("Open Device Failed!");
return 1;
}
OVERLAPPED overlap1 = {0};
OVERLAPPED overlap2 = {0};
UCHAR buffer[10];
ULONG ulRead;
BOOL bRead = ReadFile(hDevice, buffer, 10,&ulRead, &overlap1);
if(!bRead && GetLastError() == ERROR_IO_PENDING)
{
printf("The Operation is pending/n");
}
bRead = ReadFile(hDevice, buffer, 10, &ulRead,&overlap2);
if(!bRead && GetLastError() == ERROR_IO_PENDING)
{
printf("The Operation is pending/n");
}
//迫使程序中止2秒
Sleep(2000);
//创建IRP_MJ_CLEANUP IRP
CloseHandle(hDevice);
return 0;
}
还有另外一个办法可以将“挂起”的IRP逐个结束,这就是取消IRP请求。内核函数IoSetCancelRoutine可以设置取消IRP请求的回调函数,其声明如下:
PDRIVE_CANEL IoSetCancelRoutine( IN PIRP Irp, IN PDRIVER_CANEL CancelRoutine);
IoSetCancelRoutine可以将一个取消例程与该IRP关联,一旦取消IRP请求的时候,这个取消例程会被执行。IoSetCancelRoutine函数可以用来删除取消例程,当输入的CancelRoutine参数为空指针的时候,则删除原来设置的取消例程。
程序员可以用IoCancelIrp函数指定取消IRP请求。在Io CancelIrp内部,需要进行同步。DDK在IoCancelIrp内部使用一个叫做cancel的自旋锁用来进行同步。
IoCancelIrp在内部会首先获得该自旋锁,IoCancelIrp会调用取消回调例程,因此,释放该自旋锁的任务就留给了取消回调例程。获得取消自璇的函数是IoAcquireCancelSpinLock函数,而释放取消自旋锁的函数是IoReleaseCancelSpinLock函数。
在应用程序中,可以调用Cancel Win32 API函数取消IRP请求。在CancelIo的内部会枚举所有没有被完成的IRP,然后依次调用IoCancelIrp。另外,如果应用程序没有调用CancelIo函数,应用程序在关闭设备时同样会自动调用CancelIo。
VOID CancelReadIRP( IN PDEVICE_OBJECT DeviceObject, IN PIRP Irp)
{
KdPrint("Enter CancelReadIRP/n");
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
Irp->IoStatus.Status = STATUS_CANCELLED;
//结束IRP操作字节数
Irp->IoStatus.Information = 0;
IoCompleteRequest(Irp, IO_NO_INCREMENT);
//释放cancel自旋锁
IoReleaseCancelSpainLock(Irp->CancelIrql);
KdPrint("Leave CancelReadIRP/n");
}
NTSTATUS HelloDDkRead(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)
{
KdPrint("Enter HelloDDkRead/n");
PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) pDevObj->DeviceExtension;
IoSetCancelRoutine(pIrp, CancelReadIRP);
IoMarkIrpPending(pIrp);
kdPrint("Leave HelloDDkRead/n");
return STATUS_PENDING;
}
int main()
{
HANDLE hDevice = CreateFile("////.//HelloDDK",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NOMAL|FILE_FLAG_OVERLAPPED,
NULL);
if(hDevice == INVALID_HANDLE_VALUE)
{
printf("Open Device Failed!");
return 1;
}
OVERLAPPED overlap1 = {0};
OVERLAPPED overlap2 = {0};
UCHAR buffer[10];
ULONG ulRead;
BOOL bRead = ReadFile(hDevice, buffer, 10,&ulRead, &overlap1);
if(!bRead && GetLastError() == ERROR_IO_PENDING)
{
printf("The Operation is pending/n");
}
bRead = ReadFile(hDevice, buffer, 10, &ulRead,&overlap2);
if(!bRead && GetLastError() == ERROR_IO_PENDING)
{
printf("The Operation is pending/n");
}
//迫使程序中止2秒
Sleep(2000);
//显示调用CancelIo,其实在关闭设备时会自动运行CancleIo
CancelIo(hDevice);
//创建IRP_MJ_CLEANUP IRP
CloseHandle(hDevice);
return 0;
}
在设置取消例程中要注意同步问题是,当退出取消例程时,一定要释放Cancel自旋锁,否则会导致系统崩溃。另外,cancel自旋锁是全局锁,所有驱动程序都会使用这个自旋锁,因此,占用自旋锁时间不宜过长。