《Windows驱动开发技术详解》之IRP的同步

  • 应用程序对设备的同步异步操作:

大部分IRP都是由应用程序的Win32 API函数发起的。这些Win32 API本身就支持同步和异步操作。例如,ReadFile、WriteFile和DeviceIoControl等,它们都有异步和同步两种操作方式。DeviceIoControl的同步操作如图所示:

同步操作时,DeviceIoControl的内部会调用WaitForSingleObject函数去等待一个事件。这个事件直到IRP结束才会触发。如果通过反汇编IoCompleteRequest内核函数,就会发现在IoCompleteRequest内部设置了该事件。DeviceIoControl会暂时进入睡眠状态,直到IRP结束

而对于异步操作,其处理过程如图所示:

在异步操作的情况下,当DeviceIoControl被调用时,其内部会产生IRP,并将该IRP传递给驱动内部的派遣函数。但此时 DeviceIoControl不会等待该IRP结束,而是直接返回。当IRP经过一段时间被结束时,操作系统会出发一个IRP相关事件。这个事件可以通知应用程序IRP请求被执行完毕

同步操作设备

CreateFile的函数声明如下:

1 HANDLE WINAPI CreateFile(
2   _In_     LPCTSTR               lpFileName,
3   _In_     DWORD                 dwDesiredAccess,
4   _In_     DWORD                 dwShareMode,
5   _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
6   _In_     DWORD                 dwCreationDisposition,
7   _In_     DWORD                 dwFlagsAndAttributes,
8   _In_opt_ HANDLE                hTemplateFile
9 );

CreateFile中的第六个参数dwFlagsAndAttributes是同步异步操作的关键。如果这个参数中没有设置FILE_FLAG_OVERLAPPED,则以后对该设备的操作都是同步操作,否则所有操作为异步操作

异步操作设备方式一

先来看一下OVERLAPPED的结构:

其中最后一个参数hEvent,这个事件用于该操作完成之后通知应用程序。

 示例代码如下:

 1 #define BUFFER_SIZE 1024
 2 
 3 int main(){
 4     HANDLE hDevice = CreateFile("test.dat",
 5         GENERIC_READ | GENERIC_WRITE,
 6         0,
 7         NULL,
 8         CREATE_ALWAYS,
 9         FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,//这里表示异步方式打开
10         NULL
11         );
12     if (hDevice == INVALID_HANDLE_VALUE){
13         printf("Error!\n");
14         return 1;
15     }
16     UCHAR buffer[BUFFER_SIZE];
17     DWORD dwRead;
18     OVERLAPPED overlap = { 0 };
19     overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
20     ReadFile(hDevice, buffer, BUFFER_SIZE, &dwRead, &overlap);
21     WaitForSingleObject(overlap.hEvent, INFINITE);
22     CloseHandle(hDevice);
23     system("pause");
24     return 0;
25 
26 }

异步操作设备方式二:

除了使用OVERLAPPED结构之外,ReadFile、WriteFile这两个API还有对应的专门用于异步读写操作的API,它们就是ReadFileEx和WriteFileEx。

以ReadFileEx为例,其原型如下:

1 BOOL WINAPI ReadFileEx(
2   _In_      HANDLE                          hFile,
3   _Out_opt_ LPVOID                          lpBuffer,
4   _In_      DWORD                           nNumberOfBytesToRead,
5   _Inout_   LPOVERLAPPED                    lpOverlapped,
6   _In_      LPOVERLAPPED_COMPLETION_ROUTINE lpCompletionRoutine
7 );

ReadFileEx将读请求传递到驱动之后立刻返回。驱动程序在结束读操作后,会调用ReadFileEx提供的回调例程。这类似一个软中断,即当读操作后,系统立刻回调ReadFileEx提供的回调例程。Windows称这种机制为异步过程调用(APC)。

然而,APC的回调函数被调用是有条件的。只有线程处于警惕状态时,回调函数才有可能被调用。有多个API可以使系统进入警惕状态,如SleepEx、WaitForSingleObjectEx、WaitForMultipleObjectsEx等。它们的参数bAlertable如果被设置为TRUE,则进入警惕状态。当系统进入警惕模式之后,操作系统就会枚举当前线程的APC队列。驱动程序一旦结束读取操作,就会把ReadFileEx提供的完成例程插入到APC队列。回调例程一般会报告本次操作的完成状况,比如是成功还是失败。同时会报告本次读取操作实际读取的字节数等。一般回调例程声明如下:

