4. DIRQL 与中断同步:设备中断请求级别、自旋锁与中断同步、KeSynchronizeExecution

在 WDM 框架中,中断处理的核心挑战在于:中断服务例程(ISR)运行在提升的 IRQL 上,而派遣函数运行在 PASSIVE_LEVEL。两者若共享数据,必须通过严格的同步机制防止竞态条件。本章深入剖析设备中断请求级别(DIRQL)、自旋锁在中断同步中的应用,以及 KeSynchronizeExecution 的典型用法。

4.1 设备中断请求级别(DIRQL)

每个设备中断在系统中被映射到一个特定的中断请求级别(IRQL)。对于 x86/x64 平台,DIRQL 通常位于 DISPATCH_LEVEL(2) 和 PROFILE_LEVEL(27) 之间。当设备触发中断时,中断控制器将 CPU 的当前 IRQL 提升至该设备的 DIRQL,从而屏蔽所有低于或等于该 DIRQL 的其他中断。

IRQL 名称 数值(x64) 用途 可被屏蔽的中断
PASSIVE_LEVEL 0 普通线程执行、派遣函数 无
APC_LEVEL 1 异步过程调用 APC
DISPATCH_LEVEL 2 线程调度、DPC APC、普通线程
DIRQL 3–26 设备中断服务例程 低于 DIRQL 的所有中断
PROFILE_LEVEL 27 性能剖析 所有普通中断
关键理解: DIRQL 不是固定值,而是由 IoConnectInterrupt 时系统根据设备向量分配的。驱动开发者无需手动计算,但必须理解 ISR 运行在此提升级别上,不能调用需要低 IRQL 的 API(如分页内存访问)。
DIRQL 层级与中断屏蔽关系 PROFILE_LEVEL (27) — 性能剖析 DIRQL (3–26) — 设备 ISR 运行于此 DISPATCH_LEVEL (2) — DPC / 调度 APC_LEVEL (1) — 异步过程调用 PASSIVE_LEVEL (0) — 派遣函数 / 用户线程 提升 DIRQL 屏蔽低级别中断 ISR 运行在 DIRQL,所有低于此级别的软件中断(DPC、APC)和普通线程均被屏蔽

4.2 自旋锁与中断同步

当 ISR 与派遣函数(或 DPC)共享数据时,必须使用 中断自旋锁(Interrupt Spin Lock)。自旋锁的核心特性是:在获取锁的同时,将当前 CPU 的 IRQL 提升至调用者指定的 SynchronizeIrql(通常为设备的 DIRQL),从而保证临界区不会被当前 CPU 上的 ISR 打断。

4.2.1 自旋锁的分配与初始化

// 在 AddDevice 或 DriverEntry 中分配并初始化自旋锁
PKSPIN_LOCK pSpinLock = (PKSPIN_LOCK)
    ExAllocatePoolWithTag(NonPagedPool, sizeof(KSPIN_LOCK), 'lckS');
if (pSpinLock) {
    KeInitializeSpinLock(pSpinLock);
    // 保存到设备扩展中
    pDeviceExtension->SpinLock = pSpinLock;
}
重要约束: 中断自旋锁必须分配在 非分页内存(NonPagedPool)中。因为 ISR 运行在 DIRQL,无法处理页错误。如果锁位于分页内存,系统将立即崩溃。

4.2.2 在派遣函数中使用自旋锁

// 派遣函数(PASSIVE_LEVEL)中安全访问共享数据
KIRQL oldIrql;
KeAcquireSpinLock(pDeviceExtension->SpinLock, &oldIrql);
// 此时 IRQL 已提升至 DISPATCH_LEVEL,但 ISR 仍可能运行在更高的 DIRQL
// 因此还需要中断同步对象
pDeviceExtension->SharedCounter++;
KeReleaseSpinLock(pDeviceExtension->SpinLock, oldIrql);

上述代码仅能防止 DPC 级别的并发,不能 防止 ISR 的并发访问。因为 ISR 运行在更高的 DIRQL,自旋锁提升至 DISPATCH_LEVEL 不足以屏蔽 ISR。正确的做法是使用 KeSynchronizeExecution 或中断自旋锁的专用变体。

