3、Linux中断子系统架构:中断描述符与中断域
中断子系统,说白了就是Linux内核处理硬件中断的一套完整机制。我刚开始接触这块时,觉得它就是个简单的“硬件来了信号→CPU跳转执行”的过程。后来在项目里踩过几次坑,才明白这背后的分层设计有多讲究。
今天咱们就聊聊中断子系统的三层架构,以及两个核心概念:中断描述符(irq_desc)和中断域(irq_domain)。嗯,这两个东西搞明白了,中断驱动开发就算入门了。
3.1 中断子系统的三层架构
Linux中断子系统分为三层,每一层各司其职。我习惯用一张图来理解它们的关系:
这三层的关系,我总结一下:
- 硬件层:负责接收物理中断信号,通知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负责把硬件中断号(hwirq)翻译成Linux中断号(virq)。
- irq_desc以virq为索引,存储该中断的所有信息。
- 驱动通过virq来注册处理函数,内核通过virq找到对应的irq_desc,然后调用处理函数。
避坑指南:我曾经在一个项目里,设备树配了两个中断控制器,结果中断域匹配出了问题。驱动注册中断时,内核用了错误的中断域做映射,导致中断号完全不对。排查方法:在
irq_domain_associate()函数里加调试信息,打印出匹配到的中断域名称和映射结果。
3.6 小结
中断子系统的三层架构,把硬件差异、核心管理和驱动使用分得清清楚楚。irq_desc是核心层的数据枢纽,irq_domain是硬件与软件之间的翻译官。搞懂这两个概念,你写中断驱动时就不会一头雾水了。