4、中断号映射与irq_domain:线性映射、树形映射、不映射三种方式
中断号映射,说白了就是解决一个问题:硬件中断线(IRQ line)和Linux内核里的虚拟中断号(virq)怎么对应起来。
我刚开始接触中断子系统时,觉得这玩意儿挺绕的。硬件上来了一个中断,CPU怎么知道该调用哪个处理函数?嗯,这里就要靠irq_domain这个中间层来搭桥。
4.1 为什么需要irq_domain?
先说说背景。早期的嵌入式系统,中断控制器很简单,比如一颗Cortex-M3的NVIC,硬件中断号是固定的0~15。内核直接拿硬件号当虚拟号用,映射关系是1:1,省事。
但后来情况变了。现在的SoC里,中断控制器越来越复杂:
- 有级联的GIC(Generic Interrupt Controller)
- 有多个中断控制器串联(比如GPIO控制器也产生中断)
- 硬件中断号可能不连续,甚至动态分配
这时候,你没法再硬编码了。内核引入了irq_domain,专门管理这个映射关系。我个人习惯把它理解成一个“翻译官”——硬件说“我来了,我是第17号线”,irq_domain告诉内核“这个17号对应的是虚拟中断号42,去调用它的handler”。
4.2 三种映射方式
irq_domain支持三种映射方式,我按使用场景给你拆开讲。
| 映射方式 | 适用场景 | 特点 |
|---|---|---|
| 线性映射(Linear) | 硬件中断号连续且数量固定 | 查找快,O(1)复杂度,但浪费内存 |
| 树形映射(Tree) | 硬件中断号稀疏或动态分配 | 节省内存,查找O(log n) |
| 不映射(No Map) | 硬件中断号直接等于虚拟中断号 | 最简单,但灵活性差 |
4.2.1 线性映射
线性映射是最直接的方式。你预先分配一个数组,数组下标就是硬件中断号,数组元素就是对应的irq_desc结构体指针。
我在项目中遇到过一颗海思的芯片,它的GIC支持最多160个中断源,硬件号从32到191连续分布。这种场景用线性映射最合适:
struct irq_domain *domain;
domain = irq_domain_create_linear(fwnode, 160, &ops, NULL);
这里160就是最大中断数。内核会分配一个160个元素的数组,每个元素占一个指针大小。查找时直接拿硬件号做下标,速度飞快。
4.2.2 树形映射
树形映射就灵活多了。它用基数树(radix tree)来存储映射关系,硬件号不连续也没关系。
我记得有一次调试一个FPGA的中断控制器,它只有8个中断源,但硬件号是随机分配的:3、7、12、25、33、41、56、88。这种稀疏分布,用线性映射得分配88个槽位,太浪费了。树形映射就完美解决:
struct irq_domain *domain;
domain = irq_domain_create_tree(fwnode, &ops, NULL);
树形映射的查找复杂度是O(log n),比线性映射慢一点,但内存利用率高。对于中断源数量不多(比如几十个)的场景,这点性能差异可以忽略。
4.2.3 不映射
不映射,就是硬件中断号直接等于虚拟中断号。说白了,内核根本不做映射,拿来就用。
这种模式适用于最简单的场景,比如一颗单片机裸跑,或者内核里只有一个中断控制器,且硬件号从0开始连续。我早期在Cortex-M3上做裸机开发时,就是这么干的:
struct irq_domain *domain;
domain = irq_domain_create_linear(fwnode, 0, &ops, NULL);
// 注意:这里size传0,表示不映射
但说实话,在复杂SoC里,不映射基本用不上。因为一旦有多个中断控制器级联,硬件号必然冲突。比如GIC用了0~31作为PPI,GPIO控制器又用0~15作为自己的中断号,你不映射就乱套了。
4.3 irq_create_mapping 与 irq_find_mapping
这两个函数是irq_domain的核心操作,一个负责创建映射,一个负责查找映射。
4.3.1 irq_create_mapping
这个函数的作用是:给定一个硬件中断号,分配一个虚拟中断号,并建立映射关系。
原型如下:
unsigned int irq_create_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq);
返回值就是虚拟中断号。如果返回0,说明分配失败。
我在项目中写中断控制器驱动时,通常这样用:
static int my_irq_chip_domain_alloc(struct irq_domain *domain,
unsigned int virq,
unsigned int nr_irqs,
void *data)
{
struct irq_fwspec *fwspec = data;
irq_hw_number_t hwirq = fwspec->param[0];
int i;
for (i = 0; i < nr_irqs; i++) {
irq_map_generic_chip(domain, virq + i, hwirq + i);
irq_domain_set_info(domain, virq + i, hwirq + i,
&my_chip, NULL, handle_level_irq,
NULL, NULL);
}
return 0;
}
这里irq_create_mapping其实是在上层调用的,驱动里只需要在domain_alloc回调里把硬件号和虚拟号关联起来就行。
4.3.2 irq_find_mapping
这个函数是反向查找:给定硬件中断号,找到对应的虚拟中断号。
unsigned int irq_find_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq);
它通常在中断处理流程中被调用。比如在GIC驱动里,硬件中断来了,读出硬件号,然后通过irq_find_mapping找到虚拟号,再调用generic_handle_irq去分发。
我曾经踩过一个坑:在中断上下文里调用irq_find_mapping,结果它内部有spinlock,导致死锁。后来查了源码才发现,irq_find_mapping在查找时确实会拿锁,但如果是线性映射,它其实可以不用锁——因为数组是静态的。嗯,这里要注意,如果你的中断控制器是线性映射,可以考虑用irq_linear_revmap来替代,它更快:
unsigned int irq_linear_revmap(struct irq_domain *domain,
irq_hw_number_t hwirq);
这个函数直接拿硬件号做下标查数组,没有锁,适合中断上下文。
4.4 三种映射方式的选择建议
我根据实际项目经验,给你一个简单的选择流程:
- 硬件中断号连续且数量固定 → 线性映射
- 硬件中断号稀疏或动态分配 → 树形映射
- 只有一个中断控制器,且硬件号从0开始连续 → 不映射
但说实话,现在的主流SoC里,线性映射用得最多。因为GIC、NVIC这些控制器,硬件号都是连续的。树形映射更多用在PCIe MSI、GPIO控制器这些场景。
4.5 知识体系结构图
下面这张图,帮你理清irq_domain的核心逻辑:
4.6 避坑指南
最后,分享几个我实际踩过的坑:
- 中断上下文别调irq_create_mapping:这个函数可能会睡眠(分配内存、拿锁),只能在进程上下文调用。我当年在中断处理里调它,直接Oops。
- 线性映射的size别设太小:如果硬件中断号超过了size,映射会失败。我建议设成硬件支持的最大中断数,或者干脆用树形映射。
- irq_find_mapping返回0要处理:返回0表示没找到映射。如果你直接拿0去调用generic_handle_irq,会触发空指针。我曾经因为这个,排查了一整天。
好了,中断号映射这块就讲到这里。记住一句话:irq_domain是连接硬件和内核中断子系统的桥梁,选对映射方式,你的驱动就成功了一半。
公众号:蓝海资料掘金营,微信deep3321