2. 中断机制概述:Linux中断子系统、中断号、中断上下文、上半部与下半部

中断是硬件与软件之间最关键的异步通信机制。在PCIe MSI中断的驱动开发中,理解Linux中断子系统的整体架构、中断号的分配逻辑、中断上下文的划分以及上半部与下半部的协作模式,是编写高性能、无锁、低延迟驱动的基石。本节将系统梳理这些核心概念,并辅以结构图和对比表格,帮助开发者建立清晰的中断处理全景图。

2.1 Linux中断子系统架构

Linux中断子系统采用分层设计,从硬件中断信号到最终驱动处理函数,经过多个抽象层。每一层负责不同的职责:硬件封装、中断路由、通用中断处理与驱动回调。

核心分层: 硬件中断控制器 → irq_chip → irq_domain → 通用中断层 (Generic IRQ) → 驱动中断处理程序 (handler)。
Linux中断子系统分层架构 硬件层:PCIe设备 / 中断控制器 (APIC/GIC/ITS) irq_chip 层:中断控制器抽象 (mask/unmask/ack/eoi) irq_domain 层:中断号映射 (hwirq ↔ Linux IRQ number) 通用中断层 (Generic IRQ):irq_desc / 中断流处理 / 线程化 驱动层:request_irq / devm_request_irq / handler

各层职责简述:

  • 硬件层: PCIe设备通过MSI/MSI-X向中断控制器发送中断消息;中断控制器(如x86的APIC、ARM的GIC、GICv3 ITS)负责接收并路由到CPU核心。
  • irq_chip层: 封装中断控制器的底层操作(mask/unmask/ack/eoi),每个中断控制器对应一个irq_chip结构体。
  • irq_domain层: 负责将硬件中断号(hwirq)映射到Linux内核的IRQ number,支持线性映射、树形映射等。
  • 通用中断层: 管理irq_desc结构体,处理中断流控(handle_level_irq / handle_edge_irq),支持中断线程化。
  • 驱动层: 驱动通过request_irq()注册中断处理函数,直接响应设备中断。

2.2 中断号:从硬件到软件的桥梁

中断号是驱动开发者最常接触的整数标识符。在Linux中,中断号分为多个层级,理解其映射关系对PCIe MSI中断配置至关重要。

中断号类型 说明 示例
硬件中断号 (hwirq) 由中断控制器或PCIe设备指定的原始中断标识,在MSI中对应MSI Data中的Vector字段。 PCIe设备MSI Vector 0x1F
Linux IRQ number 内核全局中断编号,驱动通过此号注册处理函数。由irq_domain从hwirq映射而来。 irq = 47
虚拟中断号 (virq) 在irq_domain动态分配时使用,与hwirq解耦,支持中断迁移和复用。 virq = 128
PCIe MSI场景: 每个MSI向量对应一个独立的hwirq。驱动通过pci_alloc_irq_vectors()分配MSI向量后,使用pci_irq_vector()获取对应的Linux IRQ number,再调用request_irq()注册。切勿直接使用硬件中断号。
中断号映射流程 (PCIe MSI示例) PCIe设备 hwirq=0x1F irq_domain hwirq → Linux IRQ irq_desc[47] IRQ number = 47 request_irq(47, handler)

2.3 中断上下文:原子性与约束

中断上下文是驱动开发者必须严格区分的执行环境。中断处理程序运行在中断上下文中,具有严格的原子性约束,不能执行可能导致睡眠的操作。

特性 进程上下文 中断上下文 (上半部) 软中断 / Tasklet (下半部)
可睡眠 是 (可调用 schedule()) 否 (原子上下文) 否
可持有互斥锁 是 (mutex) 否 (只能使用 spinlock) 否 (只能使用 spinlock)
可访问用户空间 是 否 否
执行时间要求 无严格限制 极短 (微秒级) 较短 (毫秒级)
中断优先级 低 最高 (禁止同级中断) 高 (可被中断)
常见错误: 在中断上下文中调用 kmalloc(GFP_KERNEL)、copy_from_user()、mutex_lock() 等可能导致睡眠的函数,会触发内核 BUG 或死锁。必须使用 GFP_ATOMIC 标志分配内存。

2.4 上半部与下半部:分工协作

为了在中断响应速度和系统吞吐量之间取得平衡,Linux将中断处理拆分为两个阶段:上半部(Top Half)和下半部(Bottom Half)。

  • 上半部 (Hard IRQ Handler): 在中断上下文中立即执行,必须快速完成。典型职责:确认中断、清除中断标志、读取关键状态寄存器、调度下半部。
  • 下半部 (Soft IRQ / Tasklet / Workqueue): 在稍后的时间点执行,允许更复杂的处理。典型职责:数据拷贝、协议解析、唤醒等待队列、DMA完成处理。
中断处理:上半部与下半部协作流程 硬件中断触发 上半部 (Top Half) — 中断上下文 读取状态寄存器 → 清除中断标志 → 调度下半部 时间要求:微秒级,必须原子操作 软中断 / Tasklet 不可睡眠 中断上下文延续 Workqueue 可睡眠 进程上下文

2.5 下半部机制对比与选择

Linux提供多种下半部实现机制,驱动开发者应根据场景选择最合适的方案。

