3、Linux中断子系统架构:中断描述符与中断域

中断子系统,说白了就是Linux内核处理硬件中断的一套完整机制。我刚开始接触这块时,觉得它就是个简单的“硬件来了信号→CPU跳转执行”的过程。后来在项目里踩过几次坑,才明白这背后的分层设计有多讲究。

今天咱们就聊聊中断子系统的三层架构,以及两个核心概念:中断描述符(irq_desc)和中断域(irq_domain)。嗯,这两个东西搞明白了,中断驱动开发就算入门了。

3.1 中断子系统的三层架构

Linux中断子系统分为三层,每一层各司其职。我习惯用一张图来理解它们的关系:

Linux 中断子系统三层架构 驱动层(Driver Layer) request_irq() / devm_request_irq() / free_irq() 中断处理函数(handler)注册与注销 核心层(Core Layer) irq_desc 管理 / 中断流控 / 中断分发 handle_irq() / generic_handle_irq() 硬件层(Hardware Layer) 中断控制器(GIC / IOAPIC / PIC) CPU 异常向量表 / 中断信号线 数据流方向:硬件中断信号 → 核心层分发 → 驱动层处理函数

这三层的关系,我总结一下:

  • 硬件层:负责接收物理中断信号,通知CPU。比如ARM平台的GIC(通用中断控制器),x86的IOAPIC。这一层跟具体芯片强相关。
  • 核心层:内核通用的中断管理机制。它不关心你用的是GIC还是IOAPIC,只负责把中断号映射好、把处理函数调度好。
  • 驱动层:我们写驱动时打交道最多的部分。调用request_irq()注册自己的处理函数,中断来了就干活。
我的经验:写驱动时,你只需要关心驱动层。但一旦遇到中断号对不上、中断丢失、共享中断冲突这些问题,就得往核心层和硬件层去排查。我有个项目,GPIO中断死活不触发,查了两天,最后发现是中断域映射配错了。

3.2 中断描述符(irq_desc)

irq_desc是核心层最重要的数据结构。每个中断号都对应一个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 list_head        pending;       // 待处理中断列表
    struct proc_dir_entry   *dir;          // proc文件系统接口
    int                     parent_irq;
    struct module           *owner;
    const char              *name;
} ____cacheline_internodealigned_in_smp;

这里有几个关键点:

  • handle_irq:中断流控处理函数。它决定中断怎么被处理——是边沿触发还是电平触发?是否需要屏蔽?我见过不少驱动开发者直接忽略这个字段,结果中断触发方式不对,导致系统卡死。
  • action:一个链表,挂载了所有注册到该中断号的处理函数。共享中断时,这个链表就派上用场了。
  • kstat_irqs:每个CPU的中断统计信息。调试时看看这个,能知道中断是不是均衡分发到了各个核上。
注意:irq_desc是核心层的内部数据结构,驱动代码不要直接访问它。内核提供了irq_to_desc()等辅助函数,但除非你在写核心层代码,否则别碰它。我曾经见过有人直接操作irq_desc来修改中断触发方式,结果内核panic了。

3.3 中断域(irq_domain)

中断域是Linux 3.x之后引入的概念。为什么要引入它?说白了,就是为了解决硬件中断号和Linux中断号之间的映射问题。

你想想看,硬件上的中断线编号是芯片厂商定的,比如GIC的SPI中断号从32开始。但Linux内部用的中断号是线性分配的,从0到NR_IRQS。这两者怎么对应?

中断域就是做这个映射的。它的核心结构如下:

struct irq_domain {
    struct list_head        link;           // 全局链表,串联所有中断域
    const char              *name;          // 域名,调试用
    const struct irq_domain_ops *ops;       // 操作函数集
    struct device_node      *of_node;       // 设备树节点
    unsigned int            hwirq_max;      // 最大硬件中断号
    unsigned int            revmap_size;    // 线性映射表大小
    unsigned int            revmap_direct_max_irq;
    struct radix_tree_root  revmap_tree;    // 基数树,用于非线性映射
    unsigned int            linear_revmap[]; // 线性映射表
};

中断域支持三种映射方式:

映射方式 适用场景 特点
线性映射 硬件中断号连续且数量少 查找快,O(1)复杂度;但浪费内存
基数树映射 硬件中断号稀疏或不连续 节省内存;查找稍慢
不映射(直通) 硬件中断号直接作为Linux中断号 最简单,但很少用

实际项目中的选择:我在一个ARM64项目上,GIC有几百个SPI中断,硬件中断号是连续的。这种情况下用线性映射最合适。另一个项目用FPGA做了个自定义中断控制器,中断号分布很稀疏,我就选了基数树映射。

3.4 中断域的操作函数集

每个中断域都要实现一组操作函数,定义在struct irq_domain_ops中:

struct irq_domain_ops {
    int (*match)(struct irq_domain *d, struct device_node *node,
                 enum irq_domain_bus_token bus_token);
    int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
    void (*unmap)(struct irq_domain *d, unsigned int virq);
    int (*xlate)(struct irq_domain *d, struct device_node *node,
                 const u32 *intspec, unsigned int intsize,
                 unsigned long *out_hwirq, unsigned int *out_type);
};

这几个函数的作用:

  • match:判断设备树节点是否属于这个中断域。多中断域系统里,这个函数用来匹配正确的域。
  • map:建立硬件中断号到Linux中断号的映射。核心操作,必须实现。
  • unmap:解除映射。驱动卸载或设备移除时调用。
  • xlate:解析设备树中的中断描述,提取硬件中断号和触发类型。这个函数写不好,设备树配得再对也没用。
调试技巧:如果你发现中断注册失败,先检查中断域的xlate函数是否正确解析了设备树。我习惯在xlate函数里加一段printk,打印出解析到的硬件中断号和触发类型。这招帮我解决过不少问题。

3.5 中断描述符与中断域的关系

这两者的关系,我画个简图:

irq_domain 与 irq_desc 的关系 irq_domain hwirq → virq 映射 ops: map / xlate / unmap irq_desc[] 数组 每个 virq 对应一个 irq_desc handle_irq / action / status 工作流程: 硬件中断 → irq_domain.map(hwirq) → 分配 virq → 填充 irq_desc[virq] 驱动 request_irq(virq, handler) → 挂入 irq_desc[virq].action 链表

简单来说:

  • irq_domain负责把硬件中断号(hwirq)翻译成Linux中断号(virq)。
  • irq_desc以virq为索引,存储该中断的所有信息。
  • 驱动通过virq来注册处理函数,内核通过virq找到对应的irq_desc,然后调用处理函数。
避坑指南:我曾经在一个项目里,设备树配了两个中断控制器,结果中断域匹配出了问题。驱动注册中断时,内核用了错误的中断域做映射,导致中断号完全不对。排查方法:在irq_domain_associate()函数里加调试信息,打印出匹配到的中断域名称和映射结果。

3.6 小结

中断子系统的三层架构,把硬件差异、核心管理和驱动使用分得清清楚楚。irq_desc是核心层的数据枢纽,irq_domain是硬件与软件之间的翻译官。搞懂这两个概念,你写中断驱动时就不会一头雾水了。