3. 中断描述符与 irq_chip:中断控制器抽象

中断子系统是 Linux 内核响应外设事件的枢纽。在 SMP(对称多处理)系统中,中断处理路径上的每一个数据结构都直接影响中断延迟和 CPU 间的负载分布。本章深入剖析中断描述符 struct irq_desc 与中断控制器抽象 struct irq_chip,为后续中断亲和性设置与负载均衡调优奠定基础。

学习提示: 中断描述符是每个中断源的“档案”,而 irq_chip 是硬件控制器的“驱动接口”。理解这两个结构体,就等于拿到了中断调优的钥匙。

3.1 struct irq_desc:中断源的完整档案

struct irq_desc 是内核为每一个中断号(IRQ number)维护的核心数据结构。它串联了中断的硬件状态、软件处理函数、亲和性掩码以及统计信息。在 include/linux/irqdesc.h 中定义,其关键字段如下:

字段 类型 作用说明
irq_data struct irq_data 关联硬件中断控制器和中断号,包含 chip 指针和 hwirq
handle_irq irq_flow_handler_t 高层的流控处理函数(如 handle_edge_irq、handle_level_irq)
action struct irqaction * 指向中断服务例程链表的指针
irq_count unsigned int 中断发生次数统计,用于检测“失控中断”
irqs_unhandled unsigned int 未被处理的中断计数,用于中断迁移决策
affinity_hint const struct cpumask * 驱动或硬件建议的亲和性掩码
pending_mask struct cpumask 待处理的 CPU 掩码,用于中断迁移时的亲和性更新
lock raw_spinlock_t 保护该描述符的自旋锁
调优关注点: affinity_hint 字段是驱动向内核建议中断亲和性的接口。网卡驱动(如 igb、mlx5)常通过此字段将 RX 队列中断绑定到指定 CPU,实现流式负载均衡。

以下代码展示了如何通过 irq_desc 访问中断的亲和性掩码:

#include <linux/irqdesc.h>
#include <linux/irq.h>

/* 获取中断号 irq 对应的描述符 */
struct irq_desc *desc = irq_to_desc(irq);
if (!desc)
    return -EINVAL;

/* 读取当前亲和性掩码 */
const struct cpumask *affinity = desc->irq_data.common->affinity;
if (cpumask_test_cpu(smp_processor_id(), affinity))
    pr_info("IRQ %d 允许在当前 CPU 上处理\n", irq);

3.2 struct irq_chip:中断控制器的硬件抽象

struct irq_chip 是内核与具体中断控制器硬件之间的适配层。它定义了一组回调函数,用于操作中断控制器的寄存器。无论是 GIC(通用中断控制器)、IO-APIC 还是 MSI-X,都通过 irq_chip 统一接口与内核交互。

核心回调函数如下表所示:

回调函数 用途 典型实现动作
irq_startup 启动中断 使能中断控制器对应的中断线
irq_shutdown 关闭中断 屏蔽中断线并清理待处理状态
irq_enable 使能中断 写寄存器置位中断使能位
irq_disable 禁用中断 写寄存器清除中断使能位
irq_ack 应答中断 向控制器写入 EOI(End Of Interrupt)命令
irq_mask 屏蔽中断 设置屏蔽寄存器,阻止中断传递到 CPU
irq_unmask 解除屏蔽 清除屏蔽寄存器,允许中断传递
irq_set_affinity 设置亲和性 写中断控制器的路由寄存器,将中断定向到指定 CPU
irq_retrigger 重新触发中断 模拟一次硬件中断触发,用于软件重试
关键差异: irq_mask 与 irq_disable 语义不同。mask 通常用于流控处理中的临时屏蔽,而 disable 用于永久关闭中断。在中断亲和性调整过程中,内核会先 mask 中断,更新路由寄存器,再 unmask,确保不会丢失中断。

以下是一个简化的自定义 irq_chip 实现示例:

static void my_chip_mask(struct irq_data *data) {
    void __iomem *base = irq_data_get_irq_chip_data(data);
    writel(0, base + MASK_REG_OFFSET);
}

static void my_chip_unmask(struct irq_data *data) {
    void __iomem *base = irq_data_get_irq_chip_data(data);
    writel(1, base + MASK_REG_OFFSET);
}

static int my_chip_set_affinity(struct irq_data *data,
                                const struct cpumask *dest, bool force) {
    unsigned int cpu = cpumask_first(dest);
    void __iomem *base = irq_data_get_irq_chip_data(data);
    /* 将中断路由到指定 CPU 的寄存器 */
    writel(cpu, base + AFFINITY_REG_OFFSET);
    return IRQ_SET_MASK_OK;
}

