已阅读5页,还剩3页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
Windows NT提供了五种内核同步对象(Kernel Dispatcher Object),你可以用它们节制非任意线程(普通线程)的流程。表4-1列出了这些内核同步对象的类型及它们的用途。在任何时刻,任何对象都处于两种状况中的一种:信号态或非信号态。有时,今世码运行在某个线程的上下文中时,它可以阻塞这个线程的执行,调用KeWaitForSingleObject或KeWaitForMultipleObjects函数可以使代码(和配景线程)在一个或多个同步对象上等待,等待它们步入信号态。内核为初始化和节制这些对象的状况提供了例程。 表4-1. 内核同步对象 对象 数据类型 描述 Event(事件) KEVENT 阻塞一个线程直至其他线程检测到某事件发生 Semaphore(信号灯) KSEMAPHORE 与事件对象相似,但可以满足任意数量的等待 Mutex(互斥) KMUTEX 执行到关键代码段时,克制其他线程执行该代码段 Timer(定时器) KTIMER 推迟线程执行一段时期 Thread(线程) KTHREAD 阻塞一个线程直至另一个线程结束 在下几段中,我将描述如何使用内核同步对象。我将从何时可以调用等待原语阻塞线程开始讲起,然后讨论用于每种对象的支持例程。最后讨论与线程警惕(thread alert)和提交APC(异步过程调用)相关的概念。 为了理解WDM驱动程序何时和如何利用内核同步对象阻塞一个线程,你必须先对线程有一些基本了解。通常,如果在线程执行时发生了软件或硬件中断,那么在内核处理中断期间,该线程仍然是当前线程。而内核模式代码执行时所在的上下文环境就是指这个当前线程的上下文。为了响应各种中断,Windows NT调度器可能会切换线程,这样,另一个线程将成为新的当前线程。 术语任意线程上下文(arbitrary thread context)和非任意线程上下文(nonarbitrary thread context)用于准确描述驱动程序例程执行时所处于的上下文种类。如果我们知道程序正处于初始化I/O请求线程的上下文中,则该上下文不是任意上下文。然而,在大部分时间里,WDM驱动程序无法知道这个事实,因为节制哪个线程应该激活的机会通常都是在中断发生时。当应用程序发出I/O请求时,将产生一个从用户模式到内核模式的转换,而创建并发送该IRP的I/O管理器例程将继续运行在非任意线程的上下文中。我们用术语最高级驱动程序来描述第一个收到该IRP的驱动程序。 通常,只有给定装备的最高级驱动程序才能确切地知道它执行在一个非任意线程的上下文中。这是因为驱动程序派遣例程通常把请求放入行列步队后立即返回调用者。之后通过回调函数,请求被提外出列步队并下传到初级驱动程序。一旦派遣例程挂起某个请求,所有对该请求的后期处理必发须生在任意线程上下文中。 解释完线程上下文后,我们可以陈诉出关于线程阻塞的简单法则: 当我们处理某个请求时,仅能阻塞产生该请求的线程。 通常,仅有装备的最高级驱动程序才能应用这个法则。但有一个重要的例外,IRP_MN_START_DEVICE请求,所有驱动程序都以同步方式处理这个请求。即驱动程序不排队或挂起该类请求。当你收到这种请求时,你可以直接从堆栈中找到请求的发起者。正如我在第六章中讲到的,处理这种请求时你必须阻塞那个线程。 下面法则表明在晋升的IRQL级上不可能发生线程切换: 执行在高于或等于DISPATCH_LEVEL级上的代码不能阻塞线程。 这个法则表明你只能在DriverEntry函数、AddDevice函数,或驱动程序的派遣函数中阻塞当前线程。因为这些函数都执行在PASSIVE_LEVEL级上。没有必要在DriverEntry或AddDevice函数中阻塞当前线程,因为这些函数的工作仅仅是初始化一些数据布局。 你可以按下面要领调用KeWaitForSingleObject函数: ASSERT(KeGetCurrentIrql() = DISPATCH_LEVEL); LARGE_INTEGER timeout; NTSTATUS status = KeWaitForSingleObject(object, WaitReason, WaitMode, Alertable, &timeout); ASSERT语句指出必须在低于或等于DISPATCH_LEVEL级上调用该例程。 在这个调用中,object指向你有待的对象。注意该参数的类型是PVOID,它应该指向一个表4-1中列出的同步对象。该对象必须在非分页内存中,例如,在装备扩大中或其他从非分页内存池中分配的数据区。在大部分情况下,执行堆栈可以被认为是非分页的。 WaitReason是一个纯粹建议性的值,它是KWAIT_REASON列举类型。实际上,除非你指定了WrQueue参数,否则任何内核代码都不关心此值。线程阻塞的原因被保存到一个不透明的数据布局中,如果你了解这个数据布局,那么在调试某种死锁时,你也许会从这个原因代码中患上到一些线索。通常,驱动程序应把该参数指定为Executive,代表无原因。 WaitMode是MODE列举类型,该列举类型仅有两个值:KernelMode和UserMode。 Alertable是一个布尔类型的值。它不同于WaitReason,这个参数以另一种方式影响系统行为,它决定等待是不是可以提前终止以提交一个APC。如果等待发生在用户模式中,那么内存管理器就可以把线程的内核模式堆栈换出。如果驱动程序以自动变量(在堆栈中)形式创建事件对象,并且某个线程又在晋升的IRQL级上调用了KeSetEvent,而此时该事件对象恰好又被换出内存,结果将产生一个bug check。所以我们应该总把alertable参数指定为FALSE,即在内核模式中等待。 最后一个参数&timeout是一个64位超时值的地址,单位为100纳秒。正数的超时表示一个从1601年1月1日起的绝对时间。调用KeQuerySystemTime函数可以获恰当前系统时间。负数代表相对于当前时间的时间间隔。如果你指定了绝对超时,那么系统钟表的改变也将影响到你的超时时间。如果系统时间越过你指定的绝对时间,那么永远都不会超时。相反,如果你指定相对超时,那么你颠末的超时时间将不受系统钟表改变的影响。 为啥子是1601年1月1日 许早年,当我第一次学习Win32 API时,我曾迷惑为啥子选择1601年1月1日作为Windows NT的时间起点。在我写了一组时间转换函数后我明白了这个不懂的题目的原因。每小我私人都知道可被4整除的年份是闰年。许多人也知道世编年(如1900年)应例外,虽然这些年份都能被4整除但它们不是闰年。少数人还知道能被400整除的年份(如1600和2000)是例外中的例外,它们也是闰年。而1601年1月1日正好是一个400年周期的开始。如果把它作为时间信息的起点,那么把NT时间信息转换为常规日子抒发(或相反)就不用做任何跳跃操作。 指定0超时将使KeWaitForSingleObject函数立即返回,返回的状况代码指出对象是不是处于信号态。如果你的代码执行在DISPATCH_LEVEL级上,则必须指定0超时,因为在这个IRQL上不允许阻塞。每一个内核同步对象都提供一组KeReadStateXxx办事函数,使用这些函数可以直接患上到对象的状况。然而,取对象状况与0超时等待不完全等价:当KeWaitForSingleObject发现等待被满足后,它执行特殊对象要求的附加动作。相比之下,取对象状况不执行任何附加动作,即使对象已经处于信号态。 超时参数也可以指定为NULL指针,这代表无限期等待。 该函数的返回值指出几种可能的结果。STATUS_SUCCESS结果是你所希望的,表示等待被满足。即在你调用KeWaitForSingleObject时,对象或者已经步入信号态,或者后来步入信号态。如果等待以第二种情况满足,则有必要在同步对象上执行附加动作。当然,这个附加动作还要参考对象的类型,我将在后面讨论具体对象类型时再解释这一点。(例如,一个同步类型的事件在你的等待满足后需要重置该事件) 返回值STATUS_TIMEOUT指出在指定的超时期限内对象未步入信号态。如果指定0超时,则函数将立即返回。返回代码为STATUS_TIMEOUT,代表对象处于非信号态,返回代码为STATUS_SUCCESS,代表对象处于信号态。如果指定NULL超时,则不可能有返回值。 其他两个返回值STATUS_ALERTED和STATUS_USER_APC表示等待提前终止,对象未步入信号态。原因是线程接收到一个警惕(alert)或一个用户模式的APC。 KeWaitForMultipleObjects函数用于同时等待一个或多个同步对象。该函数调用方式如下: ASSERT(KeGetCurrentIrql() = DISPATCH_LEVEL); LARGE_INTEGER timeout; NTSTATUS status = KeWaitForMultipleObjects(count, objects, WaitType, WaitReason, WaitMode, Alertable, &timeout, waitblocks); 在这搭,objects指向一个指针数组,每一个数组元素指向一个同步对象,count是数组三拇指针的个数。count必须小于或等于MAXIMUM_WAIT_OBJECTS值(当前为64)。这个数组和它所指向的所有对象都必须在非分页内存中。WaitType是列举类型,其值可以为WaitAll或WaitAny,它指出你是等到所有对象都步入信号态,照旧只要有一个对象步入信号态就可以。 waitblocks参数指向一个KWAIT_BLOCK布局数组,内核用这个布局数组管理等待操作。你不需要初始化这些布局,内核仅需要知道这个布局数组在哪里,内核用它来记载每一个对象在等待中的状况。如果你仅需有待小数量的对象(不超过THREAD_WAIT_OBJECTS,该值患上前为3),你可以把该参数指定为NULL。如果该参数为NULL,KeWaitForMultipleObjects将使用线程对象中预分配的等待块数组。如果你等待的对象数超过THREAD_WAIT_OBJECTS,你必须提供一块长度至少为count * sizeof(KWAIT_BLOCK)的非分页内存。 其余参数与KeWaitForSingleObject中的对应参数作用相同,而且大部分返回码也有相同的含义。 如果你指定了WaitAll,则返回值STATUS_SUCCESS表示等待的所有对象都步入了信号态。如果你指定了WaitAny,则返回值在数值上等于步入信号态的对象在objects数组中的索引。如果碰巧有多个对象步入了信号态,则该值仅代表其中的一个,可能是第一个也可能是其他。你可以认为该值等于STATUS_WAIT_0加上数组索引。你可以先用NT_SUCCESS测试返回码,然后再从其中提取数组索引: NTSTATUS status = KeWaitForMultipleObjects(.); if (NT_SUCCESS(status) ULONG iSignalled = (ULONG) status - (ULONG) STATUS_WAIT_0; . 如果KeWaitForMultipleObjects返回成功代码,它也将执行等待被满足的那个对象的附加动作。如果多个对象同时步入信号态而你指定的WaitType参数为WaitAny,那么该函数仅执行返回值指定对象的附加动作。 表4-2列出了用于处理内核事件的办事函数。为了初始化一个事件对象,我们首先应该为其分配非分页存储,然后调用KeInitializeEvent: ASSERT(KeGetCurrentIrql() = PASSIVE_LEVEL); KeInitializeEvent(event, EventType, initialstate); event是事件对象的地址。EventType是一个列举值,可以为NotificationEvent或SynchronizationEvent。通知事件(notification event)有这样的特性,当它步入信号态后,它将一直处于信号态直至你明确地把它重置为非信号态。这个之外,当通知事件步入信号态后,所有在该事件上等待的线程都被开释。这与用户模式中的手动重置事件相似。而对于同步事件(synchronization event),只要有一个线程被开释,该事件就被重置为非信号态。这又与用户模式中的自动重置事件相同。而KeWaitXxx函数在同步事件对象上执行的附加动作就是把它重置为非信号态。最后的参数initialstate是布尔量,为TRUE表示事件的初始状况为信号态,为FALSE表示事件的初始状况为非信号态。 表4-2. 用于内核事件对象的办事函数 办事函数 描述 KeClearEvent 把事件设置为非信号态,不报告之前的状况 KeInitializeEvent 初始化事件对象 KeReadStateEvent 取事件的当前状况 KeResetEvent 把事件设置为非信号态,返回之前的状况 KeSetEvent 把事件设置为信号态,返回之前的状况 注意 在这些关于同步原语的段中,我还要再谈论一下DDK文档中对IRQL的使用限制。在当前发行的Windows 2000中,DDK有时比OS实际要求的有更多的限制。例如,KeClearEvent可以在任何IRQL上调用,但DDK却要求调用者必须在低于或等于DISPATCH_LEVEL级上调用。KeInitializeEvent也可以在任何IRQL上调用,但DDK要求仅在PASSIVE_LEVEL级上调用该函数。然而,你应该尊重DDK中的描述,也许某一天Microsoft会利用文档中的这些限制。 调用KeSetEvent函数可以把事件置为信号态: ASSERT(KeGetCurrentIrql() = DISPATCH_LEVEL); LONG wassignalled = KeSetEvent(event, boost, wait); 在上面代码中,ASSERT语句强制你必须在低于或等于DISPATCH_LEVEL级上调用该函数。event参数指向一个事件对象,boost值用于晋升等待线程的优先级。wait参数的解释见文字框KeSetEvent的第三个参数,WDM驱动程序几乎从不把wait参数指定为TRUE。如果该事件已经处于信号态,则该函数返回非0值。如果该事件处于非信号态,则该函数返回0。 多任务调度器需要人为地晋升等待I/O操作或同步对象的线程的优先级,以避免饿死长时间等待的线程。这是因为被阻塞的线程往往是抛却自己的时间片并且再也不要求患上到CPU,但只要这些线程患上到了比其他线程更高的优先级,或者其他统一优先级的线程用完了自己的时间片,它们就可以恢复执行。注意,正处于自己时间片中的线程不能被阻塞。 用于晋升阻塞线程优先级的boost值不太好选择。一个较好的笨要领是指定IO_NO_INCREMENT值,当然,如果你有更好的值,可以不用这个值。如果事件唤醒的是一个处理时间敏感数据流的线程(如声卡驱动程序),那么应该使用适合那种装备的boost值(如IO_SOUND_INCREMENT)。重要的是,不要为一个愚蠢的理由去提高等待者的优先级。例如,如果你要同步处理一个IRP_MJ_PNP请求,那么在你要停下来等待初级驱动程序处理完该IRP时,你的完成例程应调用KeSetEvent。由于PnP请求对于处理器没有特殊要求并且也不时常发生,所以即使是声卡驱动程序也也应该把boost参数指定为IO_NO_INCREMENT。 KeSetEvent的第三个参数 wait参数的目的是允许在内部快速地把节制从一个线程传递到另一个线程。除了装备驱动程序之外,大部分系统部件都可以创建双事件对象。例如,客户线程和办事器线程使用双事件对象来界定它们的通讯。当办事器线程需要唤醒对应的客户线程时,它首先调用KeSetEvent函数,并指定wait参数为TRUE,然后立即调用KeWaitXxx函数使自己进睡着眠状况。由于这两个操作都以原子方式完成,所以在节制交代时没有其他线程被唤醒。 DDK总是稍稍地描述一些内部细节,但我发现有些描述另人迷惑。我将以另一种方式解释这些内部细节,看过这些细节后你就会明白为啥子我们总指定这个参数为FALSE。在内部,内核使用一个同步数据库锁(dispatcher database lock)来保护线程的阻塞、唤醒,和调度操作。KeSetEvent函数需要获取这个锁,KeWaitXxx函数也是这样。如果你把这个参数指定为TRUE,则KeSetEvent函数将设置一个标志以便KeWaitXxx函数知道你使用了TRUE参数,然后它返回,并且不开释这个锁。当你后来(应该立即调用,因为你此时正运行在一个比任何硬件装备都高的IRQL上,并且你占有着一个被极其频繁争夺的自旋锁)调用KeWaitXxx函数时,它没必要再获取这个锁。产生的效果就是你唤醒了等待的线程并同时把自己置睡着眠状况,而不给其他线程任何运行的机会。 你应该明白,以wait参数为TRUE调用KeSetEvent的函数必须存在于非分页内存中,因为它在某段时间内执行在晋升的IRQL上。很难想象一个普通装备驱动程序会需要使用这种机制,因为驱动程序决不会比内核更了解线程的调度。底线是:对该参数总使用FALSE。实际上,Microsoft暴露该参数给我们的原因仍不十分清楚。 调用KeReadStateEvent函数(在任何IRQL上)可以测试事件的当前状况: LONG signalled = KeReadStateEvent(event); 返回值不为0代表事件处于信号态,为0代表事件处于非信号态。 注意 Windows 98不支持KeReadStateEvent函数,但支持上面描述的其他KeReadStateXxx函数。为了患上到事件的状况,我们必须使用Windows 98的其他同步原语。 调用KeResetEvent函数(在低于或等于DISPATCH_LEVEL级)可以立即患上到事件对象的当前状况,但该函数会把事件对象重置为非信号状况。 ASSERT(KeGetCurrentIrql() = DISPATCH_LEVEL); LONG signalled = KeResetEvent(event); 如果你对事件的上一个状况不感乐趣,可以调用KeClearEvent函数,象下面这样: ASSERT(KeGetCurrentIrql() = DISPATCH_LEVEL); KeClearEvent(event); KeClearEvent函数执行患上更快,因为它在读取事件的当前状况后不设置事件为非信号态。 内核模式信号灯是一个有同步语义的整数计数器。信号灯计数器为正值时代表信号态,为0时代表非信号态。计数器不能为负值。开释信号灯将使信号灯计数器增1,在一个信号灯上等待将使该信号灯计数器减1。如果计数器值被减为0,则信号灯步入非信号态,之后其他调用KeWaitXxx函数的线程将被阻塞。注意如果等待线程的个数超过了计数器的值,那么其实不是所有等待的线程都可以恢复运行。 内核提供了三个办事函数来节制信号灯对象的状况。(见表4-3) 信号灯对象应该在PASSIVE_LEVEL级上初始化: ASSERT(KeGetCurrentIrql() = PASSIVE_LEVEL); KeInitializeSemaphore(semaphore, count, limit); 在这个调用中,semaphore参数指向一个在非分页内存中的KSEMAPHORE对象。count是信号灯计数器的初始值,limit是计数器能达到的最大值,它必须与信号灯计数器的初始值相同。 表4-3. 内核信号灯对象办事函数 办事函数 描述 KeInitializeSemaphore 初始化信号灯对象 KeReadStateSemaphore 取信号灯当前状况 KeReleaseSemaphore 设置信号灯对象为信号态 如果你创建信号灯时指定limit参数为1,则该对象与仅有一个线程的互斥对象类似。但内核互斥对象有一些信号灯没有的特征,这些特征用于防止死锁。所以,没有必要创建limit为1的信号灯。 如果你以一个大于1的limit值创建信号灯, 则该信号灯允许多个线程同时访问某些资源。在行列步队理论中我们会发现同样的原理,单行列步队可以被多个办事程序使用。多个办事程序使用一个行列步队要比每一个办事程序都有各自的行列步队更合理。这两种形式的平均等待时间是相同的,但前者的等待次数更少。使用信号灯,你可以把一组软件或硬件办事程序根据行列步队原理组织起来。 信号灯的所有者可以调用KeReleaseSemaphore函数开释信号灯: ASSERT(KeGetCurrentIrql() = DISPATCH_LEVEL); LONG wassignalled = KeReleaseSemaphore(semaphore, boost, delta, wait); 这搭出现了一个delta参数,它必须为正数,该函数把delta值加到semaphore指向的信号灯计数器上,这将把信号灯带入信号态,并使等待线程开释。通常,该参数应该指定为1,代表有一个所有者开释了它的权利。boost和wait参数与在KeSetEvent函数中的作用相同。返回值为0代表信号灯的前一个状况是非信号态,非0代表信号灯的前一个状况为信号态。 KeReleaseSemaphore不允许你把计数器的值增长到超过limit指定的值。如果你这样做,该函数底子就不调整计数器的值,它将产生一个代码为STATUS_SEMAPHORE_LIMIT_EXCEEDED的异样。除非系统中存在捕捉该异样的处理程序,否则将导致一个bug check。 下面调用读取信号灯的当前状况: ASSERT(KeGetCurrentIrql() = DISPATCH_LEVEL); LONG signalled = KeReadStateSemaphore(semaphore); 非0返回值表示信号灯处于信号态,0返回值代表信号灯为非信号态。不要把该返回值假定为计数器的当前值。 内核互斥对象 互斥(mutex)就是互相排斥(mutual exclusion)的简写。内核互斥对象为多个竞争线程串行化访问共享资源提供了一种要领(不一定是最好的要领)。如果互斥对象不被某线程所领有,则它是信号态,与之相反则是非信号态。当线程为了患上到互斥对象的节制权而调用KeWaitXxx例程时,内核同时也做了一些工作以帮助避免可能的死锁。同样,互斥对象也需要与KeWaitForSingleObject类似的附加动作。内核可以确保线程不被换出,并且阻止所有APC的提交,内核专用APC(如IoCompleteRequest用以完成I/O请求的APC)不计算在内。 通常我们应该使用executive部件输出的快速互斥对象而不是内核互斥对象。这二者的主要不同是,内
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- MTSC2025 第十四届中国互联网测试开发大会(上海站):华为ADC Code Agent 评测实践
- 起重机械安全教育课件
- 机械行业安全操作规范试题集及解析
- 企业管理-系统岗位职责说明书 SOP
- 居家健身锻炼方法测试题及参考答案
- 健身爱好者体能训练计划测试及指导答案
- 开车心理测试题实战解析与指导
- 环境保护意识互动测试与答案集
- 紧急事件处理指南专业应急处理知识测试与答案详解
- 建筑教师招聘考试指南与备考策略
- 奠基仪式活动报价表
- 跌倒-坠床不良事件鱼骨图分析
- 云南省暴雨洪水查算实用手册簿92年版(正式版)
- 甲型流感患者护理查房
- 2020.2.1芜湖审计局造价咨询服务方案-技术标【暗标】
- 2022年上海商学院C语言冲刺卷(十二套试卷)及答案
- 桥梁南接线工程投标文件
- GB/T 34800-2017蛋白酶K酶活力及杂质检测方法
- GB/T 17316-2011水稻原种生产技术操作规程
- 休闲旅游人员推销步骤
- 2022年安康市交通建设投资集团有限公司招聘笔试试题及答案解析
评论
0/150
提交评论