您的位置:首页 > 其它

第8章 驱动程序的同步处理

2014-07-17 09:12 288 查看
转载自:http://blog.csdn.net/aksnzhy/article/details/6826702

如果驱动程序没有很好地处理同步问题,操作系统的性能就会下降,甚至出现死锁等现象。

基本概念

1.问题的引出

下面这段代码:

[cpp]
view plaincopy

int number;  
void Foo()  
{  
    number++;  
    //做一些事情  
    number--;  
}  

将其转换成汇编代码:

[plain]
view plaincopy

; 将number++分解成如下  
mov   eax , [number]  
add   eax , 1  
mov   [number] , eax  
; 将number--分解成如下  
mov    ecx , dword ptr [number]  
sub    ecx , 1  
mov    dword ptr [number] , ecx  

在多线程环境下这些代码可能混作一团。

2.同步和异步

运行在不同线程中的程序,各自没有相互影响的运行,成为它们之间是异步的。

当不同线程需要按照一定顺序来执行时,称为线程间的同步。

中断请求级

1.中断请求(IRQ)和可编程中断控制器(PIC)

在设计Windows的时候,设计者将中断请求划分为软件中断和硬件中断,并将这些中断映射成不同级别的中断请求级(IRQL)。

中断请求(IRQ)一般有两种,一种是外部中断,也就是硬件产生的中断,另一种由软件指令int n产生的中断。

在传统PC中,一般可以接收16个中断信号,每个中断信号对应一个中断号。外部中断分为不可屏蔽(NMI)和可屏蔽中断,分别有CPU的两根引脚NMI和INTR莱接收。

可屏蔽中断是通过可编程中断控制器(PIC)芯片想CPU发送的。

2.高级可编程控制器

现在的X86计算机基本都用高级可编程控制器(APIC)

APIC兼容PIC,但是将IRQ的数量增加到24个。

3.中断请求级(IRQL)

在APIC中,IRQ的数量倍增加到了24个,每个IRQ有各自的优先级别,正在运行的线程可能随时被中断打断,进入到中断处理程序。当优先级高的中断来临时,处于优先级低的中断处理程序,也会被打断,进入到更高级别的中断处理函数。

Windows将中断的概念进行了扩展,提出了一个中断请求级(IRQL)的概念。其中规定了32个中断请求级别,分别0~2级别(2为DISPATCH_LEVEL)为软件中断,3~31级为硬件中断(这里包括APIC中的24个中断)。数字从0~31,优先级别逐渐递增。

Windows将24个IRQ映射到了从DISPATCH_LEVEL到PROFILE_LEVEL之间,不同硬件的中断处理程序运行在不同的IRQL级别中。硬件的IRQL称为设备中断请求级,或简称DIRQL。Windows大部分时间运行在软件中断级别中。当设备中断来临时,操作系统提升IRQL至DIRQL级别,并且运行中断处理函数。当中断处理函数结束后,系统把IRQL降到原来的级别。

用户模式的代码运行在最低级别的PASSIVE_LEVEL级别。驱动程序的DriverEntry函数,派遣函数,AddDevice等函数一般运行在PASSIVE_LEVEL级别。他们在必要时可以申请进入到DISPATCH_LEVEL级别。

Windows负责线程调度的组件是运行在DISPATCH_LEVEL级别。驱动程序的StartIO函数和DPC函数也是运行在DISPATCH_LEVEL级别。

在内核模式下,可以通过KeGetCurrentIrql内核函数来得到当前的IRQL级别。

4线程调度和线程优先级

在应用程序的编程中,经常会听到线程优先级的概念。线程优先级和IRQL是两个极易混淆的概念。所有应用程序都运行在PASSIVE_LEVEL级别上,它的优先级最低,可以被其他IRQL级别的程序打断。线程优先级只针对应用程序而言,只有运行在PASSIVE_LEVEL级别才有意义。

5.IRQL的变化

线程运行在PASSIVIE_LEVEL级别,这个时候操作系统随时可能将当前线程切换至别的线程。但是如果提升IRQL到DISPATCH_LEVEL级别,这个时候不会出现线程的切换,这是一种很常用的同步处理机制,但这种方法只能使用在单核CPU的系统。

6.IRQL和内存分页

在使用分页内存时,可能会导致页故障。因为分页内存随时可能从物理内存交换到磁盘文件。读取不在物理内存中的分页内存时,会引发一个页故障,从而执行这个异常的处理函数。异常处理函数会将磁盘文件的内容交换到物理内存中。

页故障允许出现在PASSIVE_LEVEL级别的应用程序中,但是如果在DISPATCH_LEVEL或者更高级别的IRQL的程序中则会带来系统崩溃,故DISPATCH_LEVEL及以上级别只能用非分页内存。

7.控制IRQL提升与降低

首先驱动程序需要知道当前状态是什么IRQL级别,可以通过KeGetCurrentIrql内核函数获取当前IRQL级别。

然后驱动程序使用内核函数KeRaiseIrql将IRQL提高。还可以通过内核函数KeLowerIrql恢复到以前的IRQL级别。

示例代码:

[cpp]
view plaincopy

VOID RasieIRQL_Test()  
{  
    KIRQL oldIrql;  
    //确保当前IRQL等于或者小于DISPATCH_LEVEL  
    ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);  
    //提升IRQL至DISPATCH_LEVEL,并将先前的IRQL保存  
    KeRaiseIrql(DISPATCH_LEVEL,oldIrql);  
    //...............  
    //恢复到先前的IRQL  
    KeLowerIrql(oldIrql);  
}  

自旋锁
自旋锁也是一种同步处理机制,他能保证某个资源只能被一个线程所有。自旋锁只存在于内核模式中,使用前不需要初始化操作,并且只能在低于或等于DISPATCH_LEVEL的IRQL级别使用,在DISPATCH_LEVEL上使用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel内核函数。

