第 9 章 设备驱动 --- 9.6 中断处理
本节深入剖析 NT 中断处理机制。 中断是外设与 CPU 通信的核心方式,NT 内核提供 中断连接、ISR 注册、DPC 关联、IRQ 共享 等完整支持。ReactOS 的中断处理涉及 HAL(硬件抽象层)、内核(IRQL 调度、IDT 管理)、驱动(ISR/DPC)三层。关键 API 包括 KeConnectInterrupt、IoConnectInterrupt、HalGetInterruptVector,实现位于 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 中断处理的核心设计:
- IRQL(中断请求级别):32 级中断优先级(0-31),CPU 维护当前 IRQL
- IDT(中断描述符表):256 个中断向量的入口
- ISR(中断服务例程):运行在 DIRQL(设备 IRQL),执行最少量工作
- DPC(延迟过程调用):运行在 DISPATCH_LEVEL,处理 ISR 排队的工作(9.3 节)
- 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) | KeConnectInterrupt、KeDisconnectInterrupt |
| 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 的关键约束
- 运行在 DIRQL:不能访问分页内存
- 必须极快:常驻内存、不能阻塞
- 必须确认中断:向硬件写入 EOI(End of Interrupt)
- 必须返回值 :
TRUE表示已处理,FALSE表示未处理(链中下一个 ISR) - 不能调用等待原语:会死锁
- 同步原语:可用自旋锁
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 优势
- 无共享:每个设备独立中断
- 无电平/边沿问题
- 可寻址多个 CPU:MSI-X 支持 2048 个中断,每个可寻址不同 CPU
- 降低延迟:不需要电平检测
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系统由两个主要组件构成:
- Local APIC: 每个CPU核心都有一个本地APIC,负责接收和发送中断消息
- 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。
伪中断的产生原因包括:
- 中断在传递过程中被屏蔽
- 多个中断同时到达时的仲裁失败
- 硬件故障或配置错误
延迟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基本兼容,但存在一些差异:
- 中断向量分配: Windows使用更复杂的向量分配算法,支持动态调整
- 多处理器支持: Windows支持超过8个CPU的逻辑模式(Clustered Mode)
- MSI-X支持: Windows对MSI-X的2048个向量支持更完善
- 中断亲和性: Windows支持更细粒度的CPU亲和性配置
- 虚拟化支持: 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);
}
// ... 返回结果 ...
}
共享中断的要求
共享中断需要满足以下条件:
- 中断模式相同: 所有共享设备必须使用相同的触发模式(都是电平触发或都是边沿触发)
- ShareVector标志 : 所有中断对象的
ShareVector字段必须为TRUE - 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;
}
调试共享中断
共享中断的调试比单一中断更复杂,常见问题包括:
- ISR未识别中断: 设备驱动未能正确识别自己的中断,返回FALSE,导致中断未被处理
- 中断风暴: 设备持续产生中断但ISR未能清除,导致系统性能下降
- ISR顺序问题: 某些设备需要优先处理,但链表顺序不符合要求
- 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 中断处理机制的核心要点:
- 32 级 IRQL:PASSIVE_LEVEL(0)→ DIRQL(3-26)→ HIGH_LEVEL(31)
- IDT:256 个向量,每个映射到中断处理函数
- IoConnectInterrupt:驱动级 API,注册 ISR
- ISR-DPC 两阶段:ISR(DIRQL)极简,DPC(DISPATCH_LEVEL)处理
- KINTERRUPT:中断对象结构
- 共享中断:电平触发 + 多个 ISR + 设备识别
- MSI/MSI-X:现代消息触发,无共享、可寻址多 CPU
- KeSynchronizeExecution:在 DIRQL 同步访问共享资源
- ISR 限制:不能分页、不能等待、必须快速
下一节 9.7 介绍 过滤设备驱动模块 的实现示例。
本章代码索引
| 文件 | 内容 |
|---|---|
| irqobj.c(file:///d:/reactos/ntoskrnl/ke/i386/irqobj.c) | KeConnectInterrupt、KeDisconnectInterrupt、KeInitializeInterrupt |
| io/interrupt.c(file:///d:/reactos/ntoskrnl/io/iomgr/interrupt.c) | IoConnectInterrupt、IoDisconnectInterrupt |
| 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) | KINTERRUPT、PKSERVICE_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 处理 |