wdm 驱动程序设计

40
WDM 驱驱驱驱驱驱 驱驱驱驱 驱 5 驱

Upload: bowie

Post on 17-Mar-2016

249 views

Category:

Documents


11 download

DESCRIPTION

WDM 驱动程序设计. 第 5 讲. 同步技术. 主要内容. 一个同步问题的例子 中断请求级 自旋锁 内核同步对象 其它内核同步原语. 一个同步问题的例子. 下面利用静态变量 lActiveRequests 记录当前未完成的 I/O 请求数:. static LONG lActiveRequests; NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp) { ++lActiveRequests; ... // process PNP request --lActiveRequests; }. - PowerPoint PPT Presentation

TRANSCRIPT

WDM驱动程序设计同步技术第 5 讲

主要内容 一个同步问题的例子 中断请求级 自旋锁 内核同步对象 其它内核同步原语

一个同步问题的例子下面利用静态变量 lActiveRequests 记录当前未完成的I/O 请求数: static LONG lActiveRequests;NTSTATUS DispatchPnp(PDEVICE_OBJECT fdo, PIRP Irp){ ++lActiveRequests; ... // process PNP request --lActiveRequests;}

有什么问题?关于语句“ ++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 指令不能被中断,但是多处理器环境中仍然是不安全的,因为这两个指令都是由几条微代码实现的。

最终解决办法 // ++lActiveRequests; lock inc lActiveRequests // --lActiveRequests; lock dec lActiveRequests

LOCK 指令前缀可以使当前执行多微码指令的 CPU 锁定总线,从而保证数据访问的完整性。

两个最差的假定驱动程序开发者必须做如下两个最差的假定:1. 操作系统可以在任何时间抢先任何例程并停留任何长的时间,所以我们不能保证自己的任务不被干扰或延迟。2. 即使我们能防止被抢先,但其它 CPU 上执行的代码也会干扰我们代码的执行,甚至一个程序的代码可以在两个不同线程的上下文中并发执行。

同步请求级一个确定的 CPU上的活动仅能被拥有更高 IRQL的活动抢先。

IRQL 与线程优先级 线程优先级是与 IRQL 非常不同的概念。线程优先级控制着 OS 线程调度器的调度动作,决定何时抢先运行线程以及下一次运行什么线程。 当 IRQL 级高于或等于 DISPATCH_LEVEL 级时线程切换停止,无论当前活动的是什么线程都将保持活动状态直到 IRQL 降到 DISPATCH_LEVEL级之下。 在进行线程调度时会切换线程上下文;按照 IRQL进行活动抢先时不会切换线程上下文。

利用 IRQL 进行同步 方法:将所有对共享数据的访问都应该在同一 ( 提升的,高于 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) 。 第二, CPU 在等待自旋锁时不做任何有用的工作,仅仅是等待。所以,为了避免影响性能,你应该在拥有自旋锁时做尽量少的操作,因为此时某个 CPU 可能正在等待这个自旋锁。 第三,仅能在低于或等于 DISPATCH_LEVEL 级上请求自旋锁,在你拥有自旋锁期间,内核将把你的代码提升到 DI

SPATCH_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 级上 ,如 DP

C、 StartIo ,和其它执行在 DISPATCH_LEVEL 级上的驱动程序例程,可以调用两个专用函数来操作自旋锁 :KeAcquireSpinLockAtDpcLevel(&pdx->QLock);...KeReleaseSpinLockFromDpcLevel(&pdx->QLock);

内核同步对象 利用内核同步对象可以暂时阻塞一个线程的执行,同步不同线程的执行动作。 内核同步对象仅影响 OS 线程调度器的调度动作,因此一般只在低于 DISPATCH_LEVEL 级的代码中用于阻塞线程。 在驱动程序中,只能在“非任意线程上下文”条件下利用内核同步对象阻塞调用者的线程或产生该请求的线程。 在“任意线程上下文”调用等待原语只会阻塞一个“无辜”的线程。

非任意线程上下文 如果驱动程序的回调例程能确切知道处于哪个线程上下文中,则称处于“非任意线程上下文”;大部分时间里,驱动程序无法知道这个事实,即处于“任意线程上下文”中。 非任意线程上下文的例子:1. 设备的最高级驱动程序的 IRP 处理函数可以确切地知道它执行在发出该 I/O 请求的应用程序线程的上下文中。2. PNP类 IRP 的处理函数可以确切地知道它执行在一个系统线程 (System Thread) 中。3. 在你自己创建的内核模式系统线程中。 (PsCreateSystemThrea

d)4. 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参数含义 object 指向要等待的对象,它应该指向一个上面表中列出的同步对象。该对象必须在非分页内存中。 WaitReason 是一个纯粹建议性的值, KWAIT_REASON枚举型,一般取值为 Executive 。 WaitMode 是 MODE枚举类型,该枚举类型仅有两个值:

KernelMode 和 UserMode 。一般取值为 KernelMode 。 Alertable 参数一般指定为 FALSE 。 timeout 是一个 64位超时值的地址,单位为 100纳秒。正数的超时表示一个从 1601年 1月 1日起的绝对时间。负数代表相对于当前时间的时间间隔。 指定为 0 将使等待函数立即返回。指定为 NULL 代表无限期等待。

KeWaitForSingleObject(object, WaitReason, WaitMode, Alertable, &timeout);

KeWaitForSingleObject返回值含义 STATUS_SUCCESS ,表示等待被满足。即你调用 KeWaitForSingleObject 时,对象或者已经进入信号态,或者在等待中进入信号态使等待返回。 STATUS_TIMEOUT 指出在指定的超时期限内对象未进入信号态 。如果指定 0超时,则函数将立即返回。返回代码为 STATUS_TIMEOUT ,代表对象处于非信号态,返回代码为 STATUS_ SUCC

ESS ,代表对象处于信号态。 其它两个返回值 STATUS_ALERTED 和 STATU

S_USER_APC 表示等待提前终止,对象未进入信号态 。

在多个同步对象上等待

objects 指向一个指针数组,每个数组元素指向一个同步对象,count 表示数组中指针的个数 。

WaitType 是枚举类型,其值可以为 WaitAll 或 WaitAny ,它指出你是等到所有对象都进入信号态,还是只要有一个对象进入信号态就可以。 waitblocks参数指向一个 KWAIT_BLOCK 结构数组,内核用它来记录每个对象在等待中的状态。 不需要你对其进行初始化。

ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);LARGE_INTEGER timeout;NTSTATUS status = KeWaitForMultipleObjects(count, objects,

WaitType, WaitReason, WaitMode, Alertable, &timeout, waitblocks);

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 或 Syn

chronizationEvent 。 initialstate 是布尔量,为 TRUE 表示事件的初始状态为信号态,为 FALSE 表示事件的初始状态为非信号态。

KeSetEvent 函数 调用 KeSetEvent 函数可以把事件置为信号态: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, …..);……..ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);KeReleaseMutex(&mutex, wait);

