WDK tips (9.2) 同步机制与锁 (2)
就跟上回讲的一样,动不动就使用spin lock是非常不合适的行为,我们应该尽量使用别的同步机制。NT内核提供了一族统称为dispatcher lock的锁,它们各有各的特点,适应不同的应用场景,了解它们的特性可以帮助你找到最适合自己的同步机制,避免spin lock的滥用。 表征dispatcher loco的数据结构拥有一个公共的头叫做DISPATCHER_HEADER,凡是有这个结构的锁都可以通过KeWaitForSingleObject(或者KeWaitForMultipleObjects)进入临界区。NT内核诞生的那段时间正好是面向对象概念大行其道的时候,所以NT的设计也融入了很多面向对象的思想,比如DISPATCHER_HEADER这个数据结构就可以看成是KEVENT,KMUTEX等等一系列数据结构的父类,而KeWaitForSingleObject则可以看成是父类中的public方法,可以被子类继承。DISPATCHER_HEADER的定义如下:
1 typedef struct _DISPATCHER_HEADER { 2 union { 3 struct { 4 UCHAR Type; 5 union { 6 UCHAR Absolute; 7 UCHAR NpxIrql; 8 9 }; 10 union { 11 UCHAR Size; 12 UCHAR Hand; 13 }; 14 union { 15 UCHAR Inserted; 16 BOOLEAN DebugActive; 17 }; 18 }; 19 volatile LONG Lock; 20 }; 21 LONG SignalState; 22 LIST_ENTRY WaitListHead; 23 } DISPATCHER_HEADER;
我们主要关心SignalState和WaitListHead这两个域。SignalState表征该锁是否处于被占用状态,WaitListHead是一个列表,将所有等在该锁上的线程全部记录下来,等锁被释放后就从这个列表中挑选一个(或者多个)线程唤醒。它与线程的关系大致如下:
以下是几个典型的dispatcher lock:
所有dispatcher loch中最简单的是event,它只包含了DISPATCHER_HEADER数据结构,没有任何额外内容。一个线程可以用KeWaitFOrSIngleObject获得锁(使SignalState处于un-signal状态),并用KeSetEvent释放(使SignalState处于signal状态)。这里有个小问题:由于DISPATCHER_HEADER中并没有记录当前获得锁的线程是哪一个,所以一旦使用event发生死锁后,你想用windbg之类的工具分析到底谁占用了锁是不可能的,相比之下mutex等就可以。并且由于没有owner thread信息我们也没办法知道一个线程到底get了几次这个锁,所以如果同一线程获取两次并且只释放一次,那么锁会处于signal状态,你写代码的时候可要小心这一点。我不知道这是设计失误还是怎样,DISPATCHER_HEADER中已经有那么多成员了,多一个owner thread信息就会差很多吗,不知道MS怎么想的。
Mutex与évent很相似,不过ta它加了一些额外信息来记录当前获得锁的线程的信息。如果同一个线程获得mutex两次,那么里面有一个count就会被置为2,除非你release mutex两次,否则别的线程无法获得该mutex。另外作为一个副作用,当线程获得mutex后kernel APC会被禁用,所以当你的程序里同时有timer等APC机制和mutex时得多加注意。另外,释放锁的线程与获得锁的线程必须是同一个,否则就会BSOD。
Semaphore的出现为了解决这样的问题:有的资源数量并不是只有一个,它允许并且只允许(不同的)线程加锁特定次数,当资源没有用尽时,KeWaitFOrSIngleObject立即返回,到上限后则是进入等待状态。为了做到这一点Semaphore需要增加两个域来管理信息:limitation域表征资源上限,count域表征当前用掉了多少。当我们调用KeWaitFOrSingleObject时count会减1,如果count>-limitation则进入un-signal状态;调用KeReleaseSemaphore时count加1,如果count >= 0 则进入signal状态。
另外还有一些dispatcher lock没有列出,各位可以到MSDN上查阅。这些锁都大同小异,最大的不同在于决定何时进入un-signal状态何时进入signal状态的规则,这里有张表列出了大部分dispatcher lock的规则:
锁类型 | signal状态规则 | 影响到的线程 |
Mutex | Release Mutex之后 | 唤醒一条线程 |
Semaphore | count==0时 | 唤醒所有的线程 |
同步类型event | Set Event之后 | 唤醒一条线程 |
通知类型event | Set Event之后 | 唤醒所有线程 |
Notification Timer | 时间到 | 唤醒所有线程 |
Process | 进程被销毁后 | 唤醒所有线程 |
Thread | 线程被销毁后 | 唤醒所有线程 |
File | IO complete | 唤醒所有线程 |
以上就是dispatcher lock的大致内容,下次我们继续讲resource和executive lock这两种特别的锁。