核中斷深度解析(四):中斷注冊與投遞 — 從 request_irq 到 handler 的完整鏈路)
〇、全景兩條線在 irq_desc 匯合前三篇講了分類、初始化、中斷重映射——都是中斷這條路的骨架怎么搭。這一篇講運行時真正發(fā)生的事設備驅(qū)動怎么認領中斷注冊以及中斷來了怎么跑到驅(qū)動的處理函數(shù)投遞。這兩件事是兩條獨立的線在同一個結構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 調(diào)用——irq_desc是兩條線的匯合點。一、核心樞紐irq_desc 和 irqaction 兩層理解這篇的關鍵是先分清兩個結構各自管什么結構是什么關鍵字段管什么irq_desc每個 Linux 中斷號一個的描述符handle_irq函數(shù)指針、action鏈表頭、lock、depth這個中斷本身怎么處理、誰處理irqaction每次request_irq申請的一個處理者handler、thread_fn、next、dev_id、flags一個處理者哪個函數(shù)處理為什么是兩層而不是一層因為一個中斷號可以被多個設備共享——irq_desc是這個中斷action鏈表是一串處理者。irq_desc一個irqaction可以一串。// include/linux/interrupt.h:118structirqaction{irq_handler_thandler;// 中斷上半部硬中斷上下文驅(qū)動注冊的 ISRvoid*dev_id;// 設備標識共享中斷時區(qū)分是哪個設備structirqaction*next;// 鏈表指針共享中斷串起來irq_handler_tthread_fn;// 線程化處理函數(shù)下半部可空unsignedintflags;// IRQF_SHARED / IRQF_ONESHOT ...};irq_desc里兩個字段最關鍵handle_irq函數(shù)指針指向這個中斷該怎么處理流程。MSI 是handle_edge_irq邊沿INTx 是handle_level_irq電平。它是在中斷建立時根據(jù)類型設好的第四節(jié)展開。actionirqaction鏈表頭。注冊時往這里掛投遞時從這里取。二、注冊線request_irq 把 handler 掛上 irq_desc驅(qū)動 probe 時調(diào)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;// 線程化函數(shù)可空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節(jié)選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全部在硬中斷上下文處理純線程化空用默認提供處理邏輯重、要睡眠全部放內(nèi)核線程上半下半提供提供硬中斷只做緊急事返回IRQ_WAKE_THREAD剩余放線程三、投遞線中斷從硬件跑到 handler這是中斷真正發(fā)生時硬件到軟件的完整路徑設備發(fā)中斷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);// 分發(fā)到 desc}else{apic_eoi();// 沒 handler直接 EOI}}vector_irq[vector]是一個per-CPU 數(shù)組把 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。這個數(shù)組在中斷建立時填充上一篇初始化篇的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);// 調(diào) desc 的 handle_irq 函數(shù)指針}這里就到了irq_desc的多態(tài)點desc-handle_irq指向handle_edge_irqMSI或handle_level_irqINTx。它們最終都會走到handle_irq_event()handle.c:202再遍歷 action 鏈表調(diào) 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);// ← 調(diào)驅(qū)動注冊的 handlerif(resIRQ_WAKE_THREAD)__irq_wake_thread(desc,action);// 喚醒線程化 handlerretval|res;}returnretval;}注意兩件事action-handler(irq, action-dev_id)就是驅(qū)動在request_irq里注冊的那個函數(shù)。dev_id傳回給 handler共享中斷時用它判斷是不是我的設備發(fā)的中斷。handler 返回IRQ_WAKE_THREAD時喚醒線程——這就是上半下半模式的銜接點上半部handler做完緊急事返回IRQ_WAKE_THREAD內(nèi)核喚醒對應的thread_fn線程處理剩余部分。而handle_irq_event()handle.c:202在調(diào) handler 前后做了件關鍵的事——先解鎖desc-lock再調(diào) handler調(diào)完再上鎖irqreturn_thandle_irq_event(structirq_desc*desc){desc-istate~IRQS_PENDING;irqd_set(desc-irq_data,IRQD_IRQ_INPROGRESS);raw_spin_unlock(desc-lock);// ← 先放鎖再調(diào) handlerrethandle_irq_event_percpu(desc);// 遍歷鏈表調(diào) handlerraw_spin_lock(desc-lock);// ← 調(diào)完重新上鎖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調(diào)進來的handle_level_irq在 chip.c:630、handle_edge_irq在 chip.c:789 都是先raw_spin_lock(desc-lock)再調(diào)它而驅(qū)動的 handler 內(nèi)部可能再調(diào)用會去拿desc-lock的中斷管理函數(shù)如disable_irq_nosync()/enable_irq()、synchronize_irq()等如果持鎖直調(diào) 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 正在跑從而與它互斥。本質(zhì)是鎖只保護irq_desc的狀態(tài)字段本身handler 執(zhí)行期間用標志位來表示正在處理而不是讓鎖貫穿 handler 全程。四、邊沿 vs 電平兩種 handle 流程的對比desc-handle_irq的多態(tài)對應兩種最典型的中斷觸發(fā)方式的不同處理流程——這是理解handle_irq函數(shù)指針存在意義的關鍵邊沿觸發(fā)MSI電平觸發(fā)INTx處理函數(shù)handle_edge_irqchip.c:787handle_level_irqchip.c:628進入時irq_ackack不 maskmask_ack_irq先 mask 再 ack為什么邊沿只觸發(fā)一次處理期間新邊沿會記 PENDING電平一直拉高不 mask 會反復進中斷處理do-while循環(huán)handle_irq_event處理期間來新中斷繼續(xù)單次handle_irq_event退出時無需 unmaskcond_unmask_irq處理完 unmask// 電平chip.c:628先 mask處理完 unmaskvoidhandle_level_irq(structirq_desc*desc){mask_ack_irq(desc);// ← 先 mask電平不 mask 會死循環(huán)進中斷...handle_irq_event(desc);// 調(diào) handlercond_unmask_irq(desc);// ← 處理完 unmask}// 邊沿chip.c:787只 ack循環(huán)處理新邊沿voidhandle_edge_irq(structirq_desc*desc){...desc-irq_data.chip-irq_ack(desc-irq_data);// ← 只 ackdo{handle_irq_event(desc);// 調(diào) handler}while((desc-istateIRQS_PENDING)...);// ← 處理期間來新邊沿繼續(xù)}一句話電平中斷處理前要 mask、處理后 unmask否則電平一直觸發(fā)邊沿中斷只 ack、循環(huán)處理處理期間的新邊沿記 PENDING 繼續(xù)。五、完整調(diào)用鏈把注冊線和投遞線串成兩張帶行號的圖【注冊線軟件驅(qū)動 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) 函數(shù)指針多態(tài) ├─ 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六、關鍵函數(shù) / 文件索引函數(shù)位置作用request_irqinterrupt.h:165注冊入口包裝request_threaded_irqmanage.c:2146分配 irqaction 調(diào) __setup_irq__setup_irqmanage.c:1505把 action 掛到 desc-action 鏈表common_interruptirq.c:247投遞入口vector → irq_descgeneric_handle_irq_descirqdesc.h:159調(diào) 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 鏈表調(diào) handler文件v6.6關鍵內(nèi)容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調(diào)handler。irq_desc是中斷本身irqaction鏈表是一串處理者共享中斷。② 投遞線的關鍵樞紐是vector_irq[vector]這是 per-CPU 數(shù)組把 vector 號翻譯成irq_desc——因為每個 CPU 有獨立 vector 空間。common_interrupt先查它拿到irq_desc再走desc-handle_irq函數(shù)指針。③desc-handle_irq是多態(tài)點邊沿MSI走handle_edge_irq只 ack、循環(huán)處理電平INTx走handle_level_irq先 mask、處理完 unmask。兩種流程最終都到handle_irq_event遍歷action鏈表調(diào)action-handler(irq, dev_id)handler 返回IRQ_WAKE_THREAD時喚醒線程化下半部。