1. 中断处理基础

中断是硬件与软件交互的核心机制,也是网卡驱动高效收发包的基石。理解中断的基本概念、中断向量表的结构以及中断上下文与进程上下文的区别,是编写高质量中断处理函数的前提。

1.1 中断概念

中断(Interrupt)是一种硬件或软件发出的信号,用于通知 CPU 发生了需要立即处理的事件。当 CPU 收到中断信号后,会暂停当前正在执行的程序,保存现场,然后跳转到预先定义好的中断处理程序(Interrupt Service Routine, ISR)去执行,处理完成后恢复现场,继续执行被暂停的程序。

中断的主要特点包括:

  • 异步性:中断的发生与 CPU 当前执行的指令流无关,是随机、异步的。
  • 优先级:不同中断源具有不同的优先级,高优先级中断可以抢占低优先级中断的处理。
  • 可屏蔽性:CPU 可以通过设置中断屏蔽寄存器来暂时忽略某些中断请求。
提示: 在网卡驱动中,中断通常由硬件(如 DMA 完成、接收队列非空)触发,驱动开发者需要注册对应的中断处理函数来响应这些事件。

1.2 中断向量表

中断向量表(Interrupt Vector Table, IVT)是操作系统或硬件平台维护的一张表格,用于存储中断号与中断处理程序入口地址之间的映射关系。当 CPU 收到一个中断请求时,会根据中断号在向量表中查找对应的处理函数地址,并跳转执行。

在 x86 架构中,中断向量表通常包含 256 个条目(0~255),其中:

中断号范围 类型 说明
0 ~ 31 异常(Exception) 由 CPU 内部错误或指令执行异常触发,如除零错误、缺页异常
32 ~ 255 外部中断(External Interrupt) 由外部硬件设备(如网卡、磁盘、键盘)触发,通过中断控制器(如 APIC)路由到 CPU

在 Linux 内核中,中断向量表的管理由 arch/x86/kernel/idt.c 等架构相关代码负责。驱动开发者通常不需要直接操作向量表,而是通过 request_irq() 或 devm_request_irq() 等内核 API 来注册中断处理函数,内核会自动完成向量表的更新。

关键点: 中断向量表是中断处理的第一级跳转表,它决定了哪个中断号对应哪个处理函数。在 SMP(对称多处理)系统中,每个 CPU 都有自己的中断向量表副本,以实现中断的负载均衡。

1.3 中断上下文与进程上下文

理解中断上下文(Interrupt Context)与进程上下文(Process Context)的区别,是编写安全、高效中断处理函数的基础。两者在运行环境、可调用函数、资源限制等方面存在显著差异。

对比维度 中断上下文 进程上下文
运行环境 在中断处理程序中运行,不隶属于任何进程 在进程的上下文中运行,有完整的进程控制块(PCB)
可睡眠 不允许睡眠或调用可能睡眠的函数(如 kmalloc(GFP_KERNEL)) 允许睡眠,可以调用阻塞式函数
可访问地址空间 只能访问内核地址空间,不能访问用户空间 可以访问用户空间(通过 copy_from_user 等)
调度器状态 调度器被禁用,不会发生进程切换 调度器正常工作,可能发生进程切换
典型函数 tasklet_schedule、napi_schedule、spin_lock mutex_lock、schedule、copy_to_user
警告: 在中断上下文中调用可能导致睡眠的函数(如 mutex_lock、kmalloc(GFP_KERNEL))会触发内核 BUG,导致系统崩溃或死锁。网卡驱动的中断处理函数必须严格遵守这一规则。

下图展示了中断上下文与进程上下文在 Linux 内核中的切换关系:

中断上下文与进程上下文切换示意图 中断上下文与进程上下文切换 进程上下文 用户进程 A 正在执行 可睡眠、可调度、可访问用户空间 持有 mutex 锁、调用系统调用 中断上下文 网卡中断触发,CPU 跳转执行 ISR 不可睡眠、不可调度、仅内核空间 使用 spin_lock、tasklet、NAPI 中断触发 中断返回 关键区别 中断上下文不能睡眠、不能持有互斥锁、不能访问用户空间; 进程上下文则没有这些限制,但中断处理必须快速完成。 时间 进程执行 中断发生 ISR 执行 恢复进程

1.4 中断处理的基本流程

