3、中断控制器驱动框架:Linux irqchip 子系统、struct irq_chip 结构体、irq_domain 映射机制

好,咱们接着聊中断。上一章我们把 ARM GIC 的硬件行为摸了个透,但硬件是硬件,软件是软件。Linux 内核怎么管理这些中断控制器?怎么把硬件中断号和软件中断号对应起来?这就是今天要讲的核心——irqchip 子系统

说实话,我早年刚接触这部分时,也被绕得晕头转向。什么 irq_chip、irq_domain,一堆抽象概念。但后来我悟了:你只要搞懂三个东西,整个框架就通了——谁来操作硬件寄存器(irq_chip)、谁来管理映射关系(irq_domain)、谁来描述中断源(struct irq_desc)。

核心三要素:

  • irq_chip:操作中断控制器硬件寄存器的函数集合
  • irq_domain:硬件中断号 → Linux 软件中断号的映射表
  • struct irq_desc:每个软件中断号对应的描述符,包含处理函数、标志位等

3.1 irqchip 子系统:谁在管这些中断控制器?

Linux 内核里,所有中断控制器驱动都注册到 irqchip 子系统。你可以把它理解成一个「中断控制器驱动管理中心」。每个中断控制器(GIC、GPIO 中断控制器、IOAPIC 等)都对应一个 struct irq_chip 实例。

我习惯把 irqchip 子系统比作一个「硬件操作工具箱」。每个中断控制器就是一把工具,而 irq_chip 就是这把工具的使用说明书——告诉你如何打开、关闭、屏蔽、唤醒这个中断。

内核启动时,中断控制器驱动会调用 irq_chip_register() 或直接通过设备树匹配来注册。注册完成后,这个中断控制器就纳入了内核的统一管理。

我的经验:调试新板子时,我经常先检查 /proc/interrupts/sys/kernel/debug/irq/。如果某个中断控制器没注册成功,这里会直接暴露出来。我曾经花了两天查一个 GPIO 中断不触发的问题,最后发现是 irq_chip 的 irq_ack 回调没实现——硬件中断来了没人应答,系统直接卡死。

3.2 struct irq_chip:中断控制器的操作手册

这个结构体定义了中断控制器的所有操作接口。说白了,它就是一份「硬件操作函数表」。每个中断控制器驱动都要实现它。

struct irq_chip {
    const char      *name;        // 控制器名字,/proc/interrupts 里能看到
    unsigned int    (*irq_mask)(struct irq_data *data);      // 屏蔽中断
    void            (*irq_unmask)(struct irq_data *data);    // 取消屏蔽
    void            (*irq_ack)(struct irq_data *data);       // 应答中断
    void            (*irq_eoi)(struct irq_data *data);       // 中断结束(EOI)
    int             (*irq_set_type)(struct irq_data *data, unsigned int flow_type);  // 设置触发方式
    int             (*irq_set_wake)(struct irq_data *data, unsigned int on);         // 设置唤醒源
    // ... 还有其他回调,但上面这些是最常用的
};

每个回调函数接收一个 struct irq_data 参数。这个结构体包含了中断号、硬件寄存器地址等关键信息。我刚开始写驱动时,总搞不清 irq_datairq_desc 的区别——irq_data 是给芯片操作函数用的,irq_desc 是给中断处理流程用的,分工明确。

回调函数 作用 我踩过的坑
irq_mask 屏蔽某个中断源 忘记实现这个,中断风暴直接打崩系统
irq_unmask 取消屏蔽 和 mask 要成对实现,否则中断永远打不开
irq_ack 应答中断,告诉硬件我收到了 电平触发的中断,ack 必须在处理完之前调用,否则会丢失
irq_eoi 中断处理结束,硬件可以发下一个中断了 GIC 的 EOI 写错寄存器地址,导致中断无法再触发
irq_set_type 设置上升沿/下降沿/高电平/低电平触发 设备树里配了上升沿,驱动里却设成电平,中断永远不触发

注意:irq_chip 的回调函数都是在中断上下文或关中断的情况下执行的。千万别在里面做耗时操作,比如打印、申请内存、拿锁等。我曾经在 irq_ack 里加了个 printk,结果系统直接死锁——中断上下文里调用 printk 会触发控制台锁,而控制台锁又被中断打断...死循环了。

3.3 irq_domain:硬件中断号到软件中断号的桥梁

这是整个中断子系统里最巧妙的设计。为什么需要它?

你想想看,硬件中断控制器上的中断号是固定的——比如 GIC 的 SPI 中断号从 32 到 1019。但 Linux 内核里,每个中断用一个 irq 整数表示,这个数字是内核动态分配的。硬件中断号和软件中断号之间,必须有一个映射关系。

irq_domain 就是这张映射表。

内核支持三种映射方式:

  • 线性映射(Linear):硬件中断号连续,直接做偏移计算。最简单,性能最好。适合 GIC 这种中断号连续的控制器。
  • 树形映射(Tree):硬件中断号不连续,用红黑树查找。适合 GPIO 中断控制器,中断号可能很稀疏。
  • 不映射(No Map):硬件中断号和软件中断号一一对应,不做转换。适合一些简单的嵌入式控制器。
// 创建设备树的 irq_domain 示例(GIC 驱动中常见)
struct irq_domain *domain;

domain = irq_domain_add_linear(node, nr_irqs, &gic_irq_domain_ops, NULL);
if (!domain) {
    pr_err("Failed to add irq domain\n");
    return -ENOMEM;
}

这里 gic_irq_domain_ops 是关键,它定义了如何从硬件中断号翻译到软件中断号:

