10、中断处理:中断请求级别、注册中断服务例程、DPC延迟过程调用、中断共享
中断处理,说白了就是CPU被硬件“打断”后该怎么响应。我刚开始写驱动时,觉得中断就是个回调函数,后来被蓝屏教育了几次才明白——这里面的水很深。你想想看,CPU正在跑你的业务代码,突然网卡说“我有数据到了”,这时候CPU得停下来去处理网卡的事。怎么停?停多久?处理完怎么回来?这就是中断处理要解决的问题。
10.1 中断请求级别(IRQL)
Windows用IRQL来管理中断优先级。这个概念很关键,我见过不少新手在这里栽跟头。
IRQL是个数字,数字越大优先级越高。CPU只允许高IRQL的代码打断低IRQL的代码。反过来不行。举个例子:
- PASSIVE_LEVEL(0):普通线程代码,啥都能干
- APC_LEVEL(1):异步过程调用
- DISPATCH_LEVEL(2):DPC和调度器
- DIRQL(设备中断级别):具体设备的中断
- HIGH_LEVEL(31):最高级,连时钟中断都屏蔽
核心规则:在DISPATCH_LEVEL及以上,你不能访问分页内存,不能等待,不能抛异常。为什么?因为这时候内存管理器可能正在忙,你一碰分页内存就死锁了。
我在项目中遇到过一个问题:驱动在DISPATCH_LEVEL调用了KeWaitForSingleObject,结果系统直接蓝屏。查了半天才发现是IRQL没降下来。嗯,从那以后我写代码都会先检查当前IRQL。
10.2 注册中断服务例程(ISR)
ISR是中断处理的第一道关卡。它的特点是:必须快,越快越好。因为ISR运行在DIRQL上,会屏蔽同级和低级中断。
注册ISR用IoConnectInterrupt或IoConnectInterruptEx。我个人习惯用后者,更灵活。
// 注册中断服务例程
NTSTATUS RegisterInterrupt(PDEVICE_OBJECT DeviceObject) {
PDEVICE_EXTENSION devExt = DeviceObject->DeviceExtension;
IO_CONNECT_INTERRUPT_PARAMETERS params = {0};
params.Version = CONNECT_FULLY_SPECIFIED;
params.FullySpecified.PhysicalDeviceObject = DeviceObject;
params.FullySpecified.InterruptObject = &devExt->InterruptObj;
params.FullySpecified.ServiceRoutine = MyInterruptService;
params.FullySpecified.ServiceContext = devExt;
params.FullySpecified.SpinLock = &devExt->SpinLock;
params.FullySpecified.Vector = devExt->InterruptVector;
params.FullySpecified.Irql = devExt->InterruptIrql;
params.FullySpecified.SynchronizeIrql = devExt->InterruptIrql;
params.FullySpecified.Mode = LevelSensitive;
params.FullySpecified.ProcessorEnableMask = -1; // 所有CPU
params.FullySpecified.ShareVector = TRUE; // 允许共享
return IoConnectInterruptEx(¶ms);
}
ISR设计原则:ISR只做最必要的事——读取硬件状态、清除中断标志、然后调度DPC。千万别在ISR里做复杂处理,否则系统响应会变得很慢。
10.3 DPC延迟过程调用
DPC是中断处理的“下半部分”。ISR把大部分工作丢给DPC,自己赶紧退出。DPC运行在DISPATCH_LEVEL,比ISR的优先级低,但比普通线程高。
为什么要这么设计?你想想看,如果所有中断处理都在DIRQL上完成,那网卡处理数据时,鼠标键盘的中断就被屏蔽了。用户会感觉系统卡死。所以ISR只做紧急的事,DPC做剩下的。
// DPC回调函数
VOID MyDpcRoutine(
_In_ struct _KDPC *Dpc,
_In_opt_ PVOID DeferredContext,
_In_opt_ PVOID SystemArgument1,
_In_opt_ PVOID SystemArgument2
) {
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)DeferredContext;
// 处理中断数据
// 注意:这里不能访问分页内存!
ProcessInterruptData(devExt);
// 通知等待的线程
KeSetEvent(&devExt->DataReadyEvent, IO_NO_INCREMENT, FALSE);
}
// 在ISR中调度DPC
BOOLEAN MyInterruptService(
_In_ struct _KINTERRUPT *Interrupt,
_In_opt_ PVOID ServiceContext
) {
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)ServiceContext;
// 检查是否是自己的中断
if (!CheckInterruptSource(devExt)) {
return FALSE; // 不是我的中断,返回FALSE让其他驱动处理
}
// 清除硬件中断标志
ClearInterruptFlag(devExt);
// 调度DPC
IoRequestDpc(devExt->DeviceObject, NULL, NULL);
return TRUE;
}
避坑指南:我曾经在DPC里调用了ExAllocatePoolWithTag分配非分页内存,结果系统偶尔蓝屏。后来发现是分配大小超过了非分页池的可用空间。记住:DPC里只能用非分页内存,而且不能等待。
10.4 中断共享
现在的硬件资源紧张,多个设备可能共享同一个中断线。这意味着你的ISR被调用时,不一定是因为你的设备产生了中断。
中断共享的处理逻辑很简单:
- ISR被调用后,先检查硬件状态寄存器
- 如果是自己的中断,处理并返回TRUE
- 如果不是,立即返回FALSE
Windows内核会遍历所有注册在这个中断上的ISR,直到某个ISR返回TRUE。如果所有ISR都返回FALSE,说明是虚假中断。
// 共享中断的ISR实现
BOOLEAN SharedInterruptService(
_In_ struct _KINTERRUPT *Interrupt,
_In_opt_ PVOID ServiceContext
) {
PDEVICE_EXTENSION devExt = (PDEVICE_EXTENSION)ServiceContext;
UINT32 status;
// 第一步:读取硬件状态
status = READ_REGISTER_ULONG(&devExt->Registers->InterruptStatus);
// 第二步:检查是否是自己的中断
if ((status & devExt->InterruptMask) == 0) {
return FALSE; // 不是我的,让给别人
}
// 第三步:清除中断标志
WRITE_REGISTER_ULONG(&devExt->Registers->InterruptClear,
devExt->InterruptMask);
// 第四步:调度DPC处理数据
IoRequestDpc(devExt->DeviceObject, NULL, NULL);
return TRUE;
}
关键点:共享中断的ISR必须能快速判断中断来源。如果判断逻辑太慢,会影响其他设备的中断响应。我一般会在硬件初始化时就把中断掩码准备好,ISR里直接做位运算判断。
10.5 中断处理的完整流程
把上面这些串起来,一个完整的中断处理流程是这样的:
- 硬件产生中断信号
- CPU保存当前上下文,提升IRQL到DIRQL
- 调用注册的ISR(运行在DIRQL)
- ISR检查硬件状态,确认是自己的中断
- ISR清除硬件中断标志,调度DPC
- ISR返回TRUE,CPU降低IRQL
- DPC被调度执行(运行在DISPATCH_LEVEL)
- DPC处理数据,通知等待的线程
- 线程在PASSIVE_LEVEL继续处理
10.6 实际项目中的经验
最后分享几个我在实际项目中踩过的坑:
- ISR里别做复杂运算:我曾经在ISR里做了个CRC校验,结果导致鼠标卡顿。后来把校验移到DPC里,问题解决。
- 注意中断风暴:如果硬件有bug,可能会不停产生中断。我遇到过一块网卡在特定条件下疯狂中断,系统直接挂掉。解决方案是在ISR里加一个计数器,超过阈值就禁用中断并记录错误。
- DPC里别等锁:DPC运行在DISPATCH_LEVEL,这时候拿不到普通的锁。要用
KeAcquireSpinLockAtDpcLevel这类专门为DPC设计的锁。 - 共享中断的调试:调试共享中断问题很痛苦。我一般会在ISR里加一些调试输出,记录每次中断的来源和返回值。这样能快速定位是哪个设备在捣乱。
个人习惯:我写中断处理代码时,会先在ISR里加一个计数器,统计中断次数。然后在DPC里加另一个计数器,统计DPC执行次数。如果两者差距太大,说明ISR调度DPC有问题。这个小技巧帮我快速定位过好几个bug。
中断处理是驱动开发的核心技能。你想想看,整个系统的响应速度、稳定性都跟它有关。把IRQL、ISR、DPC、中断共享这几个概念吃透,写出来的驱动才能稳定高效。
公众号:蓝海资料掘金营,微信deep3321