WDM驱动程序设计 第 5 讲 同步技术
主要内容 一个同步问题的例子 中断请求级 自旋锁 内核同步对象 其它内核同步原语
一个同步问题的例子 下面利用静态变量lActiveRequests记录当前未完成的 I/O请求数: static LONG lActiveRequests; NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp) { ++lActiveRequests; ... // process PNP request --lActiveRequests; }
有什么问题? mov eax, lActiveRequests add eax, 1 mov lActiveRequests, eax 关于语句“++lActiveRequests”在X86处理器上汇编程序生成如下 代码: // ++lActiveRequests; mov eax, lActiveRequests add eax, 1 mov lActiveRequests, eax 上述代码的第三条指令被执行之前如果被同一CPU上的其它执 行线程打断,或者在不同CPU上有完全相同的代码在同时运行 都会引起++lActiveRequests的计数错误。
解决的办法 把load/add/store和load/subtract/store指令序列替换为 原子指令: // ++lActiveRequests; inc lActiveRequests // --lActiveRequests; dec lActiveRequests INC和DEC指令不能被中断,但是多处理器环境中仍 然是不安全的,因为这两个指令都是由几条微代码实 现的。
最终解决办法 lock inc lActiveRequests // --lActiveRequests; lock dec lActiveRequests LOCK指令前缀可以使当前执行多微码指令的CPU锁 定总线,从而保证数据访问的完整性。
两个最差的假定 驱动程序开发者必须做如下两个最差的假定: 操作系统可以在任何时间抢先任何例程并停留任何长的时间,所以我们不能保证自己的任务不被干扰或延迟。 即使我们能防止被抢先,但其它CPU上执行的代码也会干扰我们代码的执行,甚至一个程序的代码可以在两个不同线程的上下文中并发执行。
同步请求级 一个确定的CPU上的活 动仅能被拥有更高IRQL 的活动抢先。
IRQL与线程优先级 线程优先级是与IRQL非常不同的概念。线程优先级控制着OS线程调度器的调度动作,决定何时抢先运行线程以及下一次运行什么线程。 当IRQL级高于或等于DISPATCH_LEVEL级时线程切换停止,无论当前活动的是什么线程都将保持活动状态直到IRQL降到DISPATCH_LEVEL级之下。 在进行线程调度时会切换线程上下文;按照IRQL进行活动抢先时不会切换线程上下文。
利用IRQL进行同步 KIRQL oldirql; 方法:将所有对共享数据的访问都应该在同一(提升的,高于PASSIVE_LEVEL级的) IRQL上进行。 上述方法只适用于单CPU。 可利用KeRaiseIrql和KeLowerIrql函数改变当前IRQL。 KIRQL oldirql; ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); KeRaiseIrql(DISPATCH_LEVEL, &oldirql); ++lActiveRequests; ... KeLowerIrql(oldirql);
自旋锁(spin lock) 利用自旋锁可以解决多处理器平台上的同步问题。 一个自旋锁对应一个内存变量。 为了获得一个自旋锁,在某CPU上运行的代码需先执行一个原子操作,该操作测试并设置(test-and-set)某个内存变量,由于它是原子操作,所以在该操作完成之前其它CPU不可能访问这个内存变量。 如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行。 如果测试结果表明锁仍被占用,程序将在一个小的循环内重复这个“测试并设置(test-and-set)”操作,即开始“自旋”。 最后,锁的所有者通过重置该变量释放这个自旋锁,于是,某个等待的test-and-set操作向其调用者报告该自旋锁已释放。
使用自旋锁时的注意事项 第一,如果一个已经拥有某个自旋锁的CPU想第二次获得这个自旋锁,则该CPU将死锁(deadlock)。 第三,仅能在低于或等于DISPATCH_LEVEL级上请求自旋锁,在你拥有自旋锁期间,内核将把你的代码提升到DISPATCH_LEVEL级上运行。
如何使用自旋锁 首先,在非分页内存中为一个KSPIN_LOCK对象分配存储空间。然后调用KeInitializeSpinLock初始化这个对象。 typedef struct _DEVICE_EXTENSION { ... KSPIN_LOCK QLock; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; NTSTATUS AddDevice(...) { PDEVICE_EXTENSION pdx = ...; KeInitializeSpinLock(&pdx->QLock); }
如何使用自旋锁 当代码运行在低于或等于DISPATCH_LEVEL级时获取这个锁,并执行需要保护的代码,最后释放自旋锁。 NTSTATUS DispatchSomething(...) { KIRQL oldirql; PDEVICE_EXTENSION pdx = ...; KeAcquireSpinLock(&pdx->QLock, &oldirql); ... KeReleaseSpinLock(&pdx->QLock, oldirql); }
如何使用自旋锁 如果知道代码已经处在DISPATCH_LEVEL级上 ,如DPC、StartIo,和其它执行在DISPATCH_LEVEL级上的驱动程序例程,可以调用两个专用函数来操作自旋锁 : KeAcquireSpinLockAtDpcLevel(&pdx->QLock); ... KeReleaseSpinLockFromDpcLevel(&pdx->QLock);
内核同步对象 利用内核同步对象可以暂时阻塞一个线程的执行,同步不同线程的执行动作。 内核同步对象仅影响OS线程调度器的调度动作,因此一般只在低于DISPATCH_LEVEL级的代码中用于阻塞线程。 在驱动程序中,只能在“非任意线程上下文”条件下利用内核同步对象阻塞调用者的线程或产生该请求的线程。 在“任意线程上下文”调用等待原语只会阻塞一个“无辜”的线程。
非任意线程上下文 如果驱动程序的回调例程能确切知道处于哪个线程上下文中,则称处于“非任意线程上下文”;大部分时间里,驱动程序无法知道这个事实,即处于“任意线程上下文”中。 非任意线程上下文的例子: 设备的最高级驱动程序的IRP处理函数可以确切地知道它执行在发出该I/O请求的应用程序线程的上下文中。 PNP类IRP的处理函数可以确切地知道它执行在一个系统线程(System Thread)中。 在你自己创建的内核模式系统线程中。(PsCreateSystemThread) DriverEntry、AddDevice、DriverUnload等函数执行在一个系统线程(System Thread)中。
常用的内核同步对象 对象 数据类型 描述 Event(事件) KEVENT 阻塞一个线程直到检测到某事件发生 Semaphore(信号灯) KSEMAPHORE 控制多个线程对共享资源的访问 Mutex(互斥) KMUTEX 执行到关键代码段时,禁止其它线程执行该代码段 Timer(定时器) KTIMER 推迟线程执行一段时期 Thread(线程) KTHREAD 阻塞一个线程直到另一个线程结束
在单同步对象上等待 在任何时刻,任何对象都处于两种状态中的一种:信号态(signaled)或非信号态(not signaled) 。 调用KeWaitForSingleObject或KeWaitForMultipleObjects函数可以使代码(以及背景线程)在一个或多个同步对象上等待,等待它们进入信号态。 ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); LARGE_INTEGER timeout; NTSTATUS status = KeWaitForSingleObject(object, WaitReason, WaitMode, Alertable, &timeout);
KeWaitForSingleObject参数含义 KeWaitForSingleObject(object, WaitReason, WaitMode, Alertable, &timeout); object 指向要等待的对象,它应该指向一个上面表中列出的同步对象。该对象必须在非分页内存中。 WaitReason 是一个纯粹建议性的值,KWAIT_REASON枚举型,一般取值为Executive。 WaitMode 是MODE枚举类型,该枚举类型仅有两个值:KernelMode和UserMode。一般取值为KernelMode 。 Alertable 参数一般指定为FALSE。 timeout 是一个64位超时值的地址,单位为100纳秒。正数的超时表示一个从1601年1月1日起的绝对时间。负数代表相对于当前时间的时间间隔。 指定为0将使等待函数立即返回。指定为NULL代表无限期等待。
KeWaitForSingleObject返回值含义 STATUS_SUCCESS,表示等待被满足。即你调用KeWaitForSingleObject时,对象或者已经进入信号态,或者在等待中进入信号态使等待返回。 STATUS_TIMEOUT指出在指定的超时期限内对象未进入信号态 。如果指定0超时,则函数将立即返回。返回代码为STATUS_TIMEOUT,代表对象处于非信号态,返回代码为STATUS_ SUCCESS,代表对象处于信号态。 其它两个返回值STATUS_ALERTED和STATUS_USER_APC表示等待提前终止,对象未进入信号态 。
在多个同步对象上等待 ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); LARGE_INTEGER timeout; NTSTATUS status = KeWaitForMultipleObjects(count, objects, WaitType, WaitReason, WaitMode, Alertable, &timeout, waitblocks); objects指向一个指针数组,每个数组元素指向一个同步对象,count表示数组中指针的个数 。 WaitType是枚举类型,其值可以为WaitAll或WaitAny,它指出你是等到所有对象都进入信号态,还是只要有一个对象进入信号态就可以。 waitblocks参数指向一个KWAIT_BLOCK结构数组,内核用它来记录每个对象在等待中的状态。 不需要你对其进行初始化。
KeWaitForMultipleObjects的返回值 如果指定了WaitAll,则返回STATUS_SUCCESS表示等待的所有对象都进入了信号态。 如果指定了WaitAny,则返回值在数值上等于进入信号态的对象在objects数组中的索引。 如果碰巧有多个对象进入了信号态,则返回值仅代表其中的一个,可能是第一个也可能是其它。可以认为返回值等于STATUS_WAIT_0加上数组索引。 NTSTATUS status = KeWaitForMultipleObjects(...); if (NT_SUCCESS(status)) { iSignalled = status - STATUS_WAIT_0; ... }
内核事件(Event)对象 用途:把一个特定的事件通知给一个等待中的线程。 与该对象相关的内核服务函数如下: 服务函数 功能 KeInitializeEvent 初始化事件对象 KeSetEvent 把事件设置为信号态,返回前一个状态 KeResetEvent 把事件设置为非信号态,返回前一个状态 KeClearEvent 把事件设置为非信号态,不报告以前的状态。 KeReadStateEvent 取事件的当前状态。
通知事件与同步事件 通知事件(notification event)有这样的特性,当它进入信号态后,它将一直处于信号态直到明确地把它重置为非信号态。因此,当通知事件进入信号态后,所有在该事件上等待的线程都被释放。 同步事件(synchronization event):只要有一个线程被释放,该事件就被自动重置为非信号态。 ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); KEVENT event; KeInitializeEvent(event, EventType, initialstate); EventType是一个枚举值,可以为NotificationEvent或SynchronizationEvent。 initialstate是布尔量,为TRUE表示事件的初始状态为信号态,为FALSE表示事件的初始状态为非信号态。
KeSetEvent函数 调用KeSetEvent函数可以把事件置为信号态: event参数指向一个事件对象。 ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); LONG wassignalled = KeSetEvent(event, boost, wait); event参数指向一个事件对象。 boost值用于提升等待线程的优先级,使得该线程等待的条件被满足后可以很快获得CPU执行权。 wait参数指定为FALSE。 如果该事件已经处于信号态,则该函数返回非0值。如果该事件处于非信号态,则该函数返回0。
利用事件对象实现互斥操作 typedef struct _DEVICE_EXTENSION { ... ... ... ... ... ... KEVENT lock; } DEVICE_EXTENSION, *PDEVICE_EXTENSION; KeInitializeEvent(&pdx->lock, SynchronizationEvent, TRUE); void thread () { KeWaitForSingleObject(&pdx->lock, Executive, KernelMode, FALSE, NULL); // do something KeSetEvent(&pdx->lock, EVENT_INCREMENT, FALSE); }
在应用层异步访问设备 // CreateFile的一个参数可以规定同步方式还是异步方式访问该设备 hDevice = CreateFile(“\\\\.\\wdm1Device”, ……….); HANDLE waitEvent = CreateEvent(……….); OVERLAPPED ol; ……… ol.hEvent = waitEvent; ReadFile( hDevice, buffer, NumberOfBytesToRead, &ol); while(WaitForSingleObject(waitEvent, 100)==WAIT_TIMEOUT) { if(!KeepRunning) { CancelIo(hDevice); goto EXIT; } // 从buffer中访问数据……
内核信号灯 内核信号灯是一个有同步语义的整数计数器。 信号灯计数器为正值时代表信号态,为0时代表非信号态。计数器不能为负值。 释放信号灯将使信号灯计数器增1,在一个信号灯上等待将使该信号灯计数器减1。如果计数器值被减为0,则信号灯进入非信号态,之后其它调用KeWaitXxx函数的线程将被阻塞。 注意如果等待线程的个数超过了计数器的值,那么并不是所有等待的线程都可以恢复运行。
服务函数与使用方法 KeInitializeSemaphore:初始化信号灯对象 KeReadStateSemaphore:取信号灯当前状态 KeReleaseSemaphore:释放信号灯对象 KSEMAPHORE semaphore; ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); KeInitializeSemaphore(&semaphore, count, limit); …. KeWaitForSingleObject(&semaphore, …..); …….. ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); KeReleaseSemaphore(semaphore, boost, delta, wait);
互斥对象Mutex 互斥(mutex)就是mutual exclusion的简写。 内核互斥对象为多个竞争线程串行化访问共享资源提供了一种方法。虽然用其它方法也能实现此功能,但互斥对象加入了一些措施能防止死锁。 如果互斥对象不被某线程所拥有,则它是信号态,反之则是非信号态。 如果需要长时间串行化访问一个对象,应该首先考虑使用互斥(而不是依赖提升的IRQL和自旋锁)。 利用互斥对象控制资源的访问,可以使其它线程分布到多处理器平台上的其它CPU中运行,还允许导致页故障的代码仍能锁定资源而不被其它线程访问。
互斥对象的服务函数 KeInitializeMutex 初始化互斥对象 KeReadStateMutex 取互斥对象的当前状态 KeReleaseMutex 设置互斥对象为信号态 KMUTEX mutex; ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); KeInitializeMutex(&mutex, level); …. KeWaitForSingleObject(&mutex, …..); …….. KeReleaseMutex(&mutex, wait);
内核定时器 (Timer) Timer对象可以在指定的绝对时间或间隔时间后自动从非信号态变为信号态。它还可以周期性的进入信号态。 可以利用KeWaitXxxx函数等待一个Timer对象在某个时间间隔后进入信号态,也可以利用Timer对象安排一个在某个时间间隔后或定期执行的DPC回调函数。 定时器也分为通知型和同步型两种。通知型定时器及时结束后一直处于信号态,除非手动改变。因此,所有等待它的线程都被释放。同步定时器正相反,它只允许有一个等待线程。一旦有线程在这种定时器上等待,并且开始执行,定时器就自动进入非信号态。
内核定时器的服务函数 服务函数 功能 KeInitializeTimer 初始化一次性的通知型定时器 KeInitializeTimerEx 初始化一次性的或周期性的通知型的或同步型定时器 KeSetTimer 为通知型定时器设定时间或DPC对象 KeSetTimerEx 为定时器设定时间、周期和其它属性 KeCancelTimer 取消一个定时器 KeReadStateTimer 获取定时器的当前状态。
一次性定时器的用法 KTIMER timer; // someone gives you this ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); KeInitializeTimerEx(&timer, NotificationTimer); // KeInitializeTimer(timer); LARGE_INTEGER duetime; KeSetTimer(&timer, duetime, NULL); KeWaitForSingleObject(&timer, ......); .....
周期性定时器的用法 KTIMER timer; // someone gives you this ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); KeInitializeTimerEx(&timer, SynchronizationTimer); LARGE_INTEGER duetime; long period; KeSetTimerEx(&timer, duetime, period, NULL); while(True) { KeWaitForSingleObject(&timer, ......); ..... } KeCancelTimer(&timer);
定时器与DPC PKDPC dpc; // points to KDPC you've allocated ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); KeInitializeTimer(timer); KeInitializeDpc(dpc, DpcRoutine, context); ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL); LARGE_INTEGER duetime; KeSetTimer(timer, duetime, dpc); ...... ..... .......... ........ VOID DpcRoutine(PKDPC dpc, PVOID context, .....) { ... }
定时函数 KeDelayExecutionThread :可以在PASSIVE_LEVEL级上调用该函数并给出一个时间间隔。该函数省去了使用定时器时的麻烦操作,如创建,初始化,设置等待操作。 如果需要延迟一段非常短的时间(少于50毫秒),可以在任何IRQL级上调用KeStallExecutionProcessor。这个延迟的目的是允许硬件在程序继续执行前有时间为下一次操作做准备。实际的延迟时间可能大大超过请求的时间。 ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL); LARGE_INTEGER duetime; NSTATUS status = KeDelayExecutionThread(WaitMode, Alertable, &duetime);
内核线程对象 内核线程对象(PKTHREAD)代表一个内核线程,可以利用KeWaitXxx等待原语在一个内核线程上进行等待,等待者会被一直阻塞直到所等待的内核线程执行完毕。 HANDLE hthread; PKTHREAD thread; PsCreateSystemThread(&hthread, ...); ObReferenceObjectByHandle(hthread, THREAD_ALL_ACCESS, NULL, KernelMode, (PVOID*) &thread, NULL); ZwClose(hthread); KeWaitForSingleObject(&thread, …..);
快速互斥对象(fast mutex) FAST_MUTEX fastmutex; 快速互斥对象通过对无竞争情况的优化处理,可以提供比普通内核互斥对象更快的执行性能。 获取一个快速互斥对象后其拥有者线程一般会被提升到APC_LEVEL级,所以其拥有者在使用某些内核服务函数时会受到限制。 FAST_MUTEX fastmutex; ExInitializeFastMutex(FastMutex); ExAcquireFastMutex(FastMutex); … … … … … … ExReleaseFastMutex(FastMutex);