1.原理

在Windows内核中,有一种称为“自旋锁”(Spin Lock)的锁,它可以用于驱动程序中的同步处理。初始化自旋锁时,处于解锁状态,这是他可以被程序“获取”,“获取”后的自旋锁处于锁着状态,不能被再次“获取”。锁住的自旋锁必须被“释放”以后,才能被再次“获取”。

如果自旋锁已经被锁住,这时程序申请“获取”这个自旋锁,程序则处于“自旋”状态。所谓自旋状态,就是不停地询问是否可以“获取”自旋锁。

2.使用方法

自旋锁的作用一般是为使各个派遣函数之间的同步。尽量不要将自旋锁放在全局变量中,而应该将自旋锁放在设备扩展里。自旋锁用KSPIN_LOCK数据结构表示。

[cpp]
view plaincopy

typedef struct _DEVICE_EXTENSION  
{  
    .......  
    KSPIN_LOCK My_SpinLock;//在设备扩展中定义自旋锁  
  
}DEVICE_EXTENSION,*PDEVICE_EXTENSION;  

使用自旋锁前,需要先对其进行初始化,可以使用KeInitialSpinLock内核函数。一般在驱动程序的DriverEntry或者AddDevice函数中初始化自旋锁。
申请自旋锁可以使用内核函数KeAcquireSpinLock,它有两个参数,第一个参数为自旋锁指针,第二个参数为记录获得自旋锁以前的IRQL级别。

[cpp]
view plaincopy

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;  
KIRQL oldIrql;  
keAcquireSpinLock(&pdx->My_SpinLock,&oldIrql);  

释放自旋锁使用KeReleaseSpinLock内核函数,它也有两个参数,第一个参数为自旋锁指针,第二个是释放自旋锁后应该恢复的IRQL级别。

[plain]
view plaincopy

KeReleaseSpinLock(&pdx->SpinLock,oldIrql);  

如果在DISPATCH_LEVEL级别申请自旋锁,不会改变IRQL级别。这时可以简单的使用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel内核函数。

用户模式下的同步对象

在内核模式下可以使用很多种内核同步对象,这些内核同步对象和用户模式下的内核同步对象非常类似。同步对象包括事件(Event),互斥体(Mutex),信号灯(Semaphore)等。

1.用户模式的等待

在应用程序中,可以使用WaitForSingleObject和WaitForMultipleObjects等待同步对象。其中WaitForSingleObject用于等待一个同步对象,而WaitForMultipleObjects用于等待多个同步对象。WaitForSingleObject函数声明如下:

[plain]
view plaincopy

DWORD WaitForSingleObject(   
                HANDLE hHandle,       //同步对象句柄  
                DWORD dwMilliseconds  //等待时间  
        );  

第二个参数dwMillseconds是等待时间(毫秒ms)。同步对象有两种状态,一种是激发状态,一种是未激发状态。

WaitForMultipleObjects函数声明如下:

[cpp]
view plaincopy

WaitForMultipleObjects(  
            DWORD nCount,                  //同步对象数组元素个数  
            CONST HANDLE *lpHandles,       //同步对象数组指针  
            BOOL bWaitAll,                 //是否等待全部同步对象  
            DWORD dwMilliseconds           //等待时间  
    );  

2.用户模式开启多线程

Win32 API CreateThread函数负责创建新线程

[cpp]
view plaincopy

HANDLE CreateThread(   
         LPSECURITY_ATTRIBUTES lpThreadAttributes,  //安全属性  
         DWORD dwStackSize,                         //初始化堆栈大小  
         LPTHREAD_START_ROUTINE lpStartAddress,     //线程运行函数指针  
         LPVOID lpParameter,                        //传入函数中的参数  
         DWORD dwCreationFlags,                     //开启线程时的状态  
         LPDWORD lpThreadId                         //返回线程ID  
    );  

创建多线程的时候最好不要用CreateThread函数,而使用_beginthreadex函数,它是对CreateThread函数的封装,其参数与CreateThread完全一致。_beginthreadex函数的函数名前面有个下划线,因为它不是标准C语言提供的运行时函数。

3.用户模式的事件

事件是一种典型的同步对象。在使用之前,需要对事件进行初始化,使用CreateEvent API函数。

[cpp]
view plaincopy

HANDLE CreateEvent(   
          LPSECURITY_ATTRIBUTES lpEventAttributes,  //安全属性  
          BOOL bManualReset,                        //是否设置为手动  
          BOOL bInitialState,                       //i初始化状态  
          LPCSTR lpName                             //命名  
    );  

所有形如CreateXXX的Win32 API函数,如果他的第一个参数是LPSECURITY_ATTRIBUTES类型,那么这种API内部都会创建一个相应的内核对象,这种API返回一个句柄,操作系统可以通过这个句柄找到具体的内核对象。下面例子综合演示上面所述内容:

[cpp]
view plaincopy

#include <windows.h>  
#include <stdio.h>  
#include <process.h>  
#include <stddef.h>  
#include <stdlib.h>  
#include <conio.h>  
  
UINT WINAPI Thread1(LPVOID para)  
{  
    printf("Enter Thread1\n");  
    HANDLE *phEvent = (HANDLE*)para;  
    //设置该事件激发  
    SetEvent(*phEvent);  
    printf("Leave Thread1\n");  
    return 0;  
}  
  
int main()  
{  
    //创建同步事件  
    HANDLE hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);  
    //开启新线程,并将同步事件的句柄传递给新线程  
    HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,Thread1,&hEvent,0,NULL);  
    //等待该事件激发  
    WaitForSingleObject(hEvent,INFINITE);  
    return 0;  
}  