1 VOID CALLBACK FileIOCompletionRoutine(
2   _In_    DWORD        dwErrorCode,
3   _In_    DWORD        dwNumberOfBytesTransfered,
4   _Inout_ LPOVERLAPPED lpOverlapped
5 );

举例如下:

 1 VOID CALLBACK MyFileIoCompletionRoutine(
 2     DWORD dwErrorCode,
 3     DWORD dwNumberOfBytesTransfered,
 4     LPOVERLAPPED lpOverlapped
 5     ){
 6     printf("IO operation end!\n");
 7 }
 8 
 9 int main(){
10     HANDLE hDevice = CreateFile("\\\\.\\HelloDDK",
11         GENERIC_READ | GENERIC_WRITE,
12         0, NULL,
13         OPEN_EXISTING,
14         FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
15         NULL);
16     if (hDevice == INVALID_HANDLE_VALUE){
17         printf("Error!\n");
18         return 1;
19     }
20     UCHAR buffer[BUFFER_SIZE];
21     OVERLAPPED overlap = { 0 };
22     ReadFileEx(hDevice, buffer, BUFFER_SIZE, &overlap, MyFileIoCompletionRoutine);
23     printf("Before SleepEx!\n");
24     Sleep(5000);
25     //进入警惕状态,唤醒回调例程
26     SleepEx(0, TRUE);
27     CloseHandle(hDevice);
28     system("pause");
29     return 0;
30 }

输出结果为:

  • IRP的同步完成与异步完成

派遣函数有两种方式处理IRP请求,一种是在派遣函数中直接结束掉IRP,这可以认为是一种同步的处理方法。另一种方法是在派遣函数中不结束IRP请求,而是让派遣函数直接返回,IRP在以后的某个时刻再去处理

 IRP的同步完成

下面介绍下Win32 API是如何一层层通过调用进入到派遣函数的。

(1)在应用程序中调用CreateFile API;

(2)CreateFile API函数内部调用了ntdll.dll中的NtCreateFile函数。

(3)ntdll.dll中的NtCreateFile函数进入内核模式,然后调用ntoskrnl.exe中的NtCreateFile函数。

(4)内核模式中的ntoskrnl.exe的NtCreateFile函数创建IRP_MJ_CREATE类型的IRP,然后调用相应驱动程序的派遣函数,并将IRP的指针传递给该派遣函数。

(5)派遣函数调用IoCompleteRequest,将IRP结束。

(6)操作系统按照原路返回,一直退到CreateFile API。至此,CreateFile函数返回。

而此时如果有后续的ReadFile操作会有三种情况:

如果是用ReadFile进行同步读取时:

(1)ReadFile内部会创建一个事件,这个事件连同IRP一起被传递到派遣函数中。

(2)派遣函数调用IoCompleteRequest时,其内部会设置IRP的UserEvent事件。

(3)操作系统按照原路一直返回到ReadFile函数,ReadFile函数会等待这个事件。因为该事件已经被设置,所以无需等待。

(4)如果在派遣函数中没有调用IoCompleteRequest,该事件就没有被设置,ReadFile就会一直等IRP被结束

如果ReadFile是异步读取:

(1)ReadFile内部不会创建事件,但ReadFile函数会接收overlap参数。overlap参数中会提供一个事件,被用作同步处理。

(2)IoCompleteRequest内部会设置overlap提供的事件。

(3)在ReadFile函数退出前,它不会检测该事件是否被设置,因此可以不等待操作是否真的被完成

(4)当IRP操作被完成后,overlap提供的事件被设置,这个事件会通知应用程序IRP请求被完成

用ReadFileEx函数进行异步读取操作

(1)ReadFileEx不提供时间,但提供一个回调函数,这个回调函数的地址会作为IRP的参数传递给派遣函数。

(2)IoCompleteRequest会将这个完成函数插入到APC队列。

(3)应用程序只要进入警惕模式,APC队列会自动出队列,完成函数会被执行,这相当于通知应用层操作已经完成。

IRP的异步完成

IRP的异步完成是指不在派遣函数中调用IoCompleteRequest内核函数。调用了IoCompleteRequest意味着IRP请求的结束。如果派遣函数不调用IoCompleteRequest函数,则需要告诉操作系统此IRP处于“挂起”状态。这需要调用内核函数IoMarkIrpPending

CreateFile异步打开时,三种ReadFile如何处理IRP:

 

