1. 中断处理基础
中断是硬件与软件交互的核心机制,也是网卡驱动高效收发包的基石。理解中断的基本概念、中断向量表的结构以及中断上下文与进程上下文的区别,是编写高质量中断处理函数的前提。
1.1 中断概念
中断(Interrupt)是一种硬件或软件发出的信号,用于通知 CPU 发生了需要立即处理的事件。当 CPU 收到中断信号后,会暂停当前正在执行的程序,保存现场,然后跳转到预先定义好的中断处理程序(Interrupt Service Routine, ISR)去执行,处理完成后恢复现场,继续执行被暂停的程序。
中断的主要特点包括:
- 异步性:中断的发生与 CPU 当前执行的指令流无关,是随机、异步的。
- 优先级:不同中断源具有不同的优先级,高优先级中断可以抢占低优先级中断的处理。
- 可屏蔽性:CPU 可以通过设置中断屏蔽寄存器来暂时忽略某些中断请求。
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 来注册中断处理函数,内核会自动完成向量表的更新。
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 内核中的切换关系:
1.4 中断处理的基本流程
一个典型的中断处理流程包括以下几个步骤:
- 中断触发:硬件设备(如网卡)检测到需要处理的事件(如接收到数据包),通过中断线向中断控制器发送信号。
- 中断仲裁:中断控制器(如 I/O APIC)根据优先级和负载均衡策略,将中断分配给某个 CPU 核心。
- CPU 响应:CPU 在指令边界检查中断标志位,若允许中断,则保存当前上下文(如 CS、EIP、EFLAGS 等寄存器),并禁用本地中断。
- 向量查找:CPU 根据中断号从中断向量表中获取中断处理函数的入口地址。
- 执行 ISR:CPU 跳转到中断处理函数执行。在 Linux 中,ISR 通常分为上半部(Top Half)和下半部(Bottom Half)。
- 中断返回:ISR 执行完毕后,CPU 恢复之前保存的上下文,重新启用中断,继续执行被中断的程序。
下图展示了 Linux 内核中中断处理的完整流程:
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;
}