4.用户模式的信号灯
信号灯也是一种常用的同步对象,信号灯也有两种状态,一种是激发态,另一种是未激发态。信号灯内部有个计数器,可以理解为信号灯内部有N个灯泡,如果有一个灯泡亮着,就代表信号灯处于激发状态,如果完全熄灭,则代表信号灯处于未激发状态。使用信号灯前需要先创建信号灯,CreateSemphore函数负责创建信号灯,声明如下:

[cpp]
view plaincopy

HANDLE CreateSemaphore(  
            LPSECURITY_ATTRIBUTES lpSemaphoreAttributes, //安全属性  
            LONG lInitialCount,                          //初始化计数个数  
            LONG lMaximumCount,                          //计数器最大个数  
            LPCSTR lpName                                //命名  
    );  

另外可以使用ReleaseSemphore函数增加信号灯,其函数声明如下:

[cpp]
view plaincopy

BOOL ReleaseSemaphore(  
        HANDLE hSemaphore,        //信号灯句柄  
        LONG lReleaseCount,       //本次操作增加的计数  
        LPLONG lpPreviousCount    //记录以前的计数  
  );  

对信号灯执行一次等待操作,就会减少一个计数,相当于灭一盏灯泡。当计数器为0时,也就是所有灯泡都熄灭时,当前线程就会进入休眠状态,直到信号灯变为激发状态。
综合代码如下:

[cpp]
view plaincopy

#include <windows.h>  
#include <process.h>  
#include <stdio.h>  
  
UINT WINAPI Thread1(LPVOID para)  
{  
    printf("Enter Thread\n");  
    HANDLE* phSemaphore = (HANDLE*)para;  
    //等待5s  
    Sleep(5000);  
    printf("Leave Thread1\n");  
    //将信号灯计数器加1,使之处于激活状态  
    ReleaseSemaphore(*phSemaphore,1,NULL);  
    return 0;  
}  
  
int main()  
{  
    //创建信号灯  
    HANDLE hSemaphore = CreateSemaphore(NULL,2,2,NULL);  
    //此时的信号灯计数器为2,处于激发状态  
    WaitForSingleObject(hSemaphore,INFINITE);  
    //此时的信号灯计数器为1,处于激发状态  
    WaitForSingleObject(hSemaphore,INFINITE);  
    //此时的信号灯计数器为0,处于未激发状态  
    //开启新线程,并将同步事件句柄指针传递给新线程  
    HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,Thread1,&hSemaphore,0,NULL);  
    //等待事件激发  
    WaitForSingleObject(hSemaphore,INFINITE);  
    return 0;  
}  

5.用户模式的互斥体
互斥体也是一种常用的同步对象,互斥体可以避免多个线程争夺同一资源。与事件不同,得到互斥体的线程可以递归调用该互斥体。

互斥体也有两种状态,激发态和未激发态。如果线程获得互斥体时,此时状态是未激发状态,当释放互斥体时,互斥体状态是激发状态。

初始化互斥体的函数是CreateMutex,其声明函数如下:

[cpp]
view plaincopy

HANDLE CreateMutex(   
        LPSECURITY_ATTRIBUTES lpMutexAttributes, //安全属性  
        BOOL bInitialOwner,                      //是否被占有  
        LPCSTR lpName                            //命名  
    );  

WaiForSingleObject获得互斥体,ReleaseMutex释放互斥体。

下面代码综合互斥体的使用:

[cpp]
view plaincopy

#include <windows.h>  
#include <process.h>  
#include <stdio.h>  
  
UINT WINAPI Thread1(LPVOID para)  
{  
    HANDLE* phMutex = (HANDLE*)para;  
    //得到互斥体  
    WaitForSingleObject(*phMutex,INFINITE);  
    //对于同一个互斥体,可以多次获得  
    WaitForSingleObject(*phMutex,INFINITE);  
    printf("Enter Thread1\n");  
    //强迫等待2s  
    Sleep(2000);  
    printf("Leave Thread1\n");  
    //释放互斥体  
    ReleaseMutex(*phMutex);  
    return 0;  
}  
  
UINT WINAPI Thread2(LPVOID para)  
{  
    HANDLE* phMutex = (HANDLE*)para;  
    //得到互斥体  
    WaitForSingleObject(*phMutex,INFINITE);  
    printf("Enter Thread2\n");  
    //强迫等待2s  
    Sleep(2000);  
    printf("Leave Thread2\n");  
    //释放互斥体  
    ReleaseMutex(*phMutex);  
    return 0;  
}  
  
int main()  
{  
    //创建同步事件  
    HANDLE hMutex = CreateMutex(NULL,FALSE,NULL);  
    //开启新的线程,并将同步事件句柄指针传递给新线程  
    HANDLE hThread1 = (HANDLE)_beginthreadex(NULL,0,Thread1,&hMutex,0,NULL);  
    HANDLE hThread2 = (HANDLE)_beginthreadex(NULL,0,Thread2,&hMutex,0,NULL);  
    //强迫等待6秒,让两个线程运行完毕  
    Sleep(6000);  
    return 0;  
}  

6.等待线程完成

还有一种同步对象,这就是线程对象。每个线程同样有激发态和未激发态两个状态。

下面面例子综合演示线程对象的同步:

[cpp]
view plaincopy

#include <windows.h>  
#include <process.h>  
#include <stdio.h>  
  
UINT WINAPI Thread1(LPVOID para)  
{  
    printf("Enter Thread\n");  
    //等待5s  
    Sleep(5000);  
    return 0;  
}  
  
int main()  
{  
    HANDLE hThread[2];  
    //开启两个线程  
    hThread[0] = (HANDLE)_beginthreadex(NULL,0,Thread1,NULL,0,NULL);  
    hThread[1] = (HANDLE)_beginthreadex(NULL,0,Thread1,NULL,0,NULL);  
    //主线程等待两个线程结束  
    WaitForMultipleObject(2,Thread1,TRUE,INFINITE);  
}  

 

