2. Linux中断子系统架构:从硬件中断到CPU处理的全链路分析

中断子系统是Linux内核与硬件交互的核心枢纽。本章将沿着中断信号从外设引脚到CPU执行上下文的完整路径,逐层剖析中断控制器、CPU接口、内核入口、中断分发与执行的全链路架构。

2.1 中断子系统分层模型

Linux中断子系统采用清晰的分层设计,每一层负责不同的抽象与硬件交互。下表总结了各层的主要职责与核心数据结构:

层次 职责 核心数据结构/接口
硬件层 外设产生中断信号,中断控制器仲裁与路由 GIC、APIC、IO-APIC 寄存器
中断控制器抽象层 封装不同中断控制器的操作,提供统一接口 struct irq_chip
中断号映射层 将硬件中断号映射为Linux IRQ号 struct irq_domain
中断描述符层 管理每个IRQ的状态、处理函数、线程化信息 struct irq_desc
中断处理入口层 CPU异常向量表入口,保存/恢复上下文 arch/arm64/kernel/entry.S
通用中断处理层 调用中断描述符链上的处理函数,处理线程化 handle_irq_desc()
关键概念: 硬件中断号(hwirq)与Linux IRQ号是两套独立的编号体系。irq_domain 负责在两者之间建立映射关系,这是设备树与ACPI中断解析的核心机制。

2.2 全链路数据流与核心组件

下图展示了从外设触发中断到CPU执行中断服务例程的完整数据流,以及各阶段涉及的关键组件:

Linux中断子系统全链路架构图 中断全链路:从硬件到CPU处理 外设 (Peripheral) 中断控制器 (GIC/APIC) CPU 中断接口 中断请求线 IRQ 信号 异常向量表 (entry.S) irq_desc 描述符 handle_irq_desc() CPU 跳转 查找 分发 action 链表 (共享中断) ISR / 中断线程 遍历 调用 架构要点说明: 1. 中断控制器负责优先级仲裁、CPU路由、电平/边沿检测,是硬件与内核的桥梁。 2. irq_desc 是每个IRQ的核心管理结构,包含状态、芯片操作、action链表、线程化信息。 3. 共享中断通过 action 链表实现,多个驱动注册同一个IRQ号,内核依次调用其处理函数。

2.3 中断控制器抽象:struct irq_chip

irq_chip 是中断控制器硬件操作的抽象层。每个中断控制器驱动(如GICv3、APIC)都需要实现该结构体中的回调函数。核心回调包括:

  • irq_ack:应答中断,通常发生在中断处理早期,防止同一中断再次触发。
  • irq_mask / irq_unmask:屏蔽/取消屏蔽特定IRQ,用于流控与线程化。
  • irq_set_type:设置中断触发方式(电平触发、边沿触发)。
  • irq_set_affinity:设置中断亲和性,将中断绑定到指定CPU核心。
实战提示: 在编写中断控制器驱动时,务必实现 irq_ack 与 irq_mask/unmask。缺失 irq_ack 会导致中断无法被正确应答,系统可能陷入无限中断循环。

2.4 中断号映射:irq_domain

irq_domain 是现代Linux内核中管理硬件中断号到Linux IRQ号映射的核心机制。它支持三种映射方式:

映射类型 描述 适用场景
线性映射 (Linear) 硬件中断号直接作为Linux IRQ号的偏移 中断号连续且数量固定的控制器
树形映射 (Tree) 使用基数树存储映射关系,支持稀疏中断号 中断号不连续或数量庞大的系统
不映射 (No Map) 由驱动自行管理IRQ号的分配 特殊硬件或虚拟中断控制器

设备树中的中断描述通过 interrupts 属性指定硬件中断号,内核在初始化时调用 irq_domain_alloc_irqs() 完成映射:

// 设备树示例:外设中断描述
uart@fe000000 {
    compatible = "vendor,uart";
    reg = <0x0 0xfe000000 0x0 0x1000>;
    interrupts = <0 33 4>;  // GIC SPI 33, 高电平触发
};

// 内核映射过程(简化)
struct irq_domain *domain = irq_find_host(dev->of_node);
unsigned int virq = irq_create_mapping(domain, hwirq);
ret = request_irq(virq, my_handler, 0, "my_device", dev);

2.5 中断描述符:irq_desc 内部结构

每个注册的IRQ都对应一个 struct irq_desc 实例,它是中断管理的核心数据结构。其关键字段如下:

struct irq_desc {
    struct irq_common_data  irq_common_data;
    struct irq_data         irq_data;
    unsigned int __percpu   *kstat_irqs;    // 中断统计
    irq_flow_handler_t      handle_irq;     // 流控处理函数
    struct irqaction        *action;        // 处理函数链表
    unsigned int            status_use_accessors; // 中断状态
    struct task_struct      *thread;        // 中断线程化任务
    struct cpumask          *percpu_enabled; // CPU亲和性掩码
    ...
};
注意: irq_desc 中的 action 链表是实现共享中断的关键。多个驱动可以注册同一个IRQ号,内核在中断发生时依次调用链表上的所有处理函数。每个处理函数必须检查中断是否来自自己的设备,若不是则立即返回 IRQ_NONE。

2.6 中断处理入口与通用处理层

当CPU收到中断信号后,硬件自动跳转到异常向量表中对应的入口。以ARM64为例,中断入口在 arch/arm64/kernel/entry.S 中定义:

// ARM64 中断入口(简化)
el1_irq:
    // 保存上下文
    stp     x29, x30, [sp, #-16]!
    // 调用 C 函数
    bl      handle_arch_irq
    // 恢复上下文
    ldp     x29, x30, [sp], #16
    eret

// handle_arch_irq 最终调用到:
void handle_irq_desc(struct irq_desc *desc) {
    // 调用流控处理函数
    desc->handle_irq(desc);
    // 流控函数内部会调用 __handle_irq_event_percpu()
    // 进而遍历 action 链表,执行每个注册的处理函数
}

通用中断处理层 __handle_irq_event_percpu() 负责遍历 action 链表,依次调用每个处理函数,并处理线程化中断的唤醒逻辑:

irqreturn_t __handle_irq_event_percpu(struct irq_desc *desc) {
    irqreturn_t retval = IRQ_NONE;
    struct irqaction *action;

    for_each_action_of_desc(desc, action) {
        irqreturn_t res;
        // 调用注册的处理函数
        res = action->handler(irq, action->dev_id);
        if (res == IRQ_WAKE_THREAD)
            // 唤醒中断线程
            wake_up_process(action->thread);
        retval |= res;
    }
    return retval;
}

2.7 中断线程化机制

中断线程化是Linux内核减少关中断时间、提高实时性的重要机制。其核心思想是将中断处理分为两部分:

  • 上半部(硬中断上下文):在关中断或屏蔽中断的状态下执行,只做最必要的硬件操作(如应答中断、读取状态寄存器),然后返回 IRQ_WAKE_THREAD。
  • 下半部(线程上下文):内核唤醒一个内核线程执行剩余的处理逻辑,此时中断已重新使能,可以执行耗时操作(如数据拷贝、协议处理)。

线程化中断的注册方式:

// 注册线程化中断
request_threaded_irq(irq, 
                     hard_handler,  // 上半部,可为 NULL
                     thread_fn,     // 下半部线程函数
                     IRQF_SHARED,   // 支持共享中断
                     "my_driver", dev_id);

// 当 hard_handler 返回 IRQ_WAKE_THREAD 时,内核唤醒线程执行 thread_fn
性能考量: 线程化中断将处理函数移出硬中断上下文,允许其他中断被响应,显著降低了中断延迟抖动。但线程切换本身有开销,对于高频中断(如网络收包),建议仍使用传统上半部或使用 NAPI 轮询。

2.8 共享中断实战要点

共享中断允许多个设备共用同一个IRQ号,在嵌入式系统和PCI总线中极为常见。实现共享中断必须遵守以下规则:

  1. 注册时指定 IRQF_SHARED 标志,否则内核会拒绝共享。
  2. 每个处理函数必须检查中断是否来自自己的设备,通过读取设备状态寄存器判断。
  3. 处理函数必须返回 IRQ_NONE 或 IRQ_HANDLED,内核根据返回值判断中断是否被正确处理。
  4. 所有共享该IRQ的驱动必须使用相同的触发方式(电平或边沿),否则会导致中断丢失或风暴。

共享中断的典型实现模式:

static irqreturn_t my_shared_handler(int irq, void *dev_id) {
    struct my_device *dev = (struct my_device *)dev_id;
    u32 status;

    // 读取设备中断状态寄存器
    status = readl(dev->base + INT_STATUS);
    if (!(status & MY_DEVICE_INT_PENDING))
        return IRQ_NONE;  // 不是本设备的中断

    // 处理中断
    writel(status, dev->base + INT_CLEAR);  // 清除中断标志
    // ... 处理数据 ...

    return IRQ_HANDLED;
}

// 注册
request_irq(irq, my_shared_handler, IRQF_SHARED, "my_device", dev);
调试技巧: 通过 /proc/interrupts 可以查看每个IRQ的触发次数和注册的处理函数。如果某个共享中断的处理函数频繁返回 IRQ_NONE,说明该设备可能误触发或硬件存在异常。