vipfengxiao
驱动牛犊
驱动牛犊
  • 注册日期2009-12-29
  • 最后登录2011-12-21
  • 粉丝1
  • 关注0
  • 积分79分
  • 威望681点
  • 贡献值0点
  • 好评度0点
  • 原创分0分
  • 专家分0分
阅读:1781回复:0

KeSetEvent

楼主#
更多 发布于:2010-05-10 10:28
内核事件 表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部件输出的快速互斥对象而不是内核互斥对象。这两者的主要不同是,内核互斥可以被递归获取,而executive快速互斥则不能。即内核互斥的所有者可以调用KeWaitXxx并指定所拥有的互斥对象从而使等待立即被满足。如果一个线程真的这样做,它必须也要以同样的次数释放该互斥对象,否则该互斥对象不被认为是空闲的。 如果你需要长时间串行化访问一个对象,你应该首先考虑使用互斥(而不是依赖提升的IRQL和自旋锁)。利用互斥对象控制资源的访问,可以使其它线程分布到多处理器平台上的其它CPU中运行,还允许导致页故障的代码仍能锁定资源而不被其它线程访问。表4-4列出了互斥对象的服务函数。 表4-4. 互斥对象服务函数 服务函数 描述 KeInitializeMutex 初始化互斥对象 KeReadStateMutex 取互斥对象的当前状态 KeReleaseMutex 设置互斥对象为信号态为了创建一个互斥对象,你需要为KMUTEX对象保留一块非分页内存,然后象下面这样初始化: ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);KeInitializeMutex(mutex, level); mutex是KMUTEX对象的地址,level参数最初是用于辅助避免多互斥对象带来的死锁。但现在,内核忽略level参数。 互斥对象的初始状态为信号态,即未被任何线程拥有。KeWaitXxx调用将使调用者接管互斥对象的控制并使其进入非信号态。 利用下面函数可以获取互斥对象的当前状态: ASSERT(KeGetCurrentIrql() <= DISPATCH_LEVEL);LONG signalled = KeReadStateMutex(mutex); 返回值0表示互斥对象已被占用,非0表示未被占用。 下面函数可以使所有者放弃其占有的互斥对象并使其进入信号态: ASSERT(KeGetCurrentIrql() == PASSIVE_LEVEL);LONG wassignalled = KeReleaseMutex(mutex, wait); wait参数与KeSetEvent函数中的含义相同。该函数返回值总是0,表示该互斥对象曾被占用过,如果不是这种情况(所有者释放的不是它自己的对象),KeReleaseMutex将产生bug check。 出于完整性的考虑,我想提一下KeWaitForMutexObject函数,它是DDK中的宏(见WDM.H)。其定义如下: #define KeWaitForMutexObject KeWaitForSingleObject
游客

返回顶部