内核模式下的同步对象

在内核模式下,有一系列的同步对象和用户模式下的同步对象相对应。在用户模式下,各个函数都是以句柄操作同步对象,而无法获得真实同步对象的指针。在内核模式下,程序员可以获得真实同步对象的指针。

 

1.内核模式下的等待

在内核模式下,同样也有两个函数负责等待内核同步对象,分别是KeWaitForSingleObject和KeWaitForMultipleObject函数。

KeWaitForSingleObject函数负责等待单个同步对象,其声明如下:

[cpp]
view plaincopy

NTSTATUS  
  KeWaitForSingleObject(  
                     IN PVOID Object,  
           IN KWAIT_REASON WaitReason,   
           IN KPROCESSOR_MODE WaitMode,   
           IN BOOLEAN Alertable,   
           IN PLARGE_INTEGER Timeout OPTIONAL  
        );  

第一个参数Object是一个同步对象的指针,注意这里不是句柄。

第二个参数WaitReason表示等待的原因,一般设置为Executive

第三个参数WaitMode是等待模式,说明这个函数是在用户模式下还是在内核模式下等待。一般设置为KernelMode。

第四个参数Alertable指明等待是否“警惕”,一般设置为FALSE

最后一个参数是等待时间,如果设置为NULL,表示无限期等待。

 

KeWaitForMultipleObjects负责在内核模式下等待多个同步对象,其声明如下:

[cpp]
view plaincopy

NTSTATUS  
  KeWaitForMultipleObjects(  
                IN ULONG Count,  
                IN PVOID Object[],  
                IN WAIT_TYPE WaitType,  
                IN KWAIT_REASON WaitReason,  
                IN KPROCESSOR_MODE WaitMode,   
                IN BOOLEAN Alertable,  
                IN PLARGE_INTEGER Timeout OPTIONAL,  
                IN PKWAIT_BLOCK WaitBlockArray OPTIONAL  
            );  

第一个参数Count表示等待同步对象的个数

第二个参数Object是同步对象数组

第三个参数WaitType指示等待任意一个同步对象还是等待所有同步对象

剩下的参数和KeWaitForSingle函数参数功能基本一致

 

 

2.内核模式下开启多线程

在内核模式下,PsCreateSystemThread负责创建新线程。该函数可以创建两种线程,一种是用户线程,一种是系统线程。用户线程属于当前进程中的线程。当前进程是指当前I/O操作的发起者。如果在IRP_MJ_READ的派遣函数中调用PsCreateSystemThread函数创建用户线程,新线程就属于ReadFile的进程。

系统线程不属于当前用户进程,而是属于系统进程,系统进程是操作系统中一个特殊的进程,进程ID一般为4。

驱动程序中的DriverEntry和AddDevice等函数都是被某个系统线程调用的。

PsCreateSystemThread函数声明如下:

[cpp]
view plaincopy

NTSTATUS  
  PsCreateSystemThread(  
                     OUT PHANDLE ThreadHandle,  
           IN ULONG DesiredAccess,   
           IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,  
           IN HANDLE ProcessHandle OPTIONAL,  
           OUT PCLIENT_ID ClientId OPTIONAL,  
           IN PKSTART_ROUTINE StartRoutine,  
           IN PVOID StartContext  
        );  

第一个参数ThreadHandle用于输出,这个参数得到创建的线程句柄

第二个参数DesireAccess是创建的权限

第三个参数ObjectAttributes是该线程的属性,一般设置为NULL

第四个参数ProcessHandle指定创建的是用户线程还是系统线程。如果为NULL,则是系统线程。如果改值是一个进程句柄,则创建的线程属于这个指定的进程。DDK提供的宏NtGetCurrentProcess函数可以得到当前进程句柄。

第六个参数StartRoutine为新线程的运行地址

第七个蚕食StartContext为新线程接受的参数

在内核模式下,创建线程必须使用函数PsTerminateSystemThread强制结束线程。否则线程无法自动退出。

下面代码演示如何在驱动程序中创建线程:

[cpp]
view plaincopy

VOID SystemThread(IN PVOID pContext)  
{  
    KdPrint(("Enter SystemThread\n"));  
    PEPROCESS pEProcess = GetCurrentProcess();  
    PTSTR ProcessName = (PTSTR)((ULONG)pEProcess+0x174);  
    KdPrint(("This thread run in %s process\n",ProcessName));  
    KdPrint(("Leave SystemThread\n"));  
    //结束线程  
    PsTerminateSystemThread(STATUS_SUCCESS);  
}  
  
VOID MyProcessThread(IN PVOID pContext)  
{  
    KdPrint(("Enter MyProcessThread\n"));  
    //得到当前进程  
    PEPROCESS pEProcess = IoGetCurrentProcess();  
    PTSTR ProcessName = (PTSTR)((ULONG)pEProcess+0x174);  
    KdPrint(("This thread run in %s process!\n",ProcessName));  
  
    KdPrint(("Leave MyProcessThread\n"));  
    //结束线程  
    PsTerminateSystemThread(STATUS_SUCCESS);  
}  
  
VOID CreateThread_Test()  
{  
    HANDLE hSystemThread , hMyThread;  
    //创建系统线程,该线程是System进程的线程  
    NTSTATUS status = PsCreateSystemThread(&SystemThread,0,NULL,NULL,NULL,SystemThread,NULL);  
    //创建进程线程,该线程是用户进程的线程  
    status = PsCreateSystemThread(&hMyThread,0,NULL,NtCurrentProcess(),NULL,MyProcessThread,NULL);  
}  

3.内核模式下的事件对象

在应用程序中,程序员只能操作事件句柄,无法得到事件对象的指针。