自旋锁获取与 IRQL 提升流程 派遣函数调用 KeAcquireSpinLock IRQL → DISPATCH_LEVEL 问题:DISPATCH_LEVEL 仍低于 DIRQL,ISR 仍可抢占当前 CPU 解决方案:使用 KeSynchronizeExecution 或中断自旋锁 KeSynchronizeExecution IRQL → DIRQL 安全访问共享数据

4.3 KeSynchronizeExecution 详解

KeSynchronizeExecution 是 WDM 提供的专用同步函数,用于在 DIRQL 上执行一个回调函数。它接受一个中断对象(由 IoConnectInterrupt 返回)、一个回调函数指针以及一个上下文参数。调用时,系统会将当前 CPU 的 IRQL 提升至该中断对象的 DIRQL,并获取对应的中断自旋锁,然后执行回调。

4.3.1 函数原型与参数

BOOLEAN KeSynchronizeExecution(
    PKINTERRUPT  Interrupt,          // 中断对象指针
    PKSYNCHRONIZE_ROUTINE  Routine,  // 回调函数
    PVOID  SynchronizeContext        // 传递给回调的上下文
);
参数 类型 说明
Interrupt PKINTERRUPT 由 IoConnectInterrupt 返回的中断对象指针,驱动应将其保存在设备扩展中
Routine PKSYNCHRONIZE_ROUTINE 回调函数,原型为 BOOLEAN (*)(PVOID),运行在 DIRQL
SynchronizeContext PVOID 传递给回调的任意上下文,通常为设备扩展指针
返回值 BOOLEAN 回调返回的布尔值;若回调未执行(如中断对象无效),返回 FALSE

4.3.2 典型使用模式

// 回调函数:运行在 DIRQL,安全访问共享硬件寄存器或计数器
BOOLEAN SynchronizedReadStatus(PVOID Context) {
    PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)Context;
    // 此时 IRQL 等于设备 DIRQL,ISR 不会并发执行
    devExt->LastStatus = READ_PORT_UCHAR(devExt->BaseAddress + STATUS_REG);
    return TRUE;
}

// 派遣函数中调用
VOID DispatchRead(PDEVICE_OBJECT DeviceObject, PIRP Irp) {
    PDEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;
    BOOLEAN result = KeSynchronizeExecution(
        devExt->InterruptObject,
        SynchronizedReadStatus,
        devExt
    );
    if (result) {
        // 使用 devExt->LastStatus
    }
    // 完成 IRP...
}
最佳实践: 回调函数应保持简短,仅执行必要的共享数据访问或硬件寄存器操作。不要在回调中调用可能阻塞或需要低 IRQL 的函数(如 KeWaitForSingleObject、分页内存访问)。

4.3.3 与自旋锁的对比

特性 KeSynchronizeExecution KeAcquireSpinLock
提升 IRQL 至 设备 DIRQL DISPATCH_LEVEL
能否屏蔽 ISR 是(提升至 DIRQL) 否(仍低于 DIRQL)
使用场景 ISR 与派遣函数共享数据 DPC 与派遣函数共享数据
回调形式 函数指针,自动同步 手动获取/释放锁
中断对象依赖 需要 PKINTERRUPT 不需要
KeSynchronizeExecution 调用流程 派遣函数 (PASSIVE_LEVEL) KeSynchronizeExecution 提升 IRQL IRQL = DIRQL 回调函数执行 恢复原始 IRQL 返回派遣函数

4.4 中断同步的常见陷阱

  • 在 ISR 中调用 KeSynchronizeExecution: 绝对禁止。ISR 已经运行在 DIRQL,再次调用会导致递归提升和死锁。
  • 忘记保存中断对象: IoConnectInterrupt 返回的 PKINTERRUPT 必须保存在设备扩展的非分页内存中,否则无法调用同步函数。
  • 回调中访问分页内存: 回调运行在 DIRQL,任何分页内存访问都会触发页面错误,导致系统崩溃。
  • 混合使用自旋锁与 KeSynchronizeExecution: 如果已经通过 KeSynchronizeExecution 进入同步区,不要再内部调用 KeAcquireSpinLock,这会导致 IRQL 冲突和潜在的死锁。
调试建议: 使用 !irql 和 !locks WinDbg 命令检查当前 IRQL 和自旋锁状态。如果发现 IRQL 异常升高或锁持有时间过长,优先检查同步回调中是否有阻塞操作。