机制 上下文 可睡眠 并发模型 典型延迟 推荐场景
软中断 (SoftIRQ) 中断上下文 否 同一类型可并行 (需注意重入) 极低 (μs级) 网络收包、块设备IO、高吞吐数据路径
Tasklet 中断上下文 否 同一Tasklet串行化执行 低 (μs~ms级) 简单设备中断、DMA完成通知
Workqueue (共享) 进程上下文 是 多worker并行 中 (ms级) 需要睡眠的操作、IO操作、复杂协议处理
Workqueue (绑定) 进程上下文 是 单worker串行 中 (ms级) 需要顺序执行的复杂任务
中断线程化 (threaded IRQ) 进程上下文 是 每个中断独立内核线程 较高 (ms级) 实时性要求不高的设备、简化驱动设计
PCIe MSI驱动最佳实践: 对于高性能NVMe、网卡等设备,推荐使用 软中断 或 Tasklet 作为下半部,以最小化中断延迟。若设备处理逻辑复杂且需要睡眠(如访问I2C/SPI子设备),则使用 Workqueue 或 中断线程化。

2.6 中断处理流程代码示例

以下代码展示了一个典型的PCIe MSI中断处理框架,包含上半部和Tasklet下半部的注册与实现。

#include <linux/interrupt.h>
#include <linux/pci.h>

struct my_device {
    struct pci_dev *pdev;
    int irq;
    struct tasklet_struct tasklet;
    /* 设备状态与数据缓冲区 */
};

/* 下半部:Tasklet处理函数 */
static void my_tasklet_handler(unsigned long data)
{
    struct my_device *mydev = (struct my_device *)data;

    /* 执行耗时操作:数据拷贝、协议解析、唤醒等待队列 */
    /* 注意:此处仍不可睡眠,但可以持有spinlock */
    spin_lock(&mydev->lock);
    /* 处理完成的数据 */
    spin_unlock(&mydev->lock);

    /* 通知上层应用 */
    wake_up_interruptible(&mydev->wq);
}

/* 上半部:硬中断处理函数 */
static irqreturn_t my_irq_handler(int irq, void *dev_id)
{
    struct my_device *mydev = (struct my_device *)dev_id;
    u32 status;

    /* 1. 读取中断状态寄存器(必须原子操作) */
    status = readl(mydev->regs + REG_INT_STATUS);

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

    /* 3. 清除中断标志(防止重复触发) */
    writel(INT_CLEAR, mydev->regs + REG_INT_CLEAR);

    /* 4. 调度下半部处理数据 */
    tasklet_schedule(&mydev->tasklet);

    /* 5. 返回IRQ_HANDLED表示已处理 */
    return IRQ_HANDLED;
}

/* 驱动初始化时注册中断 */
static int my_pci_probe(struct pci_dev *pdev, const struct pci_device_id *id)
{
    struct my_device *mydev;
    int ret;

    /* 分配设备结构体 */
    mydev = devm_kzalloc(&pdev->dev, sizeof(*mydev), GFP_KERNEL);
    if (!mydev)
        return -ENOMEM;

    /* 启用PCIe设备并分配MSI中断向量 */
    ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI);
    if (ret < 0)
        return ret;

    mydev->irq = pci_irq_vector(pdev, 0);

    /* 初始化下半部Tasklet */
    tasklet_init(&mydev->tasklet, my_tasklet_handler, (unsigned long)mydev);

    /* 注册上半部中断处理函数 */
    ret = request_irq(mydev->irq, my_irq_handler, 0,
                      "my_pcie_driver", mydev);
    if (ret) {
        pci_free_irq_vectors(pdev);
        return ret;
    }

    return 0;
}
关键点: 上半部 my_irq_handler 中只做最必要的寄存器操作,将数据密集型处理推迟到 my_tasklet_handler 中执行。这种设计保证了中断响应速度,同时避免了在原子上下文中执行耗时操作。

2.7 中断上下文与下半部选择决策树

在实际驱动开发中,可以根据以下决策树快速选择下半部机制:

下半部机制选择决策树 中断处理需要睡眠? 否 → 不可睡眠 需要高并发并行处理? 是 → 软中断 (需处理重入) 否 → Tasklet (串行执行) 是 → 需要睡眠 需要顺序执行? 是 → 绑定Workqueue (单worker) 否 → 共享Workqueue (多worker并行)

2.8 本章小结

  • Linux中断子系统采用分层架构,从硬件中断控制器到驱动处理函数,经过irq_chip、irq_domain、通用中断层等多个抽象层。
  • 中断号映射是PCIe MSI驱动的关键步骤:硬件hwirq通过irq_domain转换为Linux IRQ number,驱动使用该编号注册处理函数。
  • 中断上下文具有严格的原子性约束,禁止睡眠、禁止访问用户空间、禁止使用互斥锁。
  • 上半部负责快速响应和调度下半部,下半部负责耗时数据处理。选择合适的下半部机制(软中断/Tasklet/Workqueue/线程化IRQ)直接影响驱动性能和实时性。
  • PCIe MSI驱动开发中,推荐将寄存器操作放在上半部,数据拷贝和协议处理放在下半部,以最大化吞吐量。