这篇文档记录剩下的内核调度对象。
信号灯
任何驱动程序都可以使用信号量对象在其驱动程序创建的线程和其他驱动程序例程之间同步操作。 例如,当驱动程序没有未完成的 I/O 请求时,驱动程序专用线程可能会将自身置于等待状态,并且驱动程序的调度例程可能会在将 IRP 排入队列后将信号量设置为“已信号”状态。
在请求 I/O 操作的线程上下文中运行的最高级别驱动程序的调度例程可能使用信号灯来保护调度例程之间共享的资源。 用于同步 I/O 操作的较低级别的驱动程序调度例程也可能使用信号灯来保护该调度例程子集之间共享的资源或与驱动程序创建的线程共享的资源。
任何使用信号灯对象的驱动程序都必须在等待或释放信号量之前调用 KeInitializeSemaphore 。 下图演示了具有线程的驱动程序如何使用信号灯对象:
如上图所示,此类驱动程序必须为信号灯对象提供存储,该信号量应驻留。 驱动程序可以使用驱动程序创建 的设备对象的设备扩展 、控制器扩展或驱动程序分配的非分页池。
当驱动程序的 AddDevice 例程调用 KeInitializeSemaphore 时,它必须将指针传递给驱动程序的信号灯对象的常驻存储。 此外,调用方必须为信号量对象指定 Count ,如上图所示,该对象确定信号量) 的初始状态 非零。
调用方还必须指定信号灯 的 Limit ,可以是以下任一项:
- Limit = 1:当此信号灯设置为“已信号”状态时,等待信号灯设置为“已信号”状态的单个线程将有资格执行,并且可以访问信号量保护的任何资源。这种类型的信号灯也称为 二进制信号灯 ,因为线程对信号灯保护的资源具有或没有独占访问权限。
- Limit > 1: 当此信号灯设置为“已信号”状态时,等待信号量对象设置为“已信号”状态的一些线程将有资格执行,并且可以访问信号量保护的任何资源。这种类型的信号量称为 计数信号灯 ,因为将信号量设置为“已信号”状态的例程还指定有多少等待线程可以将其状态从等待更改为“就绪”。 此类等待线程的数量可以是初始化信号灯时设置的 “限制 ”,也可以是低于此预设 限制的某个数量。
很少有设备或中间驱动程序具有单个驱动程序创建的线程;有一组线程可能等待信号灯被获取或释放,甚至更少。 系统提供的驱动程序很少使用信号灯对象,而在这些驱动程序中,使用二元信号灯的驱动程序甚至更少。 尽管二进制信号灯在功能上可能类似于互斥对象,但二进制信号灯不提供针对 SMP 计算机中运行的系统线程的互斥对象具有的死锁的内置保护。
加载具有已初始化信号灯的驱动程序后,它可以同步保护共享资源的信号量上的操作。 例如,具有管理 IRP 排队的设备专用线程的驱动程序(例如系统软盘控制器驱动程序)可能会在信号灯上同步 IRP 队列,如上图所示:
- 1. 线程调用 KeWaitForSingleObject ,其中包含指向初始化信号灯对象的驱动程序提供的存储的指针,使自身处于等待状态;
- 2. 需要设备 I/O 操作的 IRP 开始出现。 驱动程序的调度例程将每个此类 IRP 插入到旋转锁定控制下的互锁队列中,并使用指向信号量对象的指针调用 KeReleaseSemaphore,这是驱动程序确定的线程优先级提升 (增量,如上图) 所示,调整为 1,在每个 IRP 排队时添加到信号灯的 Count 中, 并将布尔等待设置为 FALSE。 非零信号灯计数将信号量对象设置为“已信号”状态,从而将等待线程的状态更改为“就绪”;
- 3. 当处理器可用时,内核会立即调度线程以执行:也就是说,当前没有其他具有更高优先级的线程处于就绪状态,并且没有内核模式例程可在更高的 IRQL 上运行;
- 线程在旋转锁定控制下从互锁队列中删除 IRP,将其传递给其他驱动程序例程以供进一步处理,并再次调用 KeWaitForSingleObject 。 如果信号灯仍设置为信号状态 (即,其计数将保持非零值,表示驱动程序的联锁队列) 中存在更多 IRP,内核再次将线程的状态从“等待”更改为“就绪”;
- 通过以这种方式使用计数信号量,此类驱动程序线程“知道”每当该线程运行时,都会从互锁队列中删除 IRP;
在大于 PASSIVE_LEVEL 的 IRQL 上运行的任何标准驱动程序例程都不能等待任何调度程序对象的非零间隔,而不会关闭系统,但是此类例程可以在 IRQL 小于或等于 DISPATCH_LEVEL 的情况下调用 KeReleaseSemaphore 。
互斥体
顾名思义,互斥对象是一种同步机制,旨在确保对一组内核模式线程之间共享的单个资源进行互斥访问。 只有使用执行工作线程的高级驱动程序,例如文件系统驱动程序 (FSD) 才可能使用互斥对象。
可能具有驱动程序创建的线程或工作线程回调例程的最高级别驱动程序也会使用互斥对象。 但是,任何具有可分页线程或工作线程回调例程的驱动程序都必须非常小心地管理其互斥对象的获取、等待和发布。
互斥对象具有内置功能,仅线程可以互斥、无死锁方式访问 SMP 计算机中的共享资源,互斥对象提供系统内核模式,内核一次将互斥体的所有权分配给单个线程。
获取互斥体的所有权会阻止 (APC) 传递正常的内核模式异步过程调用。 除非内核发出APC_LEVEL软件中断以运行特殊内核 APC(例如 I/O 管理器的 IRP 完成例程,该例程将结果返回到 I/O 操作的原始请求者)时,线程才不会被 APC 抢占。
线程可以获取它已拥有的互斥对象 (递归所有权) ,但在线程完全释放其所有权之前,递归获取的互斥对象不会设置为 Signaled 状态。 此类线程必须在获得所有权后多次显式释放互斥体,然后另一个线程才能获取互斥体。
内核绝不允许拥有互斥锁的线程在不首先释放互斥体并将其设置为“已信号”状态的情况下转换到用户模式。 如果拥有互斥体的任何 FSD 创建或驱动程序创建的线程在释放互斥体的所有权之前尝试将控制权返回到 I/O 管理器,则内核会关闭系统。
任何使用互斥对象的驱动程序都必须调用 KeInitializeMutex 一次,然后才能等待或释放其互斥对象。 下图演示了两个系统线程如何使用互斥体对象:
如上图所示,使用互斥对象的驱动程序必须为互斥对象提供存储,该互斥对象必须是驻留的。 驱动程序可以使用驱动程序创建 的设备对象的设备扩展 、控制器扩展或驱动程序分配的非分页池。
当驱动程序调用 KeInitializeMutex,通常从其 AddDevice 例程 时,它必须将指向驱动程序存储的指针传递给互斥对象,内核会将其初始化为 Signaled 状态。
在这样一个最高级别的驱动程序初始化后,它可以管理对共享资源的互斥访问,如上图所示。 例如,用于固有同步操作和线程的驱动程序调度例程可能会使用互斥体来保护驱动程序创建的 IRP 队列。
由于 KeInitializeMutex始终将互斥对象的初始状态设置为 Signaled (如上图所示) :
- 调度例程使用 Mutex 指针对 KeWaitForSingleObject 进行的初始调用会将当前线程立即进入就绪状态,授予互斥锁的线程所有权,并将互斥状态重置为 Not-Signaled。 调度例程恢复运行后,就可以安全地将 IRP 插入互斥保护的队列中;
- 当第二个线程 (另一个调度例程、驱动程序提供的工作线程回调例程或驱动程序创建的线程) 使用 Mutex 指针调用 KeWaitForSingleObject 时,第二个线程将进入等待状态;
- 当调度例程按照步骤 1 中所述完成对 IRP 的排队时,它会使用 Mutex 指针和布尔等待值调用 KeReleaseMutex,该值指示它打算在 KeReleaseMutex 返回控件后立即调用 KeWaitForSingleObject (还是 KeWaitForMutexObject) ;
- 假设调度例程在步骤 3 中释放了互斥体的所有权, Wait 设置为 FALSE),则 KeReleaseMutex 将互斥体设置为 Signaled 状态。 互斥体当前没有所有者,因此内核确定另一个线程是否正在等待该互斥体。 如果是这样,内核会使第二个线程 () 互斥体所有者看到步骤 2,可能会将线程的优先级提升到最低实时优先级值,并将其状态更改为就绪;
- 当处理器可用时,内核会立即调度第二个线程以供执行:也就是说,当前没有其他具有更高优先级的线程处于就绪状态,并且没有内核模式例程可在更高的 IRQL 上运行。 第二个线程 (调度例程排队 IRP 或驱动程序的工作线程回调例程或驱动程序创建的线程取消排队 IRP) 现在可以安全地访问受互斥保护的 IRP 队列,直到它调用 KeReleaseMutex;
如果线程以递归方式获取互斥对象的所有权,则该线程必须显式调用 KeReleaseMutex 的次数与它在互斥上等待的次数一样多,以便将互斥对象设置为 Signaled 状态。 例如,如果线程使用同一 Mutex 指针调用 KeWaitForSingleObject,然后调用 KeWaitForMutexObject,则在获取 mutex 时,它必须调用 KeReleaseMutex 两次,以便将互斥对象设置为 Signaled 状态。
使用 Wait 参数设置为 TRUE 调用 KeReleaseMutex 表示调用方打算在从 KeReleaseMutex 返回时立即调用 KeWait Xxx 支持例程。
对于将 Wait 参数设置为 KeReleaseMutex,请考虑以下准则:
- 在 IRQL PASSIVE_LEVEL运行的可分页线程或可分页驱动程序例程不应调用将 Wait 参数设置为 TRUE 的 KeReleaseMutex。 如果调用方恰好在 调用 KeReleaseMutex 和 KeWaitXxx对象 之间分页,则此类调用会导致严重页面错误;
- 在大于 PASSIVE_LEVEL 的IRQL上运行的任何标准驱动程序例程都不能等待任何调度程序对象的非零间隔,而不会关闭系统。 但是,如果此类例程在IRQL小于或等于 DISPATCH_LEVEL 运行时拥有互斥体,则可以调用 KeReleaseMutex ;
互斥体的替代
从 Windows 2000 开始,如果驱动程序要求以 IRQL <= APC_LEVEL 运行的代码采用低开销互斥形式,则可以使用快速互斥。 快速互斥可以保护一次只能由一个线程输入的代码路径。 若要输入受保护的代码路径,线程将获取互斥体。 如果另一个线程已获取互斥体,则当前线程的执行将暂停,直到释放互斥体。 若要退出受保护的代码路径,线程 会释放互斥体。
从 Windows Server 2003 开始,驱动程序还可以使用受保护的互斥体。 受保护的互斥体是快速互斥的替代项,但性能更好。 与快速互斥体一样,受保护的互斥体可以保护一次只能由一个线程输入的代码路径。 但是,与使用快速互斥体的代码相比,使用受保护的互斥体的代码的运行速度要快。
在Windows 8之前的 Windows 版本中,受保护的互斥体的实现方式与快速互斥体不同。 受快速互斥锁保护的代码路径在 IRQL = APC_LEVEL运行。 受受保护的互斥锁保护的代码路径在 IRQL <= APC_LEVEL运行,但禁用了所有 APC。 在这些早期版本的 Windows 中,获取受保护的互斥体比获取快速互斥体要快。 但是,这两种类型的互斥体的行为相同,并受到相同的限制。 具体而言,不应从受快速互斥体或受保护的互斥体保护的代码路径调用在 IRQL = APC_LEVEL 时非法调用的内核例程。
从Windows 8开始,受保护的互斥体作为快速互斥体实现。 在受受保护的互斥体或快速互斥体保护的代码路径中, 驱动程序验证程序 将内核例程的调用视为发生在 IRQL = APC_LEVEL。 与早期版本的 Windows 一样,在APC_LEVEL非法调用在受受保护的互斥体或快速互斥体保护的代码路径中是非法的。
快速互斥
快速互斥体由 FAST_MUTEX 结构表示。 驱动程序为 FAST_MUTEX 结构分配自己的存储,然后调用 ExInitializeFastMutex 例程来初始化该结构。
线程通过执行以下操作之一获取快速互斥体:
- 调用 ExAcquireFastMutex 例程。 如果互斥体已被另一个线程获取,则调用线程的执行将暂停,直到互斥锁可用;
- 调用 ExTryToAcquireFastMutex 例程以尝试在不挂起当前线程的情况下获取快速互斥体。 无论是否已获取互斥体,例程都会立即返回。 如果 ExTryToAcquireFastMutex 成功获取调用方互斥体,则返回 TRUE;否则返回 FALSE;
线程调用 ExReleaseFastMutex 来释放由 ExAcquireFastMutex 或 ExTryToAcquireFastMutex 获取的快速互斥体。
受快速互斥锁保护的代码路径在 IRQL = APC_LEVEL运行。 ExAcquireFastMutex 和 ExTryToAcquireFastMutex 将当前 IRQL 提升为 APC_LEVEL, ExReleaseFastMutex 还原原始 IRQL。 因此,当线程持有快速互斥体时,将禁用所有 APC。
如果保证代码路径始终在 APC_LEVEL 运行,则驱动程序可以改为调用 ExAcquireFastMutexUnsafe 和 ExReleaseFastMutexUnsafe 来获取和释放快速互斥。 这些例程不会更改当前 IRQL,仅当当前 IRQL APC_LEVEL时才能安全使用。
快速互斥体不能以递归方式获取。 如果已持有快速互斥锁的线程尝试获取它,该线程将死锁。 快速互斥只能在 IRQL <= APC_LEVEL 下运行的代码中使用。
受保护的互斥体
从 Windows Server 2003 开始提供的受保护的互斥体与快速互斥体具有相同的功能,但性能更高。
从Windows 8开始,受保护的互斥体和快速互斥体的实现方式相同。
在Windows 8之前的 Windows 版本中,受保护的互斥体的实现方式与快速互斥体不同。 获取快速互斥体会将当前 IRQL 提升为APC_LEVEL,而获取受保护的互斥体会进入受保护的区域,这是一个更快的操作。
受保护的互斥体由 KGUARDED_MUTEX 结构表示。 驱动程序为 KGUARDED_MUTEX 结构分配自己的存储,然后调用 KeInitializeGuardedMutex 例程来初始化该结构。
线程通过执行以下操作之一获取受保护的互斥体:
- 调用 KeAcquireGuardedMutex。 如果互斥体已被另一个线程获取,则调用线程的执行将暂停,直到互斥锁可用;
- 调用 KeTryToAcquireGuardedMutex 以尝试在不暂停当前线程的情况下获取受保护的互斥体。 无论是否已获取互斥体,例程都会立即返回。 如果 KeTryToAcquireGuardedMutex 成功获取调用方互斥体,则返回 TRUE ;否则返回 FALSE;
线程调用 KeReleaseGuardedMutex 以释放 KeAcquireGuardedMutex 或 KeTryToAcquireGuardedMutex 获取的受保护的互斥体。
保存受保护的互斥锁的线程在受保护的区域内隐式运行。 KeAcquireGuardedMutex 和 KeTryToAcquireGuardedMutex 进入受保护的区域,而 KeReleaseGuardedMutex 退出该区域。 当线程持有受保护的互斥体时,将禁用所有 APC。
如果保证代码路径在禁用所有 APC 的情况下运行,则驱动程序可以改用 KeAcquireGuardedMutexUnsafe 和 KeReleaseGuardedMutexUnsafe 来获取和释放受保护的互斥体。 这些例程不会进入或退出受保护的区域,只能在已存在的受保护区域中使用,或者在 IRQL = APC_LEVEL 中使用。
无法以递归方式获取受保护的互斥体。 如果已持有受保护的互斥锁的线程尝试获取它,该线程将死锁。 受保护的互斥体只能在 IRQL <= APC_LEVEL 下运行的代码中使用。