2. 中断基础概念:中断类型、处理流程与上下文
中断是硬件与内核交互的核心机制,也是实时性能优化的首要切入点。理解中断的类型、处理流程以及上下文切换规则,是配置 DMA 中断亲和性、降低延迟的理论基础。
2.1 中断类型:IRQ、NMI 与 MSI
根据触发方式、紧急程度和传递路径,Linux 内核将中断分为以下三种主要类型:
| 类型 | 全称 | 触发方式 | 可屏蔽 | 典型用途 |
|---|---|---|---|---|
| IRQ | Interrupt ReQuest | 硬件引脚电平/边沿 | 是 | 磁盘、网卡、USB 等常规设备 |
| NMI | Non-Maskable Interrupt | 专用引脚或总线信号 | 否 | 硬件看门狗、ECC 内存错误、PANIC |
| MSI | Message Signaled Interrupt | PCIe 内存写事务 | 是 | NVMe SSD、高速网卡、GPU |
优化要点: MSI 是现代 DMA 密集型设备的首选中断类型。它不需要专用引脚,通过 PCIe 总线直接写入中断控制器,延迟比传统 IRQ 低 30%~50%,且天然支持多队列分发(MSI-X)。
2.1.1 中断类型关系图
2.2 中断处理流程
一次完整的中断处理分为两个阶段:上半部(Top Half) 和 下半部(Bottom Half)。这种设计是为了在最短时间内关闭中断响应,同时将耗时工作推迟到安全时机。
2.2.1 标准处理步骤
- 硬件触发: 设备通过引脚或 PCIe 消息向中断控制器(APIC/IOAPIC)发送信号。
- 中断向量查找: 中断控制器将信号映射为中断向量号,通过 CPU 的 IDT(中断描述符表)找到对应的处理函数入口。
- 上半部执行: CPU 保存当前上下文,关中断(或仅屏蔽当前 IRQ 线),执行注册的
irq_handler_t。通常只做:确认中断、拷贝寄存器、调度下半部。 - 下半部调度: 上半部通过
tasklet、workqueue或softirq将剩余工作推迟。 - 下半部执行: 在开中断环境下,由内核软中断机制或内核线程完成协议解析、数据拷贝等耗时操作。
延迟陷阱: 如果上半部执行时间过长(例如在中断处理中直接做 DMA 数据拷贝),会导致该 CPU 无法响应其他中断,造成中断风暴和调度延迟。实时系统应确保上半部执行时间 < 10 µs。
2.2.2 中断处理流程 SVG 图
2.3 中断上下文与进程上下文
Linux 内核在执行代码时,根据所处的执行环境分为两种上下文:中断上下文 和 进程上下文。理解二者的区别,是编写正确中断处理程序的前提。
| 对比维度 | 中断上下文 | 进程上下文 |
|---|---|---|
| 执行主体 | 中断处理程序(上半部/下半部) | 用户进程或内核线程 |
| 当前进程指针 | current 不可用(或指向被中断进程) |
current 指向当前进程 |
| 可睡眠 | ❌ 否(上半部禁止;下半部视机制而定) | ✅ 是 |
| 可访问用户空间 | ❌ 否 | ✅ 是 |
| 可获取互斥锁 | ❌ 否(只能使用 spinlock) | ✅ 是(mutex/semaphore) |
| 调度时机 | 不可调度(上半部);下半部可被软中断抢占 | 可被调度器抢占 |
实时优化提示: 在配置 DMA 中断亲和性时,应尽量将中断上下文绑定到专用的隔离 CPU 核心(isolcpus),避免中断处理与实时任务在同一核心上竞争。同时,下半部应使用
threaded IRQ 或 workqueue 在进程上下文中执行,以减少对实时任务的干扰。
2.3.1 上下文切换与中断嵌套 SVG 图
2.4 中断上下文中的编程约束
在编写中断处理程序(尤其是上半部)时,必须遵守以下硬性约束,否则会导致内核崩溃或死锁:
- 禁止睡眠: 不能调用
schedule()、wait_event()、kmalloc(GFP_KERNEL)(可能触发页面回收而睡眠)。应使用GFP_ATOMIC。 - 禁止访问用户空间: 不能使用
copy_from_user()/copy_to_user(),因为当前进程指针可能无效。 - 锁机制限制: 只能使用
spin_lock()/spin_unlock(),不能使用mutex_lock()。 - 执行时间限制: 上半部应控制在微秒级,避免阻塞其他中断。
代码示例: 上半部与下半部的典型分离模式(伪代码)
/* 上半部:中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
struct my_device *dev = (struct my_device *)dev_id;
/* 1. 读取硬件状态寄存器(快速) */
u32 status = readl(dev->regs + STATUS_REG);
/* 2. 确认中断 */
writel(status, dev->regs + ACK_REG);
/* 3. 调度下半部(tasklet) */
tasklet_schedule(&dev->tasklet);
return IRQ_HANDLED;
}
/* 下半部:tasklet 处理函数 */
static void my_tasklet_handler(unsigned long data)
{
struct my_device *dev = (struct my_device *)data;
/* 可以睡眠、可以获取锁、可以访问用户空间缓冲区 */
mutex_lock(&dev->lock);
/* 处理 DMA 完成数据包 */
process_dma_packets(dev);
mutex_unlock(&dev->lock);
}