在内核中,用KEVENT数据结构表示一个事件。在使用前需要初始化,内核函数KeInitializeEvent负责对事件对象初始化,其声明如下:

[cpp]
view plaincopy

VOID  
 KeInitializeEvent(  
                  IN PRKEVENT Event,   
        IN EVENT_TYPE Type,   
        IN BOOLEAN State  
    );  

第一个参数Event:这个参数是初始化事件对象指针

第二个参数Type:这个参数是事件的类型。事件类型分为两类:一类是“通知事件”,对应参数是NotificationEvent,另一类是“同步事件”,相应的参数是SynchronizationEvent.

第三个参数State:这个参数如果为真,事件对象的初始化状态为激发状态,如果该参数为假,则事件对象的初始化状态为未激发。

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

下面代码演示如何在驱动程序中使用对象事件:

[cpp]
view plaincopy

VOID MyProcessThread(IN PVOID pContext)  
{  
    //获得时间指针  
    PKEVENT pEvent = (PKEVENT)pContext;  
    KdPrint(("Enter MyProcessThread\n"));  
    //设置事件  
    KeSetEvent(pEvent,IO_NO_INCREMENT,FALSE);  
    KdPrint(("Leave MyProcessThread\n"));  
    //结束线程  
    PsTerminateSystemThread(STATUS_SUCCESS);  
}  
  
#pragma PAGEDCODE  
VOID Test()  
{  
    HANDLE hMyThread;  
    KEVENT kEvent;  
    //初始化内核事件  
    KeInitializeEvent(&kEvent,NotificationEvent,FALSE);  
    //创建系统线程,该线程是System进程的线程  
    NTSTATUS status = PsCreateSystemThread(&hMyThread,0,NULL,  
        NtCurrentProcess(),NULL,MyProcessThread,&kEvent);  
    KeWaitForSingleObject(&kEvent,Executive,KernelMode,FALSE,NULL);  
}  

 

4.驱动程序与应用程序交互事件对象

应用程序中创建的事件和内核模式中创建的事件对象,本质上是同一个东西。在用户模式时,它用句柄代表,在内核模式时,它用KEVENT代表。

需要解决的第一个问题是如何将用户模式下创建的事件传递给驱动程序。解决办法是采用DeviceIOControl API函数。在用户模式下创建一个同步事件,然后用DeviceIOControl把事件句柄传递给驱动程序。DDK提供了内核函数将句柄转化为指针,该函数是ObReferenceObjectByHandle。

ObReferenceObjectByHandle函数在得到指针的同时,会为对象的指针维护一个计数。每次调用ObRefenrenceObjectByHandle会使计数加1。因此为了使计数平衡,在使用Ob后需要调用ObDereferenceObject函数,使得计数减一。

下面例子演示如何在应用程序和驱动程序中交互事件对象。

用户模式代码:

[cpp]
view plaincopy

int main()  
{  
    //打开设备  
    HANDLE hDevice =   
        CreateFile("\\\\.\\HelloDDK",  
                    GENERIC_READ | GENERIC_WRITE,  
                    0,  
                    NULL,  
                    OPEN_EXISTING,  
                    FILE_ATTRIBUTE_NORMAL,  
                    NULL);  
    //判断设备是否成功打开  
    if (hDevice == INVALID_HANDLE_VALUE)  
    {  
        printf("Failed to obtain file handle to device: "  
            "&s with Win32 error code: %d\n",  
            "MyWDMDevice",GetLastError());  
        return 1;  
    }  
    BOOL bRet;  
    DWORD dwOutPut;  
    //创建用户模式同步事件  
    HANDLE hEvent = CreateEvent(NULL,FALSE,FALSE,NULL);  
    //建立辅助线程  
    HANDLE hThread = (HANDLE)_beginthreadex(NULL,0,Thread1,&hEvent,0,NULL);  
    //将用户模式的事件句柄传递给驱动  
    bRet = DeviceIoControl(hDevice,IOCTL_TRANSMIT_EVENT,&hEvent,  
        sizeof(hEvent),NULL,0,&dwOutPut,NULL);  
    //等待线程结束  
    WaitForSingleObject(hThread1,INFINITE);  
    //关闭各个句柄  
    CloseHandle(hDevice);  
    CloseHandle(hThread);  
    CloseHandle(hEvent);  
    return 0;  
}  

下面是内核模式的代码:

[cpp]
view plaincopy

NTSTATUS HelloDDKDeviceIOControl(IN PDEVICE_OBJECT pDevObj, IN PIRP pIrp)  
{  
    NTSTATUS status = STATUS_SUCCESS;  
    KdPrint(("Enter HelloDDKDeviceIOControl\n"));  
    //获得当前IO堆栈  
    PIO_STACK_LOCATION stack = IoGetCurrentIrpStackLocation(pIrp);  
    //获得输入参数大小  
    ULONG cbin = stack->Parameters.DeviceIoControl.InputBufferLength;  
    //获得输出参数大小  
    ULONG cbout = stack->Parameters.DeviceIoControl.OutputBufferLength;  
    //得到IOCTL嘛  
    ULONG code = stack->Parameters.DeviceIoControl.IoControlCode;  
    ULONG info = 0;  
    switch(code)  
    {  
    case IOCTL_TRANSMIT_EVENT:  
        {  
            KdPrint(("IOCTL_TEST1\n"));  
            //得到应用程序传递进来的事件  
            HANDLE hUserEvent = *(HANDLE)pIrp->AssociatedIrp.SystemBuffer;  
            PKEVENT pEvent;  
            //由事件句柄得到内核事件数据结构  
            status = ObReferenceObjectByHandle(hUserEvent,EVENT_MODIFY_STATE,*ExEventObjectType,  
                KernelMode,(PVOID*)&pEvent,NULL);  
            //设置事件  
            KeSetEvent(pEvent,IO_NO_INCREMENT,FALSE);  
            //减小引用计数  
            ObReferenceObject(pEvent);  
            break;  
        }  
    default:  
        status = STATUS_INVALID_VARIANT;  
    }  
    //设置IRP完成状态  
    pIrp->IoStatus.Status = status;  
    //设置IRP操作字节  
    pIrp->IoStatus.Information = info;  
    //结束IRP请求  
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);  
    KdPrint(("Leave HelloDDKDeviceControl\n"));  
    return status;  
}  

 

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

