Linux中斷子系統(tǒng)-通用框架處理
背景
Kernel版本:4.14 ARM64處理器,Contex-A53,雙核 使用工具:Source Insight 3.5, Visio
1. 概述
《Linux中斷子系統(tǒng)(一)-中斷控制器及驅(qū)動分析》講到了底層硬件GIC驅(qū)動,以及Arch-Specific的中斷代碼,本文將研究下通用的中斷處理的過程,屬于硬件無關(guān)層。當(dāng)然,我還是建議你看一下上篇文章。
這篇文章會解答兩個問題:
用戶是怎么使用中斷的( 中斷注冊)?外設(shè)觸發(fā)中斷信號時,最終是怎么調(diào)用到中斷handler的( 中斷處理)?
2. 數(shù)據(jù)結(jié)構(gòu)分析
先來看一下總的數(shù)據(jù)結(jié)構(gòu),核心是圍繞著struct irq_desc來展開:

Linux內(nèi)核的中斷處理,圍繞著中斷描述符結(jié)構(gòu)
struct irq_desc展開,內(nèi)核提供了兩種中斷描述符組織形式:打開 CONFIG_SPARSE_IRQ宏(中斷編號不連續(xù)),中斷描述符以radix-tree來組織,用戶在初始化時進(jìn)行動態(tài)分配,然后再插入radix-tree中;關(guān)閉 CONFIG_SPARSE_IRQ宏(中斷編號連續(xù)),中斷描述符以數(shù)組的形式組織,并且已經(jīng)分配好;不管哪種形式,都可以通過 linux irq號來找到對應(yīng)的中斷描述符;圖的左側(cè)灰色部分,主要在中斷控制器驅(qū)動中進(jìn)行初始化設(shè)置,包括各個結(jié)構(gòu)中函數(shù)指針的指向等,其中
struct irq_chip用于對中斷控制器的硬件操作,struct irq_domain與中斷控制器對應(yīng),完成的工作是硬件中斷號到Linux irq的映射;圖的上側(cè)灰色部分,中斷描述符的創(chuàng)建(這里指
CONFIG_SPARSE_IRQ),主要在獲取設(shè)備中斷信息的過程中完成的,從而讓設(shè)備樹中的中斷能與具體的中斷描述符irq_desc匹配;圖中剩余部分,在設(shè)備申請注冊中斷的過程中進(jìn)行設(shè)置,比如
struct irqaction中handler的設(shè)置,這個用于指向我們設(shè)備驅(qū)動程序中的中斷處理函數(shù)了;
中斷的處理主要有以下幾個功能模塊:
硬件中斷號到 Linux irq中斷號的映射,并創(chuàng)建好irq_desc中斷描述符;中斷注冊時,先獲取設(shè)備的中斷號,根據(jù)中斷號找到對應(yīng)的 irq_desc,并將設(shè)備的中斷處理函數(shù)添加到irq_desc中;設(shè)備觸發(fā)中斷信號時,根據(jù)硬件中斷號得到 Linux irq中斷號,找到對應(yīng)的irq_desc,最終調(diào)用到設(shè)備的中斷處理函數(shù);
上述的描述比較簡單,更詳細(xì)的過程,往下看吧。
3. 流程分析
3.1 中斷注冊
這一次,讓我們以問題的方式來展開:先來讓我們回答第一個問題:用戶是怎么使用中斷的?
熟悉設(shè)備驅(qū)動的同學(xué)應(yīng)該都清楚,經(jīng)常會在驅(qū)動程序中調(diào)用 request_irq()接口或者request_threaded_irq()接口來注冊設(shè)備的中斷處理函數(shù);request_irq()/request_threaded_irq接口中,都需要用到irq,也就是中斷號,那么這個中斷號是從哪里來的呢?它是Linux irq,它又是如何映射到具體的硬件設(shè)備的中斷號的呢?
先來看第二個問題:設(shè)備硬件中斷號到
Linux irq中斷號的映射

