2. Linux 中断子系统架构:irq_desc、中断处理流程、中断线程化
中断子系统是 Linux 内核响应硬件事件的枢纽。网络驱动依赖中断来通知数据包到达,因此理解中断子系统的核心数据结构、处理流程以及线程化机制,是进行网络驱动调优的前提。本章将深入剖析 irq_desc 结构体、标准中断处理流程以及中断线程化的实现原理。
2.1 核心数据结构:irq_desc
在 Linux 内核中,每一个中断请求(IRQ)都由一个 irq_desc 结构体描述。该结构体是中断子系统的中枢,串联起硬件中断号、中断控制器、处理函数以及状态信息。
| 字段 | 类型 | 描述 |
|---|---|---|
irq_data |
struct irq_data |
包含中断控制器(chip)、硬件中断号、中断域等信息。 |
action |
struct irqaction * |
指向中断处理动作链表的头指针,每个注册的中断处理程序对应一个 irqaction。 |
status_use_accessors |
unsigned int |
中断状态标志,如 IRQ_DISABLED、IRQ_INPROGRESS、IRQ_WAKE_THREAD 等。 |
depth |
unsigned int |
中断禁用深度计数,用于管理 enable_irq/disable_irq 的嵌套调用。 |
lock |
raw_spinlock_t |
保护该 irq_desc 的自旋锁,用于同步中断上下文。 |
threads_oneshot |
atomic_t |
标记正在运行的一次性(oneshot)类型中断线程数。 |
wait_for_threads |
wait_queue_head_t |
等待所有中断线程完成的等待队列头。 |
kstat_irqs |
unsigned int * |
每个 CPU 的中断统计计数。 |
关键点:
irq_desc 数组(或基数树)以中断号(IRQ number)为索引,系统启动时初始化。网络驱动通过 request_irq() 或 request_threaded_irq() 将 irqaction 挂入对应 irq_desc 的 action 链表。
2.2 中断处理流程
当硬件设备触发中断时,CPU 会执行一系列预定义的流程。从 CPU 异常处理到最终调用设备驱动注册的处理函数,整个过程分为上半部(硬中断)和下半部(软中断/ tasklet / 工作队列)。
2.2.1 标准中断处理步骤
- CPU 异常处理: CPU 检测到中断信号,保存现场(寄存器、PC 等),跳转到中断向量表对应的入口地址。
- 中断控制器仲裁: 中断控制器(如 APIC、GIC)向 CPU 发送中断向量号,CPU 根据向量号查找 IDT(中断描述符表)。
- 内核通用入口: 执行
do_IRQ()(x86)或handle_arch_irq()(ARM),获取当前中断号对应的irq_desc。 - 调用高层中断处理函数: 根据中断类型调用
handle_level_irq()、handle_edge_irq()等通用处理函数。 - 执行 action 链表: 遍历
irq_desc->action链表,调用每个irqaction的handler(即驱动注册的中断处理函数,上半部)。 - 触发下半部: 上半部通常返回
IRQ_WAKE_THREAD或IRQ_HANDLED,内核据此决定是否唤醒中断线程或触发软中断。 - 恢复现场: 执行
iret或eret指令,恢复 CPU 状态,继续执行被中断的任务。
性能要点: 上半部(硬中断上下文)必须快速执行,通常只完成“最小必要操作”,如读取设备状态寄存器、清除中断标志、将数据放入缓冲区,然后返回
IRQ_WAKE_THREAD 将耗时处理交给中断线程。对于网络驱动,上半部通常只做“通知 NAPI 轮询开始”这一动作。
2.3 中断线程化
传统 Linux 中断的上半部运行在不可睡眠的原子上下文中,这限制了处理逻辑的复杂性。中断线程化(Threaded IRQs)允许将中断处理程序的大部分工作放到一个可睡眠的内核线程中执行,从而降低硬中断延迟,提高系统实时性。
2.3.1 中断线程化原理
驱动通过 request_threaded_irq() 注册中断时,可以指定两个处理函数:
- handler(上半部): 在硬中断上下文中执行,必须快速、不可睡眠。通常只做硬件确认和唤醒线程。
- thread_fn(线程化处理函数): 在独立的内核线程(
irq/xxx)中执行,可以睡眠、可以持有信号量、可以执行复杂的 I/O 操作。
当上半部返回 IRQ_WAKE_THREAD 时,内核会唤醒与 irq_desc 关联的中断线程,由该线程执行 thread_fn。
使用场景: 对于网络驱动,中断线程化常用于处理“慢速”设备或需要复杂协议解析的场景。对于高性能 NAPI 驱动,通常直接使用 NAPI 轮询机制替代中断线程化,以获得更低的数据包处理延迟。
2.3.2 中断线程化与 NAPI 的对比
| 特性 | 中断线程化 (Threaded IRQ) | NAPI 轮询 |
|---|---|---|
| 执行上下文 | 内核线程(可睡眠) | 软中断上下文(不可睡眠) |
| 调度方式 | 由内核调度器管理,优先级可调 | 由软中断触发,在 ksoftirqd 中执行 |
| 适用场景 | 实时性要求高、处理逻辑复杂、需要睡眠 | 高吞吐量、低延迟、数据包批量处理 |
| 中断关闭时间 | 短(上半部仅唤醒线程) | 短(上半部仅关闭中断并启动轮询) |
| 典型驱动 | GPIO、I2C、串口 | 千兆/万兆以太网、WiFi |
2.3.3 中断线程化注册示例
/* 驱动中断上半部处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
/* 快速确认硬件状态,清除中断标志 */
writel(INT_CLEAR, regs->status);
/* 唤醒中断线程处理后续工作 */
return IRQ_WAKE_THREAD;
}
/* 驱动中断线程化处理函数 */
static irqreturn_t my_irq_thread_fn(int irq, void *dev_id)
{
struct my_device *dev = dev_id;
/* 可以睡眠,可以持有 mutex */
mutex_lock(&dev->lock);
/* 执行复杂的数据处理或 I/O 操作 */
process_data(dev);
mutex_unlock(&dev->lock);
return IRQ_HANDLED;
}
/* 驱动初始化时注册中断 */
static int my_probe(struct platform_device *pdev)
{
int irq = platform_get_irq(pdev, 0);
/* 注册线程化中断 */
ret = request_threaded_irq(irq, my_irq_handler, my_irq_thread_fn,
IRQF_TRIGGER_RISING, "my_device", dev);
if (ret) {
dev_err(&pdev->dev, "failed to request threaded irq\n");
return ret;
}
return 0;
}
注意事项: 使用中断线程化时,必须确保上半部
handler 执行时间极短。如果 handler 本身耗时过长,会阻塞其他中断,导致系统响应延迟增加。对于网络驱动,建议优先使用 NAPI 机制,而非中断线程化。
2.4 本章小结
- irq_desc 是中断子系统的核心数据结构,每个 IRQ 对应一个
irq_desc,包含中断控制器信息、处理函数链表、状态标志和统计计数。 - 中断处理流程 分为硬件触发、CPU 现场保存、通用入口、高层处理函数、执行 action 链表、触发下半部、恢复现场七个步骤。
- 中断线程化 通过
request_threaded_irq()将中断处理分为不可睡眠的上半部和可睡眠的线程化下半部,适用于需要复杂处理或实时性要求高的场景。 - 在后续章节中,我们将看到 NAPI 机制如何替代传统中断处理模式,实现更高效的数据包接收。