要让驱动程序A获取驱动程序B中创建的事件对象,最简单的方法是驱动程序B创建一个有“名字”的事件对象,这样驱动程序A就可以根据“名字”寻找到事件对象的指针。

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

 

6.内核模式下的信号灯

和事件对象一样,信号灯在用户模式和内核模式下是完全统一的,只不过操作方式不同。在用户模式下,信号灯通过句柄代表,在内核模式下,信号灯对象用KSEMAPHORE数据结构表示。

在使用信号灯前需要对其进行初始化,其函数声明如下:

[cpp]
view plaincopy

VOID   
  KeInitializeSemaphore(  
                   IN PRKSEMAPHORE Semaphore,   
         IN LONG Count,   
         IN LONG Limit  
    );  

第一个参数Semaphore:这个参数获得内核信号灯对象指针

第二个参数Count:这个参数是初始化时的信号灯计数

第三个Limit:这个参数指明信号灯计数的上限值

KeReadStateSemaphore函数可以读取信号灯当前的计数

下面代码演示如何在驱动程序中使用信号灯对象:

[cpp]
view plaincopy

VOID MyProcessThread(IN PVOID pContext)  
{  
    //得到信号灯  
    PKSEMAPHORE pkSemaphore = (PKSEMAPHORE)pContext;  
    KdPrint(("Enter MyProcessThread\n"));  
    KeReleaseSemaphore(pkSemaphore,IO_NO_INCREMENT,1,FALSE);  
    KdPrint(("Leave MyProcessThread\n"));  
    //结束线程  
    PsTerminateSystemThread(STATUS_SUCCESS);  
}  
  
#pragma PAGEDCODE  
VOID Test()  
{  
    HANDLE hMyThread;  
    KSEMAPHORE kSemaphore;  
    //初始化内核信号灯  
    KeInitializeSemaphore(&kSemaphore,2,2);  
    //读取信号灯状态  
    LONG count = KeReadStateSemaphore(&kSemaphore);  
    KdPrint(("The Semaphore count is %d\n",count));  
    //等待信号灯  
    KeWaitForSingleObject(&kSemaphore,Executive,KernelMode,FALSE,NULL);  
    //读取信号灯的状态  
    count = KeReadStateSemaphore(&kSemaphore);  
    KdPrint(("The Semaphore count is %d\n",count));  
    KeWaitForSingleObject(&kSemaphore,Executive,KernelMode,FALSE,NULL);  
    //读取信号灯的状态  
    count = KeReadStateSemaphore(&kSemaphore);  
    KdPrint(("The Semaphore count is %d\n",count));  
    //创建系统线程,该线程是System进程的线程  
    NTSTATUS status = PsCreateSystemThread(*hMyThread,0,NULL,NtCurrentProcess(),  
        NULL,MyProcessThread,&kSemaphore);  
  
    WaitForSingleObject(&kSemaphore,Executive,KernelMode,FALSE,NULL);  
    KdPrint(("After KeWaitForSingleObject\n"));  
}  

 

7.内核模式下的互斥体

互斥体在内核中的数据结构是KMUTEX,使用前需要初始化互斥体对象。可以使用KeInitializeMutex内核函数初始化互斥体对象,其声明如下:

[cpp]
view plaincopy

VOID   
   KeInitializeMutex(   
            IN PRKMUTEX Mutex,   
       IN ULONG Level  
    );  

第一个参数Mutex:这个参数可以获得内核互斥体对象的指针

第二个参数Level:保留至,一般设置为0

下面例子演示如何在驱动程序中使用互斥体对象:

[cpp]
view plaincopy

VOID MyProcessThread1(IN PVOID pContext)  
{  
    PKMUTEX pkMutex = (PKMUTEX)pContext;  
    //获得互斥体  
    KeWaitForSingleObject(pkMutex,Executive,KernelMode,FALSE,NULL);  
    KdPrint(("Enter MyProcessThread1\n"));  
    //强迫停止50ms,模拟执行一段代码,模拟运行某段费时  
    KeStallExecutionProcessor(50);  
    KdPrint(("Leave MyProcessThread1\n"));  
    //释放互斥体  
    KeReleaseMutex(pkMutex,FALSE);  
    //结束线程  
    PsTerminateSystemThread(STATUS_SUCCESS);  
}  
  
VOID MyProcessThread2(IN PVOID pContext)  
{  
    PKMUTEX pkMutex = (PKMUTEX)pContext;  
    //获得互斥体  
    KeWaitForSingleObject(pkMutex,Executive,KernelMode,FALSE,NULL);  
    KdPrint(("Leave MyProcessThread2\n"));  
    //强迫停止50ms,模拟执行一段代码,模拟运行某段费时  
    KdPrint(("Leave MyProcessThread2\n"));  
    //释放互斥体  
    KeReleaseMutex(pkMutex,FALSE);  
    //结束线程  
    PsTerminateSystemThread(STATUS_SUCCESS);  
}  
  
