Linux 内核硬件中断机制深度实战:从 IRQ 到 softirq,AI 推理场景的硬中断延迟治理
一、问题的提出:为什么 AI 推理系统要关注硬中断?
大多数后端工程师对"中断"的理解停留在"硬件通知 CPU 有事要做"的抽象层面。但当你把一个 LLM 推理服务部署在单节点 8×GPU 的机型上时,网络 P99 延迟从 2ms 忽然飙升到 47ms,CPU 利用率看似不高,IO 也在水位线以下——top 里却多出一个 ksoftirqd 内核线程异常活跃。这时候,"中断"就是你需要直面的第一嫌疑人。
本文不是 API 文档复读,而是一次从硬件引脚到内核调度器、从 ftrace 到 eBPF 观测、从理论到生产实践的完整硬中断机制拆解。我们将回答三个核心问题:
- 一次网卡数据包到达后,CPU 到底经历了哪些执行阶段?
local_irq_disable()和local_bh_disable()在 x86-64 和 ARM64 上分别对应什么指令?代价差多少?- AI 推理 高速网络(RoCE/RDMA NVMe-oF)场景下,如何系统性治理软中断风暴?
- 中断遮蔽时间过长:IF=0 期间,同级/低级中断无法响应
- 实时性劣化:调度器 tick、时钟中断被屏蔽
- 延迟波动剧烈:ISR 执行时间直接贡献到尾延迟
二、硬件中断的解剖学:从 APIC 到 irq_desc
2.1 中断的物理路径
当网卡收到一个数据包,它通过 PCIe Memory Write Transaction 把描述符写入 RX Ring,然后通过 MSI-X 向目标 CPU 的 Local APIC 发送一个中断消息。在 x86-64 上,这对应以下硬件级步骤:
1. 网卡 → PCIe TLP (MSI-X write) → IOMMU 重定向 (若启用 VT-d)
2. IOMMU → LAPIC ← 中断向量号 目标 CPU APIC ID
3. LAPIC 在目标 CPU 上触发 INTR 引脚
4. CPU 在指令边界检查中断(RFLAGS.IF=1 时)
5. CPU 从中断向量号索引 IDT → 门描述符 → 跳转到 irq_entries_start
关键细节:中断并不直接打断指令执行,而是在当前指令完成后、下一条指令之前才响应。对于乱序执行的现代 CPU,这个边界检查的频率大约是每字节指令的间隙。
2.2 内核中断描述符:irq_desc 结构
每个中断向量在内核中对应一个 struct irq_desc(定义于 kernel/irq/irqdesc.c),它包含了中断处理的核心元信息:
struct irq_desc {
struct irq_common_data irq_common_data;
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq; // 流处理层(如 handle_level_irq)
struct irqaction *action; // 驱动注册的 handler 链表
unsigned int depth; // disable 嵌套计数
unsigned int irq_count; // 当前处理中计数(防止重入)
unsigned long last_unhandled;
unsigned int irqs_unhandled;
raw_spinlock_t lock;
const char *name; // /proc/interrupts 中显示的名字
};
irqaction 是链表结构——多个设备可以共享同一个中断线(如多队列网卡的不同向量),每个驱动注册自己的 handler、dev_id、flags(IRQF_SHARED)。
2.3 IDT 与 irq_entries_start
在 arch/x86/kernel/idt.c 中,中断描述符表(IDT)被设置为一组门描述符。值得注意的是,Linux 使用中断门(Interrupt Gate)而非陷阱门——关键在于中断门会自动清除 EFLAGS.IF 位,也就是 x86 上硬件自动关中断(NMI 和某些异常依然能触发)。这是为什么 ISR(中断服务例程)默认运行在关中断状态——必须尽快完成或转交 softirq。
// arch/x86/entry/entry_64.S 简化逻辑
SYM_CODE_START(irq_entries_start)
// 向量号通过宏迭代生成,vector 0x20 到 0xff
.rept 0xe0 // 224 个外部中断向量
UNWIND_HINT_IRET_REGS
pushq $(\rst - irq_entries_start) // 异常帧
jmp interrupt_entry // C 入口
.endr
SYM_CODE_END(irq_entries_start)
interrupt_entry 最终调用 dispatch_external_irq() → apic_eoi() → handle_irq() → ISR 链表调用,然后再进入 irq_exit() 按需调度 softirq。
三、上半部与下半部(Top Half / Bottom Half)的本质划分
3.1 设计动因
如果所有工作都在 ISR 中做(IRQF_DISABLED 标志后已移除,标志着 Linux 全面转向下半部模型),那么系统会面临:
因此 Linux 采用 Top Half(关中断) Bottom Half(开中断、可延迟) 的经典分层。
3.2 下半部机制的演进史
| 机制 | 引入版本 | 特点 | 当代地位 |
|---|---|---|---|
| BH (Bottom Halves) | 早期 | 全局32个静态链表 | 已废弃 |
| Tasklet | 2.3 | 基于 softirq 的 per-CPU 链表,同类型不并发 | 受限使用 |
| softirq | 2.3 | 32种静态,同类型可在不同CPU并发 | 核心机制 |
| Workqueue | 2.5 | 可睡眠、可调度、运行在进程上下文 | 广泛使用 |
| Threaded IRQ | 2.6.30 | kthread 包装 ISR,可睡眠 | 生产推荐 |
现代内核(6.x)中活跃的 softirq 类型只有 10 种:
// include/linux/interrupt.h
enum {
HI_SOFTIRQ = 0, // 高优先级 tasklet
TIMER_SOFTIRQ, // 定时器 (hrtimer 退化时)
NET_TX_SOFTIRQ, // 网络发送
NET_RX_SOFTIRQ, // 网络接收 ← AI 推理的关键瓶颈
BLOCK_SOFTIRQ, // 块层 completions (bio)
IRQ_POLL_SOFTIRQ, // NAPI poll 回退
TASKLET_SOFTIRQ, // 通用 tasklet
SCHED_SOFTIRQ, // CFS 负载均衡
HRTIMER_SOFTIRQ, // 高精度定时器
RCU_SOFTIRQ, // RCU 回调执行
NR_SOFTIRQS
};
3.3 softirq 的完整调度链路
一次 RX 数据包在 NAPI poll 中的完整执行路径:
hardirq (IRQ handler)
└── napi_schedule() // 关闭设备中断,触发 softirq
└── raise_softirq_irqoff(NET_RX_SOFTIRQ)
└── __raise_softirq_irqoff()
└── 设置 CPU 的 softirq pending 位图 (local_cpu_offset)
── 下一个 irq_exit() 或local_bh_enable() 检查点 ──
do_softirq()
└── __do_softirq()
└── h-

发表评论 取消回复