Windows驱动程序设计详解(八)--- 驱动程序的同步处理

基本概念:

   在支持多线程的操作系统下,有些函数会出现不可重入的现象。所谓“可重入”是指函数的执行结果不和执行顺序有关。反之,如果执行结果和执行顺序有关,则称这个函数是“不可重入”的。不可重入的根本原因是由于各个线程之间的切换导致的。

中断请求级别(IRQL)

                                                                                Windows驱动程序设计详解(八)--- 驱动程序的同步处理_第1张图片

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

   Windows负责线程调度的组件是运行在DISPATCH_LEVEL级别,当前线程完成时间片后,系统负责自动从PASSIVE_LEVEL级别提升到DISPATCH_LEVEL级别,当线程切换完毕后,操作系统又从DISPATCH_LEVEL级别降到PASSIVE_LEVEL级别。

   页面故障允许出现在PASSIVE_LEVEL级别的程序中,但是如果在DISPATCH_LEVEL或者更高级别的程序中会带来系统崩溃。

控制IRQL提升与降低

   有些时候需要提升IRQL级别。在运行一段时间后,再降回原来的IRQL级别。

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

   然后驱动程序使用内核函数将KeRaiseIrql 将IRQL提高。KeRaiseIrql需要两个参数,一个是提升后的IRQL级别,一个是提升前的级别。

VOID RaiseIRQL_Test()
{
   KIRQL oldirql;
   // 确保当前IRQL等于或者小于DISPATCH_LEVEL
   ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);
   // 提升IRQL至DISPATCH_LEVEL,并将原来的IRQL保存起来
   KeRaiseIrql(DISPATCH_LEVEL,&oldirql);
   
   //do  something

   //恢复到以前的状态
   KeLowerIrql(oldirql);
} 

自旋锁

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

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

自旋锁不同于线程中的等待事件,线程中如果某个等待事件,操作系统会进入休眠状态,CPU会运行其他线程,而自旋锁原理则不同,他不会切换到其他线程而是一直让这个线程“自旋”。

使用方法

   自旋锁的作用一般是为了使各派遣函数之间同步。尽量不要将自旋锁放在全局变量中,而应该将自旋锁房子设备扩展中。

typedef struct _DEVICE_EXTENSION(
   ......
   KSPIN_LOCK My_SpinLock; // 在设备扩展中定义自旋锁
)

使用自旋锁前先进行初始化,可以使用KeInitializeSpinLock内核函数。一般在驱动程序的DriverEntry或者AddDevice函数中初始化自旋锁。

申请自旋锁

PDEVICE_EXTENSION pdx = (PDEVICE_EXTENSION)pDevObj->DeviceExtension;
KIRQL oldirql;
KeAcquireSpinLock(&pdx->My_SpinLock,&oldirql);
释放自旋锁

KeReleaseSpinLock(&pdx->My_SpinLock,&oldirql);

当然在DISPATCH_LEVEL级别申请自旋锁,不会改变IRQL的级别。这样申请自旋锁可以简单地使用KeAcquireSpinLockAtDpcLevel内核函数,释放自旋锁使用KereleaseSpinLockFromDpcLevel内核函数。


用户模式下同步对象

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

用户模式的同步对象都是借助内核模式的同步对象实现的。用户模式的同步对象其实是内核模式下同步对象的再次封装。

用户模式的等待

   在用户模式下等待一个对象WaitForSingleObject和等待多个对象WaitForMultipleObjects.