#pragma PAGEDCODE  
VOID Test()  
{  
    HANDLE hMyThread1,hMyThread2;  
    KMUTEX kMutex;  
    //初始化内核互斥体  
    KeInitializeMutex(&kMutex,0);  
    //创建系统线程,该线程是进程的线程  
    PsCreateSystemThread(&hMyThread1,0,NULL,NtCurrentProcess(),  
        NULL,MyProcessThread1,&kMutex);  
    PsCreateSystemThread(&hMyThread2,0,NULL,NtCurrentProcess(),  
        NULL,MyProcessThread2,&kMutex);  
    PVOID Pointer_Array[2];  
    //得到对象指针  
    ObReferenceObjectByHandle(hMyThread1,0,NULL,KernelMode,&Pointer_Array[0],NULL);  
    ObReferenceObjectByHandle(hMyThread2,0,NULL,KernelMode,&Pointer_Array[1],NULL);  
    //等待多个对象  
    KeWaitForMultipleObjects(2,Pointer_Array,WaitAll,Executive,KernelMode,FALSE,NULL,NULL);  
    //减小计数器  
    ObDereferenceObject(Pointer_Array[0]);  
    ObDereferenceObject(Pointer_Array[1]);  
    KdPrint(("After KeWaitForMultipleObjects\n"));  


4、8 快速互斥体

 快速互斥体(Fast Mutex)是DDK提供的另外一种内核同步对象,它的特征类似前面介绍的普通互斥体对象。快速互斥体和普通互斥体作用完全一样,

之所以称为快速互斥体,是因为执行的速度比普通互斥体速度快(这里指的是获取和释放的速度)。然而,快速互斥体比普通互斥体多了一个缺点,就是

不能递归地获取互斥体对象。递归获取指的是,已经获得互斥体的线程,可以再次获得这个互斥体,换句话说,互斥体只互斥其他线程,而不能互斥自己所在

的线程。但是快速互斥体则不允许出现递归的情况。

普通互斥体在内核中使用MUTEX数据结构描述的,而快速互斥体在内核中是用FAST_MUTEX数据结构描述的。

除此之外,对快速互斥体的初始化、获取和释放对应的内核函数也和普通互斥体不同。初始化快速互斥体的内核函数是ExInitializeFastMutex,获取快速互斥体的内核函数是

ExAcquireFastMutex,释放快速互斥体的内核函数是ExReleaseFastMutex。

下面的代码演示了如何在驱动程序中使用快速互斥体:

[cpp]
view plaincopy

VOID MyProcessThread1(IN PVOID pContext)  
{  
    PFAST_MUTEX pFastMutex = (PFAST_MUTEX)pContext);  
    // 获是快速互斥体  
    ExAcquireFastMutex(pFastMutex);  
    KdPrint(("Enter MyProcesThread1\n"));  
    // 强迫停止50ms,模拟一段代码,模拟运行某段费时  
    KeStallExecutionProcessor(50);  
    KdPrint(("Leave MyProcessThread1\n"));  
    // 释放互斥体  
    KeReleaseFastMutex(pFastMutex);  
    // 结束线程  
    PsTerminateSystemThrad(STATUS_SUCCESS);  
}  
  
  
VOID MyProcessThread2(IN PVOID pContext)  
{  
    PFAST_MUTEX pFastMutex = (PFAST_MUTEX)pContext);  
    // 获是快速互斥体  
    ExAcquireFastMutex(pFastMutex);  
    KdPrint(("Enter MyProcesThread2\n"));  
    // 强迫停止50ms,模拟一段代码,模拟运行某段费时  
    KeStallExecutionProcessor(50);  
    KdPrint(("Leave MyProcessThread2\n"));  
    // 释放互斥体  
    KeReleaseFastMutex(pFastMutex, FALSE);  
    // 结束线程  
    PsTerminateSystemThrad(STATUS_SUCCESS);  
}  
  
#pragma PAGEDCODE  
VOID Test()  
{  
    HANDLE hMyThread1, hMyThread2;  
    FAST_MUTEX fastMutex;  
    // 初始化内核互斥体  
    KeInitializeFastMutext(&fastMutex, 0);  
    // 创建系统线程,该线程是System进程的线程  
    PsCreateSystemThread(&hMyThread1, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread1, &kMutex);  
    PsCreateSystemThread(&hMyThread1, 0, NULL, NtCurrentProcess(), NULL, MyProcessThread2, &kMutex);  
    PVOID Pointer_Array[2];  
    // 得到对象指针  
    ObReferenceObjectByHandle(hMyThread1, 0, NULL, KernelMode, &Pointer_Array[0], NULL);  
    ObReferenceObjectByHandle(hMyThread2, 0, NULL, KernelMode, &Pointer_Array[1], NULL);  
    // 等待多个事件  
    KeWaitForMultipleObjects(2, Pointer_Array, WaitAll, Executive, KernelMode, FALSE, NULL, NULL);  
    // 减小引用计数  
    ObDereferenceObject(Pointer_Array[0]);  
    ObDereferenceObject(Pointer_Array[0]);  
    KdPrint(("After KeWaitForMultipleObjects\n"));  
}  



4、9  使用自旋锁进行同步

在驱动程序中,经常使用自旋锁作为一种有效的同步机制。例如,在应用程序打开一个设备后,有时需要开户多个线程去操作设备(例如,都调用ReadFile函数对设备进行

读取操作)。这时,IRP_MJ_READ的派遣函数也会并发执行。但是大部分设备没有能力响应并发的读请求,必须完成一个读请求后再完成一个读请求。这时需要进行同步处理,程序员可以选择采用前面介绍的事件、信号灯、互斥体等内核同步对象,但还有另外一种选择,也就是自旋锁。

对于要同步的代码,需要用同一把自旋锁进行同步。如果程序得到了自旋锁,其他程序希望获取自旋锁时,则不停地进入自旋状态。获得自旋锁的内核函数是KeAcquireSpinLock。直到自旋锁被释放后,另外的程序才能获取自旋锁,释放自旋锁的内核函数是KeReleaseSpinLock。

