《Windows驱动开发技术详解》之StartIO例程
- StartIO例程
StartIO例程能够保证各个并行的IRP顺利执行,即串行化。假如有N个线程同时操作串口设备,必须将这些操作排队,然后一一进行处理。如果不做串行处理,当一个操作没有完毕时,新的操作又开始了,这会导致操作的混乱。因此,驱动有必要将并行的请求变成串行的请求,这需要用到队列。
将
转化为
当一个新的IRP请求到来时,首先检查设备是否处于“忙”状态,如果“空闲”则处理这个IRP,如果“忙”则将新来的IRP插入队列,留在以后处理。
操作系统为程序员提供了一个IRP队列来实现串行,这个队列用KDEVICE_QUEUE数据结构表示:
1 typedef struct _KDEVICE_QUEUE { 2 CSHORT Type; 3 CSHORT Size; 4 LIST_ENTRY DeviceListHead; 5 KSPIN_LOCK Lock; 6 7 #if defined(_AMD64_) 8 9 union { 10 BOOLEAN Busy; 11 struct { 12 LONG64 Reserved : 8; 13 LONG64 Hint : 56; 14 }; 15 }; 16 17 #else 18 19 BOOLEAN Busy; 20 21 #endif 22 23 } KDEVICE_QUEUE, *PKDEVICE_QUEUE, *PRKDEVICE_QUEUE;
这个队列的队列头保存在设备对象的DeviceObject->DeviceQueue中。插入和删除队列中的元素都是操作系统负责的。使用这个队列的时候,需要向系统提供一个叫做StartIo的例程,并将这个例程的函数名传递给系统:
pDriverObject->DriverStartIo=HelloDDKStartIO;
这个StartIO例程运行在DISPATCH_LEVEL级别,因此这个例程是不会被线程所打断的。在声明时要加上#pragma LOCKEDCODE修饰符。//将此函数加载到非分页内存中
派遣函数如果想把IRP串行化,只需要加入IoStartPacket函数首先判断当前设备是“忙”还是“空闲”。如果设备“空闲”,则提升当前IRQL到DISPATCH_LEVEL级别,并进入StartIO例程“串行处理该”IRP请求。如果设备“忙”,则将IRP插入队列后返回。IoStartPacket函数的伪代码如下:
StartIO例程结束前,应该调用IoStartNextPacket函数,其作用是从队列中抽取下一个IRP,并将这个IRP作为参数调用StartIO例程。IoStartNextPacket伪代码如下:
- 示例
在使用StartIO例程时,需要IRP的派遣函数返回挂起状态,然后调用IoStartPacket内核函数,先不考虑取消例程。
应用程代码如下,这里会并发的去进行读操作:
1 int main(){ 2 //getchar(); 3 HANDLE hDevice = 4 CreateFile("\\\\.\\HelloDDK", 5 GENERIC_READ,//| GENERIC_WRITE 6 FILE_SHARE_READ, NULL, 7 OPEN_EXISTING, 8 FILE_ATTRIBUTE_NORMAL,//|FILE_FLAG_OVERLAPPED 9 NULL); 10 printf("GetLastError:%d\n", GetLastError()); 11 //getchar(); 12 if (hDevice == INVALID_HANDLE_VALUE){ 13 printf("Open device failed!\n"); 14 //return 1; 15 } 16 else{ 17 printf("Open device succeed!\n"); 18 } 19 HANDLE hThread[2]; 20 hThread[0] = (HANDLE)_beginthreadex(NULL, 0, Thread, &hDevice, 0, NULL); 21 hThread[1] = (HANDLE)_beginthreadex(NULL, 0, Thread, &hDevice, 0, NULL); 22 WaitForMultipleObjects(2, hThread, TRUE, INFINITE); 23 CloseHandle(hDevice); 24 system("pause"); 25 return 0; 26 }
R3并发读操作,R0层对其进行串行化处理。R0层代码如下:
StartIO的代码为:
1 #pragma LOCKEDCODE 2 VOID HelloDDKStartIO(PDEVICE_OBJECT DeviceObject, PIRP pIrp){ 3 KIRQL oldirql; 4 DbgPrint("Enter HelloDDKStartIO!\n"); 5 IoAcquireCancelSpinLock(&oldirql); 6 if (pIrp != DeviceObject->CurrentIrp || pIrp->Cancel){ 7 IoReleaseCancelSpinLock(oldirql); 8 DbgPrint("Leave HelloDDKStartIO!\n"); 9 return; 10 } 11 else{ 12 //IoSetCancelRoutine(pIrp, NULL); 13 IoReleaseCancelSpinLock(oldirql); 14 } 15 KEVENT event; 16 KeInitializeEvent(&event, NotificationEvent, FALSE); 17 LARGE_INTEGER timeout; 18 timeout.QuadPart = -3 * 1000 * 1000 * 10; 19 KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &timeout); 20 pIrp->IoStatus.Status = STATUS_SUCCESS; 21 pIrp->IoStatus.Information = 0; 22 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 23 IoStartNextPacket(DeviceObject, TRUE); 24 DbgPrint("Leave HelloDDKStartIO!\n"); 25 }
运行引发了新的蓝屏错误,编号0x000000D1:
以后引发蓝屏就从这里查找蓝屏原因:https://msdn.microsoft.com/zh-cn/library/windows/hardware/hh994433%28v=vs.85%29.aspx
崩溃原因如:https://msdn.microsoft.com/zh-cn/library/windows/hardware/ff560244%28v=vs.85%29.aspx
后来发现是我自己忘记注册StartIO例程了:
StartIO例程前面添加了一句声明很重要“#pragma LOCKEDCODE”,说明StartIO例程要在非分页内存中执行,即提高到DISPATCH_LEVEL级别,而我当时没有写这句:
1 pDriverObject->DriverStartIo = HelloDDKStartIO;
就相当于在DISPATCH_LEVEL级别访问了一个没有声明“#pragma LOCKEDCODE”的内存,就引起了这个蓝屏。
运行成功,不再蓝屏:
画个图总结一下IoStartPacket、StartIO、IoStartNextPacket之间的关系(字丑请忽略):
前边这些都是书本上的介绍,下面我对StartIO进行进一步的研究。
做一个实验,应用层创建十个ReadFile的线程去模拟并发ReadFile IRP发生的过程:
1 //这个类型不是struct而是struct para 2 typedef struct para{ 3 HANDLE hDevice; 4 int i; 5 }*pPara, Para; 6 7 UINT WINAPI Thread(pPara context){ 8 //printf("Enter thread!\n"); 9 OVERLAPPED overlap = { 0 }; 10 overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 11 UCHAR buffer[10]; 12 ULONG ulRead; 13 printf("Thread %d read begin.\n", context->i); 14 BOOL bRead = ReadFile(context->hDevice, buffer, 10, &ulRead, &overlap); 15 printf("Thread %d read end.\n", context->i); 16 WaitForSingleObject(overlap.hEvent, INFINITE); 17 return 0; 18 } 19 20 int teststruct(Para context){ 21 printf("%d\n", context.i); 22 } 23 // 24 // 25 int main(){ 26 HANDLE hDevice = 27 CreateFile("\\\\.\\HelloDDK", 28 GENERIC_READ,//| GENERIC_WRITE 29 FILE_SHARE_READ, NULL, 30 OPEN_EXISTING, 31 FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, 32 NULL); 33 printf("GetLastError:%d\n", GetLastError()); 34 /*p.hDevice = hDevice;*/ 35 //getchar(); 36 if (hDevice == INVALID_HANDLE_VALUE){ 37 printf("Open device failed!\n"); 38 //return 1; 39 } 40 else{ 41 printf("Open device succeed!\n"); 42 } 43 HANDLE hThread[10]; 44 Para p[10];//如果不是在这里定义十个Para对象,而是企图在for循环中定义十个对象则会出错 45 for (int i = 0; i < 10; i++){ 46 // Para p; 47 // printf("addr:%d..", &p);//这样输出的地址相同,说明for循环中定义的变量的作用域是整个循环 48 //即便下一次循环中看似又定义了一遍,但是实际上还是刚才的那个变量 49 p[i].i = i; 50 p[i].hDevice = hDevice; 51 hThread[i] = (HANDLE)_beginthreadex(NULL, 0, Thread, &p[i], 0, NULL); 52 //_beginthreadex的第四个参数要求就是thread的参数指针,而你传递的是值的话,就会解析错误 53 } 54 WaitForMultipleObjects(2, hThread, TRUE, INFINITE); 55 CloseHandle(hDevice); 56 system("pause"); 57 return 0; 58 }
R0层代码是:
1 //如果会有并发的读操作发生,那么就可以利用IoStartPacket进行串行化的处理 2 NTSTATUS HelloDDKRead_Start(PDEVICE_OBJECT pDevObj, PIRP pIrp){ 3 DbgPrint("Enter HelloDDKRead_Start Irp:%d\n", pIrp); 4 /*PDEVICE_EXTENSION pDevExt = (PDEVICE_EXTENSION) 5 pDevObj->DeviceExtension;*/ 6 IoMarkIrpPending(pIrp); 7 //IoStartPacket(pDevObj, pIrp, 0, OnCancelIRP); 8 IoStartPacket(pDevObj, pIrp, 0, NULL); 9 DbgPrint("Leave HelloDDKRead_Start Irp:%d\n", pIrp); 10 11 return STATUS_PENDING; 12 }
1 #pragma LOCKEDCODE 2 VOID HelloDDKStartIO(PDEVICE_OBJECT DeviceObject, PIRP pIrp){ 3 KIRQL oldirql; 4 DbgPrint("************Enter HelloDDKStartIO Irp : %d\n", pIrp); 5 6 IoAcquireCancelSpinLock(&oldirql); 7 if (pIrp != DeviceObject->CurrentIrp || pIrp->Cancel){ 8 IoReleaseCancelSpinLock(oldirql); 9 DbgPrint("************Leave HelloDDKStartIO!\n"); 10 return; 11 } 12 else{ 13 //IoSetCancelRoutine(pIrp, NULL); 14 IoReleaseCancelSpinLock(oldirql); 15 } 16 KEVENT event; 17 KeInitializeEvent(&event, NotificationEvent, FALSE); 18 LARGE_INTEGER timeout; 19 timeout.QuadPart = -3 * 1000 * 1000 * 10; 20 KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &timeout); 21 pIrp->IoStatus.Status = STATUS_SUCCESS; 22 pIrp->IoStatus.Information = 0; 23 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 24 IoStartNextPacket(DeviceObject, TRUE); 25 DbgPrint("************Leave HelloDDKStartIO Irp : %d\n", pIrp); 26 }
试验中我一开始忘记加异步,也就是说我使用的是同步的方式调用CreateFile。结果,十次for循环发送的都是一个IRP
而我这次加上异步,则输出结果明显不同:
我在ReadFile的派遣函数和StartIO的调用例程的入口和出口处都进行DbgPrint输出:
观察结果发现:第一个IRP的ReadFile派遣函数最先调用,但也是最后退出的,且这个IRP的StartIO例程也是最先调用最后退出的。
而观察中间的这些IRP的StartIO例程则是“先进后出”的。Why?我来画个图:
现在我们为了更清楚的看清IRP并发处理的顺序,我们改为并发的进行DeviceIoControl操作:
1 UINT WINAPI Thread(pPara context){ 2 //printf("Enter thread!\n"); 3 OVERLAPPED overlap = { 0 }; 4 overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 5 UCHAR buffer[10]; 6 UINT uBuf = context->i; 7 ULONG ulRead; 8 printf("Thread %d read begin.\n", context->i); 9 //BOOL bRead = ReadFile(context->hDevice, buffer, 10, &ulRead, &overlap); 10 11 DeviceIoControl(context->hDevice, 12 CODE_STARTIO, 13 &uBuf, sizeof(uBuf), buffer, 10, &ulRead, &overlap);//注意sizeof和strlen的区别。这里误用strlen曾导致崩溃。 14 printf("Thread %d read end.\n", context->i); 15 WaitForSingleObject(overlap.hEvent, INFINITE); 16 return 0; 17 }
StartIO例程的代码为:
1 #pragma LOCKEDCODE 2 VOID HelloDDKStartIO_Control(PDEVICE_OBJECT DeviceObject, PIRP pIrp){ 3 KIRQL oldirql; 4 //DbgPrint("************Enter HelloDDKStartIO Irp : %d\n", pIrp); 5 // PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; 6 // PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); 7 UINT32* InputBuffer = (UINT32*)pIrp->AssociatedIrp.SystemBuffer; 8 DbgPrint("************Enter StartIO IRP:%d\n", *InputBuffer); 9 10 IoAcquireCancelSpinLock(&oldirql); 11 if (pIrp != DeviceObject->CurrentIrp || pIrp->Cancel){ 12 IoReleaseCancelSpinLock(oldirql); 13 DbgPrint("************Leave HelloDDKStartIO!\n"); 14 return; 15 } 16 else{ 17 //IoSetCancelRoutine(pIrp, NULL); 18 IoReleaseCancelSpinLock(oldirql); 19 } 20 KEVENT event; 21 KeInitializeEvent(&event, NotificationEvent, FALSE); 22 LARGE_INTEGER timeout; 23 timeout.QuadPart = -3 * 1000 * 1000 * 10; 24 KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &timeout); 25 pIrp->IoStatus.Status = STATUS_SUCCESS; 26 pIrp->IoStatus.Information = 0; 27 28 IoStartNextPacket(DeviceObject, TRUE); 29 //DbgPrint("************Leave HelloDDKStartIO Irp : %d\n", pIrp); 30 DbgPrint("************Leave StartIO IRP:%d\n", *InputBuffer); 31 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 32 }
注意这里要先DbgPrint然后再IoCompleteRequest,否则如果释放掉了这个IRP,再企图打印出指针所指位置的内容,就有可能打印出如下所示的随机数据:
派遣函数代码为:
NTSTATUS HelloDDKDeviceIoControl_Start(PDEVICE_OBJECT pDevObj, PIRP pIrp){ //PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); UINT32* InputBuffer = (UINT32*)pIrp->AssociatedIrp.SystemBuffer; DbgPrint("Enter control IRP:%d\n", *InputBuffer); //得到IOCTRL码 ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; NTSTATUS status; ULONG info = 0; switch (code) { case CODE_STARTIO: { status = STATUS_PENDING; pIrp->IoStatus.Status = status; pIrp->IoStatus.Information = info; IoMarkIrpPending(pIrp);//将IRP设置为挂起状态(异步IRP) IoStartPacket(pDevObj, pIrp, 0, NULL); //DbgPrint(("end call IoStartPacket, IRP: 0x%x\n", pIrp)); } } DbgPrint("Leave control IRP:%d\n", *InputBuffer); return STATUS_PENDING; }
输出结果为:
这样看起来更加直观。虽说这是个IRP是并发的,但是这样看起来还是每次都是按序执行的。是这样么?我们试着让两个线程“优先”启动,但是让这两个线程的IRP尽量晚到一些,再看看输出什么样的结果:
代码如下:
1 UINT WINAPI Thread(pPara context){ 2 //printf("Enter thread!\n"); 3 OVERLAPPED overlap = { 0 }; 4 overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 5 UCHAR buffer[10]; 6 UINT uBuf = context->i; 7 ULONG ulRead; 8 printf("Thread %d read begin.\n", context->i); 9 //BOOL bRead = ReadFile(context->hDevice, buffer, 10, &ulRead, &overlap); 10 11 DeviceIoControl(context->hDevice, 12 CODE_STARTIO, 13 &uBuf, sizeof(uBuf), buffer, 10, &ulRead, &overlap);//注意sizeof和strlen的区别。这里误用strlen曾导致崩溃。 14 printf("Thread %d read end.\n", context->i); 15 WaitForSingleObject(overlap.hEvent, INFINITE); 16 return 0; 17 } 18 19 UINT WINAPI Thread2(pPara context){ 20 //printf("Enter thread!\n"); 21 OVERLAPPED overlap = { 0 }; 22 overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 23 UCHAR buffer[10]; 24 UINT uBuf = context->i; 25 ULONG ulRead; 26 printf("Thread %d read begin.\n", context->i); 27 //BOOL bRead = ReadFile(context->hDevice, buffer, 10, &ulRead, &overlap); 28 Sleep(2000); 29 DeviceIoControl(context->hDevice, 30 CODE_STARTIO, 31 &uBuf, sizeof(uBuf), buffer, 10, &ulRead, &overlap);//注意sizeof和strlen的区别。这里误用strlen曾导致崩溃。 32 printf("Thread %d read end.\n", context->i); 33 WaitForSingleObject(overlap.hEvent, INFINITE); 34 return 0; 35 } 36 37 38 int main(){ 39 getchar(); 40 HANDLE hDevice = 41 CreateFile("\\\\.\\HelloDDK", 42 GENERIC_READ,//| GENERIC_WRITE 43 FILE_SHARE_READ, NULL, 44 OPEN_EXISTING, 45 FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, 46 NULL); 47 printf("GetLastError:%d\n", GetLastError()); 48 /*p.hDevice = hDevice;*/ 49 //getchar(); 50 if (hDevice == INVALID_HANDLE_VALUE){ 51 printf("Open device failed!\n"); 52 //return 1; 53 } 54 else{ 55 printf("Open device succeed!\n"); 56 } 57 HANDLE hThread[10]; 58 HANDLE hThread2[2]; 59 60 Para p[10];//如果不是在这里定义十个Para对象,而是企图在for循环中定义十个对象则会出错 61 Para p2[10];//如果不是在这里定义十个Para对象,而是企图在for循环中定义十个对象则会出错 62 for (int i = 0; i < 2; i++){ 63 p2[i].i = i+10; 64 p2[i].hDevice = hDevice; 65 hThread2[i] = (HANDLE)_beginthreadex(NULL, 0, Thread2, &p2[i], 0, NULL); 66 } 67 68 for (int i = 0; i < 10; i++){ 69 // Para p; 70 // printf("addr:%d..", &p);//这样输出的地址相同,说明for循环中定义的变量的作用域是整个循环 71 //即便下一次循环中看似又定义了一遍,但是实际上还是刚才的那个变量 72 p[i].i = i; 73 p[i].hDevice = hDevice; 74 hThread[i] = (HANDLE)_beginthreadex(NULL, 0, Thread, &p[i], 0, NULL); 75 //_beginthreadex的第四个参数要求就是thread的参数指针,而你传递的是值的话,就会解析错误 76 } 77 78 WaitForMultipleObjects(2, hThread, TRUE, INFINITE); 79 CloseHandle(hDevice); 80 system("pause"); 81 return 0; 82 }
输出结果:
由此可以看到,StartIO串行处理的顺序并不是依照线程启动的先后,而是依照底层驱动获取IRP的先后。尽管我们模拟的是并发操作,但是实际上还是会有IRP的先来后到,并不是真正的“同时”到达。所以,先来的先被处理,后来的进入队列。这里尽管10和11号线程先于其他线程启动,但是,由于其Sleep了2秒(也可以说是由于一些什么其他原因时间片没有优先分配给10和11号线程),它们并非最先把IRP交给底层的。尽管是并发,但是一般情况下时间片还是按先来后到分配的。
- 考虑到取消例程
实际上在IoStartPacket的第四个参数可以传一个取消例程进来,然而上面所示的几个例子我都没有传。
对于取消例程的介绍可以参看:https://msdn.microsoft.com/en-us/library/windows/hardware/ff540742%28v=vs.85%29.aspx
先将取消例程的实验的相关代码贴出来:
R3层代码如下:
1 typedef struct para{ 2 HANDLE hDevice; 3 int i; 4 }*pPara, Para; 5 6 UINT WINAPI Thread(pPara context){ 7 OVERLAPPED overlap = { 0 }; 8 overlap.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL); 9 UCHAR buffer[10]; 10 UINT uBuf = context->i; 11 ULONG ulRead; 12 printf("Thread %d read begin.\n", context->i); 13 14 DeviceIoControl(context->hDevice, 15 CODE_STARTIO, 16 &uBuf, sizeof(uBuf), buffer, 10, &ulRead, &overlap);//注意sizeof和strlen的区别。这里误用strlen曾导致崩溃。 17 CancelIo(context->hDevice);//cancelIO new add 18 printf("Thread %d read end.\n", context->i); 19 WaitForSingleObject(overlap.hEvent, INFINITE); 20 return 0; 21 } 22 23 24 int main(){ 25 getchar(); 26 HANDLE hDevice = 27 CreateFile("\\\\.\\HelloDDK", 28 GENERIC_READ,//| GENERIC_WRITE 29 FILE_SHARE_READ, NULL, 30 OPEN_EXISTING, 31 FILE_ATTRIBUTE_NORMAL|FILE_FLAG_OVERLAPPED, 32 NULL); 33 printf("GetLastError:%d\n", GetLastError()); 34 35 if (hDevice == INVALID_HANDLE_VALUE){ 36 printf("Open device failed!\n"); 37 38 } 39 else{ 40 printf("Open device succeed!\n"); 41 } 42 HANDLE hThread[10]; 43 44 Para p[10];//如果不是在这里定义十个Para对象,而是企图在for循环中定义十个对象则会出错 45 46 47 for (int i = 0; i < 10; i++){ 48 p[i].i = i; 49 p[i].hDevice = hDevice; 50 hThread[i] = (HANDLE)_beginthreadex(NULL, 0, Thread, &p[i], 0, NULL); 51 } 52 53 WaitForMultipleObjects(2, hThread, TRUE, INFINITE); 54 CloseHandle(hDevice); 55 system("pause"); 56 return 0; 57 }
首先因为I/O管理器在调用取消例程前会先调用IoAcquireCancelSpinLock来获取自旋锁,那么在取消例程里面切记一定要调用IoReleaseCancelSpinLock来释放自旋锁。
取消例程里面有个判断语句if (Irp == DeviceObject->CurrentIrp),
1. 当irp != DeviceObject->CurrentIrp的时候,这个很好理解,就是需求取消的irp还没有被执行,那么也就是说还在队列里面,直接把这个irp从队列里面删除就可以了。
2. 当irp == DeviceObject->CurrentIrp的时候,因为需要取消的irp已经不在队列中了,那么就无需操作队列。在取消例程里 面只要调用IoStartNextPacket就可以了(也就是跳过了当前irp的处理)。
取消例程代码为:
1 VOID OnCancelIRP(PDEVICE_OBJECT DeviceObject, PIRP pIrp){ 2 3 UINT32* InputBuffer = (UINT32*)pIrp->AssociatedIrp.SystemBuffer; 4 DbgPrint("Enter OnCancelIRP:%d!\n", *InputBuffer); 5 if (pIrp == DeviceObject->CurrentIrp){ 6 //此IRP不在队列中,正在由StartIO处理 7 DbgPrint("Current IRP OnCancelIRP!\n"); 8 KIRQL oldirql = pIrp->CancelIrql;//保存原先的IRQL 9 IoReleaseCancelSpinLock(pIrp->CancelIrql); 10 IoStartNextPacket(DeviceObject, TRUE); 11 KeLowerIrql(oldirql); 12 } 13 else{//当irp != fdo->CurrentIrp的时候,这个很好理解, 14 //就是需求取消的irp还没有被执行,那么也就是说还在队列里面,直接把这个irp从队列里面删除就可以了。 15 DbgPrint("Uncurrent IRP OnCancelIRP!\n"); 16 KeRemoveEntryDeviceQueue(&DeviceObject->DeviceQueue, 17 &pIrp->Tail.Overlay.DeviceQueueEntry); 18 IoReleaseCancelSpinLock(pIrp->CancelIrql); 19 } 20 pIrp->IoStatus.Status = STATUS_CANCELLED; 21 pIrp->IoStatus.Information = 0; 22 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 23 DbgPrint("Leave OnCancelIRP:%d!\n", *InputBuffer); 24 }
派遣函数中注册了取消例程:
NTSTATUS HelloDDKDeviceIoControl_Start(PDEVICE_OBJECT pDevObj, PIRP pIrp){ //PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension; PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); UINT32* InputBuffer = (UINT32*)pIrp->AssociatedIrp.SystemBuffer; DbgPrint("Enter control IRP:%d\n", *InputBuffer); //得到IOCTRL码 ULONG code = stack->Parameters.DeviceIoControl.IoControlCode; NTSTATUS status; ULONG info = 0; switch (code) { case CODE_STARTIO: { status = STATUS_PENDING; pIrp->IoStatus.Status = status; pIrp->IoStatus.Information = info; IoMarkIrpPending(pIrp);//将IRP设置为挂起状态(异步IRP) IoStartPacket(pDevObj, pIrp, 0, OnCancelIRP);//CancelIO new add //DbgPrint(("end call IoStartPacket, IRP: 0x%x\n", pIrp)); } } DbgPrint("Leave control IRP:%d\n", *InputBuffer); return STATUS_PENDING; }
StartIO代码:
1 #pragma LOCKEDCODE 2 VOID HelloDDKStartIO_Control(PDEVICE_OBJECT DeviceObject, PIRP pIrp){ 3 KIRQL oldirql; 4 //DbgPrint("************Enter HelloDDKStartIO Irp : %d\n", pIrp); 5 // PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)DeviceObject->DeviceExtension; 6 // PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp); 7 UINT32* InputBuffer = (UINT32*)pIrp->AssociatedIrp.SystemBuffer; 8 DbgPrint("************Enter StartIO IRP:%d\n", *InputBuffer); 9 10 IoAcquireCancelSpinLock(&oldirql); 11 if (pIrp != DeviceObject->CurrentIrp || pIrp->Cancel){ 12 IoReleaseCancelSpinLock(oldirql); 13 DbgPrint("************Leave HelloDDKStartIO!\n"); 14 return; 15 } 16 else{ 17 IoSetCancelRoutine(pIrp, NULL);//CancelIO new add 18 IoReleaseCancelSpinLock(oldirql); 19 } 20 KEVENT event; 21 KeInitializeEvent(&event, NotificationEvent, FALSE); 22 LARGE_INTEGER timeout; 23 timeout.QuadPart = -3 * 1000 * 1000 * 10; 24 KeWaitForSingleObject(&event, Executive, KernelMode, FALSE, &timeout); 25 pIrp->IoStatus.Status = STATUS_SUCCESS; 26 pIrp->IoStatus.Information = 0; 27 28 IoStartNextPacket(DeviceObject, TRUE); 29 //DbgPrint("************Leave HelloDDKStartIO Irp : %d\n", pIrp); 30 DbgPrint("************Leave StartIO IRP:%d\n", *InputBuffer); 31 IoCompleteRequest(pIrp, IO_NO_INCREMENT); 32 }
起初看这段代码时我纠结于取消例程中如果判定是当前IRP的话会调用IoStartNextPacket,那样就会递归的去调用StartIO例程了,这样的话在取消例程的第一个if里递归调用会不会造成无法返回或等待很久才能返回。与其焦虑不如从运行结果观察:
从运行结果上看,这一些列的取消例程操作中并没有进入到取消例程的else。并且只有一次进入StartIO例程。我猜测原因是,第一个IRP在 到达R0层时,执行StartIO操作,由于StartIO操作中有三秒的延迟,而这段时间内,后续的IRP就到来了,而到来一个IRP,就被 CancelIO取消掉一个。最终第一个IRP由于取消了取消例程所以没有调用取消例程。文章http://blog.csdn.net/zj510/article/details/8287712中曾提到对于取消例程来说“当irp == fdo->CurrentIrp的时候,这是个有趣的时间点,这个时间点处于 fdo->CurrentIrp=Irp(IoStartPacket或者IoStartNextPacket)和 IoAcquireCancelSpinLock(StartIo例程)之间”。这句话是正确的。情况一。不妨想象第一 个IRP到来,此时第一个StartIO例程没执行几句代码(位于上述时间节点之间)(此时还没有执行StartIO中的if语句,所以这个IRP没有在 队列中),并发发生CancelIO(当然这种情况由于切换极快,发生概率极小,但仍有可能发生),此时取消例程中的IRP就是StartIO中的 IRP,那么就要执行取消例程中的else,这时不对当前IRP进行处理而是去尝试获得队列中的下一个IRP。如果此时太快其它IRP还没到,则直接结 束,如果有其它IRP到了,这时从队列中取到这个IRP作为CurrentIRP(取消例程中的IoStartNextPacket还未返回)(此时如果 再切换到第一个IRP的StartIO例程,那么就会执行if中的return操作直接结束这个StartIO)并发发生StartIO和 CancelIO。可能对于我个人来说疑问最大的地方是为什么取消例程中要有一个IoStartNextPacket,StartIO中还有一个 IoStartNextPacket这样不会冲突么?我用图来解释下:
情况二。第一个IRP没有进入队列如果此时第一个IRP的StartIO例程执行到了if/else语句之间,那么由于加了自旋锁,这个IRP对应的CancelIO即便此时并发发生,也无法获得执行。
情况三。而如果此时StartIO执行过了if/else语句,即释放了自旋锁,那么此时要么就是已经取消了取消例程,则这个将要并发的 CancelIO不去执行(StartIO中的else导致),而是进行后续StartIO的处理,要么由于满足if条件,已经将这个IRP插入队列并返 回了。
CancelIO和StartIO并发操作的代码架构是深思熟虑考虑到各种并发情况之后设计出来的。
参考两篇文章来理解StartIO:
http://blog.csdn.net/zj510/article/details/8230071
http://blog.csdn.net/zj510/article/details/8287712
和一篇论坛的问答:
http://bbs.csdn.net/topics/340210587