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),比线性映射慢一点,但内存利用率高。对于中断源数量不多(比如几十个)的场景,这点性能差异可以忽略。

注意:树形映射在中断频繁注册/注销时,会有内存分配和释放的开销。如果你的中断是热插拔的(比如PCIe MSI中断),要留意这个。

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回调里把硬件号和虚拟号关联起来就行。

关键点:irq_create_mapping会自动检查这个硬件号是否已经映射过。如果已经映射,直接返回已有的虚拟号,不会重复分配。所以你可以放心多次调用。

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 三种映射方式的选择建议

我根据实际项目经验,给你一个简单的选择流程:

  1. 硬件中断号连续且数量固定 → 线性映射
  2. 硬件中断号稀疏或动态分配 → 树形映射
  3. 只有一个中断控制器,且硬件号从0开始连续 → 不映射

但说实话,现在的主流SoC里,线性映射用得最多。因为GIC、NVIC这些控制器,硬件号都是连续的。树形映射更多用在PCIe MSI、GPIO控制器这些场景。

4.5 知识体系结构图

下面这张图,帮你理清irq_domain的核心逻辑:

irq_domain 中断号映射体系 硬件中断源 hwirq = 17 irq_domain 映射管理器 hwirq → virq 虚拟中断号 virq = 42 线性映射 树形映射 不映射 irq_create_mapping irq_find_mapping irq_create_mapping: 分配并建立映射 irq_find_mapping: 根据hwirq查找virq 线性映射: O(1)查找 | 树形映射: O(log n)查找 | 不映射: 1:1

4.6 避坑指南

最后,分享几个我实际踩过的坑:

  • 中断上下文别调irq_create_mapping:这个函数可能会睡眠(分配内存、拿锁),只能在进程上下文调用。我当年在中断处理里调它,直接Oops。
  • 线性映射的size别设太小:如果硬件中断号超过了size,映射会失败。我建议设成硬件支持的最大中断数,或者干脆用树形映射。
  • irq_find_mapping返回0要处理:返回0表示没找到映射。如果你直接拿0去调用generic_handle_irq,会触发空指针。我曾经因为这个,排查了一整天。

好了,中断号映射这块就讲到这里。记住一句话:irq_domain是连接硬件和内核中断子系统的桥梁,选对映射方式,你的驱动就成功了一半。


公众号:蓝海资料掘金营,微信deep3321