为了演示异步处理IRP,下面的示例假设IRP_MJ_READ的派遣函数仅仅是返回“挂起”。应用程序关闭设备的时候会产生IRP_MJ_CLEANUP类型的IRP。在IRP_MJ_CLEANUP的派遣函数中结束那些“挂起”的IRP_MJ_READ。为了能够存储有那些IRP_MJ_READ被挂起,这里使用一个队列,也就把每个挂起的IRP_MJ_READ的指针都插入队列,最后IRP_MJ_CLEANUP的派遣函数将一个个IRP出队列,并且调用IoCompleteRequest函数将它们结束。首先,要定义好队列的数据结构

 在设备扩展中加入“队列”这个变量,这样,驱动程序的所有派遣函数都可以使用这个队列。在DriverEntry中初始化该队列,并在DriverUnload中收回该队列。在IRP_MJ_READ派遣函数中,将IRP插入堆栈,然后返回“挂起”状态。

示例代码如下:

首先,设备扩展内容如下:

现在DriverEntry中初始化链表:

Read的派遣函数

CloseHandle的派遣函数:

R3的代码:

 1 int main(){
 2     getchar();
 3     HANDLE hDevice =
 4         CreateFile("\\\\.\\HelloDDK",
 5         GENERIC_READ | GENERIC_WRITE,
 6         0, NULL,
 7         OPEN_EXISTING,
 8         FILE_ATTRIBUTE_NORMAL,
 9         NULL);
10     if (hDevice == INVALID_HANDLE_VALUE){
11         printf("Open device failed!\n");
12     }
13     else{
14         printf("Open device succeed!\n");
15     }
16     OVERLAPPED overlap1;
17     HANDLE g_hOverlappedEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
18 
19     if (g_hOverlappedEvent == NULL || hDevice == INVALID_HANDLE_VALUE)
20     {
21         printf("Open device failed!\n");
22     }
23 
24     memset(&overlap1, 0, sizeof(OVERLAPPED));
25     overlap1.hEvent = g_hOverlappedEvent;
26      OVERLAPPED overlap2 = { 0 };
27      UCHAR buffer[10];
28      ULONG ulRead;
29      BOOL bRead = ReadFile(hDevice, buffer, 10, &ulRead, &overlap1);
30      if (!bRead && GetLastError() == ERROR_IO_PENDING){
31          printf("The operation is pending!\n");
32      }
33      bRead = ReadFile(hDevice, buffer, 10, &ulRead, &overlap2);
34      if (!bRead && GetLastError() == ERROR_IO_PENDING){
35          printf("The operation is pending!\n");
36      }
37     Sleep(2000);
38     printf("After sleeping..\n");
39     getchar();
40     CloseHandle(hDevice);
41     system("pause");
42     return 0;
43 }


一开始蓝屏,我以为是初始化链表或者插入链表有错,但是我在DriverEntry后加了两句打印,发现数据打印没错:

会输出正确的数据:

 然后我用windbg跟踪,发现一运行到CloseHandle就蓝屏:

等到下次用windbg双机调试的时候,发现会有提示:

说我结束掉了一个已经被结束掉的IRP,事实证明,如果你的代码时这样的:

返回的是STATUS_SUCCESS,那么也会被认为是结束掉了IRP。CloseHandle中再去结束,就会造成蓝屏

而如果改成这样:

虽然避免了0x00000044蓝屏,但跟踪发现上层的ReadFile无法返回,卡在ReadFile这一步:

为什么会卡在这里?后来我发现,CreateFile没有使用异步的方式打开,书上有一段话描述的很清楚,而我却忽略了:

因此,R3层的代码应该是:

 1 int main(){
 2     getchar();
 3     HANDLE hDevice =
 4         CreateFile("\\\\.\\HelloDDK",
 5         GENERIC_READ | GENERIC_WRITE,
 6         0, NULL,
 7         OPEN_EXISTING,
 8         FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
 9         NULL);
10     if (hDevice == INVALID_HANDLE_VALUE){
11         printf("Open device failed!\n");
12     }
13     else{
14         printf("Open device succeed!\n");
15     }
16     OVERLAPPED overlap1;
17     HANDLE g_hOverlappedEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
18 
19     if (g_hOverlappedEvent == NULL || hDevice == INVALID_HANDLE_VALUE)
20     {
21         printf("Open device failed!\n");
22     }
23 
24     memset(&overlap1, 0, sizeof(OVERLAPPED));
25     overlap1.hEvent = g_hOverlappedEvent;
26      OVERLAPPED overlap2 = { 0 };
27      UCHAR buffer[10];
28      ULONG ulRead;
29      BOOL bRead = ReadFile(hDevice, buffer, 10, &ulRead, &overlap1);
30      if (!bRead && GetLastError() == ERROR_IO_PENDING){
31          printf("The operation is pending!\n");
32      }
33      bRead = ReadFile(hDevice, buffer, 10, &ulRead, &overlap2);
34      if (!bRead && GetLastError() == ERROR_IO_PENDING){
35          printf("The operation is pending!\n");
36      }
37     Sleep(2000);
38     printf("After sleeping..\n");
39     getchar();
40     CloseHandle(hDevice);
41     system("pause");
42     return 0;
43 }

