3. 中断服务例程(ISR)基础:ISR注册、ISR执行上下文、ISR最小化原则
中断服务例程(Interrupt Service Routine, ISR)是WDM驱动响应硬件中断的核心代码路径。本章聚焦ISR的注册机制、执行上下文约束以及设计时必须遵循的最小化原则,帮助开发者构建高效、稳定的中断处理逻辑。
3.1 ISR注册:从INF文件到内核调用链
WDM驱动通过INF文件声明中断资源,并在DriverEntry或AddDevice中调用IoConnectInterrupt完成ISR注册。注册过程将ISR函数与中断向量、处理器亲和性、中断模式绑定。
| 注册步骤 | 关键函数/机制 | 说明 |
|---|---|---|
| 1. 资源声明 | INF文件 [HardwareAdd.Reg] | 声明中断资源类型(CM_PARTIAL_RESOURCE_DESCRIPTOR) |
| 2. 资源解析 | IRP_MN_START_DEVICE | 从PCM_PARTIAL_RESOURCE_LIST提取中断向量和级别 |
| 3. 注册ISR | IoConnectInterrupt | 绑定ISR函数、中断对象、处理器掩码、共享模式 |
| 4. 中断对象存储 | 设备扩展(Device Extension) | 保存PKINTERRUPT句柄,用于后续注销 |
注册要点:IoConnectInterrupt必须在PASSIVE_LEVEL调用,且中断资源必须来自IRP_MN_START_DEVICE成功返回的资源列表。共享中断(InterruptShare)需要ISR具备识别自身设备中断的能力。
// ISR注册核心代码片段
NTSTATUS RegisterInterrupt(PDEVICE_OBJECT DeviceObject, PDEVICE_EXTENSION DevExt) {
CM_PARTIAL_RESOURCE_LIST *pResourceList = NULL;
// 从IRP_MN_START_DEVICE中获取资源
// ... 资源解析逻辑 ...
// 注册ISR
NTSTATUS status = IoConnectInterrupt(
&DevExt->InterruptObject, // 输出:中断对象句柄
(PKSERVICE_ROUTINE)InterruptServiceRoutine, // ISR函数
DevExt, // ISR上下文(ServiceContext)
NULL, // 自旋锁(可选)
DevExt->InterruptVector, // 中断向量
DevExt->InterruptLevel, // 中断级别(IRQL)
DevExt->InterruptAffinity, // 处理器亲和性
FALSE, // 是否共享中断
EdgeTriggered, // 触发模式
FALSE, // 是否支持DPC
0, // 旋转向量(x86专用)
Latched // 中断模式
);
return status;
}
注册后检查:务必验证IoConnectInterrupt返回的PKINTERRUPT句柄非NULL。在DriverUnload或设备移除时,必须调用IoDisconnectInterrupt注销ISR,否则系统会残留中断引用导致蓝屏。
3.2 ISR执行上下文:DIRQL与中断优先级
ISR在DIRQL(Device IRQL)上执行,这是WDM中仅次于高IRQL(如PROFILE_LEVEL)的优先级。在DIRQL上,当前CPU屏蔽了所有低于或等于该IRQL的中断,确保ISR对设备寄存器的访问不被其他中断干扰。
上下文约束:ISR运行在DIRQL,不能访问分页内存、不能等待、不能调用大部分内核同步对象(如事件、互斥体)。任何阻塞操作都会导致系统死锁或IRQL违规(BSOD 0x0A)。
ISR执行上下文的三个关键特征:
- 不可抢占:同一CPU上,DIRQL中断不能被同级或低级中断抢占,但可被更高级中断(如PROFILE_LEVEL)打断。
- 无分页:DIRQL上无法处理页错误,所有ISR访问的内存必须是非分页内存(NonPagedPool)。
- 无上下文切换:ISR不能等待、不能睡眠、不能获取需要等待的锁(如快速互斥体)。
3.3 ISR最小化原则:快速响应与延迟处理
ISR最小化原则是WDM中断设计的核心准则:ISR只做必须立即完成的工作,其余全部推迟到DPC(Deferred Procedure Call)中处理。这是因为ISR在DIRQL上执行,长时间占用DIRQL会阻塞其他设备的中断,降低系统整体响应性。
| 处理阶段 | IRQL | 允许的操作 | 禁止的操作 |
|---|---|---|---|
| ISR(中断服务例程) | DIRQL | 读取设备状态寄存器、清除中断标志、记录最小状态、请求DPC | 内存分配、文件I/O、等待、同步对象、分页内存访问 |
| DPC(延迟过程调用) | DISPATCH_LEVEL | 批量数据处理、链表操作、事件设置、启动I/O | 用户模式访问、文件系统操作、等待用户输入 |
| 工作者线程 | PASSIVE_LEVEL | 文件I/O、内存分配、同步对象等待、用户模式交互 | 无(几乎全部允许) |
最小化检查清单:编写ISR时,逐项确认——是否访问了分页内存?是否调用了KeWaitForSingleObject?是否执行了超过100条指令?是否在ISR内完成了本可推迟的数据处理?任何一项为“是”,都需要重构。
3.4 ISR返回值与中断识别
ISR返回BOOLEAN值,告知系统是否真正处理了中断。对于共享中断,此返回值至关重要。
| 返回值 | 宏定义 | 含义 | 系统行为 |
|---|---|---|---|
| TRUE | TRUE | ISR识别并处理了中断 | 系统不再调用其他共享中断的ISR,并调度DPC |
| FALSE | FALSE | ISR未识别到设备中断 | 系统继续调用链中下一个共享中断的ISR |
// ISR标准实现模板
BOOLEAN InterruptServiceRoutine(
_In_ struct _KINTERRUPT *Interrupt,
_In_ PVOID ServiceContext // 设备扩展指针
) {
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)ServiceContext;
BOOLEAN recognized = FALSE;
// 1. 读取设备中断状态寄存器
ULONG status = READ_REGISTER_ULONG(devExt->InterruptStatusReg);
// 2. 判断是否为本设备中断
if (status & devExt->InterruptMask) {
recognized = TRUE;
// 3. 清除中断标志(必须立即执行)
WRITE_REGISTER_ULONG(devExt->InterruptClearReg, devExt->InterruptMask);
// 4. 记录最小状态(非分页内存)
devExt->LastInterruptStatus = status;
// 5. 请求DPC处理剩余工作
IoRequestDpc(devExt->DeviceObject, NULL, NULL);
}
// 6. 返回是否识别
return recognized;
}
共享中断陷阱:如果ISR返回TRUE但实际并未清除中断标志,会导致该中断被“吞掉”,其他共享设备无法收到中断。如果ISR返回FALSE但实际设备已产生中断,会导致中断丢失。务必通过硬件状态寄存器准确判断。
3.5 ISR设计常见错误与避坑
- 错误1:在ISR中调用KeSetEvent — 如果事件是自动重置事件且等待线程在DISPATCH_LEVEL以下,会导致IRQL违规。应改为在DPC中设置事件。
- 错误2:ISR中访问分页内存 — 分页内存可能在DIRQL上引发页错误,导致系统崩溃。所有ISR访问的数据必须分配自NonPagedPool。
- 错误3:ISR执行时间过长 — 超过100微秒的ISR会显著增加系统中断延迟。使用性能分析工具(如Xperf)测量ISR耗时。
- 错误4:未处理共享中断识别 — 在共享中断配置下,ISR必须通过硬件寄存器确认中断归属,不能假设每次调用都是本设备中断。
- 错误5:ISR中调用KeAcquireSpinLock — 自旋锁可以在DIRQL上获取,但必须确保锁的IRQL匹配,且释放顺序正确。推荐使用中断自旋锁(InterruptSpinLock)。
最佳实践:ISR代码应保持精简,通常不超过50~100条指令。将ISR视为“中断通知”而非“中断处理”,真正的处理逻辑交给DPC。在开发阶段,可以在ISR入口和出口添加性能计数器(QueryPerformanceCounter),监控ISR执行时间。