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 中断类型关系图

中断类型分类与传递路径 硬件中断源 IRQ(引脚/边沿) NMI(不可屏蔽) MSI/MSI-X(PCIe) 特性 可屏蔽、共享 IRQ 线 传统 PIC/IOAPIC 特性 不可屏蔽、立即处理 用于致命错误 特性 无引脚、多队列 亲和性灵活

2.2 中断处理流程

一次完整的中断处理分为两个阶段:上半部(Top Half) 和 下半部(Bottom Half)。这种设计是为了在最短时间内关闭中断响应,同时将耗时工作推迟到安全时机。

2.2.1 标准处理步骤

  1. 硬件触发: 设备通过引脚或 PCIe 消息向中断控制器(APIC/IOAPIC)发送信号。
  2. 中断向量查找: 中断控制器将信号映射为中断向量号,通过 CPU 的 IDT(中断描述符表)找到对应的处理函数入口。
  3. 上半部执行: CPU 保存当前上下文,关中断(或仅屏蔽当前 IRQ 线),执行注册的 irq_handler_t。通常只做:确认中断、拷贝寄存器、调度下半部。
  4. 下半部调度: 上半部通过 tasklet、workqueue 或 softirq 将剩余工作推迟。
  5. 下半部执行: 在开中断环境下,由内核软中断机制或内核线程完成协议解析、数据拷贝等耗时操作。
延迟陷阱: 如果上半部执行时间过长(例如在中断处理中直接做 DMA 数据拷贝),会导致该 CPU 无法响应其他中断,造成中断风暴和调度延迟。实时系统应确保上半部执行时间 < 10 µs。

2.2.2 中断处理流程 SVG 图

中断处理流程:上半部与下半部 ① 设备触发中断信号 ② 中断控制器(APIC)映射向量 ③ 上半部:关中断,快速处理 ④ 调度下半部(tasklet/softirq) ⑤ 下半部:开中断,完成剩余工作 上半部约束 • 必须快速(< 10 µs) • 不能睡眠/调度 • 不能获取锁(可睡眠锁) 下半部约束 • 可以睡眠(workqueue) • 可以获取锁 • 允许页面调度

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);
}