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

《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

相关推荐