《Windows驱动开发技术详解》之驱动程序的同步处理

  • 中断请求级

中断请求被分为软件中断和硬件中断两种,这些中断都映射成不同级别的中断请求级。每个中断请求都有各自的优先级别,正在运行的线程随时都可以被中断打断,进入到中断处理程序。优先级高的中断来临时,处在优先级低的中断处理程序,也会被打断,进入到更高级别的中断处理函数。

Windows规定了32个中断请求级别,分别是0~2级别为软件中断,3~31为硬件中断。0~31,优先级别递增。硬件的IRQL称为设备中断请求级,或者DIRQL。Windows大部分时间运行在软件中断级别中。当硬件中断请求来临时,操作系统将提升IRQL至DIRQL级别,并运行中断处理函数。当中断处理函数结束后,操作系统把IRQL降回到原来的级别。

用户模式的代码时运行在最低优先级的PASSIVE_LEVLE级别。驱动程序的DriverEntry函数、派遣函数、AddDevice等函数一般都运行在PASSIVE_LEVEL级别,它们在必要时可以申请进入DISPATCH_LEVEL级别。Windows负责线程调度的组件是运行在DISPATCH_LEVEL级别,当前的线程运行完时间片后,系统自动从PASSIVE_LEVEL级别提升到DISPATCH_LEVEL级别。当钱的线程切换完毕后,操作系统又从DISPATCH_LEVEL级别降到PASSIVE_LEVEL级别。

所有的应用程序都运行在PASSIVE_LEVEL级别上,它的优先级别最低,可以被其它级别的IRQL级别的程序打断。线程优先级只针对应用程序而言,只有程序运行在PASSIVE_LEVEL级别才有意义。线程优先级高的线程有更多的机会被内核调度。负责调度线程的内核组件运行在DISPATCH_LEVEL级别的IRQL上,这个时候所有应用程序都停止,等待着被调度。

下图描述了一个IRQL的变化:

首先,一个普通的线程A正在运行;这个时刻有一个中断发生,它的IRQL为0x0D。CPU中断当前运行的线程A,将IRQL提升至0x0D级别;这个时候又有一个更高优先级的中断发生,它的IRQL是0x1A。这时候CPU将IRQL提升至0x1A级别;这时候又有一个中断发生,它的IRQL是0x18,低于上一个中断优先级。CPU不会理睬这个中断;这时候IRQL为0x1A的中断结束了,操作系统进入IRQL为0x18的中断服务;这时候IRQL为0x18的中断结束,于是进入IRQL为0x0D的中断服务;最后IRQL为0x0D的中断结束,操作系统恢复线程A。

IRQL与内存分页:在使用分页内存时,可能会导致页故障。因为分页内存随时可能从物理内存交换到磁盘文件。读取不在物理内存中的分页内存时,会引发一个页故障,从而执行这个异常的处理函数。异常处理函数会重新将磁盘文件的内容交换到物理内存中。页故障允许出现在PASSIVE_LEVEL级别的程序中,但是如果在DISPATCH_LEVEL或者更高级别IRQL的程序中会带来系统崩溃

But why?

这里引用看雪上的两段话进行解释吧:http://bbs.pediy.com/showthread.php?t=160200

 内核模式下的事件对象

先来看一下用户模式下的事件对象,其代码示例如下:

 1 UINT WINAPI Thread1(LPVOID para){
 2     printf("Enter thread1!\n");
 3     HANDLE *phEvent = (HANDLE*)para;
 4     SetEvent(*phEvent);
 5     return 0;
 6 }
 7 
 8 int main(){
 9     HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
10     HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hEvent, 0, NULL);
11     WaitForSingleObject(hEvent, INFINITE);
12     system("pause");
13     return 0;
14 }

等待执行完毕:

如果创建的事件对象是“通知事件”,当事件对象变为激发态时,程序员需要手动将其改为为激发态。如果创建的事件对象是“同步事件”,当时间对象为激发态时,如遇到KeWaitForXX等内核函数,事件对象则自动变回为未激发态。示例代码如下:

驱动程序与应用程序交互事件对象:在应用程序中创建的事件对象和在内核中创建的事件对象本质上是同一个东西。将用户模式下创建的时间传递给驱动程序的办法就是采用DeviceIoControl。DeviceIoControl将事件对象的句柄传递给内核,内核需要利用ObReferenceObjectByHandle将这个句柄转化为指针。

 现在应用层设置一个未激活的event:

 1 #define IOCTL_TRANSMIT_EVENT CTL_CODE(FILE_DEVICE_UNKNOWN,0x800,METHOD_BUFFERED,FILE_ANY_ACCESS)
 2 
 3 int main(){
 4     HANDLE hDevice = 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("Error!\n");
12         return 1;
13     }
14     BOOL bRet;
15     DWORD dwOutput;
16     getchar();
17     //创建event对象,设置为未激发状态
18     HANDLE hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
19     printf("Create event!\n");
20     bRet = DeviceIoControl(hDevice, IOCTL_TRANSMIT_EVENT, &hEvent,
21         sizeof(hEvent), NULL, 0, &dwOutput, NULL);
22     WaitForSingleObject(hEvent, INFINITE);
23     printf("After wait!\n");
24     CloseHandle(hDevice);
25     CloseHandle(hEvent);
26     system("pause");
27     return 0;
28 }