static const struct irq_domain_ops gic_irq_domain_ops = {
    .map = gic_irq_domain_map,      // 建立映射
    .unmap = gic_irq_domain_unmap,  // 解除映射
    .xlate = gic_irq_domain_xlate,  // 从设备树解析中断号
};

设备树里写 interrupts = <0 42 4>,内核怎么知道 42 对应哪个软件中断号?就是通过 xlate 回调来解析的。解析完后,map 回调负责建立真正的映射关系。

一句话总结映射流程:

  1. 设备树解析中断属性 → 得到硬件中断号(如 42)
  2. irq_domain 的 xlate 回调解析出硬件中断号和触发类型
  3. irq_domain 分配一个空闲的软件中断号(如 168)
  4. map 回调建立 42 → 168 的映射,并初始化 irq_desc
  5. 以后硬件触发中断 42,内核自动找到软件中断号 168,执行对应的处理函数

3.4 实战:一个简单的 irqchip 驱动骨架

光说不练假把式。我给你看一个我实际项目中用过的 irqchip 驱动骨架。这个控制器是一个 FPGA 里实现的中断控制器,支持 32 个中断源。

static void myic_irq_mask(struct irq_data *d)
{
    struct myic_chip *chip = irq_data_get_irq_chip_data(d);
    u32 mask = readl(chip->base + MYIC_MASK);
    mask &= ~BIT(d->hwirq);  // 屏蔽对应位
    writel(mask, chip->base + MYIC_MASK);
}

static void myic_irq_unmask(struct irq_data *d)
{
    struct myic_chip *chip = irq_data_get_irq_chip_data(d);
    u32 mask = readl(chip->base + MYIC_MASK);
    mask |= BIT(d->hwirq);   // 取消屏蔽
    writel(mask, chip->base + MYIC_MASK);
}

static struct irq_chip myic_chip = {
    .name = "my_irq_controller",
    .irq_mask = myic_irq_mask,
    .irq_unmask = myic_irq_unmask,
    .irq_ack = myic_irq_mask,  // 电平触发时,ack 就是 mask
    .irq_set_type = myic_irq_set_type,
};

static int myic_domain_map(struct irq_domain *d, unsigned int irq,
                           irq_hw_number_t hw)
{
    irq_set_chip_and_handler(irq, &myic_chip, handle_level_irq);
    irq_set_chip_data(irq, d->host_data);
    irq_set_noprobe(irq);
    return 0;
}

static const struct irq_domain_ops myic_domain_ops = {
    .map = myic_domain_map,
    .xlate = irq_domain_xlate_twocell,  // 使用标准的两单元解析
};

static int myic_probe(struct platform_device *pdev)
{
    struct myic_chip *chip;
    struct irq_domain *domain;

    chip = devm_kzalloc(&pdev->dev, sizeof(*chip), GFP_KERNEL);
    chip->base = devm_platform_ioremap_resource(pdev, 0);

    domain = irq_domain_add_linear(pdev->dev.of_node, 32,
                                   &myic_domain_ops, chip);
    if (!domain)
        return -ENOMEM;

    chip->domain = domain;
    platform_set_drvdata(pdev, chip);

    return 0;
}

这段代码虽然简单,但麻雀虽小五脏俱全。它展示了 irqchip 驱动的核心流程:

  • 实现 irq_chip 的操作函数(mask/unmask/ack/set_type)
  • 创建 irq_domain,建立硬件中断号到软件中断号的映射
  • 在 map 回调中,为每个中断设置对应的芯片操作函数和中断处理类型

我的建议:写 irqchip 驱动时,先别急着写全部回调。先把 mask/unmask 实现好,然后用 cat /proc/interrupts 看看中断号有没有注册成功。确认映射没问题后,再逐步添加 ack、set_type 等回调。这样调试起来轻松很多。

3.5 知识体系图

下面这张图展示了 irqchip 子系统的核心架构。我画这张图时,特意把数据流和控制流分开了,方便你理解。

Linux irqchip 子系统核心架构 硬件层 中断控制器(GIC / GPIO / FPGA 等) 硬件中断号:SPI 32~1019 / GPIO 0~N / 自定义 映射层:irq_domain 线性映射 硬件号连续,直接偏移 树形映射 硬件号稀疏,红黑树查找 不映射 硬件号 = 软件号 核心层:irq_chip + irq_desc struct irq_chip mask/unmask/ack/eoi/set_type struct irq_desc 中断处理函数、标志位、线程化信息 应用层 request_irq() / devm_request_irq() / /proc/interrupts 硬件中断触发 映射查找 软件中断处理

从这张图你能看到,整个 irqchip 子系统是分层设计的。硬件中断从底层往上走,经过 irq_domain 映射,找到对应的 irq_chip 和 irq_desc,最终到达应用层的处理函数。每一层各司其职,耦合度很低。

再强调一次:irq_domain 的 xlate 回调一定要和设备树里的 interrupts 属性格式匹配。设备树用 <0 42 4> 三单元格式,你的 xlate 就得解析三个参数。我曾经接手一个项目,设备树是两单元格式,驱动里却用了三单元的 xlate,结果所有中断号都偏移了 32——查了整整一天才找到原因。

好了,irqchip 子系统的核心内容就这些。你掌握了 irq_chip 的操作函数、irq_domain 的映射机制,再配合设备树的中断解析,基本上就能搞定大部分中断控制器驱动的移植工作了。下一章我们聊聊中断处理流程的完整链路——从硬件触发到软件执行,中间到底发生了什么。


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