历尽千辛万苦,终于成功了:

通过这次蓝屏事件,受到的启示是:我们可以有空去研究研究蓝屏的编号所对应的问题:

 取消IRP:

上面讲到的方法是在CloseHandle中将挂起的IRP结束掉,还有另外一个办法将挂起的IRP结束,这就是取消IRP请求。内核函数IoSetCancelRoutine可以设置取消IRP请求的回调函数。IoSetCancelRoutine可以将一个取消例程与该IRP关联,一旦取消IRP请求的时候,这个取消例程会被执行

在IoCancelIrp内部使用了一个叫做cancel的自旋锁来进行同步。IoCancelIrp在内部会首先获得该自旋锁,IoCancelIrp会调用取消回调例程,因此,释放该自旋锁的任务就留给了取消回调例程。获得取消自旋锁的函数是IoAcquireCancelSpinLock函数,而释放取消自旋锁的函数是IoReleaseCancelSpinLock函数

在应用程序中,可以调用CancelIo函数取消IRP请求。在CancelIo的内部会枚举所有没有被完成的IRP,然后依次调用IoCancelIrp。另外,如果应用程序没有调用CancelIo函数,应用程序在关闭设备时同样会自动调用CancelIo

R0代码如下:

 1 VOID CancelReadIRP(PDEVICE_OBJECT pDevObj, PIRP pIrp){
 2     UNREFERENCED_PARAMETER(pDevObj);
 3     DbgPrint("Enter CancelReadIRP!\n");
 4 //     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
 5 //         pDevObj->DeviceExtension;
 6     pIrp->IoStatus.Status = STATUS_SUCCESS;
 7     pIrp->IoStatus.Information = 0;
 8     IoCompleteRequest(pIrp, IO_NO_INCREMENT);
 9     IoReleaseCancelSpinLock(pIrp->CancelIrql);
10     DbgPrint("Leave CancelReadIRP!\n");
11 }
12 
13 NTSTATUS HelloDDKRead_Cancel(PDEVICE_OBJECT pDevObj,
14     PIRP pIrp){
15     DbgPrint("Enter HelloDDKRead_Cancel!\n");
16     UNREFERENCED_PARAMETER(pDevObj);
17 //     PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
18 //         pDevObj->DeviceExtension;
19     IoSetCancelRoutine(pIrp, CancelReadIRP);
20     IoMarkIrpPending(pIrp);
21     DbgPrint("Leave HelloDDKRead_Cancel!\n");
22     return STATUS_SUCCESS;
23 }

R3代码如下:

 

程序结束之后会出现0x00000076号蓝屏:

其原因主要如下:原因是我忘记更改CloseHandle的分发函数了,仍然用的是之前的派遣函数:

第二个错误就是这里写错了(但这个错误不会导致蓝屏):

这个值只是返回一个调用IoCompleteRequest前IRP的状态。

真正的错误出现在这里:

要改成:

否则我们看输出内容会发现,并没有进入取消例程:

所以才会造成Clean up uncorrectly错误。

但是还是要问一句,Why?

我们来看下微软MSDN怎么解释这个0x00000076号蓝屏:https://msdn.microsoft.com/zh-cn/library/windows/hardware/ff559194

意思是,一个驱动没有正确释放那些被锁住的页面,就会造成这个崩溃。现在来想想,为什么return STATUS_SUCCESS会蓝屏。就是因为如果Read分发函数返回的是STATUS_SUCCESS那么就不会调用CancelReadIrp这个取消例程了,也就不会调用这个取消例程中释放自旋锁的代码了。但是由于R3的代码中调用了CancelIo,这个函数中会有获取自旋锁的代码,这一步是肯定会被执行到的。所以,只上了锁却没有释放锁,就造成了这个蓝屏

修改之后输出正确结果:

 

同步异步参考微软的文档:

https://msdn.microsoft.com/en-us/library/windows/hardware/ff549422(v=vs.85).aspx

https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683%28v=vs.85%29.aspx

https://msdn.microsoft.com/en-us/library/aa365683(v=vs.85).aspx 

 

posted @ 2016-06-01 10:11  _No.47  阅读(1914)  评论(0编辑  收藏  举报