Linux 6.6内核中断深度解析(四):中断注册与投递 — 从 request_irq 到 handler 的完整链路

📅 2026/8/23 6:44:48
Linux 6.6内核中断深度解析(四):中断注册与投递 — 从 request_irq 到 handler 的完整链路
〇、全景两条线在 irq_desc 汇合前三篇讲了分类、初始化、中断重映射——都是中断这条路的骨架怎么搭。这一篇讲运行时真正发生的事设备驱动怎么认领中断注册以及中断来了怎么跑到驱动的处理函数投递。这两件事是两条独立的线在同一个结构irq_desc上汇合注册线request_irqrequest_threaded_irq__setup_irq 挂 action 到desc-action 链表irq_desc两条线的汇合点投递线设备中断 → IDTcommon_interruptvector_irq[vector] → irq_descdesc-handle_irqhandle_irq_event → 遍历action 链表 → handler一句话主线注册线把 handler 挂到irq_desc-action链表投递线从irq_desc-action链表取出 handler 调用——irq_desc是两条线的汇合点。一、核心枢纽irq_desc 和 irqaction 两层理解这篇的关键是先分清两个结构各自管什么结构是什么关键字段管什么irq_desc每个 Linux 中断号一个的描述符handle_irq函数指针、action链表头、lock、depth这个中断本身怎么处理、谁处理irqaction每次request_irq申请的一个处理者handler、thread_fn、next、dev_id、flags一个处理者哪个函数处理为什么是两层而不是一层因为一个中断号可以被多个设备共享——irq_desc是这个中断action链表是一串处理者。irq_desc一个irqaction可以一串。// include/linux/interrupt.h:118structirqaction{irq_handler_thandler;// 中断上半部硬中断上下文驱动注册的 ISRvoid*dev_id;// 设备标识共享中断时区分是哪个设备structirqaction*next;// 链表指针共享中断串起来irq_handler_tthread_fn;// 线程化处理函数下半部可空unsignedintflags;// IRQF_SHARED / IRQF_ONESHOT ...};irq_desc里两个字段最关键handle_irq函数指针指向这个中断该怎么处理流程。MSI 是handle_edge_irq边沿INTx 是handle_level_irq电平。它是在中断建立时根据类型设好的第四节展开。actionirqaction链表头。注册时往这里挂投递时从这里取。二、注册线request_irq 把 handler 挂上 irq_desc驱动 probe 时调request_irq()把我要处理这个中断这件事登记进去。它其实是request_threaded_irq()的简化包装thread_fn传 NULL// include/linux/interrupt.h:165staticinlineintrequest_irq(unsignedintirq,irq_handler_thandler,unsignedlongflags,constchar*name,void*dev){returnrequest_threaded_irq(irq,handler,NULL,flags,name,dev);}request_threaded_irq()manage.c:2146做两件事分配并填充irqaction然后交给__setup_irq挂链表。// kernel/irq/manage.c:2146intrequest_threaded_irq(unsignedintirq,irq_handler_thandler,irq_handler_tthread_fn,unsignedlongirqflags,constchar*devname,void*dev_id){...actionkzalloc(sizeof(structirqaction),GFP_KERNEL);// ① 分配 irqactionaction-handlerhandler;// ② 填充 handleraction-thread_fnthread_fn;// 线程化函数可空action-flagsirqflags;action-namedevname;action-dev_iddev_id;retval__setup_irq(irq,desc,action);// ③ 挂到 desc-action 链表}真正的挂链表在__setup_irq()manage.c:1505的末尾// kernel/irq/manage.c节选raw_spin_lock_irqsave(desc-lock,flags);old_ptrdesc-action;// 指向链表头的指针old*old_ptr;if(old){// 已有 action → 共享中断do{...old_ptrold-next;old*old_ptr;}while(old);// 遍历到尾部shared1;}...*old_ptrnew;// :1800 把新 action 挂到链表尾部关键就一句话desc-action是链表头新irqaction挂到链表尾。共享中断时多个设备各自的irqaction用next串成一条链。三种 handler 用法对比request_threaded_irq支持三种组合对应三种中断处理方式用法handlerthread_fn场景普通中断提供空request_irq全部在硬中断上下文处理纯线程化空用默认提供处理逻辑重、要睡眠全部放内核线程上半下半提供提供硬中断只做紧急事返回IRQ_WAKE_THREAD剩余放线程三、投递线中断从硬件跑到 handler这是中断真正发生时硬件到软件的完整路径设备发中断MSI / INTx → local APIC → CPU → CPU 查 IDT[vector] → 外部中断门 → 通用入口 → common_interrupt()common_interrupt()arch/x86/kernel/irq.c:247是投递线的软件入口它干的第一件也是最关键的一件事把 vector 号翻译成irq_desc。// arch/x86/kernel/irq.c:247DEFINE_IDTENTRY_IRQ(common_interrupt){structirq_desc*desc;desc__this_cpu_read(vector_irq[vector]);// ← vector 号 → irq_descif(likely(!IS_ERR_OR_NULL(desc))){handle_irq(desc,regs);// 分发到 desc}else{apic_eoi();// 没 handler直接 EOI}}vector_irq[vector]是一个per-CPU 数组把 vector 号映射到irq_desc指针。为什么是 per-CPU因为 x86 的 vector 分配器是按 CPU 独立管理的向量矩阵irq_matrixkernel/irq/matrix.c——分配 vector 时只在投递目标 CPU 的位图上找空闲号。所以同一个 vector 号在不同 CPU 上可以对应不同的中断两个中断亲和性不相交时复用同一个 vector 号但在同一个 CPU 上一个 vector 号只能对应一个中断IDT 的约束apic_update_vector里的BUG_ON断言这一点vector.c:184。这个数组在中断建立时填充上一篇初始化篇的init_IRQ建了 legacy 的映射MSI 的映射在分配 vector 时建。拿到irq_desc后handle_irq()irq.c:234在 64 位上就是generic_handle_irq_desc()// include/linux/irqdesc.h:159staticinlinevoidgeneric_handle_irq_desc(structirq_desc*desc){desc-handle_irq(desc);// 调 desc 的 handle_irq 函数指针}这里就到了irq_desc的多态点desc-handle_irq指向handle_edge_irqMSI或handle_level_irqINTx。它们最终都会走到handle_irq_event()handle.c:202再遍历 action 链表调 handler// kernel/irq/handle.c:139irqreturn_t__handle_irq_event_percpu(structirq_desc*desc){for_each_action_of_desc(desc,action){// 遍历 action 链表resaction-handler(irq,action-dev_id);// ← 调驱动注册的 handlerif(resIRQ_WAKE_THREAD)__irq_wake_thread(desc,action);// 唤醒线程化 handlerretval|res;}returnretval;}注意两件事action-handler(irq, action-dev_id)就是驱动在request_irq里注册的那个函数。dev_id传回给 handler共享中断时用它判断是不是我的设备发的中断。handler 返回IRQ_WAKE_THREAD时唤醒线程——这就是上半下半模式的衔接点上半部handler做完紧急事返回IRQ_WAKE_THREAD内核唤醒对应的thread_fn线程处理剩余部分。而handle_irq_event()handle.c:202在调 handler 前后做了件关键的事——先解锁desc-lock再调 handler调完再上锁irqreturn_thandle_irq_event(structirq_desc*desc){desc-istate~IRQS_PENDING;irqd_set(desc-irq_data,IRQD_IRQ_INPROGRESS);raw_spin_unlock(desc-lock);// ← 先放锁再调 handlerrethandle_irq_event_percpu(desc);// 遍历链表调 handlerraw_spin_lock(desc-lock);// ← 调完重新上锁irqd_clear(desc-irq_data,IRQD_IRQ_INPROGRESS);returnret;}为什么要解锁不是让 handler 能睡眠——handler 依然跑在硬中断上下文局部中断是关的__handle_irq_event_percpu里甚至有WARN_ONCE(!irqs_disabled(), ...)兜底handle.c:161照样不能睡眠。真正原因是避免自死锁handle_irq_event是被 flow handler持着desc-lock调进来的handle_level_irq在 chip.c:630、handle_edge_irq在 chip.c:789 都是先raw_spin_lock(desc-lock)再调它而驱动的 handler 内部可能再调用会去拿desc-lock的中断管理函数如disable_irq_nosync()/enable_irq()、synchronize_irq()等如果持锁直调 handler就会自己锁死自己。那放锁这期间irq_desc谁来保护靠IRQD_IRQ_INPROGRESS标志进 handler 前置位、出 handler 后再上锁清位。其他路径——irq 线程的irq_finalize_oneshotmanage.c:1082、synchronize_irqmanage.c:136、spurious 检测的note_interruptspurious.c:272——都在desc-lock下检查这个标志看到它就知道硬中断 handler 正在跑从而与它互斥。本质是锁只保护irq_desc的状态字段本身handler 执行期间用标志位来表示正在处理而不是让锁贯穿 handler 全程。四、边沿 vs 电平两种 handle 流程的对比desc-handle_irq的多态对应两种最典型的中断触发方式的不同处理流程——这是理解handle_irq函数指针存在意义的关键边沿触发MSI电平触发INTx处理函数handle_edge_irqchip.c:787handle_level_irqchip.c:628进入时irq_ackack不 maskmask_ack_irq先 mask 再 ack为什么边沿只触发一次处理期间新边沿会记 PENDING电平一直拉高不 mask 会反复进中断处理do-while循环handle_irq_event处理期间来新中断继续单次handle_irq_event退出时无需 unmaskcond_unmask_irq处理完 unmask// 电平chip.c:628先 mask处理完 unmaskvoidhandle_level_irq(structirq_desc*desc){mask_ack_irq(desc);// ← 先 mask电平不 mask 会死循环进中断...handle_irq_event(desc);// 调 handlercond_unmask_irq(desc);// ← 处理完 unmask}// 边沿chip.c:787只 ack循环处理新边沿voidhandle_edge_irq(structirq_desc*desc){...desc-irq_data.chip-irq_ack(desc-irq_data);// ← 只 ackdo{handle_irq_event(desc);// 调 handler}while((desc-istateIRQS_PENDING)...);// ← 处理期间来新边沿继续}一句话电平中断处理前要 mask、处理后 unmask否则电平一直触发边沿中断只 ack、循环处理处理期间的新边沿记 PENDING 继续。五、完整调用链把注册线和投递线串成两张带行号的图【注册线软件驱动 probe 时】 request_irq() interrupt.h:165 └─ request_threaded_irq() manage.c:2146 ├─ kzalloc(struct irqaction) 分配 irqaction ├─ 填充 handler / thread_fn / flags / dev_id └─ __setup_irq() manage.c:1505 ├─ old_ptr desc-action 取链表头 ├─ 遍历到尾部共享中断 old_ptr old-next └─ *old_ptr new manage.c:1800 挂链表 【投递线硬件→软件运行时】 设备中断 → local APIC → CPU → IDT → 通用入口 └─ common_interrupt() irq.c:247 ├─ desc vector_irq[vector] irq.c:255 vector → irq_desc └─ handle_irq() → generic_handle_irq_desc() irqdesc.h:159 └─ desc-handle_irq(desc) 函数指针多态 ├─ handle_edge_irq() chip.c:787 MSI/边沿 └─ handle_level_irq() chip.c:628 INTx/电平 └─ handle_irq_event() handle.c:202 └─ __handle_irq_event_percpu() handle.c:139 └─ action-handler(irq, dev_id) handle.c:158六、关键函数 / 文件索引函数位置作用request_irqinterrupt.h:165注册入口包装request_threaded_irqmanage.c:2146分配 irqaction 调 __setup_irq__setup_irqmanage.c:1505把 action 挂到 desc-action 链表common_interruptirq.c:247投递入口vector → irq_descgeneric_handle_irq_descirqdesc.h:159调 desc-handle_irqhandle_edge_irqchip.c:787边沿中断流程handle_level_irqchip.c:628电平中断流程handle_irq_eventhandle.c:202解锁 desc-lock 后遍历链表__handle_irq_event_percpuhandle.c:139遍历 action 链表调 handler文件v6.6关键内容include/linux/interrupt.hstruct irqaction、request_irqkernel/irq/manage.crequest_threaded_irq、__setup_irqkernel/irq/handle.chandle_irq_event、遍历链表kernel/irq/chip.chandle_edge_irq、handle_level_irqarch/x86/kernel/irq.ccommon_interrupt、vector_irq七、记住三点① 两条线在irq_desc汇合注册线request_irq→__setup_irq把irqaction挂到desc-action链表投递线common_interrupt→handle_irq_event从desc-action链表取出irqaction调handler。irq_desc是中断本身irqaction链表是一串处理者共享中断。② 投递线的关键枢纽是vector_irq[vector]这是 per-CPU 数组把 vector 号翻译成irq_desc——因为每个 CPU 有独立 vector 空间。common_interrupt先查它拿到irq_desc再走desc-handle_irq函数指针。③desc-handle_irq是多态点边沿MSI走handle_edge_irq只 ack、循环处理电平INTx走handle_level_irq先 mask、处理完 unmask。两种流程最终都到handle_irq_event遍历action链表调action-handler(irq, dev_id)handler 返回IRQ_WAKE_THREAD时唤醒线程化下半部。