表格中描述的是常用的同步机制,这些机制的相关描述以及他们在windows系统下面的实现。
同步方法 |
描述 |
Windows下的机制 |
Interlocked operations |
提供原子的算术,逻辑,和列表操作,不仅是多线程安全的同时也是多处理器安全的 |
InterlockedXxx and ExInterlockedXxx routines |
Mutexes |
提供内存的互斥访问权限 |
Spin locks, fast mutexes, kernel mutexes, synchronization events |
Shared/exclusive lock |
运行一个线程专享写多个线程共享读 |
Executive resources |
Counted semaphore |
允许固定大小的获取权限 |
Semaphores |
一个互斥体保证共享资源的专有访问权限,任意能够保证互斥专有权限的锁都能被认为是互斥体。比如,自旋锁和同步事件都可以被认为是互斥体,因为当一个同步事件被唤醒的时候,仅有一个线程能够获取访问权限,同样自旋锁在并发条件下最多只能有一个线程能够被选中执行。互斥体的类型依赖于互斥体的使用环境,选择互斥体需要遵循下列规则:
1 互斥体可以运行的IRQL级别,也就是互斥体可以在哪一级IRQL当中获取和释放
2 获取互斥体是否会提升当前的IRQL?如果IRQL得到提升,那么原来的IRQL被保存在哪里?
3 是否互斥体的释放和获取必须在同一个线程环境上下文当中?
4 互斥体是否能够被递归获取,也就是说一个线程能否在不释放互斥体的条件下,再次获取该互斥体?
5 当一个持有互斥体的线程被终止而没有释放互斥体会出现什么情况?
互斥体类型 |
IRQL限制 |
递归和线程方面的细节 |
Interrupt spin lock |
获取的时候提升IRQL到DIRQL并且返回之前的IRQL给调用者 |
不能递归获取,获取和释放在同一个线程上下文 |
Spin lock |
获取的时候提升IRQL到DISPATCH_LEVEL并且返回之前的IRQL给调用者 |
不能递归获取,获取和释放在同一个线程上下文 |
Queued spin lock |
获取的时候提升IRQL到DISPATCH_LEVEL并且保存之前的IRQL在一个锁持有者句柄当中 |
不能递归获取,获取和释放在同一个线程上下文 |
Fast mutex |
获取的时候提升IRQL到APC_LEVEL并且保存之前的IRQL到锁当中 |
不能递归获取,获取和释放在同一个线程上下文 |
Kernel mutex (a kernel dispatcher object) |
在获取的时候进入代码互斥区域,在释放的时候离开代码互斥区域 |
可以递归获取,获取和释放在同一个线程上下文 |
Synchronization event (a kernel dispatcher object) |
获取并不会改变IRQL,在小于等于APC_LEVEL级别等待,而在小于等于DISPATCH_LEVEL级别激发 |
不能递归获取,获取和释放不需要在同一个线程上下文 |
Unsafe fast mutex |
获取的时候不改变IRQL并且在小于等于APC_LEVEL级别下面释放 |
不能递归获取,释放和获取在同一个线程上下文 |
Windows同步机制 |
描述 |
IRQL限制 |
InterlockedXxx routines |
在换页内存上执行原子的算术和逻辑运算 |
能够在任何IRQL当中获取 |
Spin locks |
在非换页内存上提供专享的内存访问 |
在IRQL <= DISPATCH_LEVEL级别上获取 |
ExInterlockedXxx routines |
执行算术、逻辑和列表控制的原子操作,不仅是多线程安全的同样也是多处理器安全的 |
在IRQL <= DISPATCH_LEVEL级别上访问 SList 历程; 其他历程可以在任意IRQL上面访问 |
Fast mutexes |
在APC_LEVEL级别保护数据,同时防止线程被打断 |
在IRQL <= APC_LEVEL级别上获取 |
Executive resources |
允许一个线程专享写而其他的线程共享读 |
在IRQL<=APC_LEVEL级别上获取 |
Kernel dispatcher objects (events, kernel mutexes, semaphores, timers, files, threads, processes) |
在IRQL<=APC_LEVEL级别上提供不同形式的同步,能够和用户模式下的应用程序同步 |
在 IRQL <= APC_LEVEL下等待; 在IRQL <= DISPATCH_LEVEL下激发 |
Callback objects |
在IRQL<=DISPATCH_LEVEL内核模式下面提供代码同步,能够用于驱动之间同步 |
在 IRQL <= DISPATCH_LEVEL被通知; 回调例程运行在激发线程的上下文当中,和激发线程的IRQL一样 |
自旋锁和它的名称所暗示的一样:当一个线程拥有自旋锁的时候,其他的线程在内存的某一个位置上通过忙等待自旋,直到锁是可用的。也就是说线程并不会阻塞,而是继续保持CPU的控制权,以防止执行相同或者更低IRQL中的代码。自旋锁是一个没有完全公开的KSPIN_LOCK结构体,他们必须从非分页内存当中分配,比如设备扩展结构体,或者由用户调用的非分页内存分配操作获得的内存。
自旋锁类型 |
描述 |
Ordinary spin lock |
保护DISPATCH_LEVEL级别或者更高级的共享数据 |
Queued spin lock |
保护DISPATCH_LEVEL级别或者更高级的共享数据,排队自旋锁可以在XP或者以后的系统版本上面使用 |
Interrupt spin lock |
保护DIRQL级别的共享数据,在InterruptService和SynchCritSection例程当中使用 |
所有的自旋锁提升IRQL到DISPATCH_LEVEL或者更高,自旋锁是唯一一个能够被用于高于DISPATCH_LEVEL的同步机制。也就是说系统线程不能够切换,并且当前线程也不能被打断。所有持有自旋锁的代码都需要遵从高于DISPATCH_LEVEL级别的IRQL的执行规则。一个自旋锁的单处理器实现很简单,只需要提升IRQL就可以了,但是多处理器的实现则需要两步,第一步提升IRQL,第二步通过一个原子操作测试并设置相应的数值。
普通自旋锁工作在DISPATCH_LEVEL,为了创建一个普通自旋锁,驱动程序在非换页内存当中分配一个KSPIN_LOCK结构体,并且调用KeInitializeSpinLock进行初始化。运行在低于DISPATCH_LEVEL的代码必须通过KeAcquireSpinLock和KeReleaseSpinLock进行自旋锁获取和释放。而已经运行在DISPATCH_LEVEL的代码应该调用KeAcquireSpinLockAtDpcLevel和KeReleaseSpinLockFromDpcLevel进行自旋锁的获取和释放,这两个例程不会引起IRQL的改变。
不论何时多个线程请求队列自旋锁,都需要在自旋锁的队列上面排队。另外,队列自旋锁仅仅测试和设置本地CPU,因此总线开销更小,对NUMA体系架构尤其高效。一个排队自旋锁需要一个KLOCK_QUEUE_HANDLE结构体来辅助KSPIN_LOCK——前者为后者提供一个队列存储的句柄。这个结构体能够在堆栈当中分配,为了初始化排队自旋锁需要调用KeInitializeSpinLock例程。同样的,运行在低于DISPATCH_LEVEL的驱动历程应该调用KeAcquireInStackQueuedSpinLock和KeReleaseInStackQueuedSpinLock来获取和释放自旋锁。而运行在DISPATCH_LEVEL大的驱动历程则需要调用KeAcquireInStackQueuedSpinLocktDpcLevel和KeReleaseInStackQueuedSpinLockFromDpcLevel来获取和释放自旋锁。
一个中断自旋锁保护设备寄存器和驱动的InterruptService例程以及可以在DIRQL访问的SynchCritSection例程。当一个设备驱动与中断对象关联的时候,操作系统为中断对象创建一个中断自旋锁。当InterruptService例程运行在DIRQL,并且持有相关的中断自旋锁。当InterruptService退出的时候,操作系统释放自旋锁并且降低IRQL。
当一个驱动调用KeSynchronizeExecution来运行一个SynchCritSection例程的时候,同样会申请默认的中断自旋锁。操作系统提升IRQL到DIRQL,获取自旋锁,并且调用SynchCritSection例程。其他的驱动需要访问中断自旋锁应该调用KeAcquireInterruptSpinLock来对共享数据进行访问。然而,一个设备可能在不同的IRQL上面产生多个中断。在这种情况下,驱动必须创建一个中断能够到达的最高的IRQL的自旋锁。当驱动连接中断对象的时候,传递一个KSPIN_LOCK结构体指针。这个结构体和一个中断能够达到的最高的DIRQL关联。系统会将这个自旋锁与中断对象关联起来。
ExInterLockedXxx例程由汇编代码进行编码并且通常禁止相关的处理器的中断。实际上ExInterLockedXxx例程的代码运行在HIGH_LEVEL。为了保护SMP系统上的数据,在操作之前,操作系统提升IRQL并且获取自旋锁。当例程完成操作的时候,系统释放自旋锁并且返回到原来的IRQL。
另外,ExInterLockedXxx例程能够管理下列三种类型的列表:
1 单链表
2 双链表
3 S链表
目的 |
非InterLocked例程 |
Interlocked例程 |
Insert entry at front of singly linked list. |
PushEntryList |
ExInterlockedPushEntryList |
Remove entry from front of singly linked list. |
PopEntryList |
ExInterlockedPopEntryList |
Insert entry at front of doubly linked list. |
InsertHeadList |
ExInterlockedInsertHeadList |
Remove entry from front of doubly linked list. |
RemoveHeadList |
ExInterlockedRemoveHeadList |
Insert entry at end of doubly linked list. |
InsertTailList |
ExInterlockedInsertTailList |
Remove entry from end of doubly linked list. |
RemoveTailList |
None. |
Initialize doubly linked list. |
InitializeListHead |
None. |
Check whether list has entries. |
IsListEmpty |
None. |
Remove entry from doubly linked list. |
RemoveListEntry |
None. |
Initialize S-list. |
None. |
ExInitializeSListHead |
Insert entry at front of Slist. |
None. |
ExInterlockedPushEntrySList |
Remove entry from end of S-list. |
None. |
ExInterlockedPopEntrySList |
Remove all entries from an S-list. |
None. |
ExInterlockedFlushSList |
ExInterLockedXxxList例程使用了一个为驱动分配的自旋锁,这些例程能够在任何IRQL进行调用,而S-链表只能在低于DISPATCH_LEVEL运行。windows内部使用S链表实现lookaside链表。
一个快速互斥体是一个不完全公开的FAST_MUTEX结构体,这个结构体必须在非换页内存当中分配。FAST_MUTEX运行在APC_LEVEL级别,因此可以阻止所有的APC提交,也不会被其他的线程打断。
例程 |
描述 |
ExAcquireFastMutex |
在获取快速互斥体之前提升IRQL到APC_LEVEL ,阻塞直到快速互斥体可用 |
ExAcquireFastMutexUnsafe |
在当前的IRQL获取快速互斥体,阻塞直到互斥体可用 |
ExTryToAcquireFastMutex |
在获取快速互斥体之前提升IRQL到APC_LEVEL ,不可用时不阻塞 |
ExAcquireFastMutex和ExAcquireFastMutexUnsafe引起线程阻塞直到互斥体可用。而ExTryToAcquireFastMutex如果互斥体不可用立刻返回FALSE。当出现下面两种情况之一的时候,使用ExAcquireFastMutexUnsafe获取互斥体:
· 线程已经运行在APC_LEVEL
· 线程在获取互斥体之前通过调用KeEnterCriticalRegion和FsRtlEnterFileSysytem进入了关键代码段
在上面两种情况下的任意一种,用户模式和内核模式的APC提交已经被禁止了。
一个驱动程序应该按照下面的步骤使用快速互斥体:
1 从非换页内存当中分配一个结构体FAST_MUTEX
2 初始化这个快速互斥体的结构体
3 在进入保护区域之前,调用ExAcquireFastMutex,ExAcquireFastUnsafe或者ExTryToAcquireFastMutex
4 执行受保护的操作
5 通过调用ExReleaseFastMutex或者ExReleaseFastMutexUnsafe释放快速互斥体
快速互斥体具有下列局限性:
1 快速互斥体不能够递归获取,这样做的结果是导致一个死锁
2 持有快速互斥体的驱动代码运行在APC_LEVEL级别。因此,在持有快速互斥体的代码中不能调用只能在PASSIVE_LEVEL级别可调用的例程,比如IoBuildDeviceIoControlRequest
3 快速互斥体不是系统内核分发对象。因此,一个驱动不能够利用KeWaitForMultipleObjects例程来等待快速互斥体。
操纵系统定义了一些内核分发对象,这些对象提供不同类型的同步机制。内核调度机制相对简单,并且能够在PASSIVE_LEVEL级别进行申请。
对象类型 |
描述 |
IRQL限制 |
Kernel mutex |
提供在PASSIVE_LEVEL或者APC_LEVEL专享访问 |
在 IRQL <=APC_LEVEL级别等待 |
Event |
提供驱动程序下的同步,能够用于和用户模式的应用程序同步can be used to synchronize with user-mode applications. |
在IRQL<=APC_LEVEL级别等待,在IRQL<=DISPATCH_LEVEL设置 |
Semaphore |
保护一组同类的数据 |
在IRQL<=APC_LEVEL等待,在IRQL<=DISPATCH_LEVEL. |
Timer |
在一个绝对或者绝对的定时时间间隔内提供通知或者同步定时器 |
在IRQL <=APC_LEVEL等待,在IRQL <=DISPATCH_LEVEL. |
Threads, processes, and files |
在文件、线程和进程创建或者文件IO结束,线程和进程终止的时候进行同步 |
在IRQL <=APC_LEVEL等待 |
一个驱动程序将内核调度对象作为参数传递给KeWaitForSingleObject或者KeWaiForMultipleObject例程。通这两个例程,驱动程序能够等待超时或者直到内核分发对象被设置。
内核分发数据库通过内核分发对象的名称管理所有内核调度对象。为了访问这些对象,系统必须提升IRQL到DISPATCH_LEVEL,并且获取系统范围内的分发数据库的自旋锁。分发锁的获取很频繁,大多数情况下系统需要等待锁就绪,因此,驱动代码应该优先考虑使用快速互斥体而不是分发对象。
内核分发对象有一个公有头部(DISPATCHER_HEADER),但是每一种类型的分发对象都有自己特有的初始化和释放例程。内核分发对象必须在非换页内存当中分配。驱动代码能够通过句柄或者指针管理内核分发对象。如果驱动通过句柄管理内核分发对象,但是这个驱动程序在任何线程上下文运行,那么驱动必须设置OBJ_KERNEL_HANDLE属性来防止用户模式下的访问。驱动代码通常利用内核分发对象来等待一个同步IO操作的结果。最顶层的驱动创建并且发送IRP给底层的驱动,然后在发出IO请求的线程上下文等待。更底层的驱动有可能需要在PASSIVE_LEVEL和APC_LEVEL级别的任意线程上下文等待同步执行。
内核分发对象有两种状态,激发状态和非激发状态。激发状态表明当前分发对象能够被获取,一个激发状态的对象还没有被任何线程锁获取,而一个非激发状态的对象则被一个或多个线程所获取。分发对象的初始激发状态和分发对象的类型有关,比如内核互斥体在初始化的时候立刻就被激发,而事件对象则需要手动调用KeSetEvent例程来激发。
一个线程能够在低于等于DISPATCH_LEVEL级别激发内核分发对象,但是仅仅能够在低于等于APC_LEVEL级别的情况下等待这个内核分发对象。也就是说驱动程序不能在IoCompletion,StartIo或者其他的延迟调用例程当中等待分发对象。最高层的驱动程序在他的读写例程当中等待分发对象,而底层的驱动程序不能在读写例程当中等待分发对象,因为他们的读写例程能够在DISPATCH_LEVEL被调用。
然而,如果一个线程不要等待分发对象的话,能够在DISPATCH_LEVEL级别获取一个内核分发对象。这一功能通过给KeWaitForSingleObject或者KeWaitMultipleObjects传递超时值为0。这种特性可用于测试一个对象是否被激发。比如,一个DPC例程和其他例程同步的时候,可能需要测试对象是否被激发。如果对象被激发则DPC处理这个任务,否则做其他的任务,然后将这个原始任务进行排队。因为work item是在PASSIVE_LEVEL当中执行,可以在work item当中等待相应的分发对象。
KeWaitForSingleObject和KeWaitForMultipleObjects函数的参数Alterts和WaitMode决定在线程等待的时候系统怎样处理用户模式下的APC。
Value of Alertable and WaitMode parameters |
Special |
Normal |
|
|||
Terminate wait? |
Deliver and run APC? |
Terminate wait? |
Deliver and run APC? |
Terminate wait? |
Deliver and run APC? |
|
Alertable = TRUE WaitMode = UserMode |
No |
If (A*), then Yes |
No |
If (B**), then Yes |
Yes |
Yes, after thread returns to user mode |
Alertable = TRUE WaitMode = KernelMode |
No
|
If (A), then Yes |
No |
If (B), then Yes |
No |
No |
Alertable = FALSE WaitMode = UserMode |
No |
If (A), then Yes |
No |
If (B), then Yes |
No |
No (with exceptions, such as CTRL+C to terminate) |
Alertable = FALSE WaitMode = KernelMode |
No |
If (A), then Yes |
No |
If (B), then Yes |
No |
No |
*A: IRQL < APC_LEVEL.
**B: IRQL < APC_LEVEL,线程既不在APC_LEVEL也不在关键代码段
当一个线程从系统模式的切换到用户模式的时候,系统会提交大多数用户模式的APC。用户模式APC不会中断用户模式下的代码,在一个应用程序为一个线程排队用户模式的APC之后,这个应用程序通过调用等待函数并传递参数Alterable为TRUE会引起系统提交APC。
当驱动程序调用KeWaitForSingleObject或者KeWaitForMultipleObjects,并且设置传参数Alterable为TRUE同时WaitMode为UserMode。那么等待将会返回STATUS_USER_APC或者STATUS_ALTERTED,只要存在悬挂的用户模式APC。
驱动程序调用KeWaitForXxx例程不应该传递Alterable参数为TRUE并同时设置WaitMode为UserMode。除非应用程序明确要求驱动程序在等待期间提交用户模式下的APC。
当一个驱动程序调用KeWaitForSingleObject或者KeWaitForMultipleObjects,参数WaitMode被设为UserMode,而Alterable设为FALSE。那么等待将会在线程被终止的时候返回STATUS_USER_APC。然而驱动程序必须在PASSIVE_LEVEL级别而不能在关键代码区域等待。
WaitMode同样也可以决定线程的内核堆栈在等待的时候是否能够被换页出去。当等待模式是UserMode的时候,系统将内核模式的堆栈换页出去。当正在等待的驱动是堆栈上的唯一的驱动,在UserMode下等待才是安全的。如果一个或者多个驱动在堆栈上,这些驱动当中的更新堆栈变量操作可能导致缺页错误。
windows在\\kernelObject对象目录当中定义了一些标准的事件对象。KeSetEvent函数有三个参数:第一个参数是一个将要被激发的事件指针,第二个是事件激发之后想要获取的优先级提升,第三个参数是一个Wait布尔值。当Wait布尔值为TRUE的时候表示线程在KeSetEvenet之后立刻就会调用KeWaitXxx例程。
一般情况下,驱动程序调用KeSetEvent——设置Wait为FALSE,当Wait设置为FALSE的时候,KeSetEvent提升IRQL到DISPATCH_LEVEL,获取分发锁,修改事件对象的激发状态,激活任意等待的线程,解锁分发对象数据库,降低IRQL到原来的数值,然后返回。然而,当Wait为TRUE的时候,KeSetEvnet不释放分发锁或者降低IRQL。这个优化能够避免不必要的上下文切换,因为调用者已经在一个原子操作当中激发了这个事件。如果一个驱动程序使用了这个特性,那么它必须在小于DISPATCH_LEVEL级别下调用KeSetEvent,并且保证不在任意线程上下文当中。一个类似生产者和消费者场景下的驱动历程可能使用这个特性。这种驱动通常以下列方式和事件进行交互,生产者驱动历程激发第一个事件通知应可以发送数据。然后立刻等待第二个事件被另一个线程激发,第二个线程设置第二个事件通知数据已经收到,并且做好接收更多数据的准备。驱动应该使用这些特性仅仅在请求IO的线程的上下文当中,一个驱动应该避免阻塞一个不相关的线程。
一个通知事件唤醒任何在等待的线程,并且保持激发状态,直到显式调用KeResetEvent。在win32 API当中通知事件被称作手工重置事件。驱动程序通常用通知事件等待IRP的完成。比如,一个驱动可能通过IoBuildDeviceIoControlRequest发送IO控制代码给设备对战当中的更底层的驱动。这个例程的一个参数就是一个事件对象的指针。在驱动例程创建并且发送IRP之后,驱动在这个事件对象上面等待。当IRP完成,IO管理器激发这个事件,这个事件保持激发状态直到调用KeResetEvent。
同步事件也被称为自动重置事件,在唤醒线之后立刻返回到非激发状态,驱动程序较少使用同步事件。一个需要长时间初始化的设备驱动程序可能在StartDevice例程上等待同步事件来确保设备被完整初始化。在设备被中断并且任何在DISPATCH_LEVEL级别的处理都结束时,驱动的DpcForIsr例程激发这个事件。控制权然后转移到StartDevice例程,在这里可能继续初始化驱动和设备。同样的,一个驱动能够在同步事件上等待DispatchPnp例程来确保IO已经在停止或者移除设备之前完成。
一种在内核驱动和用户模式应用程序下协作的方式。在驱动当中:
1 定义一个私有的IO控制代码,应用程序可以通过这个IO控制代码传递一个事件
2 提供一个DispatchDeviceControl例程来处理私有的IRP_MJ_DEVICE_CONTROL下面的IOCTL请求
3 通过调用ObReferenceObjectByHandle来获取有效的事件指针,在DesiredAccess参数当中,指定SYNCHRONIZE权限,而在ObjectType参数当中,指定*ExEventObjectType
4 通过KeSetEvent激发事件,通过调用KeResetEvent来重置通知事件
5 调用ObDereferenceObject来释放句柄,当句柄值不再需要的时候
在应用程序当中:
1 通过调用CreateEvent函数创建一个有名事件
2 通过调用DeviceIoControl函数,将句柄传递给驱动程序
3 通过调用调用WaitForSingleObject或者WaitForMultipleObjects等待内核模式下的驱动程序激发这个事件
4 在退出之前调用CloseHandle删除这个事件句柄
内核互斥体是一种可以用于分页内存的同步技术,同时它还可以用于需要相对较长的执行时间的情况下。驱动程序能够在小于等于APC_LEVEL的情况下使用。
内核互斥体依赖于线程上下文,通常是运行在请求线程上下文的最高层次的驱动例程使用内核互斥体。一个获取互斥体的线程应该在同一个线程上下文当中释放互斥体。
内核互斥体和快速互斥体的不同之处表现在以下几个方面:
1 内核互斥体能够被递归获取,而快速互斥体不行
2 内核互斥体通过调用KeWaitForSingleObject,KeWaitForMultipleObjects以及KeWaitForMutexObject。而快速互斥体通过ExAcquireFastMutex、ExTryToAcquireFastMutex,和ExAcquireFastMutexUnsafe
3 内核互斥体的获取需要使用系统范围内的锁。因此,他们效率相对快速互斥体低
为了使用内核互斥体,驱动程序必须按照下列步骤来执行:
1 从一个非换页内存当中分配一个KMUTEX数据结构
2 传递之前分配的数据结构指针给KeInitializeMutex函数进行互斥体初始化
3 通过KeWaitForSingleObject、KeWaitForMultipleObjects或者KeWaitForMutexObject等待互斥体
4 执行保护的操作
5 通过KeReleaseMutex释放互斥体
系统在初始化内核互斥体的时候已经让这个互斥体处于激发状态,第一个等待互斥体的线程将获取互斥体。驱动历程应该总是定义KernelMode,当他们等待内核互斥体的时候。在内核模式下等待将阻止内核模式堆栈被换页出去,并且禁止用户模式下的APC和普通内核模式的APC提交,因此可以阻止线程被终止和被打断。而内核模式下的特殊APC仍然可以提交。在北部,获取一个内核模式的互斥体需要调用KeEnterCriticalRegion。如果这个获取互斥体的线程运行在PASSIVE_LEVEL,会禁止普通内核模式的APC提交直到内核释放互斥体。当线程已经在APC_LEVEL的时候,进入关键代码段不起作用,因为此时普通内核模式APC已经禁止了。
一个持有互斥体的线程在转入到用户模式之前必须释放互斥体。如果再切换到用户模式过程当中线程持有互斥体,那么系统将崩溃。
KeReleaseMutex和KeSetEvent有同样意义的Wait参数。一个线程递归获取互斥体必须释放互斥体同样的次数,操作系统不会使得互斥体处于激发状态或者调用KeLeaveCriticalRegion直到所有的申请都释放。
信号量的处理和互斥体类似,但是当释放次数超过信号量的上限的时候,系统会跳出异常,这一点和事件不同——激发状态的事件可以进行设置。另外信号量的释放参数Wait和互斥体的处理一样,线程能够通过KeReadStateSemphore来测试信号量是否被激发。
定时器有通知和同步两种,一个驱动程序通过KeInitializeTimer创建一个通知定时器,或者通过调用KeInitializeTimer创建两种定时器之一。相对时间通过计算机器运行时间并且不受系统时钟的影响。相对时间包括系统休眠的时间,当系统被唤醒,系统调整机器时间来包含计算机休眠的时间。结果是一旦系统唤醒许多定时器同时超时。
当一个通知定时器超时的时候,所有的等待线程都被激发。定时器保持激发状态直到一个线程调用KeSetTimer显式重置定时器。当一个同步定时器超时,仅仅一个线程被激发。同时系统立刻重置定时器到非激发状态。
驱动能够在小于等于APC_LEVEL级别上等待IRQL,或定义一个CustomTimerDpc例程被调用当超时的时候。这个历程能够取代驱动程序创建的协助线程来执行相应的操作。这个历程同样可以用于超时来自DISPATCH_LEVEL级别的请求。
为了使用定时器,驱动应该遵从下面的步骤:
1 从非换页内存当中分配一个KTIMER结构体
2 调用KeIntializeTimer或者KeInitializeTimerEx创建并且初始化这个定时器
3 为了绑定一个CustomTimerDpc到定时器上,还需要额外的调用KeInitializeDpc初始化一个DPC对象,并注册CustomTimerDpc
4 通过KeSetTimer或者KeSetTimerEx设置定时器,指定超时时限。超时的时候排队CustomTimerDpc例程,包括可选的DPC参数
5 调用KeWaitForSingleObject或者KeWaitForMultipleObjects来等待定时器
6 如果需要在定时器超时之前取消定时器,可以调用KeCancelTimer
7 为了在超时之后重置定时器,可以调用KeSetTimer
线程、进程以及文件也是内核调度对象。驱动程序能够用KeWaitXxx来同步进程、线程或者文件。另外,驱动程序能够监听到新线程或者进程的创建。为了在线程、进程或者文件对象上面等待,内核模式的驱动必须定义KernelMode等待模式。当线程或者进程终止、文件操作完成的时候,等待会被唤醒。
一个文件IO操作完成的时候系统会自动激发一个内置在文件对象当中的事件。这个事件是同步事件,也就是说这个事件会在等待线程被通知之后自动重置。特定线程的同步只有在驱动创建一个辅助线程的时候才非常有用。大多数驱动例程,除了最高层次的驱动分发例程,都是在任意线程的环境上下文当中调用。因此,与当前线程环境上下文的同步是没有意义的。,当一个系统范围的线程或者进程被创建或者删除的时候,驱动能够获得通知。为了得到通知,驱动可以通过函数PsSetCreateProcessNotifyRoutine或者PsSetCreateThreadNotifyRoutine设置一个回调例程。设置回调例程的驱动在系统关闭之前不能退出。
通过使用可执行资源,驱动能够实现读写锁。可执行资源设计用于专有写而可以共享读。执行资源不需要获取系统分发数据库的自旋锁,因此速度比较快。一个运行在小雨等于APC_LEVEL级别的线程代码可以使用执行资源。一个执行资源是一个ERESOURCE结构体,这个结构体必须从非分页内存当中分配。一个ERESOURCE必须是自动对齐的,不过ERESOURCE结构体是不完全公开的。
Table 9. Executive Resource Acquisition Routines
例程 |
访问权限 |
条件 |
ExAcquireResourceSharedLite |
共享 |
如果资源没有被专享访问并且没有任何线程在等待专享访问,或者请求的线程已经包含共享或者独享访问权限 |
ExAcquireResourceExclusiveLite |
独享 |
如果资源没有被独享或者共享访问 |
ExAcquireSharedStarveExclusive |
共享 |
如果资源没有被专享访问并且没有任何线程在等待专享访问,或者请求的线程已经包含共享或者独享访问权限,等待独享权限的线程将继续等待 |
ExAcquireSharedWaitForExclusive |
共享 |
如果资源没有被专享访问并且没有任何线程在等待专享访问,如果请求线程有权限访问这个资源但是其他线程正在等待的时候,递归获取权限需要先释放权限然后和其他的线程竞争 |
线程可以将独享特权变换到共享特权,而不能将共享特权变换为专有特权。ExConvertExclusiveToSharedLite例程可以将线程的专享特权转换为共享特权。一个线程可以代替其他线程来释放资源通过调用ExReleaseResourceForThread。文件系统驱动使用这个例程,当一个线程获取了资源然后发送这个IO请求给其他线程的时候。在这种情况下,线程完成IO请求可以调用这个例程代替第一个线程来释放资源。
驱动程序能够通过ExIsResourceAcquiredLite,ExIsResourceAcquiredSharedLite和ExIsResourceAcquiredExclusiveLite来判断资源是否被任何线程占用。另外,驱动还能够通过ExGetSharedWaiterCount或者ExGetExclusiveWaiterCount来获取等待的线程数目。中断一个拥有互斥资源所的线程可能引起死锁。
使用执行资源的要点:
1 从非换页内存当中分配一个ERESOURCE结构体
2 在DriverEntry或者AddDevice例程当中调用ExInitializeResourceLite初始化资源
3 在获取资源之前禁止普通内核模式的APC,驱动程序通过调用KeEnterCriticalRegion,文件系统调用FsRtlEnterFileSystem。如果驱动例程运行在系统线程,可以不必要禁止APC,因为系统线程不会被中断。
4 通过调用表9当中的线程来获取资源
5 执行代码
6 通过调用ExReleaseResourceLite释放资源
7 重新使能普通内核模式的APC提交,通过调用KeLeaveCriticalRegion或者FsRtlLeaveFileSystem
所有的资源获取例程返回一个指示获取成功与否的布尔值。执行资源能够被递归获取。比如,一个文件系统可能通过映射文件到虚拟内存当中的保留区域来实现cache。如果这个过程引起页错误,那么操作系统将产生额外的IO请求。这些IO请求同样会送到这个文件系统驱动并且打断驱动处理cache IO的过程。为了处理额外的IO请求,文件系统必须递归的获取在cache IO过程当中使用的锁。
回调对象仅仅能够在内核模式下使用,他们不能与用户模式下的应用程序共享。驱动程序通过调用ExCreateCallBack来创建一个回调对象。用户通过ExRegisterCallback来注册一个回调例程。当驱动程序指定的回调条件发生的时候,驱动程序调用ExNotifyCallback来通知回调例程运行。ExNotifyCallback能够在小于等于DISPATCH_LEVEL级别上被调用。而回调例程在在通知线程的环境上下文当中和ExNotifyCallback例程相同的IRQL上面运行。如果驱动注册一个回调例程,确定你知道通知发生的IRQL,并且适合回调例程运行。
当使用用户自定义的锁的时候,需要牢记编译器可能会对指令进行位置调整,这就需要我们使用内存栅栏来防止这种优化。内存栅栏是一个处理器指令防止读写操作的指令顺序调整。ExInterLockedXxx和InterlockedXxx例程和KeMemoryBarrier或KeMemoryBarrierWithoutFence例程都可以插入内存栅栏防止重排序。
多种同步机制的使用
比如,一个驱动程序可能包含两个需要在DISPATCH_LEVEL级别保护的列表,当驱动例程需要讲一个列表当中的数据转移到另一个列表的时候,仅仅只是用一个自旋锁是不够的。但代码需要访问两个链表的时候,就需要两个锁。获取锁的顺序可以通过简历锁层次得到。锁的层次通过他们的IRQL递增形式排列。首先列出需要最低IRQL的锁,然后列出第二低的锁…当代码需要一次获取多个锁的时候,它应该以IRQL递增的方式获取。当存在IRQL相同的时候,优先获取访问次数比较频繁的锁。
一下步骤可以防止死锁:
1 绝对不要在任何大于等于DISPATCH_LEVEL级别被调用的驱动例程当中等待一个内核分发对象,在大于小于DISPATCH_LEVEL级别调用的例程包括IoCompletetion例程和存储驱动的IO分发例程以及USB的hub驱动
2 禁止正常内核模式的APC调用,在任何可执行资源获取之前,或者调用KeWaitXxx等待事件、信号量、定时器、线程、文件对象或者进程之前
3 使用驱动验证程序的死锁检测选项来发现潜在的死锁
4 总是通过锁层次来编码
在PASSIVE_LEVEL级别的关键代码段外使用锁有可能导致DOS攻击,当一个持有锁的驱动被中断,这是因为windows排队一个普通内核模式的APC来中断线程,即使驱动定义了KernelMode等待,普通内核模式APC仍然会提交,当下面所有条件成立的时候:
1 目标线程运行在APC_LEVEL
2 目标线程没有运行APC
3 目标线程不在关键代码段