然后在内核层中将这个event激活并返回用户层:

 1 NTSTATUS HelloDDKDeviceIoControl_Event(PDEVICE_OBJECT pDevObj, PIRP pIrp){
 2     DbgPrint("Enter DeviceIoControl event!\n");
 3     UNREFERENCED_PARAMETER(pDevObj);
 4     NTSTATUS status = STATUS_SUCCESS;
 5     PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);
 6     //ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;
 7     //ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;
 8     ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;
 9     ULONG info = 0;
10     switch (code){
11     case IOCTL_TRANSMIT_EVENT:
12     {
13         HANDLE hUserEvent = *(HANDLE*)pIrp->AssociatedIrp.SystemBuffer;
14         PKEVENT pEvent;
15         status = ObReferenceObjectByHandle(hUserEvent,
16             EVENT_MODIFY_STATE,
17             *ExEventObjectType,
18             KernelMode,
19             (PVOID*)&pEvent,
20             NULL);
21         KeSetEvent(pEvent, IO_NO_INCREMENT, FALSE);
22         ObDereferenceObject(pEvent);
23         break;
24     }
25     default:
26         status = STATUS_INVALID_VARIANT;
27     }
28     pIrp->IoStatus.Status = status;
29     pIrp->IoStatus.Information = info;
30     IoCompleteRequest(pIrp, IO_NO_INCREMENT);
31     DbgPrint("Leave DeviceIoControl event!\n");
32     return status;
33 }

运行效果如下:

驱动程序与驱动程序交互事件对象

 有时候,需要在驱动程序与驱动程序中交互事件对象。例如,驱动程序A的某个派遣函数要与驱动程序B的派遣函数进行同步,就需要两个驱动程序之间交互事件对象。让驱动程序A获取驱动程序B中创建的事件对象的最简单的方法是驱动程序B创建一个有“名字”的事件对象,这样驱动程序A就可以根据“名字”寻找到事件对象的指针

创建有名的事件可以通过IoCreateNotificationEvent和IoCreateSynchronizationEvent内核函数。其中,IoCreateNotificationEvent函数创建“同步事件”对象,而IoCreateSynchronizationEvent创建通知事件对象。

内核模式下信号灯

这里先演示一下用户层的信号灯的使用:

 1 UINT WINAPI Thread1(LPVOID para){
 2     printf("Enter Thread1!\n");
 3     HANDLE *phSemaphore = (HANDLE*)para;
 4     Sleep(3000);
 5     printf("Leave Thread1!\n");
 6     ReleaseSemaphore(*phSemaphore, 1, NULL);
 7     return 0;
 8 }
 9 
10 int main(){
11     HANDLE hSemaphore = CreateSemaphore(NULL, 2, 2, NULL);
12     WaitForSingleObject(hSemaphore, INFINITE);
13     WaitForSingleObject(hSemaphore, INFINITE);
14     HANDLE hThread1 = (HANDLE)_beginthreadex(NULL, 0,
15         Thread1,
16         &hSemaphore,
17         0, NULL);
18     WaitForSingleObject(hSemaphore, INFINITE);
19     system("pause");
20     return 0;
21 }

可以正常运行并结束:

再来看内核下使用信号灯的示例代码:

1 VOID UserThreadSem(PVOID pContext){
2     DbgPrint("Enter UserThreadSem!\n");
3     PKSEMAPHORE pkSemaphore = (PKSEMAPHORE)pContext;
4     KeReleaseSemaphore(pkSemaphore, IO_NO_INCREMENT, 1, FALSE);
5     DbgPrint("Leave UserThreadSem!\n");
6     PsTerminateSystemThread(STATUS_SUCCESS);
7 }

 

 1 VOID SemTest(){
 2     HANDLE hMyThread;
 3     KSEMAPHORE kSemaphore;
 4     KeInitializeSemaphore(&kSemaphore, 2, 2);
 5     LONG count = KeReadStateSemaphore(&kSemaphore);
 6     DbgPrint("The semaphore count:%d\n", count);
 7     KeWaitForSingleObject(&kSemaphore, Executive, KernelMode, FALSE, NULL);
 8     count = KeReadStateSemaphore(&kSemaphore);
 9     DbgPrint("The semaphore count:%d\n", count);
10     KeWaitForSingleObject(&kSemaphore, Executive, KernelMode, FALSE, NULL);
11     count = KeReadStateSemaphore(&kSemaphore);
12     DbgPrint("The semaphore count:%d\n", count);
13     PsCreateSystemThread(&hMyThread, 0, NULL,
14         NtCurrentProcess(),
15         NULL,
16         UserThreadSem,
17         &kSemaphore);
18     KeWaitForSingleObject(&kSemaphore, Executive, KernelMode, FALSE, NULL);
19 }

运行结果如下:

 内核模式下的互斥体

 1 VOID UserThreadMutex1(PVOID pContext){
 2     DbgPrint("Enter UserThreadMutex1!\n");
 3     PKMUTEX pkMutex = (PKMUTEX)pContext;
 4     //等待获取mutex
 5     KeWaitForSingleObject(pkMutex, Executive, KernelMode, FALSE, NULL);
 6     KeStallExecutionProcessor(50);
 7     //释放已经获取了的mutex
 8     KeReleaseMutex(pkMutex, FALSE);
 9     DbgPrint("Leave UserThreadMutex1!\n");
10     PsTerminateSystemThread(STATUS_SUCCESS);
11 }
12 
13 VOID UserThreadMutex2(PVOID pContext){
14     DbgPrint("Enter UserThreadMutex2!\n");
15     PKMUTEX pkMutex = (PKMUTEX)pContext;
16     //等待获取mutex
17     KeWaitForSingleObject(pkMutex, Executive, KernelMode, FALSE, NULL);
18     KeStallExecutionProcessor(50);
19     //释放已经获取了的mutex
20     KeReleaseMutex(pkMutex, FALSE);
21     DbgPrint("Leave UserThreadMutex2!\n");
22     PsTerminateSystemThread(STATUS_SUCCESS);
23 }
24 
25 VOID MutexTest(){
26     HANDLE hMyThread1, hMyThread2;
27     KMUTEX kMutex;
28     KeInitializeMutex(&kMutex, 0);
29     PsCreateSystemThread(&hMyThread1, 0, NULL, NtCurrentProcess(), NULL, UserThreadMutex1, &kMutex);
30     PsCreateSystemThread(&hMyThread2, 0, NULL, NtCurrentProcess(), NULL, UserThreadMutex2, &kMutex);
31     PVOID Poiner_Array[2];
32     ObReferenceObjectByHandle(hMyThread1, 0, NULL, KernelMode, &Poiner_Array[0], NULL);
33     ObReferenceObjectByHandle(hMyThread2, 0, NULL, KernelMode, &Poiner_Array[1], NULL);
34     KeWaitForMultipleObjects(2, Poiner_Array, WaitAll, Executive, KernelMode, FALSE, NULL, NULL);
35     ObDereferenceObject(Poiner_Array[0]);
36     ObDereferenceObject(Poiner_Array[1]);
37 
38 }

运行结果如下:

使用自旋锁进行同步

如果程序获得了自旋锁,其它程序希望获取自旋锁时,则不停的进入自旋状态。无法获得自旋锁的线程会不停地自旋,这会浪费很多CPU时间。因此,需要同步的代码区域不能过长。

示例代码如下:

 1 UINT WINAPI Thread1(LPVOID pContext){
 2     BOOL bRet;
 3     DWORD dwOutput;
 4     bRet = DeviceIoControl(*(PHANDLE)pContext, IOCTL_TEST1, NULL, 0, NULL, 0, &dwOutput, NULL);
 5     return 0;
 6 }
 7 
 8 int main(){
 9         HANDLE hDevice = CreateFile("\\\\.\\HelloDDK",
10             GENERIC_READ | GENERIC_WRITE,
11             0, NULL,
12             OPEN_EXISTING,
13             FILE_ATTRIBUTE_NORMAL,
14             NULL);
15         if (hDevice == INVALID_HANDLE_VALUE){
16             printf("Error!\n");
17             return 1;
18         }
19         HANDLE hThread[2];
20         hThread[0] = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hDevice, 0, NULL);
21         hThread[1] = (HANDLE)_beginthreadex(NULL, 0, Thread1, &hDevice, 0, NULL);
22         WaitForMultipleObjects(2, hThread, TRUE, INFINITE);
23         CloseHandle(hThread[0]);
24         CloseHandle(hThread[1]);
25         CloseHandle(hDevice);
26         return 0;
27 }
 1 NTSTATUS HelloDDKDeviceIoControl_SpinLock(PDEVICE_OBJECT pDevObj, PIRP pIrp){
 2     ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);
 3     PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
 4     KIRQL oldIrql;
 5     //获取自旋锁,保存原先的Irql
 6     KeAcquireSpinLock(&pdx->My_SpinLock, &oldIrql);
 7     NTSTATUS status = STATUS_SUCCESS;
 8     DbgPrint("Enter HelloDDKDeviceIoControl_SpinLock!\n");
 9     //使用自旋锁后,IRQL提升至Dispatch级别
10     ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);
11     pIrp->IoStatus.Status = status;
12     pIrp->IoStatus.Information = 0;
13     IoCompleteRequest(pIrp, IO_NO_INCREMENT);
14     DbgPrint("Leave HelloDDKDeviceIoControl_SpinLock!\n");
15     KeReleaseSpinLock(&pdx->My_SpinLock, oldIrql);
16     return status;
17 }

运行结果如下:

 

posted @ 2016-05-30 17:22  _No.47  阅读(801)  评论(0编辑  收藏  举报