尧图精选

Linux中断子系统详解:从IRQ Domain到设备树移植实战

🕒 发布时间:2026/9/25 17:36:56 📁 来源:尧图网络
如果你做过linux驱动移植一定遇到过这种场景代码从老平台搬到新平台设备probe也成功了可中断就是不进你写的回调函数。我上次移植一块网卡驱动就栽在这上面折腾了两天才发现问题根本不在驱动代码而是对中断子系统的整体框架只知其一不知其二。这篇文章把Linux中断子系统从硬件到驱动的完整链路拆开讲清楚包括irq domain映射、设备树中断配置、中断处理流程、以及我实际移植过程中踩过的两个典型坑希望能让刚接触驱动移植的人少走弯路。1. 中断子系统地图四层结构先记住三个“号”1.1 Linux为什么要把中断号“包装”一层很多做单片机的朋友刚接触Linux驱动时最不习惯的一件事就是明明硬件中断引脚就是GPIO14到了Linux驱动里却要用一个莫名其妙的“IRQ号”去申请中断。其实这个设计不是故意折腾人而是为了在复杂SoC上实现可移植性。在嵌入式Linux平台上一颗SoC内部往往有多个中断源可能几百个甚至上千个。这些中断源经过中断控制器比如GIC汇集后送给CPU而不同SoC的中断控制器类型不同、中断源数量不同、编号规则也不同。如果驱动里直接写死“我用的是第37号中断”那换个平台、换颗芯片所有代码都要跟着改。所以Linux内核把中断号做成了虚拟资源就像进程号一样。硬件中断源是“硬件中断号”hwirq驱动看到的是“Linux IRQ号”irq number两者之间的对应关系由irq domain维护。驱动只需要拿到一个irq number然后申请中断、写处理函数至于这个irq number背后挂的是哪条硬件中断线驱动根本不用关心。这一层抽象正是Linux驱动可以在不同平台间搬来搬去的底气。1.2 一张地图看清中断的路从硬件触发到驱动回调函数被执行中断数据要经过四层中断控制器硬件层负责采集多个中断源的电平或边沿信号通过GIC等控制器仲裁、屏蔽、优先级管理最终向CPU发出中断请求。IRQ Domain层维护硬件中断号到Linux IRQ号的映射关系负责“翻译”。通用中断层Generic IRQ Layer内核本身就有的中断描述符、中断流程处理函数handle_irq、中断动作链表irqaction这层不依赖具体硬件。设备驱动层驱动调用request_irq注册自己的回调函数中断来临时最终执行到这里的handler。用生活化一点的说法硬件中断号是“门牌号”Linux IRQ号是“收件箱编号”irq domain是把门牌号翻译成收件箱编号的物业管理员。驱动不需要知道邮件在小区里怎么流转只需要在收件箱里放好自己的回执收到信就处理。中断子系统框架要理解的也是这么一条路硬件信号怎么进来、内核在哪里翻译、最终怎么送到驱动手里。2. 硬件中断号到Linux IRQ号irq domain是那道“翻译官”2.1 irq domain到底做了什么irq domain是在3.4版本左右引入内核的解决的是旧内核里中断号映射混乱的问题。在更老的平台比如三星S3C2440、S5PV210中断号通常是板级文件里静态定义的一个平台一个编号表谁用哪个号直接写死。这种方式在芯片型号单一、平台固定的时代还能接受但一旦做移植就会发现不同平台的中断号根本无法复用。irq domain的思路是每个中断控制器对应一个domaindomain里维护一张映射关系。当外部设备需要中断时通过irq_create_mapping这类接口把硬件中断号翻译成一个可用的Linux IRQ号。翻译完成后驱动后续所有操作都使用这个Linux IRQ号不再关心硬件编号。内核中每个irq domain都有一个irq_domain结构体包含操作函数指针translate、map等和内部映射数据结构。中断控制器驱动会注册自己的domainGIC有GIC的domainGPIO控制器有自己的GPIO domain各管各的设备互不抢占。2.2 三种映射模型的选型逻辑irq domain的映射方式并不是只有一种常见的有线性映射、树状映射和直映射内核在不同场景会选择不同方式。映射方式实现结构适用场景特点线性映射linear数组位图中断源数量固定且不多如GIC、MCU内部外设查找效率高占用内存与中断源数量成正比树状映射treeradix tree中断源非常多且稀疏如大型中断扩展芯片内存占用动态增长适合中断号不连续的场景直映射direct无中间结构硬件中断号与Linux IRQ号一一对应效率最高但不适用于中断控制器复杂的情况做驱动移植时经常会遇到一个问题“为什么这个平台上我得到的irq号看起来和dts里写的数字对不上”答案往往就在映射方式里。比如GIC这类中断控制器通常用线性映射dts里写的是SPI编号16内部可能被翻译成hwirq 48再映射成Linux IRQ号48。如果驱动直接拿dts里的数字去申请中断自然就错了。2.3 驱动代码里拿到irq号的两条路径驱动代码里获取中断号的常见方式有两种第一种是platform驱动方式最常用struct platform_device *pdev; int irq platform_get_irq(pdev, 0); if (irq 0) { dev_err(pdev-dev, failed to get irq: %d\n, irq); return irq; }这个接口会去解析设备树节点里的interrupts属性完成irq domain映射最后返回Linux IRQ号。第二种是通用设备树解析方式struct device_node *np dev-of_node; int irq irq_of_parse_and_map(np, 0); if (irq 0) { dev_err(dev, failed to parse and map irq\n); return irq; }两者的本质都是走of_irq_parse_one解析设备树属性再通过irq_create_of_mapping完成映射。在移植时如果发现返回的irq不对要优先检查设备树里的interrupt-parent是否指向了正确的中断控制器节点以及该中断控制器节点的#interrupt-cells是否和外设节点的interrupts属性匹配。这两个对不上映射出来的号一定是错的。3. 设备树里的中断三件套移植时改得最多的就是这里3.1 interrupt-parent、#interrupt-cells和interrupts怎么配设备树里描述中断关系最核心的是三个属性interrupt-parent、#interrupt-cells和interrupts。很多人刚接触时会把这三个属性搞混其实它们的角色完全不同。interrupt-parent用在外设节点上表示这个设备的中断信号接到哪个中断控制器上值是一个phandle即指向某个中断控制器节点的引用。比如我的设备中断接到GIC就写interrupt-parent gic。中断控制器节点自己是“中断的提供方”它不需要interrupt-parent除非它自己也接在另一个更上层的控制器上比如级联场景。#interrupt-cells定义在中断控制器节点上表示这个控制器使用几个cell来描述一个中断请求。对于arm,gic通常是3个cell含义分别是中断类型SGI/PPI/SPI、中断编号、触发类型。interrupts写在外设节点上按照父节点定义的#interrupt-cells格式具体描述这个设备用的哪一路中断。这三个属性的关系可以理解为interrupt-parent告诉内核“去哪个翻译官那里”#interrupt-cells规定“翻译官要收几个参数”interrupts就是“你实际交上去的参数”。3.2 一个真实外设节点的完整解读拿一个常见的以太网控制器来说它在设备树里大概是这样的ethernet10000000 { compatible vendor,eth; reg 0x10000000 0x4000; interrupt-parent gic; interrupts GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH; phy-mode rgmii; };对应的中断控制器节点gic: interrupt-controllerf6800000 { compatible arm,cortex-a9-gic; #interrupt-cells 3; interrupt-controller; reg 0xf6800000 0x1000, 0xf6801000 0x1000; };GIC_SPI是一个宏定义在include/dt-bindings/interrupt-controller/arm-gic.h里值为0GIC_PPI值为1。所以上面这行interrupts的含义是这是一个SPI类型的中断编号是116高电平触发。如果有两个中断就在interrupts里写两段比如interrupts GIC_SPI 116 IRQ_TYPE_LEVEL_HIGH, GIC_SPI 117 IRQ_TYPE_LEVEL_HIGH;驱动里先platform_get_irq(pdev, 0)拿到第一个platform_get_irq(pdev, 1)拿到第二个。注意这里的index序号和设备树里的顺序是对应的不是从1开始而是从0开始。3.3 移植时最容易写错的三个细节第一个细节是SPI编号的偏移。GIC的硬件中断号中0到15是SGI处理器间中断16到31是PPI私有外设中断32及之后的才是SPI共享外设中断。设备树里写SPI类型的第几个中断时这个编号是相对SPI基地址的偏移还是绝对编号不同内核版本、不同GIC型号可能不一样。多数情况下dts里第一个cell写GIC_SPI、第二个cell写具体SPI序号时内核会做内部转换。但如果你在旧平台上见过直接写32、33这种绝对编号的写法就知道这件事没有统一标准移植时必须对照目标平台BSP里的原始dts看。第二个细节是触发类型。GPIO按键、外设中断这类信号实际硬件上可能是低电平有效、高电平有效、上升沿有效、下降沿有效不同厂商的芯片设计习惯不一样。dts里一旦写错最常见的结果是中断风暴或者中断不触发。触发类型宏定义在include/dt-bindings/interrupt-controller/irq.h里有IRQ_TYPE_EDGE_RISING、IRQ_TYPE_EDGE_FALLING、IRQ_TYPE_LEVEL_HIGH、IRQ_TYPE_LEVEL_LOW等别写错。第三个细节是interrupt-controller节点里忘了写interrupt-controller这个空属性。这个属性本身就是个标志位告诉内核“我是一个中断控制器可以被interrupt-parent引用”。如果漏写外设节点解析中断时会报错或者返回错误的映射。4. 中断处理链路从硬件触发到驱动回调中间发生了什么4.1 硬件触发与内核入口GIC的中断分发流程Earlier章节都在说映射现在来看真正中断发生时的流程。以GIC为例当外设产生中断信号时GIC会把这个中断标记为pending状态经过优先级仲裁后向CPU核发送一个信号。CPU响应后自动切换到IRQ模式跳到中断向量表执行。在ARM Linux系统中中断向量的最终处理函数在启动早期通过set_handle_irq注册。GIC的初始化代码会把自己对应的flow handler和无中断号处理函数挂到这个入口上。中断进来后CPU读取GICC_IAR寄存器拿到硬件中断号然后调用generic_handle_irq通过irq_desc结构体中的handle_irq函数指针执行具体的中断流程处理函数。对于GIC这个flow handler通常不是handle_level_irq也不是handle_edge_irq而是handle_fasteoi_irq。fasteoi的意思是“快速End Of Interrupt”处理完中断后直接往GICC_EOIR寄存器写EOI即可不需要像传统控制器那样反复mask/unmask。这套流程就把“硬件层”和“内核通用层”衔接起来了。之后内核会根据irq_desc的action链表把每一个注册在这个中断号上的回调函数依次执行。4.2 上半部、下半部与threaded irq怎么选驱动里的中断回调函数写在action链表的handler位置但一个非常重要的原则是中断回调里不能做耗时操作。因为中断上下文会关抢占、甚至可能关掉其他中断在这里面拖得越久系统实时性越差其他中断可能被饿死。所以Linux把中断处理拆成了上半部和下半部。上半部就是handler本身要求快速完成通常只做硬件的ACK、读状态寄存器、把数据放到队列里这类轻量操作。下半部负责真正的数据处理包括tasklet、workqueue和threaded irq三种形式。Tasklet运行在软中断上下文不能被阻塞适合轻量、短小的处理。Workqueue运行在进程上下文可以睡眠适合需要访问I2C、SPI这类慢速总线的操作。Threaded irq是另一种选择它把整个中断处理放到一个内核线程里执行handler只负责唤醒线程。三者怎么选实际经验是能放tasklet的用tasklet比如网卡的收包通知必须睡眠访问总线的用workqueue既需要快速响应又需要睡眠能力的用threaded irq。比如GPIO按键防抖需要开一个定时器或者直接睡几十毫秒你就可以用request_threaded_irq把处理函数放到线程里。4.3 注册中断回调request_irq的参数细节与共享中断驱动注册中断的标准接口是request_irqint request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev);irq是前面通过platform_get_irq拿到的中断号。handler是上半部回调。flags用来配置触发类型IRQF_TRIGGER_RISING等以及是否共享IRQF_SHARED。name会显示在/proc/interrupts里便于排查问题。dev是一个cookie在共享中断时用来区分不同设备的action。使用IRQF_SHARED标志时要特别注意一个中断线被多个设备共享时内核会依次调用链表上所有handler最终由handler自己判断是不是自己的设备产生的中断。判断方法就是读取设备的状态寄存器如果中断源不是自己立即返回IRQ_NONE如果确实是自己处理的返回IRQ_HANDLED。如果没有正确使用dev_id卸载时使用free_irq会无法准确找到对应的action导致释放失败或者误删别人的handler。还有一种变体是request_threaded_irqint request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id);handler为NULL时内核会用一个默认的handler代替然后直接调用thread_fn在线程上下文执行处理逻辑。我在移植一个触摸屏驱动时就用了这个接口因为touch controller的状态寄存器需要通过I2C读取而I2C访问在原子上下文里根本跑不了。5. 移植现场实录两个中断问题的完整排查过程5.1 问题一电平触发写成边沿触发中断风暴把CPU打满有一次把一块GPIO按键驱动从旧平台移植到新平台硬件上按键接在GPIO模块的某个引脚上按下时引脚被拉低松开恢复高电平属于低电平有效。设备树里我照着某个参考板卡写成了IRQ_TYPE_EDGE_FALLING结果一上电啥都没按CPU占用率直接飙到90%多。当时的排查链路是这样的第一步先执行cat /proc/interrupts发现GPIO对应的中断号计数在疯狂增长几秒钟就涨了几万次。这说明中断确实一直在触发但处理函数里没有读到任何有效按键事件。第二步用示波器测GPIO引脚电平发现按键没按时引脚持续为低电平。这就完了硬件是低电平有效我把中断配置成下降沿触发电平一直保持低于是GPIO控制器在内部不断把这种“本来就是低电平”的状态当成边沿事件上报中断风暴就产生了。第三步确认硬件有效电平后把设备树改成IRQ_TYPE_LEVEL_LOW或者IRQ_TYPE_LEVEL_HIGH匹配实际硬件重启后中断计数恢复正常。这个案例里真正让我印象深刻的不是改一个宏的事而是“电平触发”和“边沿触发”对中断控制器的mask/unmask行为影响完全不同。电平触发下如果中断没有被正确清除内核会在处理完后立刻再次触发所以纯粹靠上半部做“清中断”都不一定稳妥还要配合硬件去清掉真正的电平状态。移植到新平台的时候必须对照原理图确认硬件信号的有效极性而不是凭旧平台的经验猜。5.2 问题二中断号映射偏移中断跑到了别的设备上另一个案例是一个I2C触摸屏probe成功request_irq也成功但触摸完全没有反应。更奇怪的是查看/proc/interrupts时发现另一个USB hub设备的中断计数在不断增加而触摸屏的irq计数几乎为0。一开始怀疑是触摸屏硬件有问题后来一步步查第一步看驱动代码发现这个触摸屏驱动是从一个很老的kernel版本搬过来的里面直接用了硬编码中断号比如IRQ 37。在新平台的dts里触摸屏的interrupts写的是GIC_SPI 5 IRQ_TYPE_LEVEL_HIGH这个5是SPI编号。第二步查新平台GIC的irq domain映射发现SPI编号5会被翻译成Linux IRQ号37。也就是说驱动里硬编码的“37”对应的不是触摸屏而是另一个已经被USB hub占用的中断。真正属于触摸屏的irq号platform_get_irq会返回3732之类的值。具体偏移多少取决于平台但核心问题就是驱动绕过了标准接口。第三步把驱动里硬编码的中断号改成platform_get_irq(pdev, 0)问题直接解决。这件事暴露出来的问题是老驱动的硬编码在新平台上就是一颗雷。中断子系统有了irq domain以后中断号已经彻底虚拟化驱动里再也不应该出现“假设中断号是多少”的代码。移植时如果发现驱动里有用#define定义IRQ号的地方一定要高度警惕全部替换成标准接口获取。5.3 排查工具链先用这些确认中断是不是“真的来了”排查中断问题时我一般按照这样的顺序来确认先看/proc/interrupts确认申请的中断号有没有在触发、在哪个CPU上触发、每秒增长量是多少。如果计数完全不增长说明中断根本没送到CPU问题可能在硬件侧或者设备树解析阶段。如果计数疯狂增长那就先考虑触发类型错误、中断没有清除、硬件自锁这类原因。再看/sys/kernel/debug/irq/irqs/目录下的内容可以看到每个irq的irq_desc信息包括注册的action链表、irq chip信息、触发类型等。确认申请时的flags和实际配置是否一致。然后用dmesg检查中断相关的日志比如irq handler类型不匹配时内核会打印“irq xxx: nobody cared”之类的警告。这句话出现时往往意味着驱动的handler对中断线来说不够用或者根本就是共享中断里没有正确处理IRQ_NONE。最后再用perf或trace-cmd追踪中断事件trace-cmd record -e irq:irq_handler_entry -e irq:irq_handler_exit trace-cmd report这个能精确看到每次中断进入哪个handler、耗时多少。我之前在一次网络中断性能问题中就是用trace-cmd确认了中断处理耗时异常最后发现是DMA描述符没有及时回收导致的和中断框架本身没关系。6. 移植后的中断性能体检别再等到线上出问题了6.1 /proc/interrupts几秒钟看懂中断分布移植完成后我建议养成一个习惯开机后先看一眼/proc/interrupts。CPU0 CPU1 CPU2 CPU3 29: 123456 34567 0 0 GIC 116 Level eth0 39: 0 0 12345 5678 GIC 27 Level touch每一列的含义第一个数字是Linux IRQ号后面几列是每个CPU处理这个中断的次数如果某个中断只在一个CPU上有计数其他CPU都是0说明这个中断当前亲和性绑定在某一个核上GIC表示使用的中断控制器名116是硬件中断号Level是触发类型最后是name即request_irq时传入的设备名。如果看到某个中断的名字显示为“eth0”但计数没有变化可能是中断没有正确产生如果计数一直增长但业务功能异常那就要回到5.1里的排查思路。另外如果系统里有太多中断挤在一个CPU核上可以通过负载均衡把中断分发到多个核上。6.2 中断延迟怎么查ftrace与irqsoff中断性能的核心指标之一是中断延迟也就是从硬件触发到驱动handler真正执行的时间。影响这个时间的主要因素是“当前CPU是否关中断”以及“抢占是否被禁止”。排查关中断导致的延迟可以用ftrace的irqsoff追踪器echo irqsoff /sys/kernel/debug/tracing/current_tracer echo 1 /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace输出里会显示最长一次关中断的时间是多少、发生在哪个函数。如果发现某个驱动在关中断状态下做了耗时操作比如在自旋锁里打印日志那就需要把它挪到下半部去。另一个常见指标是/proc/softirqs里的软中断计数如果HI_SOFTIRQ或NET_RX增长过快说明中断收包模式可能有瓶颈可以考虑开启NAPI或者调整中断合并参数。我调网卡驱动时经常用ethtool -C eth0 adaptive-rx on这类命令减少中断频率把吞吐提上去。6.3 中断负载均衡与线程化SMP平台上的调优思路多核平台上中断默认会落在某个CPU上。如果这个核既要处理中断又要跑应用很容易出现“单个核跑满、其他核闲着”的情况。调整中断亲和性可以改善echo 2 /proc/irq/29/smp_affinity写入值是一个位掩码bit位对应CPU编号。1表示CPU02表示CPU15表示CPU0和CPU2。具体调整完可以用/proc/interrupts观察分布是否变化。还有一个实践是中断线程化。对于处理逻辑复杂、需要睡眠的外设中断直接用request_threaded_irq把处理放到线程上下文能避免长时间占用硬中断资源。需要注意的是线程化之后中断优先级会比硬中断低实时性略微下降所以对硬实时要求高的场景不要盲目线程化。我几年前移植过一个指纹识别模块的驱动中断处理里要读取几十个字节的数据并通过SPI总线与协处理器通信CPU占用高得离谱。改成threaded irq之后把SPI操作全部移到线程里负载立刻降下来而且整个系统的响应也稳定了。这个改动看似简单却需要你对中断子系统的执行模型有清晰认识不然你都不知道该朝哪个方向优化。在Linux的中断子系统里一切设计都围绕着“安全、快速、可扩展”三个目标。安全是别让驱动之间互相踩踏快速是别让中断处理拖垮系统可扩展是让同一份驱动代码在不同硬件平台上都能跑起来。你理解了这三个目标再回头看irq domain、设备树三件套、上下半部机制就会发现它们全都是围绕这三个目标展开的。移植驱动多了之后我的经验是遇到中断问题先别急着动手改代码先梳理一遍这个平台的中断控制器类型、irq domain映射方式、设备树里的中断属性设置再对应看驱动的中断申请方式。把框架在脑子里理清楚之后绝大多数中断问题都能快速定位到具体那一层。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →