资讯详情

Linux中断子系统移植指南:从irq_chip到irq_domain的适配与调试

📅 2026/9/21 17:59:56 | 华诺云谱 👁 阅读
Linux中断子系统移植指南:从irq_chip到irq_domain的适配与调试
1. 中断子系统到底在解决什么问题做Linux驱动移植的人早晚都会撞上中断这块硬骨头。你从一颗芯片换到另一颗芯片GPIO、时钟、引脚复用这些改改寄存器还能对付但一旦涉及中断控制器换了型号或者从ARM Cortex-A切到RISC-V中断子系统的适配就会变成整个移植工作中最耗时的部分。我做过几次不同平台的驱动移植每次中断这块都要花掉将近三分之一的时间去调试。中断子系统的本质是让外设能在事件发生时主动通知CPU而不是让CPU傻等着轮询。举个生活化的例子你寄快递可以选择每隔十分钟下楼看快递到没到轮询也可以留个电话让快递员到了打给你中断。显然打电话更高效但问题在于——你只有一个手机号如果同时有快递、外卖、物业都打这个号码你就需要一套规则来决定先接谁、谁可以稍后、谁直接挂断。Linux中断子系统干的就是这个事。这套框架要解决的核心问题包括中断号的统一管理、中断控制器的抽象、中断服务程序的注册与调用、中断上下文的限制、中断嵌套与优先级、以及多核环境下的中断亲和性。对于做驱动移植的人来说你不需要从头实现这些但你必须理解框架的分层逻辑否则遇到request_irq返回失败、中断触发不了、或者中断风暴的时候你连从哪一层开始排查都不知道。这篇文章适合两类人一是正在做Linux驱动移植、需要适配新平台中断控制器的工程师二是已经写过字符设备驱动、但对中断子系统只有模糊认识的开发者。我会从整体框架讲起然后逐层拆解关键数据结构和移植时需要改动的部分最后给出实际调试中踩过的坑和排查方法。2. 中断子系统的整体分层设计2.1 从硬件到软件的三层抽象Linux中断子系统在设计上采用了典型的分层思想从下往上大致可以分为三层最底层是硬件层也就是具体的SoC中断控制器比如GICGeneric Interrupt Controller、NVIC、或者各种GPIO中断控制器。这一层的差异最大也是驱动移植时主要改动的部分。中间层是中断控制器抽象层Linux用irq_chip和irq_domain这两个结构体来抽象不同中断控制器的行为。irq_chip定义了中断控制器的操作函数集比如使能、禁用、应答、设置触发方式等irq_domain则负责硬件中断号到Linux虚拟中断号的映射。最上层是通用中断处理层包括irq_desc、irqaction、generic_handle_irq等这一层对所有平台都是通用的驱动开发者调用request_irq注册中断处理函数就是和这一层打交道。这种分层的意义在于驱动开发者只需要关心最上层用统一的API注册中断而做平台移植的人只需要实现中间层的irq_chip和irq_domain不需要改动通用代码。我第一次理解这个分层的时候感觉就像USB子系统的设计思路——HCD负责硬件差异USB Core负责通用逻辑设备驱动只管调API。2.2 为什么不用一个结构体搞定所有事有人可能会问为什么不直接用一个大的结构体把中断控制器和中断描述都塞进去非要拆成irq_chip、irq_domain、irq_desc、irqaction这么多层原因在于关注点分离。irq_chip只关心“怎么操作这个中断控制器硬件”irq_domain只关心“硬件中断号怎么映射到虚拟中断号”irq_desc只关心“这个中断线上挂了哪些处理函数”irqaction只关心“某个具体的处理函数及其上下文”。如果混在一起当你要支持中断控制器级联比如GPIO中断控制器挂在GIC的某条中断线上时代码会变得极其混乱。我实际移植过一个级联中断控制器的平台父控制器是GIC子控制器是一个自定义的GPIO中断聚合器。正是因为有了irq_domain的层级映射机制子控制器的中断号可以通过irq_domain_add_hierarchy挂到父domain下面整个映射关系清晰可控。如果当初设计成一个扁平结构这种级联场景几乎没法优雅处理。2.3 移植时你需要关注哪些层对于驱动移植来说你的工作范围通常落在中间层。具体来说如果新平台的中断控制器和已有驱动兼容比如都是标准GICv3那你可能什么都不用改只需要在设备树里填对interrupt-parent和interrupts属性。如果是自定义中断控制器你需要实现一个irq_chip定义irq_mask、irq_unmask、irq_ack、irq_set_type等回调。如果中断控制器有级联关系你还需要创建irq_domain并建立映射。通用中断处理层和驱动API层基本不需要动除非你要支持一些特殊的中断类型比如NMI。理解这个分工之后你在移植时就能快速定位中断触发不了先看设备树的中断号对不对中断号映射失败看irq_domain的xlate回调中断处理函数没被调用看irq_chip的irq_ack和irq_unmask是否实现正确。3. 核心数据结构拆解与移植关键点3.1 irq_desc中断线的全局描述符irq_desc是中断子系统的核心数据结构每条中断线对应一个irq_desc。它里面最关键的成员是irqaction链表和irq_chip指针。struct irq_desc { struct irq_common_data irq_common_data; struct irq_data irq_data; struct irqaction *action; /* 中断处理函数链表 */ unsigned int status_use_accessors; unsigned int core_internal_state__do_not_mess_with_it; unsigned int depth; /* 禁用嵌套深度 */ unsigned int wake_depth; unsigned int tot_count; unsigned int irq_count; unsigned long last_unhandled; unsigned int irqs_unhandled; raw_spinlock_t lock; struct cpumask *percpu_enabled; ... };depth这个字段值得单独说。它记录了中断被禁用的嵌套层数每次调用disable_irq会加一enable_irq会减一只有减到零才真正使能中断。我踩过一个坑在中断处理函数里调用了disable_irq结果导致死锁因为disable_irq会等待当前正在执行的中断处理完成而在中断上下文里等自己完成就是死锁。正确做法是用disable_irq_nosync。移植时你一般不需要直接操作irq_desc但调试时可以通过/proc/irq/num/下的文件查看它的状态比如/proc/irq/45/spurious可以看到该中断线上未处理中断的统计。3.2 irq_chip中断控制器的操作集irq_chip是移植时最需要关注的结构体它定义了中断控制器的所有操作struct irq_chip { const char *name; unsigned int (*irq_startup)(struct irq_data *data); void (*irq_shutdown)(struct irq_data *data); void (*irq_enable)(struct irq_data *data); void (*irq_disable)(struct irq_data *data); void (*irq_ack)(struct irq_data *data); void (*irq_mask)(struct irq_data *data); void (*irq_unmask)(struct irq_data *data); void (*irq_eoi)(struct irq_data *data); int (*irq_set_type)(struct irq_data *data, unsigned int type); int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); ... };其中irq_ack、irq_mask、irq_unmask、irq_eoi是最核心的四个回调。irq_ack用于应答中断控制器告诉它“这个中断我已经收到了”irq_mask和irq_unmask用于屏蔽和解除屏蔽中断irq_eoi用于中断结束通知在GIC中通常是写EOI寄存器。移植时的关键决策是你的中断控制器需不需要irq_ack。有些中断控制器在进入中断处理函数时会自动应答有些则需要软件显式应答。如果该应答的没应答中断会一直触发形成中断风暴如果不该应答的乱应答可能会丢失中断。我移植过一个自定义中断控制器硬件手册上写的是“中断状态寄存器在读取时自动清除”所以irq_ack实现为空函数但irq_mask和irq_unmask必须正确实现否则无法屏蔽中断。3.3 irq_domain硬件中断号到虚拟中断号的桥梁irq_domain解决的是中断号映射问题。硬件中断号hwirq是中断控制器视角的编号虚拟中断号virq是Linux全局视角的编号。两者之间的映射关系由irq_domain维护。struct irq_domain { struct list_head link; const char *name; const struct irq_domain_ops *ops; void *host_data; unsigned int flags; struct fwnode_handle *fwnode; struct irq_domain *parent; ... };irq_domain_ops中的xlate回调负责解析设备树中的interrupts属性map回调负责建立映射alloc回调负责分配虚拟中断号。移植时最常见的场景是设备树里写了interrupts 0 45 4但驱动里request_irq拿到的中断号不对。这时候就要检查xlate回调是否正确解析了设备树参数。GIC的xlate会解析三个cell中断类型SPI/PPI/SGI、中断号、触发方式。如果你的中断控制器设备树绑定是自定义的xlate的实现必须和绑定文档一致。3.4 irqaction中断处理函数的载体irqaction是每次request_irq调用时创建的结构体它记录了处理函数指针、设备ID、中断标志等struct irqaction { irq_handler_t handler; void *dev_id; void __percpu *percpu_dev_id; struct irqaction *next; irq_handler_t thread_fn; struct task_struct *thread; unsigned int irq; unsigned int flags; ... };flags字段决定了中断的行为IRQF_SHARED表示共享中断线IRQF_TRIGGER_RISING表示上升沿触发IRQF_ONESHOT表示中断处理函数执行期间保持屏蔽。移植时如果设备树里配置的触发方式和驱动里request_irq的flags不一致中断可能触发不了或者反复触发。我遇到过设备树写的是下降沿触发但驱动里传了IRQF_TRIGGER_RISING结果中断死活不来改成一致后立刻正常。4. 中断注册与处理的完整流程4.1 从设备树到request_irq的调用链当驱动调用request_irq时内核内部经历了一系列转换。以设备树平台为例驱动调用platform_get_irq或irq_of_parse_and_map从设备树节点中解析interrupts属性。irq_of_parse_and_map调用irq_create_of_mapping进而调用irq_find_mapping查找或创建虚拟中断号。如果虚拟中断号不存在调用irq_domain_alloc_irqs分配一个并通过irq_domain_ops.map回调建立hwirq到virq的映射。驱动拿到virq后调用request_irq内核创建irqaction并挂到对应irq_desc的action链表上。如果中断控制器还没使能该中断线调用irq_chip.irq_startup或irq_enable使能。这个流程中第3步的map回调是移植时最可能出问题的地方。如果map回调没有正确设置irq_data中的hwirq和chip后续的中断使能和应答都会失败。4.2 中断触发后的处理路径当硬件中断触发后CPU会跳转到中断向量表最终调用到handle_irq。以ARM64为例流程大致是CPU保存现场读取中断控制器寄存器获取hwirq。调用irq_find_mapping将hwirq转换为virq。调用generic_handle_irq(virq)进而调用generic_handle_irq_desc。generic_handle_irq_desc调用irq_desc-handle_irq对于电平触发中断通常是handle_level_irq对于边沿触发是handle_edge_irq。handle_level_irq会先调用irq_chip.irq_ack应答中断然后调用irq_chip.irq_mask屏蔽中断接着遍历action链表调用每个handler最后调用irq_chip.irq_unmask解除屏蔽。如果是线程化中断IRQF_ONESHOT或thread_fn还会唤醒中断线程。这里的关键点是handle_level_irq和handle_edge_irq的行为不同。电平触发中断在屏蔽后如果硬件仍然保持电平解除屏蔽后会再次触发边沿触发中断则不会。移植时如果触发方式配错会导致中断丢失或重复。4.3 中断上下文的限制与注意事项中断处理函数运行在中断上下文这意味着不能调用可能睡眠的函数比如kmalloc(GFP_KERNEL)、mutex_lock、copy_to_user。不能调用disable_irq只能用disable_irq_nosync。栈空间有限不能做大量局部变量分配。执行时间要尽可能短否则会影响系统响应。我移植过一个触摸屏驱动中断处理函数里做了I2C读取结果系统卡顿严重。后来改成中断里只做标记实际读取放到工作队列里问题解决。这就是典型的中断上下文限制导致的性能问题。5. 移植实操从零适配一个中断控制器5.1 硬件手册需要看哪些寄存器拿到一个新的中断控制器硬件手册里必须确认以下信息中断使能寄存器每个中断线的使能位在哪里是分开的还是分组的。中断屏蔽寄存器和使能寄存器是同一个还是分开的。中断状态寄存器如何读取当前pending的中断读取后是否自动清除。中断应答寄存器是否需要显式写应答还是读取状态即应答。触发方式配置寄存器如何配置上升沿、下降沿、高电平、低电平。优先级寄存器是否支持优先级如何配置。中断号范围支持多少条中断线编号从几开始。这些信息决定了irq_chip中各个回调的实现方式。比如如果状态寄存器读取后自动清除irq_ack就可以是空函数如果需要写应答寄存器irq_ack就必须实现。5.2 实现一个最小可用的irq_chip假设我们有一个简单的中断控制器寄存器基地址为base支持32条中断线每条线有独立的使能位和状态位状态位读取后自动清除触发方式固定为高电平。struct my_intc { void __iomem *base; struct irq_domain *domain; raw_spinlock_t lock; }; static void my_intc_irq_mask(struct irq_data *d) { struct my_intc *intc irq_data_get_irq_chip_data(d); u32 offset d-hwirq / 32; u32 bit d-hwirq % 32; u32 val; raw_spin_lock(intc-lock); val readl(intc-base ENABLE_REG offset * 4); val ~(1 bit); writel(val, intc-base ENABLE_REG offset * 4); raw_spin_unlock(intc-lock); } static void my_intc_irq_unmask(struct irq_data *d) { struct my_intc *intc irq_data_get_irq_chip_data(d); u32 offset d-hwirq / 32; u32 bit d-hwirq % 32; u32 val; raw_spin_lock(intc-lock); val readl(intc-base ENABLE_REG offset * 4); val | (1 bit); writel(val, intc-base ENABLE_REG offset * 4); raw_spin_unlock(intc-lock); } static void my_intc_irq_ack(struct irq_data *d) { /* 状态寄存器读取后自动清除无需额外操作 */ } static struct irq_chip my_intc_chip { .name my-intc, .irq_mask my_intc_irq_mask, .irq_unmask my_intc_irq_unmask, .irq_ack my_intc_irq_ack, };这个最小实现里irq_mask和irq_unmask操作使能寄存器irq_ack为空。注意加锁保护因为多核环境下可能同时有多个CPU操作同一个寄存器。5.3 建立irq_domain和中断映射有了irq_chip之后需要创建irq_domain并建立映射static int my_intc_domain_map(struct irq_domain *d, unsigned int virq, irq_hw_number_t hwirq) { struct my_intc *intc d-host_data; irq_set_chip_data(virq, intc); irq_set_chip_and_handler(virq, my_intc_chip, handle_level_irq); irq_set_status_flags(virq, IRQ_LEVEL); return 0; } static const struct irq_domain_ops my_intc_domain_ops { .map my_intc_domain_map, .xlate irq_domain_xlate_twocell, }; static int my_intc_probe(struct platform_device *pdev) { struct my_intc *intc; struct resource *res; int irq, ret; intc devm_kzalloc(pdev-dev, sizeof(*intc), GFP_KERNEL); if (!intc) return -ENOMEM; res platform_get_resource(pdev, IORESOURCE_MEM, 0); intc-base devm_ioremap_resource(pdev-dev, res); if (IS_ERR(intc-base)) return PTR_ERR(intc-base); raw_spin_lock_init(intc-lock); intc-domain irq_domain_add_linear(pdev-dev.of_node, 32, my_intc_domain_ops, intc); if (!intc-domain) return -ENOMEM; /* 初始化硬件禁用所有中断 */ writel(0, intc-base ENABLE_REG); writel(0, intc-base ENABLE_REG 4); platform_set_drvdata(pdev, intc); return 0; }这里用irq_domain_add_linear创建线性映射domain支持32条中断线。xlate用irq_domain_xlate_twocell表示设备树中每个中断用两个cell描述中断号和触发方式。5.4 设备树节点的编写要点对应的设备树节点my_intc: interrupt-controller10000000 { compatible myvendor,my-intc; reg 0x10000000 0x1000; interrupt-controller; #interrupt-cells 2; interrupt-parent gic; interrupts 0 45 4; /* 挂在GIC的SPI 45上高电平触发 */ }; device20000000 { compatible myvendor,my-device; reg 0x20000000 0x1000; interrupt-parent my_intc; interrupts 5 4; /* 使用my_intc的第5号中断高电平触发 */ };关键点#interrupt-cells 2要和xlate的cell数量一致interrupt-parent指向父控制器interrupts属性的格式要和xlate解析逻辑匹配。6. 常见问题与排查技巧实录6.1 中断触发不了怎么办这是移植时最常见的问题。排查顺序建议如下排查步骤检查内容常见问题1设备树中断号和触发方式中断号写错、触发方式与硬件不匹配2irq_domain映射是否成功xlate回调解析失败、map回调未设置chip3irq_chip的unmask是否被调用irq_startup未实现、depth不为零4硬件寄存器是否使能使能寄存器未写、时钟未开5中断是否被屏蔽mask寄存器误写、父控制器未使能我遇到过一次中断触发不了查了两天最后发现是设备树里interrupt-parent写成了另一个控制器。这种低级错误在移植时特别容易犯因为设备树是手写的没有编译器帮你检查。6.2 中断风暴怎么定位中断风暴的表现是系统卡死top看到某个CPU的si软中断占用率100%。原因通常是中断应答没有正确执行硬件一直认为中断未被处理。排查方法检查irq_chip.irq_ack是否实现是否需要写应答寄存器。检查handle_level_irq和handle_edge_irq是否选对。电平触发中断如果硬件电平一直保持屏蔽后解除屏蔽会再次触发这是正常的但如果应答没做就会无限触发。用cat /proc/interrupts观察中断计数如果某个中断号计数飞涨就是它的问题。我移植过一个GPIO中断控制器硬件手册说“读取状态寄存器自动清除”但实际上只有读取特定偏移的状态寄存器才清除读错了偏移就不会清除。这种硬件手册的坑只能靠实测发现。6.3 中断号映射失败的排查如果request_irq返回-EINVAL很可能是中断号映射失败。排查步骤确认设备树interrupts属性的cell数量和#interrupt-cells一致。确认xlate回调能正确解析设备树参数。确认irq_domain的map回调被调用可以在map里加pr_info打印。确认父控制器的irq_domain已经创建。有个技巧在irq_create_of_mapping里加打印可以看到每次映射的hwirq和virq非常直观。6.4 共享中断的注意事项共享中断IRQF_SHARED要求所有共享同一中断线的驱动都传入了非NULL的dev_id且dev_id必须唯一。如果两个驱动传了相同的dev_id第二个request_irq会失败。另外共享中断的处理函数必须能判断中断是否来自自己的设备如果不是就返回IRQ_NONE。如果所有处理函数都返回IRQ_NONE内核会认为这个中断是spurious的累计到一定次数后会禁用该中断线。6.5 中断亲和性设置多核平台上可以通过/proc/irq/num/smp_affinity设置中断亲和性让特定中断只在指定CPU上处理。移植时如果发现某个中断总是落在CPU0上导致负载不均可以调整亲和性。在代码里可以用irq_set_affinity_hint设置建议亲和性irq_set_affinity_hint(irq, cpumask_of(cpu));注意这只是hint实际亲和性还受irq_chip.irq_set_affinity是否实现影响。如果中断控制器不支持亲和性设置这个hint会被忽略。7. 移植后的验证与性能调优7.1 验证中断是否正常工作移植完成后最基本的验证方法是# 查看中断统计 cat /proc/interrupts # 查看某个中断的详细信息 cat /proc/irq/45/spurious ls /proc/irq/45/ # 查看中断控制器的domain信息 cat /sys/kernel/debug/irq/domains//proc/interrupts会列出每个中断号的中断计数、中断控制器名称、以及注册的处理函数名。如果中断触发后计数增加说明中断路径是通的。7.2 中断延迟的测量对于实时性要求高的场景需要测量中断延迟。可以用ftrace的irqsofftracerecho irqsoff /sys/kernel/debug/tracing/current_tracer echo 1 /sys/kernel/debug/tracing/tracing_on # 触发中断 cat /sys/kernel/debug/tracing/traceirqsoff会记录中断被关闭的最长时间帮助定位延迟热点。7.3 中断线程化的取舍对于处理时间较长的中断可以用request_threaded_irq将处理函数线程化。线程化后硬中断处理函数只做最少的应答和屏蔽实际处理放到内核线程里可以睡眠、可以调度。但线程化也有代价中断延迟会增加因为需要唤醒线程。对于GPIO按键这种对延迟不敏感的场景线程化很合适对于网络收包这种对延迟敏感的场景还是用传统中断处理更好。我移植过一个音频驱动DMA完成中断里需要做较多处理线程化后系统响应明显改善但音频延迟略有增加。最终通过调整线程优先级找到了平衡点。7.4 中断嵌套与优先级Linux中断子系统默认不支持中断嵌套即一个中断处理函数执行期间同类型中断会被屏蔽。如果硬件支持优先级可以通过IRQF_NO_THREAD和IRQF_PERCPU等标志做更细粒度的控制。移植时如果发现高优先级中断被低优先级中断阻塞需要检查中断控制器的优先级配置和irq_chip是否实现了irq_set_priority如果有这个回调的话。8. 几个容易忽略的移植细节8.1 中断控制器的初始化顺序中断控制器的初始化必须在设备驱动之前完成否则设备驱动request_irq时会找不到中断控制器。Linux通过IRQCHIP_DECLARE宏和of_irq_init机制保证初始化顺序。移植时如果用IRQCHIP_DECLARE需要确保compatible字符串和设备树一致。如果用platform_driver需要设置合适的initcall级别通常用postcore_initcall。8.2 中断控制器的电源管理如果中断控制器支持电源管理需要在irq_chip中实现irq_set_wake回调并在系统休眠时保存和恢复寄存器状态。移植时如果忽略这一点系统休眠唤醒后中断可能失效。8.3 多核启动时的中断分发多核平台上CPU启动过程中中断的分发需要特别注意。从CPU启动时中断控制器需要正确配置否则从CPU可能收不到中断。GIC通过gic_starting_cpu和gic_dying_cpu回调处理这个问题自定义中断控制器也需要类似的机制。8.4 中断号空间的分配irq_domain_add_linear适合中断号连续且数量固定的场景irq_domain_add_tree适合中断号稀疏的场景irq_domain_add_nomap适合不需要映射的场景。移植时根据中断控制器的特点选择合适的domain类型可以节省内存并提高查找效率。我在一个支持动态中断号的控制器上用了irq_domain_add_linear结果中断号范围很大但实际用的很少浪费了不少内存。后来改成irq_domain_add_tree内存占用明显下降。8.5 调试中断子系统的常用手段除了/proc/interrupts和ftrace还有几个实用手段CONFIG_GENERIC_IRQ_DEBUGFS打开后/sys/kernel/debug/irq/下会有每个中断的详细信息。CONFIG_IRQ_DOMAIN_DEBUG打开后可以看到domain的映射关系。在generic_handle_irq里加打印可以看到每次中断的virq和hwirq。用irq_set_chip_and_handler时传入不同的handler可以观察行为差异。这些手段在排查疑难问题时非常有用尤其是当硬件手册和实际行为不一致的时候。中断子系统的移植工作说到底是对硬件行为的精确建模。你对硬件理解得越透彻irq_chip的实现就越准确调试时间就越短。我个人的经验是拿到新硬件后先别急着写代码把中断相关的寄存器手册反复读三遍把中断触发、应答、屏蔽的时序图搞清楚后面写代码就是水到渠成的事。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。