一个典型的中断处理流程包括以下几个步骤:

  1. 中断触发:硬件设备(如网卡)检测到需要处理的事件(如接收到数据包),通过中断线向中断控制器发送信号。
  2. 中断仲裁:中断控制器(如 I/O APIC)根据优先级和负载均衡策略,将中断分配给某个 CPU 核心。
  3. CPU 响应:CPU 在指令边界检查中断标志位,若允许中断,则保存当前上下文(如 CS、EIP、EFLAGS 等寄存器),并禁用本地中断。
  4. 向量查找:CPU 根据中断号从中断向量表中获取中断处理函数的入口地址。
  5. 执行 ISR:CPU 跳转到中断处理函数执行。在 Linux 中,ISR 通常分为上半部(Top Half)和下半部(Bottom Half)。
  6. 中断返回:ISR 执行完毕后,CPU 恢复之前保存的上下文,重新启用中断,继续执行被中断的程序。
实践要点: 网卡驱动的中断处理函数应尽量短小精悍,只做最必要的硬件操作(如读取状态寄存器、清除中断标志),将耗时的数据包处理工作推迟到下半部(如 tasklet、workqueue 或 NAPI poll 函数)中执行。

下图展示了 Linux 内核中中断处理的完整流程:

Linux 内核中断处理流程 Linux 内核中断处理流程 ① 硬件中断触发(网卡接收数据) ② 中断控制器仲裁(APIC 分配) ③ CPU 响应(保存上下文、关中断) ④ 执行 ISR 上半部(快速处理) ⑤ 调度下半部(tasklet/NAPI/workqueue) 上半部必须快速完成(通常微秒级),只做寄存器操作和中断标志清除; 下半部在中断上下文或进程上下文中执行,处理数据包拷贝、协议栈递交等耗时操作。

1.5 中断处理中的关键数据结构

在 Linux 内核中,中断处理涉及多个关键数据结构,驱动开发者需要熟悉它们:

数据结构 定义位置 作用
irq_desc kernel/irq/irqdesc.h 描述一个中断号的所有属性,包括处理函数链表、中断状态、芯片操作等
irqaction include/linux/interrupt.h 描述一个中断处理动作,包含处理函数指针 handler、设备 ID、触发标志等
irq_chip include/linux/irq.h 封装中断控制器的硬件操作,如 irq_mask、irq_unmask、irq_ack
softirq_action include/linux/interrupt.h 描述软中断处理函数,NAPI 的 poll 机制基于软中断实现
提示: 在编写网卡驱动时,最常打交道的是 irqaction 结构体。通过 request_irq(irq, handler, flags, name, dev) 注册的 handler 函数指针会被封装到 irqaction 中,并挂载到对应的 irq_desc 链表上。

1.6 中断处理函数编写规范

基于以上基础,编写网卡中断处理函数时应遵循以下规范:

  • 快速返回:中断处理函数应尽可能快地执行,避免在中断上下文中进行复杂操作。
  • 不可睡眠:禁止调用任何可能导致睡眠的函数,如 kmalloc(GFP_KERNEL)、mutex_lock、copy_to_user 等。
  • 正确使用锁:在中断上下文中应使用 spin_lock 而非 mutex_lock,因为自旋锁不会导致睡眠。
  • 清除中断源:在 ISR 中必须清除设备的中断标志位,否则设备会持续触发中断,导致 CPU 陷入中断风暴。
  • 返回值规范:中断处理函数应返回 IRQ_NONE(不是本设备的中断)或 IRQ_HANDLED(已处理),以便内核进行中断统计和调试。
/* 网卡中断处理函数示例(上半部) */
static irqreturn_t my_nic_interrupt(int irq, void *dev_id)
{
    struct my_nic_priv *priv = (struct my_nic_priv *)dev_id;
    u32 status;

    /* 1. 读取中断状态寄存器 */
    status = readl(priv->ioaddr + INT_STATUS_REG);

    /* 2. 检查是否是本设备的中断 */
    if (!(status & INT_RX_DONE))
        return IRQ_NONE;

    /* 3. 清除中断标志(防止重复触发) */
    writel(INT_RX_DONE, priv->ioaddr + INT_STATUS_REG);

    /* 4. 调度 NAPI 下半部处理接收数据 */
    if (likely(napi_schedule_prep(&priv->napi))) {
        __napi_schedule(&priv->napi);
    }

    return IRQ_HANDLED;
}
总结: 中断处理基础是网卡驱动开发的入门必修课。理解中断上下文与进程上下文的区别,掌握中断向量表的结构,遵循中断处理函数的编写规范,才能为后续学习 NAPI 与传统中断选择策略打下坚实基础。