驱动层面的调度和同步一向是内核中比较困难的部分,和应用层不一样,内核位于系统进程下,所以它的调度和同步一旦出现纰漏,那会影响所有的程序,而内核并不具备对于这种情况下的纠错能力,没有异常手段能够让挂起的系统恢复原样,除了关闭电源之外,并且这个过程可能会需要进入安全模式来处理,这对于病毒来说可能是一个好消息,但对于要稳定运行的驱动程序来说,就意味着编写难度的上升。
内核定义一组称为内核调度程序对象的对象类型,或仅定义调度程序对象。分发器对象包括计时器对象、事件对象、信号灯对象、互斥对象和线程对象。
驱动程序可以将调度程序对象用作非比特线程上下文中的同步机制,同时在 IRQL 执行时等于PASSIVE_LEVEL。
内核对象对于windows开发工程师来说并不是很稀奇的名称,在应用层编程的时候,事件、信号量、线程这些对象本身就是内核对象,看起来它们的用法和应用层一样,不过,区别在于,某些情况下使用内核对象不当,后果是灾难性的。
分发器的对象状态
每个内核定义的调度程序对象类型都有一个状态,该状态要么设置为 Signaled,要么设置为 Not-Signaled。
如果一个或多个线程调用 KeWaitForSingleObject、KeWaitForMutexObject 或 KeWaitForMultipleObjects,则一组线程可以同步其操作。 这些函数将调度程序对象指针作为输入并等待,直到另一个例程或线程将一个或多个调度程序对象设置为“已信号”状态。
当线程调用 KeWaitForSingleObject 以等待调度程序对象 (或 KeWaitForMutexObject 以获取互斥) 时,线程将进入等待状态,直到调度程序对象设置为Signaled状态。 线程可以调用 KeWaitForMultipleObjects 来等待一组调度程序对象的任意或全部设置为Signaled。
每当调度程序对象设置为“已发出信号”状态时,内核就会更改等待该对象 就绪的任何线程的状态。 (同步计时器和同步事件是此规则的例外情况;当向同步事件或计时器发出信号时,只会将一个等待线程设置为就绪状态。有关详细信息,请参阅 计时器对象和 DPC 和 事件对象。) 处于就绪状态的线程将根据其当前运行时 线程优先级 和具有该优先级的任何线程的处理器的当前可用性来计划运行。
如何等待分发器对象?
通常,仅当以下至少一种情况为 true 时,驱动程序才能等待调度程序对象设置:
- 驱动程序在非比特线程上下文中执行,可以标识将进入等待状态的线程。 实际上,在非比特线程上下文中执行的唯一驱动程序例程是任何驱动程序的 DriverEntry、 AddDevice、 Reinitialize 和 Unload 例程,以及最高级别驱动程序的调度例程。 所有这些例程都由系统直接调用。
- 驱动程序正在执行完全同步的 I/O 请求,在处理 I/O 请求时,没有驱动程序会将任何操作排入队列,并且直到其下面的驱动程序处理完请求,否则不会返回任何驱动程序。
此外,如果驱动程序在 IRQL 或高于等于 DISPATCH_LEVEL 执行,则无法进入等待状态。根据这些限制,必须使用以下规则:
- 任何驱动程序的 DriverEntry、 AddDevice、 Reinitialize 和 Unload 例程都可以等待调度程序对象;
- 最高级别驱动程序的调度例程可以等待调度程序对象;
- 如果 I/O操作是同步的,则较低级别驱动程序的调度例程可以等待调度对象,例如创建、刷新、关闭和关闭操作、某些设备 I/O 控制操作以及某些 PnP 和电源操作;
- 较低级别驱动程序的调度例程不能等待调度程序对象完成异步 I/O 操作;
- 在 IRQL DISPATCH_LEVEL或更高位置执行的驱动程序例程不得等待调度程序对象设置为“已信号”状态;
- 驱动程序不得尝试等待调度程序对象设置为信号状态,以便完成与分页设备的传输操作;
- 为读/写请求提供服务的驱动程序调度例程通常不能等待调度程序对象设置为“已信号”状态;
- 仅当 I/O 控制代码的传输类型METHOD_BUFFERED时,设备 I/O 控制请求的调度例程才能等待调度程序对象设置为“已信号”状态;
- SCSI 微型端口驱动程序不应使用内核调度程序对象。 SCSI 微型端口驱动程序应仅调用 SCSI 端口库例程;
所有其他标准驱动程序例程在任意线程上下文中执行:调用驱动程序例程处理排队操作或处理设备中断时,任何线程的线程都恰好是当前线程。 此外,大多数标准驱动程序例程都是在引发的 IRQL 中运行,无论是在 DISPATCH_LEVEL还是DIRQL 。
如有必要,驱动程序可以创建设备专用线程,该线程可以等待驱动程序的其他例程 ,但 ISR 或 SynchCritSection 例程除外, 将调度程序对象设置为信号状态并重置为Not-Signaled状态。
一般指导原则是,如果预计新设备驱动程序在 I/O 操作期间等待设备状态更改时通常需要停止超过 50 微秒,请考虑使用设备专用线程实现驱动程序。 如果设备驱动程序也是最高级别的驱动程序,请考虑使用 系统工作线程 并实现一个或多个工作线程回调例程。
事件
任何使用事件对象的驱动程序都必须调用 KeInitializeEvent、 IoCreateNotificationEvent 或 IoCreateSynchronizationEvent ,然后才能等待、设置、清除或重置事件。 下图演示了具有线程的驱动程序如何使用事件对象进行同步:
如上图所示,此类驱动程序必须为事件对象提供存储,该事件对象必须是驻留的。 驱动程序可以使用驱动程序创建的设备对象的设备扩展 、控制器扩展或驱动程序分配的非分页池。
当驱动程序调用 KeInitializeEvent 时,它必须传递指向事件对象的驱动程序常驻存储的指针。 此外,调用方必须为事件对象指定初始状态 (已发出信号或未发出信号) 。 调用方还必须指定事件类型,可以是以下任一类型:
- SynchronizationEvent: 当同步事件设置为“已信号”状态时,等待事件重置为Not-Signaled的单个线程将有资格执行,并且事件的状态会自动重置为“未发出信号”。这种类型的事件有时称为 自动清理事件,因为每次满足等待时,其信号状态都会自动重置。
- NotificationEvent:当通知事件设置为“已信号”状态时,等待事件重置为Not-Signaled的所有线程都将符合执行条件,并且事件将保持为“已信号”状态,直到发生显式重置Not-Signaled:也就是说,使用给定的事件指针调用 KeClearEvent 或 KeResetEvent。
很少有设备或中间驱动程序具有单个驱动程序专用线程,更不用说一组线程,这些线程可能会通过等待保护共享资源的事件来同步其操作。
大多数使用事件对象等待 I/O操作完成的驱动程序在调用 KeInitializeEvent 时将输入类型设置为 NotificationEvent。 为驱动程序使用 IoBuildSynchronousFsdRequest 或 IoBuildDeviceIoControlRequest 创建的 IRP 设置的事件对象几乎总是初始化为 NotificationEvent, 因为调用方将等待事件的通知,指示其请求已被一个或多个较低级别的驱动程序满足。
驱动程序初始化自身后,其驱动程序专用线程(如果有)和其他例程可以同步其在事件上的操作。 例如,具有管理 IRP 排队的线程的驱动程序(例如系统软盘控制器驱动程序)可能会同步事件上的 IRP 处理,如上图所示:
- 1. 线程已取消排队以在设备上处理 IRP,它使用指向初始化事件对象的驱动程序提供的存储的指针调用 KeWaitForSingleObject ;
- 2. 其他驱动程序例程执行满足 IRP 所需的 I/O 操作,当这些操作完成时,驱动程序的 DpcForIsr 例程使用指向事件对象的指针调用 KeSetEvent ,驱动程序确定的线程优先级提升 (增量,如上图) 所示,布尔 等待 设置为 FALSE。 调用 KeSetEvent 会将事件对象设置为“已信号”状态,从而将等待线程的状态更改为“就绪”;
- 3. 当处理器可用时,内核会立即调度线程以执行:也就是说,当前没有其他具有更高优先级的线程处于就绪状态,并且没有内核模式例程可在更高的IRQL上运行。如果DpcForIsr尚未使用 IRP 调用 IoCompleteRequest ,线程现在可以完成 IRP,并且可以取消排队以在设备上处理的另一个 IRP;
调用将 Wait 参数设置为 TRUE 的 KeSetEvent 表示调用方打算在从 KeSetEvent 返回时立即调用 KeWaitForSingleObject 或 KeWaitForMultipleObjects 支持例程。
请考虑以下有关将Wait参数设置为KeSetEvent 的准则:
- 在 IRQL < DISPATCH_LEVEL运行的可分页线程或可分页驱动程序例程不应调用将 Wait 参数设置为 TRUE 的 KeSetEvent。 如果调用方碰巧在调用 KeSetEvent 和 KeWaitForSingleObject 或KeWaitForMultipleObjects 之间分页,则此类调用会导致严重页面错误;
- 在 IRQL = DISPATCH_LEVEL 下运行的任何标准驱动程序例程都不能等待任何调度程序对象的非零间隔,而不会关闭系统。 但是,此类例程可以在 IRQL 小于或等于 DISPATCH_LEVEL 运行时调用 KeSetEvent ;
- KeResetEvent 返回给定 事件的先前状态:调用 KeResetEvent 时,它是否设置为 Signaled。 KeClearEvent 只是将给定 事件 的状态设置为“未发出信号”;
对于何时调用上述支持例程,请考虑以下准则:
为了提高性能,每个驱动程序都应调用 KeClearEvent ,除非调用方需要 KeResetEvent 返回的信息来确定接下来要执行的操作。
标准事件
系统提供多个标准事件对象。 驱动程序可以使用这些事件对象,每当出现某些情况时,系统就会收到通知。 以下列出了系统包含标准事件对象:
\KernelObjects\HighMemoryCondition:每当可用物理内存量超过系统定义的量时,将设置此事件。 驱动程序可以等待将此事件设置为主动分配内存的信号。
\KernelObjects\LowMemoryCondition:每当可用物理内存量低于系统定义的量时,将设置此事件。 已分配大量内存的驱动程序可以等待将此事件设置为释放未使用的内存的信号。
对于 Microsoft Windows Server 2003 及更高版本的 Windows,驱动程序还可以使用以下其他标准事件对象:
\KernelObjects\HighPagedPoolCondition:每当可用分页池的数量超过系统定义的量时,将设置此事件。 驱动程序可以等待将此事件设置为主动从分页池分配内存的信号。
\KernelObjects\LowPagedPoolCondition:每当可用分页池的数量低于系统定义的量时,将设置此事件。 已分配大量内存的驱动程序可以等待将此事件设置为从分页池中释放未使用的内存的信号。
\KernelObjects\HighNonPagedPoolCondition:每当可用非分页池的数量超过系统定义的量时,将设置此事件。 驱动程序可以等待将此事件设置为主动分配非分页池内存的信号。
\KernelObjects\LowNonPagedPoolCondition:每当可用非分页池的数量低于系统定义的量时,将设置此事件。 已分配大量内存的驱动程序可以等待将此事件设置为从非分页池中释放未使用的内存的信号。
对于 Windows Vista 和更高版本的 Windows,驱动程序还可以使用以下其他标准事件对象:
\KernelObjects\LowCommitCondition:当操作系统的 提交费用 相对于 当前提交限制较低时,将设置此事件。 换句话说,内存使用率较低,物理内存或分页文件中有大量可用空间。
\KernelObjects\HighCommitCondition:当操作系统的提交费用相对于当前提交限制较高时,将设置此事件。 换句话说,内存使用率较高,物理内存或分页文件中的可用空间非常少,但操作系统可能能够增加其分页文件的大小。
\KernelObjects\MaximumCommitCondition:当操作系统的提交费用接近 最大提交限制时,将设置此事件。 换句话说,内存使用率非常高,物理内存或分页文件中的可用空间非常少,操作系统无法增加其分页文件的大小。 如果存在足够的存储资源,系统管理员始终可以增加分页文件的大小或数量,而无需重新启动计算机。
其中每个事件都是通知事件。 只要触发条件保持为 true,它们就会保持设置。
若要打开其中任何事件的句柄,请使用 IoCreateNotificationEvent 例程。 等待其中任何事件的驱动程序应创建一个专用线程来执行等待。 线程可以通过调用 KeWaitForSingleObject 或 KeWaitForMultipleObjects 来等待其中一个或多个事件。