Reactos 第 9 章 设备驱动 — 9.6 中断处理

第 9 章 设备驱动 --- 9.6 中断处理

本节深入剖析 NT 中断处理机制。 中断是外设与 CPU 通信的核心方式,NT 内核提供 中断连接、ISR 注册、DPC 关联、IRQ 共享 等完整支持。ReactOS 的中断处理涉及 HAL(硬件抽象层)、内核(IRQL 调度、IDT 管理)、驱动(ISR/DPC)三层。关键 API 包括 KeConnectInterruptIoConnectInterruptHalGetInterruptVector,实现位于 ntoskrnl/ke/i386/irqobj.c(file:///d:/reactos/ntoskrnl/ke/i386/irqobj.c) 和 hal/halx86/apic/apic.c(file:///d:/reactos/hal/halx86/apic/apic.c)。


概述

NT 中断处理的核心设计:

  1. IRQL(中断请求级别):32 级中断优先级(0-31),CPU 维护当前 IRQL
  2. IDT(中断描述符表):256 个中断向量的入口
  3. ISR(中断服务例程):运行在 DIRQL(设备 IRQL),执行最少量工作
  4. DPC(延迟过程调用):运行在 DISPATCH_LEVEL,处理 ISR 排队的工作(9.3 节)
  5. I/O APIC / MSI:现代多核的中断控制器

本节内容概览

  • 9.6.0 框架图
  • 9.6.1 IRQL 与中断优先级
  • 9.6.2 IDT(中断描述符表)
  • 9.6.3 中断连接:KeConnectInterrupt / IoConnectInterrupt
  • 9.6.4 ISR 注册与执行
  • 9.6.5 KINTERRUPT 数据结构
  • 9.6.6 共享中断与中断对象链
  • 9.6.7 现代中断:MSI/MSI-X
  • 9.6.8 总结与代码索引

学习目标

  • 理解 32 级 IRQL 的语义
  • 掌握 IoConnectInterrupt 的使用
  • 理解 ISR-DPC 两阶段协作
  • 知道现代系统中 MSI 的优势

涉及的内核子系统

子系统 头文件/源文件 核心作用
中断对象 ntoskrnl/ke/i386/irqobj.c(file:///d:/reactos/ntoskrnl/ke/i386/irqobj.c) KeConnectInterruptKeDisconnectInterrupt
I/O 管理中断 ntoskrnl/io/iomgr/interrupt.c(file:///d:/reactos/ntoskrnl/io/iomgr/interrupt.c) IoConnectInterrupt
IDT 初始化 ntoskrnl/ke/i386/trap.s(file:///d:/reactos/ntoskrnl/ke/i386/trap.s) KeInitExceptions
HAL APIC hal/halx86/apic/apic.c(file:///d:/reactos/hal/halx86/apic/apic.c) APIC 中断控制器
HAL PIC hal/halx86/generic/8259.c(file:///d:/reactos/hal/halx86/generic/8259.c) 8259 PIC(遗留)
KINTERRUPT sdk/include/xdk/ketypes.h(file:///d:/reactos/sdk/include/xdk/ketypes.h) 中断对象结构
同步执行 ntoskrnl/ke/synch.c(file:///d:/reactos/ntoskrnl/ke/synch.c) KeSynchronizeExecution

9.6.0 框架图

复制代码
  +-------------------------+
  | 硬件中断源              |
  | (网卡、磁盘、键盘)      |
  +-------------------------+
         |
         v
  +-------------------------+  CPU 引脚
  | I/O APIC / Local APIC  |  ----> 向量号
  +-------------------------+  ----> IRQL
         |
         v
  +-------------------------+
  | CPU IDT 表             |  ----> 调用 ISR
  +-------------------------+
         |
         v
  +-------------------------+  IRQL = DIRQL(设备特定)
  | ISR (中断服务例程)      |  1. 确认中断
  | (DIRQL 上下文)         |  2. 读硬件状态
  |                        |  3. KeInsertQueueDpc
  |                        |  4. 退出
  +-------------------------+
         |
         v  (IRQL 降低)
  +-------------------------+  IRQL = DISPATCH_LEVEL
  | DPC (延迟过程调用)      |  1. 实际数据处理
  |                        |  2. IoStartNextPacket
  |                        |  3. IoCompleteRequest
  +-------------------------+
         |
         v
  +-------------------------+  IRQL = PASSIVE_LEVEL
  | 驱动/用户态继续        |
  +-------------------------+

9.6.1 IRQL 与中断优先级

32 级 IRQL

c 复制代码
#define PASSIVE_LEVEL  0         // 线程调度、可分页
#define APC_LEVEL      1         // APC 中断
#define DISPATCH_LEVEL 2         // DPC、调度器
#define DEVICE_LEVEL_BASE 3      // DIRQL 起始
#define PROFILE_LEVEL  27        // 性能分析
#define CLOCK_LEVEL    28        // 时钟
#define IPI_LEVEL      29        // 处理器间中断
#define POWER_LEVEL    30        // 电源故障
#define HIGH_LEVEL     31        // 最高
范围 名称 用法
0 PASSIVE_LEVEL 普通线程上下文
1 APC_LEVEL 异步过程调用
2 DISPATCH_LEVEL 调度器、DPC
3-26 DIRQL 设备 ISR(每设备/每向量)
27-31 高优先级 系统保留

IRQL 的语义

  • CPU 维护当前 IRQL:在 EFlags 或 CR8 中
  • 提升 IRQL 屏蔽低 IRQL 中断KeRaiseIrql(NewIrql, &OldIrql)
  • 降低 IRQL 解除屏蔽KeLowerIrql(OldIrql)
  • ISR 在 DIRQL 运行:与设备相关
  • DPC 在 DISPATCH_LEVEL 运行:紧接 ISR 之后
  • 线程代码在 PASSIVE_LEVEL 运行:可被中断

x86 上的实现

x86 通过 EFlags 的 IF 标志 + 软件模拟实现:

c 复制代码
// 在 CR8 中存软件 IRQL(仅 64 位模式支持)
// 在 32 位模式,软件 IRQL 通过 IF 标志 + KeSoftwareInterruptRequest

VOID KeRaiseIrql(KIRQL NewIrql, PKIRQL OldIrql)
{
    KIRQL CurrentIrql = KeGetCurrentIrql();
    *OldIrql = CurrentIrql;
    if (NewIrql > CurrentIrql)
    {
        if (CurrentIrql < DISPATCH_LEVEL && NewIrql >= DISPATCH_LEVEL)
        {
            /* 屏蔽 DPC 直到 IRQL 降低 */
        }
        KeSetCurrentIrql(NewIrql);
    }
}

9.6.2 IDT(中断描述符表)

x86 CPU 用 IDT(Interrupt Descriptor Table) 把中断向量映射到处理函数:

c 复制代码
typedef struct _KIDTENTRY {
    USHORT OffsetLow;       // 处理函数低 16 位
    USHORT Selector;        // 代码段选择子
    USHORT Flags;           // 32 位 + DPL + 类型
    USHORT OffsetHigh;      // 处理函数高 16 位
} KIDTENTRY, *PKIDTENTRY;

IDT 初始化

KeInitExceptions(在 ntoskrnl/ke/i386/trap.s(file:///d:/reactos/ntoskrnl/ke/i386/trap.s)):

asm 复制代码
PUBLIC _KeInitExceptions
_KeInitExceptions:
    sidt fword ptr [esp-2]    ; 取当前 IDT
    mov eax, [esp-2]          ; IDT 基址
    mov edx, [esp+2]          ; IDT 限制

    /* 设置各异常向量 */
    lea esi, _KiTrap00
    mov ebx, KGDT_R0_CODE
    mov ecx, 0x0EE00          ; 中断门,DPL=0
    call _KeSetTrap            ; 安装到 IDT

    lea esi, _KiTrap01
    ; ... 重复 21 个异常向量

    ret

中断门 vs 任务门

  • 中断门0x8E):32 位处理函数,清 IF
  • 任务门0x85):通过 TSS 切换(用于 NMI、DualFault)
  • 陷阱门0x8F):不清 IF(用于 int 0x80 遗留)

硬件中断向量分配

范围 用途
0-19 CPU 异常
20-31 保留
32-47 8259 PIC(遗留)
48-255 I/O APIC 分配

I/O APIC 初始化

c 复制代码
VOID HalpInitApicInterruptVectors(VOID)
{
    ULONG Vector;

    for (Vector = 0; Vector < 16; Vector++)
    {
        /* ISA 中断:IRQ 0-15 映射到向量 0x30-0x3F */
        HalpPicSpuriousInt = APIC_SPURIOUS_VECTOR;
        ApicWrite(APIC_IO_REG(APIC_IO_INT_VECTOR(Vector)), Vector + 0x30);
    }
}

9.6.3 中断连接:KeConnectInterrupt / IoConnectInterrupt

IoConnectInterrupt

驱动级 API(推荐):

c 复制代码
NTSTATUS IoConnectInterrupt(
    OUT PKINTERRUPT *InterruptObject,
    IN PKSERVICE_ROUTINE ServiceRoutine,
    IN PVOID ServiceContext,
    IN PKSPIN_LOCK SpinLock OPTIONAL,
    IN ULONG Vector,
    IN KIRQL Irql,
    IN KIRQL SynchronizeIrql,
    IN KINTERRUPT_MODE InterruptMode,
    IN BOOLEAN ShareVector,
    IN KAFFINITY ProcessorEnableMask,
    IN BOOLEAN FloatingSave);
参数 含义
InterruptObject 输出中断对象指针
ServiceRoutine ISR 函数指针
ServiceContext 驱动上下文
SpinLock ISR 与同步代码共享的自旋锁
Vector 中断向量(HAL 分配)
Irql DIRQL
SynchronizeIrql 同步执行 IRQL
InterruptMode Latched / LevelSensitive
ShareVector 是否允许共享
ProcessorEnableMask CPU 亲和性
FloatingSave 是否保存 FPU 状态

典型用法

c 复制代码
NTSTATUS MyConnectInterrupt(PDEVICE_EXTENSION Ext)
{
    NTSTATUS Status;
    DEVICE_DESCRIPTION DeviceDesc;
    ULONG Vector;
    KIRQL Dirql, SynchronizeIrql;
    KAFFINITY Affinity;
    INTERRUPT_TYPE InterruptType;

    /* 1. 准备设备描述 */
    RtlZeroMemory(&DeviceDesc, sizeof(DEVICE_DESCRIPTION));
    DeviceDesc.Version = DEVICE_DESCRIPTION_VERSION;
    DeviceDesc.InterfaceType = PCIBus;
    DeviceDesc.BusNumber = Ext->BusNumber;
    DeviceDesc.PartialResourceList = &Ext->ResourceList;
    DeviceDesc.InterruptVector = Ext->InterruptVector;
    DeviceDesc.InterruptLevel = Ext->InterruptLevel;
    DeviceDesc.Affinity = Ext->ProcessorEnableMask;

    /* 2. 翻译资源 */
    Vector = HalGetInterruptVector(InterfaceType, BusNumber, InterruptLevel, InterruptVector, &Dirql, &Affinity);

    /* 3. 注册 ISR */
    Status = IoConnectInterrupt(&Ext->InterruptObject,
                                 MyIsr,
                                 Ext,           // ServiceContext
                                 &Ext->IsrLock, // SpinLock
                                 Vector,
                                 Dirql,
                                 Dirql,         // SynchronizeIrql
                                 LevelSensitive,
                                 TRUE,          // ShareVector
                                 Affinity,
                                 FALSE);
    return Status;
}

KeConnectInterrupt

内核级 API(不推荐驱动使用):

c 复制代码
BOOLEAN KeConnectInterrupt(IN PKINTERRUPT InterruptObject);

它激活已经通过 IoConnectInterrupt 分配的中断对象。

IoDisconnectInterrupt

c 复制代码
VOID IoDisconnectInterrupt(IN PKINTERRUPT InterruptObject);

断开中断连接(在驱动 Unload 时调用)。


9.6.4 ISR 注册与执行

ISR 函数签名

c 复制代码
BOOLEAN MyIsr(
    IN PKINTERRUPT Interrupt,
    IN PVOID ServiceContext)
{
    PDEVICE_EXTENSION Ext = (PDEVICE_EXTENSION)ServiceContext;
    UCHAR Status;

    /* 1. 确认是否是本设备的中断 */
    Status = READ_PORT_UCHAR(Ext->StatusPort);
    if (!(Status & MY_INTERRUPT_FLAG))
    {
        return FALSE;  /* 不是本设备的中断 */
    }

    /* 2. 确认中断(写入 EOI) */
    WRITE_PORT_UCHAR(Ext->EoiPort, 0);

    /* 3. 排队 DPC */
    KeInsertQueueDpc(&Ext->Dpc, NULL, NULL);

    return TRUE;  /* 已处理 */
}

ISR 的关键约束

  1. 运行在 DIRQL:不能访问分页内存
  2. 必须极快:常驻内存、不能阻塞
  3. 必须确认中断:向硬件写入 EOI(End of Interrupt)
  4. 必须返回值TRUE 表示已处理,FALSE 表示未处理(链中下一个 ISR)
  5. 不能调用等待原语:会死锁
  6. 同步原语:可用自旋锁

KeSynchronizeExecution

驱动需要在 ISR 之外的代码同步访问共享资源:

c 复制代码
BOOLEAN NTAPI
MySynchRoutine(PVOID Context)
{
    /* 运行在 DIRQL */
    /* 访问共享资源 */
    return TRUE;
}

/* 调用 */
KeSynchronizeExecution(Ext->InterruptObject, MySynchRoutine, Ext);

KeSynchronizeExecution 在 DIRQL 上调用回调,确保 ISR 不会同时运行。


9.6.5 KINTERRUPT 数据结构

c 复制代码
typedef struct _KINTERRUPT {
    CSHORT Type;
    CSHORT Size;
    LIST_ENTRY InterruptListEntry;
    PKINTERRUPT_ROUTINE DispatchCode;       // ISR 入口
    ULONG DispatchCodeSize;
    ULONG Vector;                            // 中断向量
    KIRQL Irql;                              // DIRQL
    KIRQL SynchronizeIrql;                   // 同步 IRQL
    KINTERRUPT_MODE Mode;                    // Latched/LevelSensitive
    KSHARE_LEVEL ShareVector;                // 中断共享
    KAFFINITY ProcessorEnableMask;           // CPU 亲和性
    ULONG NumberFloatingPoint;               // FPU 上下文
    PULONG ServiceRoutine;                   // ISR 指针
    PVOID ServiceContext;
    PKSPIN_LOCK SpinLock;                    // ISR/DPC 共享锁
    ULONG TickCount;                         // 中断计数
    PKSPIN_LOCK ActualLock;
    PBOOLEAN Connected;
    BOOLEAN FloatingSave;                    // 是否保存 FPU
    BOOLEAN SpinLockUsed;                    // 是否使用 SpinLock
    UCHAR Pad[3];
    PDRIVER_OBJECT DriverObject;
} KINTERRUPT;

KINTERRUPT 关键字段

字段 作用
DispatchCode 汇编 stub:保存寄存器、调用 C ISR、恢复、IRET
Vector 中断向量号
Irql DIRQL
ServiceRoutine C ISR 函数
ServiceContext 驱动上下文
SpinLock ISR/DPC 共享的自旋锁
Connected 是否已连接到 IDT

DispatchCode(汇编 stub)

asm 复制代码
_KiInterruptTemplate:
    ; 保存寄存器
    push ebp
    push ebx
    push esi
    push edi
    push fs

    ; 调用 C ISR
    ; C ISR 接受 (KINTERRUPT*, PVOID ServiceContext) 两个参数
    mov ecx, [esp + 24]  ; InterruptObject
    mov edx, [esp + 28]  ; ServiceContext
    call _CServiceRoutine

    ; 检查返回值
    test al, al
    jz NotHandled

    ; 发送 EOI
    ...

NotHandled:
    ; 恢复寄存器
    pop fs
    pop edi
    pop esi
    pop ebx
    pop ebp
    iret

9.6.6 共享中断与中断对象链

中断共享

多个设备可共用一个 IRQ(如 PCI 设备):

复制代码
IRQ 16 (共享)
  +-- KINTERRUPT #1 (NIC 驱动)
  +-- KINTERRUPT #2 (声卡驱动)

中断对象链表

NT 内核为每个 IRQ 维护一个 KINTERRUPT 链表。KiInterruptHandler(汇编入口)依次调用每个 ISR,直到有一个返回 TRUE

asm 复制代码
_KiInterruptHandler:
    ; 屏蔽中断
    ; 取当前 CPU 的中断对象链
    ; 遍历链表:
    ;   if (ISR returns TRUE) goto done
    ;   else continue
    ; done:
    ; 恢复中断
    ; iret

共享要求

  • 所有 ISR 必须是 电平触发LevelSensitive
  • 每个 ISR 必须能够识别 是否是自己的中断(读硬件状态寄存器)
  • 如果不是自己的中断,返回 FALSE

共享示例

c 复制代码
BOOLEAN MyIsr(IN PKINTERRUPT Interrupt, IN PVOID ServiceContext)
{
    PDEVICE_EXTENSION Ext = (PDEVICE_EXTENSION)ServiceContext;

    /* 检查是否是本设备的中断 */
    if (!Ext->IsOurInterrupt())
    {
        return FALSE;  /* 不是本设备 */
    }

    /* 处理 */
    ...
    return TRUE;
}

9.6.7 现代中断:MSI/MSI-X

MSI(Message Signaled Interrupts)

传统 IRQ 是 电平触发 ,多个设备共享一个 IRQ。MSI 改为 消息触发

  • 设备写入特定地址(0xFEE00000)触发中断
  • 每个设备有独立的中断向量
  • 不再需要共享

MSI 优势

  1. 无共享:每个设备独立中断
  2. 无电平/边沿问题
  3. 可寻址多个 CPU:MSI-X 支持 2048 个中断,每个可寻址不同 CPU
  4. 降低延迟:不需要电平检测

MSI 注册

c 复制代码
NTSTATUS MyEnableMsi(PDEVICE_EXTENSION Ext)
{
    PCI_CAPABILITIES_HEADER Cap;
    UCHAR CapOffset;

    /* 查找 MSI 能力 */
    CapOffset = PciFindCap(Ext->PciConfig, PCI_CAP_MSI);
    if (CapOffset == 0) return STATUS_NOT_SUPPORTED;

    /* 设置 MSI 寄存器 */
    PciWriteConfig(Ext->BusNumber, Ext->Slot, CapOffset + 0x4, 0xFEE00000);  // 地址
    PciWriteConfig(Ext->BusNumber, Ext->Slot, CapOffset + 0x8, 0x0041);      // 数据

    /* 启用 MSI */
    USHORT Control = PciReadConfig(Ext->BusNumber, Ext->Slot, CapOffset + 0x2);
    Control |= 0x1;  // MSI Enable
    PciWriteConfig(Ext->BusNumber, Ext->Slot, CapOffset + 0x2, Control);

    return STATUS_SUCCESS;
}

HAL 中的 MSI 支持

c 复制代码
/* hal/halx86/apic/apic.c */
VOID HalpEnableMsi(PDEVICE_EXTENSION Ext)
{
    /* 配置 APIC IO Unit 接受 MSI */
    /* 设置 APIC IO_REDIR_TBL */
    ApicWrite(APIC_IO_REG(APIC_IO_INT_VECTOR(Ext->Vector)), 
              EXTINT | (Ext->DestCpu << 24));
}

9.6.8 APIC中断控制器深度剖析

ReactOS的APIC(Advanced Programmable Interrupt Controller)实现位于hal/halx86/apic/apic.c,为现代多处理器系统提供中断管理支持。APIC相比传统的8259 PIC具有更多中断向量、支持多处理器亲和性和消息信号中断(MSI)。

APIC架构概述

APIC系统由两个主要组件构成:

  1. Local APIC: 每个CPU核心都有一个本地APIC,负责接收和发送中断消息
  2. I/O APIC: 位于芯片组中,负责将外部设备中断转换为中断消息并发送到Local APIC

在ReactOS中,APIC的内存映射基地址通常是0xFEC00000(I/O APIC)和0xFEE00000(Local APIC)。

IRQL到TPR映射表

APIC使用任务优先级寄存器(TPR)来屏蔽低优先级中断。ReactOS维护了一个IRQL到TPR的映射表,定义在hal/halx86/apic/apic.c中:

c 复制代码
const UCHAR HalpIRQLtoTPR[32] =
{
    0x00, /*  0 PASSIVE_LEVEL */
    0x3d, /*  1 APC_LEVEL */
    0x41, /*  2 DISPATCH_LEVEL */
    0x41, /*  3  \ */
    0x51, /*  4  \ */
    0x61, /*  5  | */
    0x71, /*  6  | */
    0x81, /*  7  | */
    0x91, /*  8  | */
    0xa1, /*  9  | */
    0xb1, /* 10  | */
    0xb1, /* 11  | */
    0xb1, /* 12  | */
    0xb1, /* 13  | */
    0xb1, /* 14  | */
    0xb1, /* 15 DEVICE IRQL */
    0xb1, /* 16  | */
    0xb1, /* 17  | */
    0xb1, /* 18  | */
    0xb1, /* 19  | */
    0xb1, /* 20  | */
    0xb1, /* 21  | */
    0xb1, /* 22  | */
    0xb1, /* 23  | */
    0xb1, /* 24  | */
    0xb1, /* 25  / */
    0xb1, /* 26 /  */
    0xc1, /* 27 PROFILE_LEVEL */
    0xd1, /* 28 CLOCK2_LEVEL */
    0xe1, /* 29 IPI_LEVEL */
    0xef, /* 30 POWER_LEVEL */
    0xff, /* 31 HIGH_LEVEL */
};

这个映射表的关键设计是:PASSIVE_LEVEL(0)对应TPR 0x00,允许所有中断;DISPATCH_LEVEL(2)对应TPR 0x41,屏蔽所有设备中断;HIGH_LEVEL(31)对应TPR 0xFF,屏蔽所有可屏蔽中断。

I/O APIC寄存器操作

I/O APIC通过两个寄存器进行访问:选择寄存器(IOREGSEL)和窗口寄存器(IOWIN)。ReactOS提供了简单的读写函数:

c 复制代码
FORCEINLINE ULONG IOApicRead(UCHAR Register)
{
    ASSERT(Register <= 0x3F);
    WRITE_REGISTER_ULONG((PULONG)(IOAPIC_BASE + IOAPIC_IOREGSEL), Register);
    return READ_REGISTER_ULONG((PULONG)(IOAPIC_BASE + IOAPIC_IOWIN));
}

FORCEINLINE VOID IOApicWrite(UCHAR Register, ULONG Value)
{
    ASSERT(Register <= 0x3F);
    WRITE_REGISTER_ULONG((PULONG)(IOAPIC_BASE + IOAPIC_IOREGSEL), Register);
    WRITE_REGISTER_ULONG((PULONG)(IOAPIC_BASE + IOAPIC_IOWIN), Value);
}

每个I/O APIC支持最多24个中断输入,每个中断对应一个64位的重定向表项(Redirection Table Entry)。重定向表项包含以下字段:

  • Vector (8位): 中断向量号
  • DeliveryMode (3位): 传递模式(Fixed、Lowest Priority、SMI、NMI、INIT、ExtINT)
  • DestinationMode (1位): 目标模式(物理/逻辑)
  • DeliveryStatus (1位): 传递状态(只读)
  • Polarity (1位): 极性(高/低)
  • RemoteIRR (1位): 远程IRR(只读,用于电平触发)
  • TriggerMode (1位): 触发模式(边沿/电平)
  • Mask (1位): 屏蔽位
  • Destination (8位): 目标APIC ID

Local APIC初始化

ApicInitializeLocalApic函数负责初始化每个CPU的Local APIC:

c 复制代码
VOID NTAPI ApicInitializeLocalApic(ULONG Cpu)
{
    APIC_BASE_ADDRESS_REGISTER BaseRegister;
    APIC_SPURIOUS_INERRUPT_REGISTER SpIntRegister;
    LVT_REGISTER LvtEntry;
    
    /* 启用APIC */
    BaseRegister.LongLong = __readmsr(MSR_APIC_BASE);
    BaseRegister.Enable = 1;
    BaseRegister.BootStrapCPUCore = (Cpu == 0);
    __writemsr(MSR_APIC_BASE, BaseRegister.LongLong);
    
    /* 设置伪中断向量和软件启用 */
    SpIntRegister.Long = ApicRead(APIC_SIVR);
    SpIntRegister.Vector = APIC_SPURIOUS_VECTOR;
    SpIntRegister.SoftwareEnable = 1;
    SpIntRegister.FocusCPUCoreChecking = 0;
    ApicWrite(APIC_SIVR, SpIntRegister.Long);
    
    /* 设置平坦模式(最多支持8个CPU) */
    ApicWrite(APIC_DFR, APIC_DF_Flat);
    
    /* 设置逻辑APIC ID */
    ApicWrite(APIC_LDR, ApicLogicalId(Cpu) << 24);
    
    /* 初始化并屏蔽LVT条目 */
    LvtEntry.Long = 0;
    LvtEntry.Vector = APIC_FREE_VECTOR;
    LvtEntry.MessageType = APIC_MT_Fixed;
    LvtEntry.Mask = 1;
    
    ApicWrite(APIC_TMRLVTR, LvtEntry.Long);  // 定时器
    ApicWrite(APIC_THRMLVTR, LvtEntry.Long); // 温度传感器
    ApicWrite(APIC_PCLVTR, LvtEntry.Long);   // 性能计数器
    ApicWrite(APIC_EXT0LVTR, LvtEntry.Long); // 外部中断0
    ApicWrite(APIC_EXT1LVTR, LvtEntry.Long); // 外部中断1
}

中断重定向表配置

ApicWriteIORedirectionEntry函数用于配置I/O APIC的中断重定向表:

c 复制代码
FORCEINLINE VOID ApicWriteIORedirectionEntry(
    UCHAR Index,
    IOAPIC_REDIRECTION_REGISTER ReDirReg)
{
    ASSERT(Index < APIC_MAX_IRQ);
    IOApicWrite(IOAPIC_REDTBL + 2 * Index, ReDirReg.Long0);
    IOApicWrite(IOAPIC_REDTBL + 2 * Index + 1, ReDirReg.Long1);
}

配置重定向表项时需要注意:必须分两次写入(先低32位,后高32位),并且在写入过程中应该屏蔽该中断,避免产生不完整的中断消息。

自中断请求

Local APIC支持向自身发送中断,这在某些同步场景中非常有用。ApicRequestSelfInterrupt函数实现了这一功能:

c 复制代码
FORCEINLINE VOID ApicRequestSelfInterrupt(IN UCHAR Vector, UCHAR TriggerMode)
{
    ULONG Flags;
    APIC_INTERRUPT_COMMAND_REGISTER Icr;
    APIC_INTERRUPT_COMMAND_REGISTER IcrStatus;
    
    ULONG VectorHigh = Vector / 32;
    ULONG VectorLow = Vector % 32;
    ULONG Irr = APIC_IRR + 0x10 * VectorHigh;
    ULONG IrrBit = 1UL << VectorLow;
    
    /* 设置命令寄存器 */
    Icr.LongLong = 0;
    Icr.Vector = Vector;
    Icr.MessageType = APIC_MT_Fixed;
    Icr.TriggerMode = TriggerMode;
    Icr.DestinationShortHand = APIC_DSH_Self;
    
    /* 禁用中断以保护IRR操作 */
    Flags = __readeflags();
    _disable();
    
    /* 等待APIC空闲 */
    do
    {
        IcrStatus.Long0 = ApicRead(APIC_ICR0);
    } while (IcrStatus.DeliveryStatus);
    
    /* 先写高32位,后写低32位发送中断 */
    ApicWrite(APIC_ICR1, Icr.Long1);
    ApicWrite(APIC_ICR0, Icr.Long0);
    
    /* 等待中断请求确认 */
    while (!(ApicRead(Irr) & IrrBit))
    {
        YieldProcessor();
    }
    
    /* 恢复中断状态 */
    if (Flags & EFLAGS_INTERRUPT_MASK)
    {
        _enable();
    }
}

EOI(End of Interrupt)处理

当中断处理完成后,必须向Local APIC发送EOI信号:

c 复制代码
FORCEINLINE VOID ApicSendEOI(void)
{
    ApicWrite(APIC_EOI, 0);
}

对于电平触发的中断,I/O APIC会等待EOI后才允许相同中断的再次传递。对于边沿触发的中断,EOI主要用于更新中断服务寄存器(ISR)。

伪中断(Spurious Interrupt)处理

伪中断是指APIC报告的中断向量在重定向表中被屏蔽或不存在。ReactOS为伪中断分配了特殊向量APIC_SPURIOUS_VECTOR(通常是0xFF),并注册了专门的处理函数ApicSpuriousService

伪中断的产生原因包括:

  1. 中断在传递过程中被屏蔽
  2. 多个中断同时到达时的仲裁失败
  3. 硬件故障或配置错误

延迟IRQL更新机制

在32位x86系统上,ReactOS实现了延迟IRQL更新优化(APIC_LAZY_IRQL)。传统的IRQL更新需要立即写入TPR寄存器,而延迟更新只在必要时才写入:

c 复制代码
#ifdef APIC_LAZY_IRQL
FORCEINLINE VOID ApicLowerIrql(KIRQL Irql)
{
    __writefsbyte(FIELD_OFFSET(KPCR, Irql), Irql);
    
    /* 如果新IRQL低于TPR中设置的IRQL */
    if (Irql < KeGetPcr()->IRR)
    {
        /* 保存新的硬件IRQL到IRR字段 */
        KeGetPcr()->IRR = Irql;
        
        /* 更新TPR */
        ApicWrite(APIC_TPR, IrqlToTpr(Irql));
    }
}
#else
#define ApicLowerIrql ApicSetIrql
#endif

这种优化减少了TPR写入次数,提高了中断处理性能。

与Windows APIC实现的对比

ReactOS的APIC实现与Windows基本兼容,但存在一些差异:

  1. 中断向量分配: Windows使用更复杂的向量分配算法,支持动态调整
  2. 多处理器支持: Windows支持超过8个CPU的逻辑模式(Clustered Mode)
  3. MSI-X支持: Windows对MSI-X的2048个向量支持更完善
  4. 中断亲和性: Windows支持更细粒度的CPU亲和性配置
  5. 虚拟化支持: Windows包含对Hyper-V等虚拟化环境的中断优化

尽管如此,ReactOS的APIC实现已经能够支持标准的双处理器系统和大多数PCI设备的Interrupt和MSI中断。


9.6.9 中断对象链与链式派发

ReactOS的中断系统支持多个设备共享同一个中断向量,这通过中断对象链(Interrupt Chain)实现。ntoskrnl/ke/i386/irqobj.c中的KiChainedDispatch函数实现了链式中断派发逻辑。

链式派发机制

当多个设备共享同一个IRQ时,内核会将这些设备的中断对象链接成一个链表。当中断发生时,KiChainedDispatch函数会遍历链表,依次调用每个设备的ISR,直到有ISR返回TRUE表示已处理该中断:

c 复制代码
VOID FASTCALL KiChainedDispatch(
    IN PKTRAP_FRAME TrapFrame,
    IN PKINTERRUPT Interrupt)
{
    KIRQL OldIrql, OldInterruptIrql = 0;
    BOOLEAN Handled;
    PLIST_ENTRY NextEntry, ListHead;
    
    /* 增加中断计数 */
    KeGetCurrentPrcb()->InterruptCount++;
    
    /* 开始中断处理,检查是否为伪中断 */
    if (HalBeginSystemInterrupt(Interrupt->Irql, Interrupt->Vector, &OldIrql))
    {
        /* 获取链表指针 */
        ListHead = &Interrupt->InterruptListEntry;
        NextEntry = ListHead;
        
        while (TRUE)
        {
            /* 检查是否需要提升IRQL */
            if (Interrupt->SynchronizeIrql > Interrupt->Irql)
            {
                OldInterruptIrql = KfRaiseIrql(Interrupt->SynchronizeIrql);
            }
            
            /* 获取中断锁 */
            KxAcquireSpinLock(Interrupt->ActualLock);
            
            /* 调用ISR */
            Handled = Interrupt->ServiceRoutine(Interrupt, Interrupt->ServiceContext);
            
            /* 释放中断锁 */
            KxReleaseSpinLock(Interrupt->ActualLock);
            
            /* 降低IRQL */
            if (Interrupt->SynchronizeIrql > Interrupt->Irql)
            {
                KfLowerIrql(OldInterruptIrql);
            }
            
            /* 如果中断已处理且为电平触发,停止遍历 */
            if ((Handled) && (Interrupt->Mode == LevelSensitive)) break;
            
            /* 移动到下一个中断对象 */
            NextEntry = NextEntry->Flink;
            
            /* 检查是否回到链表头 */
            if (NextEntry == ListHead)
            {
                /* 电平触发中断不应该到达这里 */
                if (Interrupt->Mode == LevelSensitive) break;
                
                /* 边沿触发中断,如果没有任何ISR处理,退出 */
                if (!Handled) break;
            }
            
            /* 获取下一个中断对象 */
            Interrupt = CONTAINING_RECORD(NextEntry, KINTERRUPT, InterruptListEntry);
        }
        
        /* 退出中断 */
        KiExitInterrupt(TrapFrame, OldIrql, FALSE);
    }
    else
    {
        /* 伪中断处理 */
        KiExitInterrupt(TrapFrame, OldIrql, TRUE);
    }
}

链式中断的连接

当驱动调用KeConnectInterrupt连接共享中断时,内核会检查该向量是否已被连接。如果已连接且两个中断对象都允许共享,新中断对象会被插入到链表中:

c 复制代码
BOOLEAN NTAPI KeConnectInterrupt(IN PKINTERRUPT Interrupt)
{
    // ... 验证参数 ...
    
    /* 获取向量派发信息 */
    KiGetVectorDispatch(Vector, &Dispatch);
    
    /* 检查向量是否已连接 */
    if (Dispatch.Type == NoConnect)
    {
        /* 首次连接 */
        Interrupt->Connected = Connected = TRUE;
        InitializeListHead(&Interrupt->InterruptListEntry);
        KiConnectVectorToInterrupt(Interrupt, NormalConnect);
        Status = HalEnableSystemInterrupt(Vector, Irql, Interrupt->Mode);
    }
    else if ((Dispatch.Type != UnknownConnect) &&
             (Interrupt->ShareVector) &&
             (Dispatch.Interrupt->ShareVector) &&
             (Dispatch.Interrupt->Mode == Interrupt->Mode))
    {
        /* 共享连接 */
        Interrupt->Connected = Connected = TRUE;
        
        /* 如果是首次共享,切换到链式处理器 */
        if (Dispatch.Type != ChainConnect)
        {
            KiConnectVectorToInterrupt(Dispatch.Interrupt, ChainConnect);
        }
        
        /* 插入到链表尾部 */
        InsertTailList(&Dispatch.Interrupt->InterruptListEntry,
                       &Interrupt->InterruptListEntry);
    }
    
    // ... 返回结果 ...
}

共享中断的要求

共享中断需要满足以下条件:

  1. 中断模式相同: 所有共享设备必须使用相同的触发模式(都是电平触发或都是边沿触发)
  2. ShareVector标志 : 所有中断对象的ShareVector字段必须为TRUE
  3. ISR识别能力: 每个ISR必须能够识别是否是自己设备的中断,如果不是则返回FALSE

电平触发vs边沿触发

共享中断通常使用电平触发模式,因为:

  • 电平触发: 中断线保持有效状态直到设备清除中断。这确保了如果某个ISR未能处理中断,中断会持续存在,其他ISR有机会处理。
  • 边沿触发: 中断只在边沿时触发一次。如果ISR未能处理,中断会丢失。

在ReactOS中,PCI设备默认使用电平触发,ISA设备可以使用边沿触发。

中断断开

当驱动断开共享中断时,KeDisconnectInterrupt函数会从链表中移除中断对象。如果链表只剩一个元素,会切换回普通(非链式)处理器:

c 复制代码
BOOLEAN NTAPI KeDisconnectInterrupt(IN PKINTERRUPT Interrupt)
{
    // ... 获取派发信息 ...
    
    if (Dispatch.Type == ChainConnect)
    {
        /* 如果移除的是链表头,更新头指针 */
        if (Interrupt == Dispatch.Interrupt)
        {
            Dispatch.Interrupt = CONTAINING_RECORD(
                Dispatch.Interrupt->InterruptListEntry.Flink,
                KINTERRUPT,
                InterruptListEntry);
            KiConnectVectorToInterrupt(Dispatch.Interrupt, ChainConnect);
        }
        
        /* 从链表中移除 */
        RemoveEntryList(&Interrupt->InterruptListEntry);
        
        /* 检查是否只剩一个元素 */
        NextInterrupt = CONTAINING_RECORD(
            Dispatch.Interrupt->InterruptListEntry.Flink,
            KINTERRUPT,
            InterruptListEntry);
        
        if (Dispatch.Interrupt == NextInterrupt)
        {
            /* 切换回普通模式 */
            KiConnectVectorToInterrupt(Dispatch.Interrupt, NormalConnect);
        }
    }
    else
    {
        /* 非共享中断,直接禁用 */
        HalDisableSystemInterrupt(Interrupt->Vector, Irql);
        KiConnectVectorToInterrupt(Interrupt, NoConnect);
    }
    
    Interrupt->Connected = FALSE;
    return TRUE;
}

调试共享中断

共享中断的调试比单一中断更复杂,常见问题包括:

  1. ISR未识别中断: 设备驱动未能正确识别自己的中断,返回FALSE,导致中断未被处理
  2. 中断风暴: 设备持续产生中断但ISR未能清除,导致系统性能下降
  3. ISR顺序问题: 某些设备需要优先处理,但链表顺序不符合要求
  4. IRQL不匹配: 共享中断的SynchronizeIrql设置不一致

调试技巧:

  • 使用!interrupt命令查看中断向量状态
  • 使用!devobj查看设备对象的中断连接
  • 检查设备状态寄存器确认中断源
  • 使用性能计数器监控中断频率

9.6.10 十问为什么

1. 为什么 ISR 必须运行在 DIRQL 而不是 PASSIVE_LEVEL?

因为中断的本质是 打断当前正在执行的代码 。如果 ISR 运行在 PASSIVE_LEVEL,它本身就会被其他同优先级或更高优先级的代码抢占,导致中断响应延迟不可控。DIRQL(Device IRQL,3-26)高于 DISPATCH_LEVEL(2),这意味着 ISR 执行期间:DPC 被屏蔽、线程调度被暂停、其他低优先级中断被延迟。只有 高优先级中断(时钟 CLOCK_LEVEL、处理器间 IPI_LEVEL)才能抢占 ISR,这保证了设备中断的响应时间是确定的。

2. 为什么 ISR 中不能访问分页内存?

因为 ISR 运行在 DIRQL,而 分页内存的访问可能触发缺页异常(page fault)。缺页异常处理程序需要访问分页文件、执行 I/O 操作,这些操作最终需要在 PASSIVE_LEVEL 上完成。但 ISR 的 IRQL 远高于 PASSIVE_LEVEL,如果 ISR 触发缺页异常,系统将陷入死锁------缺页处理程序无法降低 IRQL 来完成 I/O,ISR 也无法继续执行。这是 NT 内核的硬性规则:DIRQL 及以上只能访问常驻(NonPaged)内存。

3. 为什么 ISR 必须尽快返回,把实际工作交给 DPC?

因为 ISR 执行期间会屏蔽所有同级及更低 IRQL 的中断。如果 ISR 做太多工作(如数据处理、缓冲区拷贝、完成 IRP),同一设备或其他设备的后续中断就会被延迟,造成中断丢失或系统响应迟缓。ISR 的正确做法是:确认中断、读取硬件状态、排队 DPC,然后立即返回。DPC 运行在 DISPATCH_LEVEL(2),虽然仍高于 PASSIVE_LEVEL,但已经允许线程调度和更多操作,且不会屏蔽设备中断。

4. 为什么 ISR 必须向硬件写入 EOI(End of Interrupt)?

对于 电平触发(LevelSensitive)中断,中断线在硬件被确认之前会一直保持有效状态。如果不写 EOI,中断控制器(I/O APIC 或 8259 PIC)会认为该中断仍在处理中,不会允许相同或更低优先级的中断再次传递。这意味着:同一设备无法产生下一个中断,其他共享该 IRQ 的设备也无法中断 CPU。写 EOI 是告诉中断控制器"我已经处理完了,可以接收下一个中断了"。

5. 为什么共享中断必须是电平触发,边沿触发不能共享?

电平触发 的中断线保持有效电平直到设备清除中断源。当多个设备共享同一个 IRQ 时,如果设备 A 产生中断但 ISR A 未能处理(例如设备 A 的驱动还未加载),中断线持续有效,设备 B 的 ISR 仍然有机会被调用------因为中断线一直"举着"。而 边沿触发只在信号跳变时产生一次中断脉冲,如果 ISR A 返回 FALSE(不是它的中断),这个脉冲就丢失了,设备 B 永远收不到中断。因此共享中断必须使用电平触发。

6. 为什么 KINTERRUPT 需要 DispatchCode 汇编 stub,而不是直接调用 C ISR?

因为 x86 CPU 进入中断时 不会自动保存所有寄存器 ,也不会按照 C 调用约定传递参数。DispatchCode 这个汇编 stub 的作用是:

  • 保存被 C 编译器可能修改的寄存器(EBP、EBX、ESI、EDI、FS)
  • 按照 C 调用约定设置参数(ecx = KINTERRUPT*edx = ServiceContext
  • 调用 C ISR
  • 检查返回值,决定是否发送 EOI
  • 恢复寄存器,执行 iret 返回中断前状态

没有这个 stub,C ISR 会直接破坏调用者的寄存器上下文,导致系统崩溃。

7. 为什么 MSI(Message Signaled Interrupts)不再需要中断共享?

传统 IRQ 受限于中断控制器的物理引脚数量(I/O APIC 最多 24 个输入),多个 PCI 设备被迫共享同一个 IRQ。MSI 改变了机制:设备不再通过物理引脚发信号,而是 向内存映射的 APIC 消息地址(0xFEE00000)写入一个消息。每个 MSI 设备可以分配独立的中断向量(0-255 范围内),相当于每个设备有了自己专属的"中断通道"。MSI-X 更是将独立向量数扩展到 2048 个,彻底消除了共享中断的必要性。

8. 为什么 APIC 使用 TPR(Task Priority Register)而不是像 PIC 那样直接关中断?

8259 PIC 只能全局开关中断 (通过 STI/CLI),这意味着一旦关闭中断,所有中断都被屏蔽。APIC 的 TPR 提供了按优先级屏蔽 的能力:TPR 值越高,被屏蔽的中断优先级越低。例如 TPR 设为 0x41(对应 DISPATCH_LEVEL)时,所有设备中断(DIRQL)被屏蔽,但高优先级中断(时钟、IPI)仍然可以到达。这种精细化控制是现代操作系统调度器、电源管理和多处理器同步的基础。

9. 为什么链式中断中 ISR 返回 FALSE 后还要继续遍历下一个中断对象?

因为 共享 IRQ 的多个设备同时产生中断的概率不为零 。当 IRQ 16 被触发时,可能是网卡产生了中断,也可能是声卡产生了中断。KiChainedDispatch 遍历链表中每个 KINTERRUPT,调用其 ISR。如果当前 ISR 检查硬件状态后发现"这不是我的中断",返回 FALSE,派发器就会继续尝试下一个 ISR。只有当某个 ISR 返回 TRUE(表示已处理)且是电平触发时,遍历才会停止。如果遍历完整个链表都没有 ISR 返回 TRUE,说明是伪中断或硬件故障。

10. 为什么 32 位 x86 要实现延迟 IRQL 更新(APIC_LAZY_IRQL)?

因为 频繁写入 TPR 寄存器是有开销的 。TPR 是内存映射的 APIC 寄存器,每次写入都要经过内存总线。在 32 位 x86 上,ReactOS 采用延迟更新策略:只在软件 IRQL 字段(KPCR->Irql)中记录当前 IRQL,只有在实际需要屏蔽更低优先级中断时才写入硬件 TPR。这减少了大量的寄存器写入操作,提高了中断密集型工作负载(如网络收发、磁盘 I/O)的性能。注意 64 位 x86 有 CR8 寄存器直接映射 IRQL,写入开销小得多,所以延迟优化的收益不大。


9.6.10 总结

NT 中断处理机制的核心要点:

  1. 32 级 IRQL:PASSIVE_LEVEL(0)→ DIRQL(3-26)→ HIGH_LEVEL(31)
  2. IDT:256 个向量,每个映射到中断处理函数
  3. IoConnectInterrupt:驱动级 API,注册 ISR
  4. ISR-DPC 两阶段:ISR(DIRQL)极简,DPC(DISPATCH_LEVEL)处理
  5. KINTERRUPT:中断对象结构
  6. 共享中断:电平触发 + 多个 ISR + 设备识别
  7. MSI/MSI-X:现代消息触发,无共享、可寻址多 CPU
  8. KeSynchronizeExecution:在 DIRQL 同步访问共享资源
  9. ISR 限制:不能分页、不能等待、必须快速

下一节 9.7 介绍 过滤设备驱动模块 的实现示例。


本章代码索引

文件 内容
irqobj.c(file:///d:/reactos/ntoskrnl/ke/i386/irqobj.c) KeConnectInterruptKeDisconnectInterruptKeInitializeInterrupt
io/interrupt.c(file:///d:/reactos/ntoskrnl/io/iomgr/interrupt.c) IoConnectInterruptIoDisconnectInterrupt
i386/trap.s(file:///d:/reactos/ntoskrnl/ke/i386/trap.s) KiInterruptHandler、IDT 初始化
i386/traphdlr.c(file:///d:/reactos/ntoskrnl/ke/i386/traphdlr.c) C ISR 派发
apic.c(file:///d:/reactos/hal/halx86/apic/apic.c) APIC 中断控制器
8259.c(file:///d:/reactos/hal/halx86/generic/8259.c) 8259 PIC 遗留
ketypes.h(file:///d:/reactos/sdk/include/xdk/ketypes.h) KINTERRUPTPKSERVICE_ROUTINE
synch.c(file:///d:/reactos/ntoskrnl/ke/synch.c) KeSynchronizeExecution
pci/pdo.c(file:///d:/reactos/drivers/bus/pci/pdo.c) PCI 设备的 MSI/MSI-X 处理
相关推荐
caimouse1 小时前
Reactos 第 7 章 视窗报文 — 7.6 键盘输入线程
windows
yinhunzw2 小时前
Claude code windows 安装
windows
七仔啊2 小时前
windows server 2022 部署前后端项目
windows
qq3621967053 小时前
第三方安卓应用商店安全评测 2026:Appteka、Aptoide、APKPure 等 7 家横评
android·网络·人工智能·安全·chatgpt·智能手机
AI科技星3 小时前
数术工坊・八卷全书【本源创世终极版・万世定稿】
开发语言·网络·量子计算·拓扑学
AI科技星3 小时前
数术工坊・八卷全书(番外・实战升华副卷)【终极典藏定稿|完整无删减】
c语言·开发语言·网络·量子计算·agi
DreamLife☼3 小时前
OpenBCI-脑电信号的隐私与安全保护
网络·安全·开源硬件·脑机接口·eeg·openbci·神经科技
yyuuuzz3 小时前
云服务器软件部署的几个常见问题
运维·服务器·开发语言·网络·云计算·php·apache
dust_and_stars3 小时前
为什么ubuntu24 snap install code-server 不需要--classic?
网络·数据库