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 | 性能剖析 | 所有普通中断 |
IoConnectInterrupt 时系统根据设备向量分配的。驱动开发者无需手动计算,但必须理解 ISR 运行在此提升级别上,不能调用需要低 IRQL 的 API(如分页内存访问)。
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;
}
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 或中断自旋锁的专用变体。
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...
}
KeWaitForSingleObject、分页内存访问)。
4.3.3 与自旋锁的对比
| 特性 | KeSynchronizeExecution | KeAcquireSpinLock |
|---|---|---|
| 提升 IRQL 至 | 设备 DIRQL | DISPATCH_LEVEL |
| 能否屏蔽 ISR | 是(提升至 DIRQL) | 否(仍低于 DIRQL) |
| 使用场景 | ISR 与派遣函数共享数据 | DPC 与派遣函数共享数据 |
| 回调形式 | 函数指针,自动同步 | 手动获取/释放锁 |
| 中断对象依赖 | 需要 PKINTERRUPT | 不需要 |
4.4 中断同步的常见陷阱
- 在 ISR 中调用 KeSynchronizeExecution: 绝对禁止。ISR 已经运行在 DIRQL,再次调用会导致递归提升和死锁。
- 忘记保存中断对象:
IoConnectInterrupt返回的PKINTERRUPT必须保存在设备扩展的非分页内存中,否则无法调用同步函数。 - 回调中访问分页内存: 回调运行在 DIRQL,任何分页内存访问都会触发页面错误,导致系统崩溃。
- 混合使用自旋锁与 KeSynchronizeExecution: 如果已经通过
KeSynchronizeExecution进入同步区,不要再内部调用KeAcquireSpinLock,这会导致 IRQL 冲突和潜在的死锁。
!irql 和 !locks WinDbg 命令检查当前 IRQL 和自旋锁状态。如果发现 IRQL 异常升高或锁持有时间过长,优先检查同步回调中是否有阻塞操作。