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用IoConnectInterruptIoConnectInterruptEx。我个人习惯用后者,更灵活。

// 注册中断服务例程
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(&params);
}

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被调用时,不一定是因为你的设备产生了中断。

中断共享的处理逻辑很简单:

  1. ISR被调用后,先检查硬件状态寄存器
  2. 如果是自己的中断,处理并返回TRUE
  3. 如果不是,立即返回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 中断处理的完整流程

把上面这些串起来,一个完整的中断处理流程是这样的:

  1. 硬件产生中断信号
  2. CPU保存当前上下文,提升IRQL到DIRQL
  3. 调用注册的ISR(运行在DIRQL)
  4. ISR检查硬件状态,确认是自己的中断
  5. ISR清除硬件中断标志,调度DPC
  6. ISR返回TRUE,CPU降低IRQL
  7. DPC被调度执行(运行在DISPATCH_LEVEL)
  8. DPC处理数据,通知等待的线程
  9. 线程在PASSIVE_LEVEL继续处理
中断处理完整流程图 硬件产生中断信号 CPU保存上下文,提升IRQL到DIRQL ISR执行(DIRQL) 返回FALSE,让给其他驱动 清除中断标志,调度DPC 返回TRUE,CPU降低IRQL DPC执行(DISPATCH_LEVEL) IRQL变化: PASSIVE → DIRQL → DISPATCH → PASSIVE

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