2. 中断机制概述:Linux中断子系统、中断号、中断上下文、上半部与下半部
中断是硬件与软件之间最关键的异步通信机制。在PCIe MSI中断的驱动开发中,理解Linux中断子系统的整体架构、中断号的分配逻辑、中断上下文的划分以及上半部与下半部的协作模式,是编写高性能、无锁、低延迟驱动的基石。本节将系统梳理这些核心概念,并辅以结构图和对比表格,帮助开发者建立清晰的中断处理全景图。
2.1 Linux中断子系统架构
Linux中断子系统采用分层设计,从硬件中断信号到最终驱动处理函数,经过多个抽象层。每一层负责不同的职责:硬件封装、中断路由、通用中断处理与驱动回调。
核心分层: 硬件中断控制器 → irq_chip → irq_domain → 通用中断层 (Generic 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()注册。切勿直接使用硬件中断号。
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完成处理。
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 中断上下文与下半部选择决策树
在实际驱动开发中,可以根据以下决策树快速选择下半部机制:
2.8 本章小结
- Linux中断子系统采用分层架构,从硬件中断控制器到驱动处理函数,经过irq_chip、irq_domain、通用中断层等多个抽象层。
- 中断号映射是PCIe MSI驱动的关键步骤:硬件hwirq通过irq_domain转换为Linux IRQ number,驱动使用该编号注册处理函数。
- 中断上下文具有严格的原子性约束,禁止睡眠、禁止访问用户空间、禁止使用互斥锁。
- 上半部负责快速响应和调度下半部,下半部负责耗时数据处理。选择合适的下半部机制(软中断/Tasklet/Workqueue/线程化IRQ)直接影响驱动性能和实时性。
- PCIe MSI驱动开发中,推荐将寄存器操作放在上半部,数据拷贝和协议处理放在下半部,以最大化吞吐量。