IRP的同步处理和异步处理

1.DeviceIoControl的同步和异步操作:

image

 

IRP同步操作示意图

同步操作时,在DeviceIoControl的内部,会调用WaitForSingleObject函数去等待一个事件,这个事件知道IRP被结束时,才会被触发。DeviceIoControl会暂时进入睡眠状态,直到IRP被结束。而对于异步操作:

image

IRP异步操作示意图

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

 

2.同步操作设备

如果需要同步操作设备,那么在打开设备的时候就要指定以“同步”的方式打开设备。打开设备用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.

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#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 = "buffer";
         DWORD dwRet;
         //最后一个参数为NULL,标明采用同步方式写文件
         BOOL bRet = WriteFile(hFile, Buffer, strlen(Buffer), &dwRet, NULL);
         CloseHandle(hFile);
         return 0;
}

3.异步操作设备(方式一)

异步操作设备时主要需要设置OVERLAPPED结构:

typedef struct _OVERLAPPED { 、

    DWORD  Internal;

    DWORD  InternalHigh;

    DWORD  Offset;                 //操作设备会指定一个偏移量,从设备的偏移量进行读取。该偏移量是用一个64位整型表示,这个是偏移量的低32位

    DWORD  OffsetHigh;  //偏移量的高32位

    HANDLE hEvent;  //这个事件用于该操作完成后通知应用程序

} OVERLAPPED;

注意:在使用OVERLAPPED结构前,需要对其内部清零,并为其创建事件。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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 = "buffer";
         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;
}

4.异步操作设备(方式二)

除了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

);

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <windows.h>
#include <stdio.h>
VOID CALLBACK MyCompletionRoutine(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 = "MyCompletionRoutine";
         //定义OVERLAPPED结构,这里不需要Event事件
         OVERLAPPED ovlp = {0};
         BOOL bRet = WriteFileEx(hFile, Buffer, strlen(Buffer), &ovlp, MyCompletionRoutine);
         //....
         //其他操作
         //运行到这个点,必须等待写操作完成,否则不能继续完成别的任务
         SleepEx(0, TRUE);
         //....
         //其他操作
         printf("finish\n");
         CloseHandle(hFile);
         return 0;
}

5.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函数结束他们。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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;
}

6.取消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。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
取消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;
}

7.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例程。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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;
}

8.自定义的StartIO

我们发现,系统为我们提供的StartIo虽然可以很方便的将IRP串行化,但是存在一个问题,就是读写操作都被一起串行化。而我们有时候需要将读,写分开串行化。此时,希望有两个队列。一个队列串行IRP_MJ_READ类型的IRP,另一个队列串行IRP_MJ_WRITE类型的IRP。

此时我们需要自定义StartIo,自定义StartIo类似于前面介绍的StartIo例程,不同的是程序要需要自己维护IRP队列。程序员可以灵活的维护多个队列,分别应用于不同的IRP。

DDK提供KDEVICE_QUEUE数据结构存储队列,队列中每个元素用KDEVICE_QUEUE_ENTRY数据结构表示,在使用队列前,应该初始化队列(KeInitializeDeviceQueue

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
自定义设备扩展:
typedef struct _DEVICE_EXTENSION {
         PDEVICE_OBJECT pDevice;
         UNICODE_STRING ustrDeviceName;      //设备名称
         UNICODE_STRING ustrSymLinkName;   //符号链接名
         KDEVICE_QUEUE deviceQueue;              //支持串行化的队列
} DEVICE_EXTENSION, *PDEVICE_EXTENSION;
CreateDevice中初始化队列
    RtlZeroBytes(&pDevExt->device_queue,sizeof(pDevExt->device_queue));
    KeInitializeDeviceQueue(&pDevExt->device_queue);
IRP_MJ_READ派遣函数:
NTSTATUS HelloDDKRead(IN PDEVICE_OBJECT pDevObj,
                                 IN PIRP pIrp)
{
    KdPrint(("Enter HelloDDKRead\n"));
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
            pDevObj->DeviceExtension;
    //将IRP设置为挂起
    IoMarkIrpPending(pIrp);
    IoSetCancelRoutine(pIrp,OnCancelIRP);
    KIRQL oldirql;
    //提升IRP至DISPATCH_LEVEL
    KeRaiseIrql(DISPATCH_LEVEL, &oldirql);
    KdPrint(("HelloDDKRead irp :%x\n",pIrp));
    KdPrint(("DeviceQueueEntry:%x\n",&pIrp->Tail.Overlay.DeviceQueueEntry));
    if (!KeInsertDeviceQueue(&pDevExt->device_queue, &pIrp->Tail.Overlay.DeviceQueueEntry))
        MyStartIo(pDevObj,pIrp);
    //将IRP降至原来IRQL
    KeLowerIrql(oldirql);
    KdPrint(("Leave HelloDDKRead\n"));
    //返回pending状态
    return STATUS_PENDING;
}
自定义的StartIo函数:
#pragma LOCKEDCODE
VOID
  MyStartIo(
    IN PDEVICE_OBJECT  DeviceObject,
    IN PIRP pFistIrp
    )
{
    KdPrint(("Enter MyStartIo\n"));
    PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION)
        DeviceObject->DeviceExtension;
    PKDEVICE_QUEUE_ENTRY device_entry;
    PIRP Irp = pFistIrp;
    do
    {
        KEVENT event;
        KeInitializeEvent(&event,NotificationEvent,FALSE);
        //等3秒
        LARGE_INTEGER timeout;
        timeout.QuadPart = -3*1000*1000*10;
        //定义一个3秒的延时,主要是为了模拟该IRP操作需要大概3秒左右时间
        KeWaitForSingleObject(&event,Executive,KernelMode,FALSE,&timeout);
        KdPrint(("Complete a irp:%x\n",Irp));
        Irp->IoStatus.Status = STATUS_SUCCESS;
        Irp->IoStatus.Information = 0;  // no bytes xfered
        IoCompleteRequest(Irp,IO_NO_INCREMENT);
        device_entry=KeRemoveDeviceQueue(&pDevExt->device_queue);
        KdPrint(("device_entry:%x\n",device_entry));
        if (device_entry==NULL)
        {
            break;
        }
        Irp = CONTAINING_RECORD(device_entry, IRP, Tail.Overlay.DeviceQueueEntry);
    }while(1);
    KdPrint(("Leave MyStartIo\n"));
}

9.延迟过程调用例程(DPC)

它运行在DISPATCH_LEVEL的IRQL级别。因此除了ISR其他例程是不会将其打断的。ISR会将DPC插入一个内部队列,这样当IRQL从高的IRQL恢复到DISPATCH_LEVEL时,DPC会依次从队列中弹出,并执行相应的DPC例程。

初始化:KeInitializeDpc

ISR中执行简短的处理,将不重要的代码放到DPC例程中。

IoRequestDpc(device,device->CurremtIrp,NULL);

本文链接:http://www.alonemonkey.com/irp-synchronization-asynchronous.html