内核定时器 (Timer) Timer 对象可以在指定的绝对时间或间隔时间后自动从非信号态变为信号态。它还可以周期性的进入信号态。 可以利用 KeWaitXxxx 函数等待一个 Timer 对象在某个时间间隔后进入信号态,也可以利用 Timer 对象安排一个在某个时间间隔后或定期执行的 DPC回调函数。 定时器也分为通知型和同步型两种。通知型定时器及时结束后一直处于信号态,除非手动改变。因此,所有等待它的线程都被释放。同步定时器正相反,它只允许有一个等待线程。一旦有线程在这种定时器上等待,并且开始执行,定时器就自动进入非信号态。

内核定时器的服务函数服务函数 功能

KeInitializeTimer 初始化一次性的通知型定时器KeInitializeTimerEx 初始化一次性的或周期性的通知型的或同步型定时器 KeSetTimer 为通知型定时器设定时间或 DPC 对象KeSetTimerEx 为定时器设定时间、周期和其它属性KeCancelTimer 取消一个定时器KeReadStateTimer 获取定时器的当前状态。

一次性定时器的用法KTIMER timer; // someone gives you thisASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);KeInitializeTimerEx(&timer, NotificationTimer);// KeInitializeTimer(timer);LARGE_INTEGER duetime;KeSetTimer(&timer, duetime, NULL);KeWaitForSingleObject(&timer, ......);..........

周期性定时器的用法KTIMER timer; // someone gives you thisASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);KeInitializeTimerEx(&timer, SynchronizationTimer);LARGE_INTEGER duetime;long period;KeSetTimerEx(&timer, duetime, period, NULL);while(True) {

KeWaitForSingleObject(&timer, ......);..........

}KeCancelTimer(&timer);

定时器与 DPCPKDPC dpc; // points to KDPC you've allocatedASSERT(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) 代表一个内核线程,可以利用 K

eWaitXxx 等待原语在一个内核线程上进行等待,等待者会被一直阻塞直到所等待的内核线程执行完毕。HANDLE hthread;PKTHREAD thread;PsCreateSystemThread(&hthread, ...);ObReferenceObjectByHandle(hthread, THREAD_ALL_ACCESS,

NULL, KernelMode, (PVOID*) &thread, NULL);ZwClose(hthread);KeWaitForSingleObject(&thread, …..);

快速互斥对象 (fast mutex) 快速互斥对象通过对无竞争情况的优化处理,可以提供比普通内核互斥对象更快的执行性能。 获取一个快速互斥对象后其拥有者线程一般会被提升到 A

PC_LEVEL 级,所以其拥有者在使用某些内核服务函数时会受到限制。FAST_MUTEX fastmutex;ExInitializeFastMutex(FastMutex);ExAcquireFastMutex(FastMutex);… … … … … … ExReleaseFastMutex(FastMutex);