停止运行保护
从 Windows XP 开始,内核模式驱动程序便可使用停运保护。 驱动程序可使用停运保护来安全地访问共享系统内存中的对象,而这些对象会由其他内核模式驱动程序来创建和删除。
当某一对象的所有未完成访问均已完成,且不会授予访问该对象的新请求,此对象则被称为处于停运状态。 例如,可能需停运共享对象,以便将其删除并替换为新对象。
拥有共享对象的驱动程序可让其他驱动程序获取和释放针对该对象的停运保护。 启用停运保护后,除所有者之外的驱动程序均可访问该对象,而不存在所有者在访问完成之前删除该对象的风险。 在访问开始之前,访问驱动程序会请求针对该对象的停运保护。 对于长期存在的对象,几乎总会授予此请求。 访问完成后,进行访问的驱动程序会释放其先前获取的针对该对象的停运保护。
主要的停运保护例程
若要开始共享对象,拥有该对象的驱动程序会调用 ExInitializeRundownProtection 例程来初始化针对该对象的停运保护。 在此调用之后,访问该对象的其他驱动程序可获取并释放针对该对象的停运保护。
访问共享对象的驱动程序会调用 ExAcquireRundownProtection 例程来请求针对该对象的停运保护。 访问完成后,此驱动程序会调用 ExReleaseRundownProtection 例程来释放针对该对象的停运保护。
如果所属驱动程序确定必须删除该共享对象,则此驱动程序会等待删除该对象,直到该对象的所有未完成访问均已完成。
准备删除共享对象时,所属驱动程序会调用 ExWaitForRundownProtectionRelease 例程来等待此对象停运。 在此调用期间,ExWaitForRundownProtectionRelease 会等待以前授予的针对该对象的所有停运保护实例被释放,但同时也会阻止授予针对该对象的停运保护的新请求。 在最后一个受保护的访问完成并释放所有停运保护实例后,ExWaitForRundownProtectionRelease 便会返回,而所属驱动程序也可安全地删除该对象。
ExWaitForRundownProtectionRelease 会阻止调用驱动程序线程的执行,直到对共享对象保留停运保护的所有驱动程序均已释放此保护。 为了防止 ExWaitForRundownProtectionRelease 长时间阻止执行,访问共享对象的驱动程序线程应避免在保留针对该对象的停运保护时被挂起。 因此,进行访问的驱动程序应在关键区域或受保护区域内调用 ExAcquireRundownProtection 和 ExReleaseRundownProtection,或是在 IRQL = APC_LEVEL 的运行时进行调用。
用于停运保护
停运保护对于提供对共享对象的访问尤其有用;此时,该对象几乎始终可用,但有时可能需被删除和替换。 访问此对象中的数据或调用例程的驱动程序不得在删除对象后尝试访问该对象。 否则,这些无效访问可能会导致无法预知的行为、数据损坏,甚至还有系统故障。
例如,当操作系统运行时,反病毒驱动程序通常会在内存中保持为已加载状态。 有时,可能需卸载此驱动程序并将其替换为该驱动程序的更新版本。 其他驱动程序会向该反病毒驱动程序发送 I/O 请求,从而访问此驱动程序中的数据和例程。 发送 I/O 请求之前,内核组件(如,文件系统筛选器管理器)可获取停运保护,从而防止在处理 I/O 请求时过早卸载此反病毒驱动程序。 I/O 请求完成后,便可释放停运保护。
停运保护不会序列化对共享对象的访问。 如果两个或多个进行访问的驱动程序可同时对某一对象保留停运保护,且须序列化针对该对象的访问,则须使用某些其他机制(如互斥锁)来序列化这些访问。
EX_RUNDOWN_REF 结构
EX_RUNDOWN_REF 结构可跟踪针对共享对象的停运保护状态。 此结构对驱动程序不透明。 系统提供的停运保护例程使用此结构来计算当前对该对象生效的停运保护实例数。 这些例程还会使用此结构来跟踪该对象是已停运还是正在运行。
若要开始共享对象,拥有该对象的驱动程序会调用 ExInitializeRundownProtection 来初始化与该对象关联的 EX_RUNDOWN_REF 结构。 初始化完成后,所属驱动程序可将此结构提供给需要访问该对象的其他驱动程序。 进行访问的驱动程序会将此结构作为参数传递给 ExAcquireRundownProtection 和 ExReleaseRundownProtection 调用,而这些调用会获取和释放针对该对象的停运保护。 所属驱动程序会将此结构作为参数传递给 ExWaitForRundownProtectionRelease 调用,而该调用会等待该对象停运,以便将其安全地删除。
与锁的比较
停运保护是保证安全访问共享对象的若干方法之一。 另一种方法是使用互斥软件锁。 如果某一驱动程序需要访问当前由另一驱动程序锁定的对象,则前一驱动程序必须等待后一个驱动程序释放该锁。 但是,获取和释放锁可能会造成性能瓶颈,同时锁还会消耗大量内存。 如果使用不当,锁可能会导致争用相同共享对象的驱动程序陷入死锁。 检测和避免死锁通常需转移大量计算资源。
与锁相比,停运保护具有相对较短的处理时间和较低的内存要求。 该对象会与一个简易引用计数相关联,从而确保在完成该对象的所有未完成访问之前延迟删除该对象。 通过此方法,便可使用原子、互锁的硬件指令来代替互斥的软件锁,从而保证针对对象的安全访问。 用于获取和释放停运保护的调用通常速度很快。 使用轻型机制(如,停运保护)的好处对于具有较长寿命且由众多驱动程序共享的共享对象来说可能十分重要。
其他停运保护例程
除前面以及的那些例程之外,还有其他几个停运保护例程可供使用。 某些驱动程序可能会使用这些附加例程。
ExReInitializeRundownProtection 例程可让以前使用的 EX_RUNDOWN_REF 结构与新对象相关联,并初始化针对此对象的停运保护。
ExRundownCompleted 例程会更新 EX_RUNDOWN_REF 结构来表明关联对象的停运操作已完成。
ExAcquireRundownProtectionEx 和 ExReleaseRundownProtectionEx 例程类似于 ExAcquireRundownProtection 和 ExReleaseRundownProtection。 这四个例程可递增或递减对共享对象生效的停运保护实例的计数。 其中,ExAcquireRundownProtection 和 ExReleaseRundownProtection 可将此计数递增和递减 1,ExAcquireRundownProtectionEx 和 ExReleaseRundownProtectionEx 则可按任意数量递增和递减该计数。
缓存感知停运保护
停运引用是一种紧凑且快速的数据结构,但当大量处理器尝试同时获取该引用时,则可能会导致缓存争用。 此问题可能会影响驱动程序的性能和可伸缩性。
若要避免此问题,可使用缓存感知停运引用以便在多个缓存行之间分散引用跟踪。 此举可减少缓存争用,并提高多处理器计算机上驱动程序的性能。
若要使用缓存感知停运引用,请执行以下步骤:
- 通过执行以下操作之一来创建 EX_RUNDOWN_REF_CACHE_AWARE 对象:
- 调用 ExAllocateCacheAwareRundownProtection。 请注意,此操作将负责初始化。
- 或者,若要控制内存分配,则请调用 ExSizeOfRundownProtectionCacheAware,分配返回大小的缓冲区,然后将该缓冲区和大小传递给 ExInitializeRundownProtectionCacheAware。
- 在访问该对象之前,通过调用 ExAcquireRundownProtectionCacheAware 例程来请求针对该对象的停运保护。 如果请求被批准,此例程则会返回 TRUE;如果该对象正在停运,则会返回 FALSE。
- 在访问该对象之后,通过调用 ExReleaseRundownProtectionCacheAware 例程来释放针对该对象的停运保护。
- 等待该对象完成停运,然后再通过调用 ExWaitForRundownProtectionReleaseCacheAware 例程来删除该对象。 此例程会阻止当前线程,直到释放针对该对象的所有停运保护实例。
- 如果驱动程序之前已调用 ExAllocateCacheAwareRundownProtection,则应调用 ExFreeCacheAwareRundownProtection 来释放停运引用。
若要重用缓存感知停运引用,请执行以下步骤:
- 调用 ExWaitForRundownProtectionReleaseCacheAware 之后,调用 ExRundownCompletedCacheAware 以表明旧对象的停运操作已完成。
- 调用 ExReInitializeRundownProtectionCacheAware 以便在关联对象停运后重新初始化该引用。
- 现在,驱动程序便可再次调用 ExAcquireRundownProtectionCacheAware。
缓存感知停运引用在特定情况下具有更好的性能和可伸缩性等优点,但它会比常规停运引用消耗更多内存。 在两种类型的停运引用之间进行选择时,应考虑此权衡因素。