DWORD WaitForSingleObject( HANDLE hHandle,      // 同步等待句柄
                           DWORD dwlMilliseconds  //等待时间 ms(毫秒)

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

用户模式开启多线程

等待同步对象一般出现在多线程的编程中,因此这里介绍一下应用程序如何创建新线程。

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

微软编写了一套多线程版本的运行时函数库。在使用这个版本的运行时库时,需要指定链接这些库,修改的方法如图示:

                                                               Windows驱动程序设计详解(八)--- 驱动程序的同步处理_第2张图片

另外创建多线程最好不要使用CreateThread函数,而使用_beginthreadex函数。_beginthreadex函数是对CreateThread函数的封装,其参数与CreateThread完全一致。另外许多第三方库对CreateThread函数进行封装,如MFC库的CWinThread类,这个类对线程的各个操作函数进行了封装。


用户模式的事件

事件是一种典型的同步对象。用户模式下的事件和内核模式下的事件对象紧密相连。使用前先要初始化

HANDLE CreateEvent( LPSECURITY_ATTRIBUTES lpEventAttributes, // 安全属性  一般为NULL
                    BOOL bManualReset,                       // 是否设定为手动 如果是手动模式,事件处于激发态后需要手动设置才能回到未激发状态。
                                                             // 如果设为自动,当处于激发状态后,遇到任一个等待则自动变回未激发状态。
                    BOOL bInitialState,                      // i 初始状态
                    LPCTSTR lpName                           // 命名
)
CreateEvent函数内部会使操作系统创建一个内核事件对象。返回的句柄值就代表这个内核事件对象。应用程序无法获得这个内核事件对象的指针,而用一个句柄代表事件对象。


用户模式的信号灯(信号量)

信号灯也是一种常用的同步对象,信号灯由两个状态,一种是激发状态,另外一种是为激发状态,信号灯内部有个计数器,可以理解信号灯内部有N的灯泡。使用之前要先创建信号灯。

HANDLE CreateSemaphore( LPSECURITY_ATTRIBUTES LPSemaphoreAttributes, // 安全属性
                        LONG lInitialCount,                          // 初始化计数个数  计数器的值是多少 初始值为0 处于为激发态,非0 激发态。
                        LONG lMaximumCount,                          // 计数器最大个数  信号灯计数器最大值是多少
                        LPCTSTR lpName                               // 命名
);
另外可以使用ReleaseSemaphone 函数增加信号灯的计数器,函数声明

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

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

用户模式的互斥体

互斥体是一种常见的同步对象,可以避免多线程争夺同一个资源。多线程环境中只能有一个线程占有互斥体。互斥体的概念类似于同步事件,所不同的是同一个线程可以递归获取互斥体,即互斥体对于已经获得互斥体的线程不产生“互斥”关系,而同步事件不能递归获取。互斥体也有两种状态,激发态和未激发态。如果线程获得互斥体时,此时的状态为未激发态,当释放互斥体时,互斥体的状态为激发态。初始化互斥体函数声明

HANDLE CreateMutex( LPSECURITY_ATTRIBUTES lpMutexAttributes,   //安全属性
                    BOOL bInitialOwner,                        // 是否被占有 没有被占用,则是激发态,否则未激发态。
                    LPCTSTR lpName                             // 命名
)
另外获得互斥体的函数是WaitForSingleObject函数,而释放互斥体的函数是ReleaseMutex函数

等待线程完成

还有一种同步对象,这就是线程对象。每个线程同样有两个状态,激发态和未激发态。当线程处于运行之中的时候,是未激发状态。当线程终止后,线程处于激发状态。可以用WaitForObject对线程句柄进行等待。

内核模式下的同步对象

在内核模式下,有一系列的同步对象与用户模式下的同步对象相对应,在用户模式下,各个函数都可以以句柄操作同步对象的。而用户模式下,程序员无法获取真正同步对象指针,而用一个句柄(其实是一个32位整数)代表对象,在内核模式下,程序员可以获取真正同步对象的指针。每个同步对象在内核模式下有对应一个数据结构,但是在内核模式下程序员可以很自由的操作这些对象。但是要仔细使用同步对象,否则会引起死锁。

内核模式下的等待

在内核模式下两个函数负责内核模式下同步对象,分别是KeWaitForSingleObject和KeWaitForMultipleObjects函数。

NTSTATUS KeWaitForSingleObject( IN PVOID Object,                      // 一个同步对象的指针,这里不再是句柄
                                IN KWAIT_REASON WaitReason,           // 等待原因,一般设为Executive
                                IN KPROCESSOR_MODE WaitMode,          // 等待模式,是用户模式下等待还是内核模式下等待,一般设置为KernelMode
                                IN BOOLEAN Alertable,                 // 等待是否“警惕”,一般为FALSE
                                IN PLARGE_INTEGER Timeout OPTIONAL    // 等待时间,如果这个参数为NULL 表明无限期等待
);
如果等待的同步对象变为激发态,这个函数退出睡眠状态,并返回STATUS_SUCCESS。如果因为超时而退出,则会返回STATUS_TIMEOUT。

NTSTATUS KeWaitForMultipleObjects( IN ULONG Count,
                                   IN PVOID Object[],
                                   IN WAIT_TYPE WaitType,
                                   IN KWAIT_REASON WaitReason,
                                   IN KPROCESSOR_MODE WaitMode,
                                   IN BOOLEAN Altertable,
                                   IN PLARGE_INTEGER Timeout OPTIONAL,
                                   IN PKWAIT_BLOCK WaitBlockArray OPTIONAL
 );
   
内核模式下开启多线程

这里介绍一下内核模式下创建新线程,内核函数PsCreateSystemThread负责创建新的线程,该函数可以创建两种线程,用户线程和系统线程。该函数的声明如下:

NTSTATUS PsCreateSystemThread( OUT PHANDLE ThreadHandle,                        // 用于输出,是新创建的线程句柄
                               IN ULONG DesireAccess,                           // 创建的权限
                               IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL, // 该线程的属性,一般设为NULL
                               IN HANDLE ProcessHandle OPTIONAL,                // 创建用户线程还是系统线程。如果该值为NULL,则为系统线程,
                                                                                // 如果是一个进程句柄,则创建的这个线程属于这个句柄。 NtCurrentProcess                              OUT PCLIENT_ID ClientId OPTIONAL,
                               OUT PCLIENT_ID ClientId OPTIONAL,
                               IN PKSTART_ROUTINE StartRoutine,                 // 新线程的运行地址
                               IN PVOID StartContext                            // 新线程接收的参数
);
内核模式下必须使用函数PsTerminateSystemThread强制线程结束。否则该线程无法自动退出。

接下来介绍一种方法可以方便地让线程知道自己属于哪个进程。首先使用IoGetCurrentProcess函数得到当前线程。这个函数会得到一个PEPROCESS数据结构。PEPROCESS数据结构记录进程的信息,包括进程名。遗憾的是微软没有提供定义PEPROCESS结构体。所以借助WinDbg查看这个结构体。在Windows XP中PEPROCESS结构体的0x174偏移位置记录着进程名。 

内核模式下的事件对象

    在内核中用KEVENT数据结构表示一个事件对象。在使用事件对象前,需要进行初始化。内核函数KeInitializeEvent负责对事件对象初始化。

VOID KeInitializeEvent( IN PRKEVENT Event,  // 初始化事件对象的指针
                        IN EVENT_TYPE Type, // 这个参数是事件类型。分两类,一类是“通知事件”,对应参数NotificationEvent。
                                            // 另一类是“同步事件”,对应参数是SynchronizationEvent。
                        IN BOOLEAN State    // 如果这个参数为真,事件对象初始化状态为激活态。如果为假,事件对象初始化状态为未激活状态
);

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

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

应用程序中创建的对象和在内核中创建的时间对象本质上是一个东西,用户模式下用句柄表示,内核模式下用KEVENT数据结构表示。这里我们主要了解如何在应用程序和驱动程序中共同使用一个事件对象。

需要解决的问题是如何在用户模式下创建的时间传递给驱动程序。解决办法是DeviceIoControl API函数。在用户模式下创建一个同步事件,然后用DeviceIoControl把事件句柄传递给驱动程序。同时需要注意句柄与进程相关,一个进程中的句柄只能在这个进程中有效。DDK提供内核函数将句柄转化为内核数据结构指针,该函数是ObReferenceObjectByHandle。这个函数得到指针的同时会为对象的指针维护一个计数。每次调用ObReferenceObjectByHandle会使计数加1.因此为计数平衡,在使用完ObReferenceObjectByHandle函数后,需要调用ObDereferenceObject函数。ObDereferenceObject函数使计数减一。

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

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

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

内核模式下的信号灯

内核模式和用户模式下完全统一只不过操作不同,在用户模式下信号灯对象用句柄代表,而在内核模式下,信号灯对象用KSEMAPHORE数据结构表示。信号灯使用前必须初始化。

VOID KeInitializeSemaphore( IN PRKSEMAPHORE Semaphore, // 获取内核信号灯对象指针
                            IN LONG Count,             // 初始化时的信号计数
                            IN LONG Limit              // 信号灯计数的上限值
);
 KeReadStateSemaphore 函数可以读取信号灯当前的计数。KeReleaseSemaphore 释放信号灯会增加信号灯计数。获取信号灯可以使用KeWaitXX系列函数,如果获取成功,就将计数减一,否则陷入等待。

内核模式下的互斥体

互斥体在内核中的数据结构是KMUTEX,使用前需要初始化互斥体,使用函数KeInitialize内核函数初始化互斥体对象。

VOID KeInitializeMutex( IN PRKUTEX Mutex, // 内核互斥对象指针
                        IN ULONG Level    // 保留值 一般设为0
);

初始化后的互斥体对象,就可以使线程之间互斥了。获取互斥对象用KeWaitXX系列内核函数,释放互斥体用KeReleaseMutex内核函数。

快速互斥体

他的特征与普通互斥体对象完全一样,之所以称为快速互斥体,是因为执行速度比普通互斥体快(这里指的是获取和释放的速度)。当然还有缺点,这里就不介绍了。

其他方法

使用自旋锁进行同步

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

使用互斥锁进行同步

语句number++不是执行的最小单位,最小执行单位是汇编指令。为了让number++称为最小的执行单位,保证运行的原子性,可以采用自旋锁。当然对于自增、自减需要同步时,可以使用DDK提供的InterlockedXX和ExInterlockedXX函数

int number = 0;
void Foo()
{
   // 原子方式的自增
   InterlockedIncrement(&number);
   // 做一些事情
   // 原子方式的自减
   InterlockedDecrement(&number);
}

DDK 提供了两类互锁操作来提供简单德尔同步处理,一类是InterlockedXX函数,另一类是ExInterlockedXX函数。其中InterlockedXX系列函数不通过自锁实现,因此可以操作非分页数据,也可以操作分页的数据。而ExInterlockedXX系列函数通过自旋锁实现,所以不能操作分页内存的数据。


你可能感兴趣的:(Windows驱动程序设计详解(八)--- 驱动程序的同步处理)