如果希望同步某段代码区域,需要在这段代码区域前获取自旋锁,在代码区域后释放自旋锁。在单CPU的系统中,获取自旋锁是通过提升IRQL实现的,而在多CPU系统中,

实现方法比较复杂,有兴趣的可以自己研究。

无法获得自旋锁的线程会不停地自旋,这会浪费很多CPU时间,因此需要同步的代码不能过长,换句话说就是占有自旋锁时间不能过长。

下面的代码模拟了应用程序创建一个设备后,同时开户多个线程对设备进行请求情况,这个例子采用的同步机制是使用自旋锁:

[cpp]
view plaincopy

#include <windows.h>  
#include <process.h>  
#include <stdio.h>  
#include <winioctl.h>  
#include "..\NT_Driver\Ioctls.h"  
  
UITN WINAPI Thread1(LPVOID pCOntext)  
{  
    BOOL bRet;  
    DWORD dwOutpt;  
    // 发送IOCTL码  
    bRet = DeviceIoControl(*(PHANDLE)pContext, IOCTL_TEST1, NULL, 0, NULL, 0, &dwOutput, NULL);  
    return 0;  
}  
  
  
int main()  
{  
    // 打开设备  
    HANDLE hDevice = Create("\\\\.\\HelloDDK",   
        GENERIC_READ|GENERIC_WRITE,   
        0,   
        NULL,   
        OPEN_EXISTING,   
        FILE_ATTRIBUTE_NORMAL,   
        NULL);  
    // 判断是否成功打开设备句柄  
    if(hDevice == INVALID_HANDLE_VALUE)  
    {  
        printf("Failed to obtain file handle to device\n");  
        return 1;  
    }  
    HANDLE hThread[2];  
    // 开启两个新线程,每个线程执行DeviceIoControl  
    // 因此在IRP_MJ_DEVICE_CONTROL的派遣函数会并行进行  
    // 为了让派遣函数不并行运行,而是串行运行,必须进行同步处理!  
    // 本例在派遣函数中采用自旋锁进行同步处理  
    hThread[0] = (HANDLE)_beginthradex(NULL, 0, Thread1, &hDevice, 0, NULL);  
    hTrhead[1] = (HANDLE)_beginthreaex(NULL, 0, Thread2, &hDevice, 0, NULL);  
    // 等待两个进程全部运行完毕  
    WaitForMultipleObjects(2, hThread, TRUE, INFINITE);  
    // 关闭句柄  
    CloseHandle(hThread[0]);  
    CloseHandle(hThread[1]);  
    CloseHandle(hDevice);  
    return 0;  
}  

驱动程序的派遣函数需要进行同步处理,下面是示例代码:

[cpp]
view plaincopy

NTSTATUS HelloDDKDeviceIOControl(IN PDEVICE_OBJECT pDevObj,   
                                 IN PIRP pIrp)  
{  
    // 为了避免多个派遣函数并行运行,所以进行同步处理  
    // 此处采用自旋处理同步  
    // DeviceIoControl调用,来源自用户线程,因此处于PASSIVCE_LEVEL  
    ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);  
  
    PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;  
    KIRQL oldirql;  
    KeAccequireSpinLock(&pdx->My_SpinLock, &oldirql); // 获是自旋锁  
    // A点===========================================================  
    // 从A点到B点为同步区域,不会被其他派遣函数  
    NTSTATUS status = STATUS_SUCCESS;  
    KdPrint(("Enter HeloDDKDeviceIOControl\n"));  
    // 使用自旋锁后,IRQL提升到DISPATCH_LEVEL  
    ASSERT(KeGetCurrentIrql() == DISPATCH_LEVEL);  
    // 设置IRP完成状态  
    pIrp->IoStatus.Status = status;  
    // 设置IRP操作字节数  
    pIrp->IoStatus.Information = 0;  
    // 结束IRP请求  
    IoCompleteRequest(pIrp, IO_NO_INCREMENT);  
    KdPrint(("Leave HelloDDKDeviceIOControl\n"));  
    // B点=========================================================  
    KeReleaseSpinLock(&pdx->My_SpinLock, oldirql); //   
    return status;  
}  

 

4、10  使用互锁操作进行同步

C语言中变量自增的语句,会被编译成一段汇编指令。例如,下面的代码在多线程环境中,就存在“条件竞争”问题,语句number++不是执行的最小单位,最小的执行单位

是汇编指令。每条汇编都有可能被打断。出现这个问题的原因是语句number++不是最小的执行单位。

[cpp]
view plaincopy

int number = 0;  
void Foo()  
{  
    number++;  
    // 做一些事件....  
    number--;  
}  

为了让number++称为最小的执行单位,保证运行的原子性,可以采用很多种办法,例如,可以使用自旋锁,下面的代码是更改后的代码:

[cpp]
view plaincopy

int number = 0;  
void Foo()  
{  
    // 获取自旋锁  
    KeAcquireSpinLock(..);  
    number++;  
    // 释放自旋锁  
    KeReleaseSpinLock(..);  
  
    // 做一些事件....  
    // 获取自旋锁  
    KeAcquireSpinLock(..);  
    number--;  
    // 释放自旋锁  
    KeReleaseSpinLock(..);  
}  

 DDK提供了两类互锁操作来提供简单的同步处理,一类是InterLockedXX函数,另一类是ExInterLockedXX函数。

其中,InterLockedXX系列函数不需要程序员提供自旋锁,内部不会提升IRQL,因此InterLockedXX函数可以操作非分页的数据,也可以操作分页的数据。

而ExInterLockedXX需要程序员提供一个自旋锁,内部依靠这个自旋锁实现同步,所有ExInterLockedXX不能操作非分页内存的数据。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息