static struct irq_chip my_irq_chip = {
    .name           = "my_controller",
    .irq_mask       = my_chip_mask,
    .irq_unmask     = my_chip_unmask,
    .irq_set_affinity = my_chip_set_affinity,
};

3.3 中断控制器抽象与层级关系

现代 SoC 通常包含多个中断控制器,形成树形层级。内核通过 irq_domain 将硬件中断号(hwirq)映射到 Linux IRQ 号,而每个 irq_chip 负责操作其对应的控制器硬件。

下图展示了典型的中断控制器层级与数据结构关系:

中断控制器层级与数据结构关系图 中断控制器层级与 irq_chip/irq_desc 关系 CPU 核心 (0-3) GIC (通用中断控制器) irq_chip: gic_chip irq_domain (hwirq → Linux IRQ 映射) hwirq 32 → IRQ 128 | hwirq 33 → IRQ 129 irq_desc[] 数组 irq_desc[128] irq_desc[129] irq_desc[130] 每个 irq_desc 包含 irq_data.chip 指针,指向对应的 irq_chip(如 gic_chip)

3.4 irq_data:连接描述符与控制器的桥梁

struct irq_data 是 irq_desc 与 irq_chip 之间的纽带。它包含了中断控制器相关的所有硬件信息:

  • irq:Linux IRQ 号。
  • hwirq:硬件中断号,用于控制器内部寻址。
  • chip:指向 irq_chip 结构体的指针。
  • chip_data:芯片私有数据,通常指向控制器的 MMIO 基地址。
  • domain:指向所属的 irq_domain,用于反向查找映射关系。
  • affinity:当前中断的 CPU 亲和性掩码。
调优技巧: 在设置中断亲和性时,内核最终调用 chip->irq_set_affinity(data, mask, force)。对于不支持动态亲和性的老旧控制器,此回调可能为空,此时亲和性设置会失败。使用 /proc/irq/<irq>/smp_affinity 写入前,建议先检查芯片能力。

3.5 中断流控处理函数

irq_desc->handle_irq 指向流控处理函数,它根据中断类型(边沿触发、电平触发)调用不同的处理逻辑。常见的流控函数包括:

流控函数 适用场景 行为特点
handle_level_irq 电平触发中断 进入 handler 前 mask 中断,处理完成后 unmask
handle_edge_irq 边沿触发中断 处理期间不 mask,但需要 ack 清除边沿锁存
handle_fasteoi_irq GIC 等支持 EOI 的控制器 不 mask,直接调用 chip->irq_eoi 结束中断
handle_percpu_devid_irq Per-CPU 中断(如本地定时器) 不需要锁,直接在当前 CPU 上执行

流控函数的选择直接影响中断延迟和亲和性调整的时机。例如,handle_level_irq 在 mask 期间会丢失中断,因此亲和性迁移必须等待处理完成。

3.6 中断描述符的初始化流程

从硬件中断触发到软件处理的完整链路如下:

  1. 硬件触发:外设拉高中断线,中断控制器检测到有效信号。
  2. 控制器仲裁:GIC/IO-APIC 根据优先级和亲和性设置,将中断路由到目标 CPU。
  3. CPU 异常入口:CPU 保存上下文,跳转到 handle_arch_irq 或 do_IRQ。
  4. 查找 irq_desc:根据硬件中断号通过 irq_domain 找到对应的 irq_desc。
  5. 调用流控函数:执行 desc->handle_irq,进行 mask/ack/eoi 操作。
  6. 执行 action 链表:遍历 desc->action,调用注册的中断服务例程。
  7. 清理与返回:unmask 中断(如果需要),恢复 CPU 上下文。
性能关键路径: 步骤 4 的 irq_domain 查找和步骤 5 的流控函数调用是中断延迟的主要开销。使用 CONFIG_IRQ_DOMAIN_HIERARCHY 和线性映射域可以降低查找开销。

3.7 本章小结

  • struct irq_desc 是每个中断的“档案袋”,包含状态、处理函数、亲和性掩码和统计信息。
  • struct irq_chip 是中断控制器的软件抽象,通过回调函数统一操作硬件寄存器。
  • irq_data 作为桥梁,将 irq_desc 与 irq_chip 关联,并携带硬件中断号和芯片私有数据。
  • 流控处理函数(handle_level_irq、handle_edge_irq 等)决定了中断处理过程中的 mask/ack 策略,直接影响亲和性迁移的时机。
  • 理解这些数据结构是进行中断亲和性设置和负载均衡调优的前提,后续章节将基于此深入 /proc/irq 接口和动态平衡算法。