硬件設(shè)備的中斷信息都在設(shè)備樹 device tree中進(jìn)行了描述,在系統(tǒng)啟動過程中,這些信息都已經(jīng)加載到內(nèi)存中并得到了解析;驅(qū)動中通常會使用 platform_get_irq或irq_of_parse_and_map接口,去根據(jù)設(shè)備樹的信息去創(chuàng)建映射關(guān)系(硬件中斷號到linux irq中斷號映射);《Linux中斷子系統(tǒng)(一)-中斷控制器及驅(qū)動分析》提到過 struct irq_domain用于完成映射工作,因此在irq_create_fwspec_mapping接口中,會先去找到匹配的irq domain,再去回調(diào)該irq domain中的函數(shù)集,通常irq domain都是在中斷控制器驅(qū)動中初始化的,以ARM GICv2為例,最終回調(diào)到gic_irq_domain_hierarchy_ops中的函數(shù);如果已經(jīng)創(chuàng)建好了映射,那么可以直接進(jìn)行返回 linux irq中斷號了,否則的話需要irq_domain_alloc_irqs來創(chuàng)建映射關(guān)系;irq_domain_alloc_irqs完成兩個工作:針對 linux irq中斷號創(chuàng)建一個irq_desc中斷描述符;調(diào)用 domain->ops->alloc函數(shù)來完成映射,在ARM GICv2驅(qū)動中對應(yīng)gic_irq_domain_alloc函數(shù),這個函數(shù)很關(guān)鍵,所以下文介紹一下;
gic_irq_domain_alloc函數(shù)如下:

gic_irq_domain_translate:負(fù)責(zé)解析出設(shè)備樹中描述的中斷號和中斷觸發(fā)類型(邊緣觸發(fā)、電平觸發(fā)等);gic_irq_domain_map:將硬件中斷號和linux中斷號綁定到一個結(jié)構(gòu)中,也就完成了映射,此外還綁定了irq_desc結(jié)構(gòu)中的其他字段,最重要的是設(shè)置了irq_desc->handle_irq的函數(shù)指針,這個最終是中斷響應(yīng)時往上執(zhí)行的入口,這個是關(guān)鍵,下文講述中斷處理過程時還會提到;根據(jù)硬件中斷號的范圍設(shè)置 irq_desc->handle_irq的指針,共享中斷入口為handle_fasteoi_irq,私有中斷入口為handle_percpu_devid_irq;
上述函數(shù)執(zhí)行完成后,完成了兩大工作:
硬件中斷號與Linux中斷號完成映射,并為Linux中斷號創(chuàng)建了 irq_desc中斷描述符;數(shù)據(jù)結(jié)構(gòu)的綁定及初始化,關(guān)鍵的地方是設(shè)置了中斷處理往上執(zhí)行的入口;
再看第一個問題:中斷是怎么來注冊的?
設(shè)備驅(qū)動中,獲取到了irq中斷號后,通常就會采用request_irq/request_threaded_irq來注冊中斷,其中request_irq用于注冊普通處理的中斷,request_threaded_irq用于注冊線程化處理的中斷;
在講具體的注冊流程前,先看一下主要的中斷標(biāo)志位:
#define IRQF_SHARED 0x00000080 //多個設(shè)備共享一個中斷號,需要外設(shè)硬件支持#define IRQF_PROBE_SHARED 0x00000100 //中斷處理程序允許sharing mismatch發(fā)生#define __IRQF_TIMER 0x00000200 //時鐘中斷#define IRQF_PERCPU 0x00000400 //屬于特定CPU的中斷#define IRQF_NOBALANCING 0x00000800 //禁止在CPU之間進(jìn)行中斷均衡處理#define IRQF_IRQPOLL 0x00001000 //中斷被用作輪訓(xùn)#define IRQF_ONESHOT 0x00002000 //一次性觸發(fā)的中斷,不能嵌套,1)在硬件中斷處理完成后才能打開中斷;2)在中斷線程化中保持關(guān)閉狀態(tài),直到該中斷源上的所有thread_fn函數(shù)都執(zhí)行完#define IRQF_NO_SUSPEND 0x00004000 //系統(tǒng)休眠喚醒操作中,不關(guān)閉該中斷#define IRQF_FORCE_RESUME 0x00008000 //系統(tǒng)喚醒過程中必須強(qiáng)制打開該中斷#define IRQF_NO_THREAD 0x00010000 //禁止中斷線程化#define IRQF_EARLY_RESUME 0x00020000 //系統(tǒng)喚醒過程中在syscore階段resume,而不用等到設(shè)備resume階段#define IRQF_COND_SUSPEND 0x00040000 //與NO_SUSPEND的用戶共享中斷時,執(zhí)行本設(shè)備的中斷處理函數(shù)

request_irq也是調(diào)用request_threaded_irq,只是在傳參的時候,線程處理函數(shù)thread_fn函數(shù)設(shè)置成NULL;由于在硬件中斷號和Linux中斷號完成映射后, irq_desc已經(jīng)創(chuàng)建好,可以通過irq_to_desc接口去獲取對應(yīng)的irq_desc;創(chuàng)建 irqaction,并初始化該結(jié)構(gòu)體中的各個字段,其中包括傳入的中斷處理函數(shù)賦值給對應(yīng)的字段;__setup_irq用于完成中斷的相關(guān)設(shè)置,包括中斷線程化的處理:中斷線程化用于減少系統(tǒng)關(guān)中斷的時間,增強(qiáng)系統(tǒng)的實時性; ARM64默認(rèn)開啟了 CONFIG_IRQ_FORCED_THREADING,引導(dǎo)參數(shù)傳入threadirqs時,則除了IRQF_NO_THREAD外的中斷,其他的都將強(qiáng)制線程化處理;中斷線程化會為每個中斷都創(chuàng)建一個內(nèi)核線程,如果中斷進(jìn)行共享,對應(yīng) irqaction將連接成鏈表,每個irqaction都有thread_mask位圖字段,當(dāng)所有共享中斷都處理完成后才能unmask中斷,解除中斷屏蔽;
3.2 中斷處理
當(dāng)完成中斷的注冊后,所有結(jié)構(gòu)的組織關(guān)系都已經(jīng)建立好,剩下的工作就是當(dāng)信號來臨時,進(jìn)行中斷的處理工作。
來回顧一下《Linux中斷子系統(tǒng)(一)-中斷控制器及驅(qū)動分析》中的Arch-specific處理流程:

中斷收到之后,首先會跳轉(zhuǎn)到異常向量表的入口處,進(jìn)而逐級進(jìn)行回調(diào)處理,最終調(diào)用到 generic_handle_irq來進(jìn)行中斷處理。
generic_handle_irq處理如下圖:

generic_handle_irq函數(shù)最終會調(diào)用到desc->handle_irq(),這個也就是對應(yīng)到上文中在建立映射關(guān)系的過程中,調(diào)用irq_domain_set_info函數(shù),設(shè)置好了函數(shù)指針,也就是handle_fasteoi_irq和handle_percpu_devid_irq;handle_fasteoi_irq:處理共享中斷,并且遍歷irqaction鏈表,逐個調(diào)用action->handler()函數(shù),這個函數(shù)正是設(shè)備驅(qū)動程序調(diào)用request_irq/request_threaded_irq接口注冊的中斷處理函數(shù),此外如果中斷線程化處理的話,還會調(diào)用__irq_wake_thread()喚醒內(nèi)核線程;handle_percpu_devid_irq:處理per-CPU中斷處理,在這個過程中會分別調(diào)用中斷控制器的處理函數(shù)進(jìn)行硬件操作,該函數(shù)調(diào)用action->handler()來進(jìn)行中斷處理;
來看看中斷線程化處理后的喚醒流程吧__handle_irq_event_percpu->__irq_wake_thread:

__handle_irq_event_percpu->__irq_wake_thread將喚醒irq_thread中斷內(nèi)核線程;irq_thread內(nèi)核線程,將根據(jù)是否為強(qiáng)制中斷線程化對函數(shù)指針handler_fn進(jìn)行初始化,以便后續(xù)進(jìn)行調(diào)用;irq_thread內(nèi)核線程將while(!irq_wait_for_interrupt)循環(huán)進(jìn)行中斷的處理,當(dāng)滿足條件時,執(zhí)行handler_fn,在該函數(shù)中最終調(diào)用action->thread_fn,也就是完成了中斷的處理;irq_wait_for_interrupt函數(shù),將會判斷中斷線程的喚醒條件,如果滿足了,則將當(dāng)前任務(wù)設(shè)置成TASK_RUNNING狀態(tài),并返回0,這樣就能執(zhí)行中斷的處理,否則就調(diào)用schedule()進(jìn)行調(diào)度,讓出CPU,并將任務(wù)設(shè)置成TASK_INTERRUPTIBLE可中斷睡眠狀態(tài);
3.3 總結(jié)
中斷的處理,總體來說可以分為兩部分來看:
從上到下:圍繞 irq_desc中斷描述符建立好連接關(guān)系,這個過程就包括:中斷源信息的解析(設(shè)備樹),硬件中斷號到Linux中斷號的映射關(guān)系、irq_desc結(jié)構(gòu)的分配及初始化(內(nèi)部各個結(jié)構(gòu)的組織關(guān)系)、中斷的注冊(填充irq_desc結(jié)構(gòu),包括handler處理函數(shù))等,總而言之,就是完成靜態(tài)關(guān)系創(chuàng)建,為中斷處理做好準(zhǔn)備;從下到上,當(dāng)外設(shè)觸發(fā)中斷信號時,中斷控制器接收到信號并發(fā)送到處理器,此時處理器進(jìn)行異常模式切換,并逐步從處理器架構(gòu)相關(guān)代碼逐級回調(diào)。如果涉及到中斷線程化,則還需要進(jìn)行中斷內(nèi)核線程的喚醒操作,最終完成中斷處理函數(shù)的執(zhí)行。

