背景
Read the fucking source code!
--By 魯迅A picture is worth a thousand words.
--By 高爾基
說明:
- 使用工具:Source Insight 3.5, Visio
1. 概述
從這篇文章開始,來聊一聊中斷子系統(tǒng)。中斷是處理器用于異步處理外圍設(shè)備請求的一種機制,可以說中斷處理是操作系統(tǒng)管理外圍設(shè)備的基石,此外系統(tǒng)調(diào)度、核間交互等都離不開中斷,它的重要性不言而喻。
來一張概要的分層圖:
- 硬件層:最下層為硬件連接層,對應(yīng)的是具體的外設(shè)與SoC的物理連接,中斷信號是從外設(shè)到中斷控制器,由中斷控制器統(tǒng)一管理,再路由到處理器上;
- 硬件相關(guān)層:這個層包括兩部分代碼,一部分是架構(gòu)相關(guān)的,比如ARM64處理器處理中斷相關(guān),另一部分是中斷控制器的驅(qū)動代碼;
- 通用層:這部分也可以認(rèn)為是框架層,是硬件無關(guān)層,這部分代碼在所有硬件平臺上是通用的;
- 用戶層:這部分也就是中斷的使用者了,主要是各類設(shè)備驅(qū)動,通過中斷相關(guān)接口來進行申請和注冊,最終在外設(shè)觸發(fā)中斷時,進行相應(yīng)的回調(diào)處理;
中斷子系統(tǒng)系列文章,會包括硬件相關(guān)、中斷框架層、上半部與下半部、Softirq、Workqueue等機制的介紹,本文會先介紹硬件相關(guān)的原理及驅(qū)動,前戲結(jié)束,直奔主題。
2. GIC硬件原理
- ARM公司提供了一個通用的中斷控制器
GIC(Generic Interrupt Controller)
,GIC
的版本包括V1 ~ V4
,由于本人使用的SoC中的中斷控制器是V2
版本,本文將圍繞GIC-V2
來展開介紹;
來一張功能版的框圖:
GIC-V2
從功能上說,除了常用的中斷使能、中斷屏蔽、優(yōu)先級管理等功能外,還支持安全擴展、虛擬化等;GIC-V2
從組成上說,主要分為Distributor
和CPU Interface
兩個模塊,Distributor
主要負(fù)責(zé)中斷源的管理,包括優(yōu)先級的處理,屏蔽、搶占等,并將最高優(yōu)先級的中斷分發(fā)給CPU Interface
,CPU Interface
主要用于連接處理器,與處理器進行交互;Virtual Distributor
和Virtual CPU Interface
都與虛擬化相關(guān),本文不深入分析;
再來一張細(xì)節(jié)圖看看Distributor
和CPU Interface
的功能:
SGI(software-generated interrupts)
:軟件產(chǎn)生的中斷,主要用于核間交互,內(nèi)核中的IPI:inter-processor interrupts
就是基于SGI
,中斷號ID0 - ID15
用于SGI
;PPI(Private Peripheral Interrupt)
:私有外設(shè)中斷,每個CPU都有自己的私有中斷,典型的應(yīng)用有local timer
,中斷號ID16 - ID31
用于PPI
;SPI(Shared Peripheral Interrupt)
:共享外設(shè)中斷,中斷產(chǎn)生后,可以分發(fā)到某一個CPU上,中斷號ID32 - ID1019
用于SPI
,ID1020 - ID1023
保留用于特殊用途;
- 全局開關(guān)控制
Distributor
分發(fā)到CPU Interface
; - 設(shè)置每個外設(shè)中斷的觸發(fā)方式:電平觸發(fā)、邊緣觸發(fā);
- 設(shè)置每個中斷的Group:Group0或Group1,其中Group0用于安全中斷,支持FIQ和IRQ,Group1用于非安全中斷,只支持IRQ;
- 將
SGI
中斷分發(fā)到目標(biāo)CPU上; - 提供軟件機制來設(shè)置和清除外設(shè)中斷的pending狀態(tài);
- 確定處理器的最高優(yōu)先級pending中斷;
中斷處理的狀態(tài)機如下圖:
Pending
:硬件或軟件觸發(fā)了中斷,但尚未傳遞到目標(biāo)CPU,在電平觸發(fā)模式下,產(chǎn)生中斷的同時保持pending
狀態(tài);Active
:發(fā)生了中斷并將其傳遞給目標(biāo)CPU,并且目標(biāo)CPU可以處理該中斷;Active and pending
:發(fā)生了中斷并將其傳遞給目標(biāo)CPU,同時發(fā)生了相同的中斷并且該中斷正在等待處理;
GIC檢測中斷流程如下:
- GIC捕獲中斷信號,中斷信號assert,標(biāo)記為pending狀態(tài);
Distributor
確定好目標(biāo)CPU后,將中斷信號發(fā)送到目標(biāo)CPU上,同時,對于每個CPU,Distributor
會從pending信號中選擇最高優(yōu)先級中斷發(fā)送至CPU Interface
;CPU Interface
來決定是否將中斷信號發(fā)送至目標(biāo)CPU;- CPU完成中斷處理后,發(fā)送一個完成信號
EOI(End of Interrupt)
給GIC;
3. GIC驅(qū)動分析
3.1 設(shè)備信息添加
ARM平臺的設(shè)備信息,都是通過Device Tree
設(shè)備樹來添加,設(shè)備樹信息放置在arch/arm64/boot/dts/
下
下圖就是一個中斷控制器的設(shè)備樹信息:
compatible
字段:用于與具體的驅(qū)動來進行匹配,比如圖片中arm, gic-400
,可以根據(jù)這個名字去匹配對應(yīng)的驅(qū)動程序;interrupt-cells
字段:用于指定編碼一個中斷源所需要的單元個數(shù),這個值為3。比如在外設(shè)在設(shè)備樹中添加中斷信號時,通常能看到類似interrupts = <0 23 4>;
的信息,第一個單元0,表示的是中斷類型(1:PPI,0:SPI
),第二個單元23表示的是中斷號,第三個單元4表示的是中斷觸發(fā)的類型;reg
字段:描述中斷控制器的地址信息以及地址范圍,比如圖片中分別制定了GIC Distributor(GICD)
和GIC CPU Interface(GICC)
的地址信息;interrupt-controller
字段:表示該設(shè)備是一個中斷控制器,外設(shè)可以連接在該中斷控制器上;- 關(guān)于設(shè)備數(shù)的各個字段含義,詳細(xì)可以參考
Documentation/devicetree/bindings
下的對應(yīng)信息;
設(shè)備樹的信息,是怎么添加到系統(tǒng)中的呢?Device Tree
最終會編譯成dtb
文件,并通過Uboot傳遞給內(nèi)核,在內(nèi)核啟動后會將dtb
文件解析成device_node
結(jié)構(gòu)。關(guān)于設(shè)備樹的相關(guān)知識,本文先不展開,后續(xù)再找機會補充。來一張圖,先簡要介紹下關(guān)鍵路徑:
- 設(shè)備樹的節(jié)點信息,最終會變成
device_node
結(jié)構(gòu),在內(nèi)存中維持一個樹狀結(jié)構(gòu); - 設(shè)備與驅(qū)動,會根據(jù)
compatible
字段進行匹配;
3.2 驅(qū)動流程分析
GIC驅(qū)動的執(zhí)行流程如下圖所示:
- 首先需要了解一下鏈接腳本
vmlinux.lds
,腳本中定義了一個__irqchip_of_table
段,該段用于存放中斷控制器信息,用于最終來匹配設(shè)備; - 在GIC驅(qū)動程序中,使用
IRQCHIP_DECLARE
宏來聲明結(jié)構(gòu)信息,包括compatible
字段和回調(diào)函數(shù),該宏會將這個結(jié)構(gòu)放置到__irqchip_of_table
字段中; - 在內(nèi)核啟動初始化中斷的函數(shù)中,
of_irq_init
函數(shù)會去查找設(shè)備節(jié)點信息,該函數(shù)的傳入?yún)?shù)就是__irqchip_of_table
段,由于IRQCHIP_DECLARE
已經(jīng)將信息填充好了,of_irq_init
函數(shù)會根據(jù)arm,gic-400
去查找對應(yīng)的設(shè)備節(jié)點,并獲取設(shè)備的信息。中斷控制器也存在級聯(lián)的情況,of_irq_init
函數(shù)中也處理了這種情況; or_irq_init
函數(shù)中,最終會回調(diào)IRQCHIP_DECLARE
聲明的回調(diào)函數(shù),也就是gic_of_init
,而這個函數(shù)就是GIC驅(qū)動的初始化入口函數(shù)了;- GIC的工作,本質(zhì)上是由中斷信號來驅(qū)動,因此驅(qū)動本身的工作就是完成各類信息的初始化,注冊好相應(yīng)的回調(diào)函數(shù),以便能在信號到來之時去執(zhí)行;
set_smp_process_call
設(shè)置__smp_cross_call
函數(shù)指向gic_raise_softirq
,本質(zhì)上就是通過軟件來觸發(fā)GIC的SGI中斷
,用于核間交互;cpuhp_setup_state_nocalls
函數(shù),設(shè)置好CPU進行熱插拔時GIC的回調(diào)函數(shù),以便在CPU熱插拔時做相應(yīng)處理;set_handle_irq
函數(shù)的設(shè)置很關(guān)鍵,它將全局函數(shù)指針handle_arch_irq
指向了gic_handle_irq
,而處理器在進入中斷異常時,會跳轉(zhuǎn)到handle_arch_irq
執(zhí)行,所以,可以認(rèn)為它就是中斷處理的入口函數(shù)了;- 驅(qū)動中完成了各類函數(shù)的注冊,此外還完成了
irq_chip
, irq_domain
等結(jié)構(gòu)體的初始化,這些結(jié)構(gòu)在下文會進一步分析; - 最后,完成GIC硬件模塊的初始化設(shè)置,以及電源管理相關(guān)的注冊等工作;
3.3 數(shù)據(jù)結(jié)構(gòu)分析
先來張圖:
- GIC驅(qū)動中,使用
struct gic_chip_data
結(jié)構(gòu)體來描述GIC控制器的信息,整個驅(qū)動都是圍繞著該結(jié)構(gòu)體的初始化,驅(qū)動中將函數(shù)指針都初始化好,實際的工作是由中斷信號觸發(fā),也就是在中斷來臨的時候去進行回調(diào); struct irq_chip
結(jié)構(gòu),描述的是中斷控制器的底層操作函數(shù)集,這些函數(shù)集最終完成對控制器硬件的操作;struct irq_domain
結(jié)構(gòu),用于硬件中斷號和Linux IRQ中斷號(virq,虛擬中斷號)之間的映射;
還是上一下具體的數(shù)據(jù)結(jié)構(gòu)代碼吧,關(guān)鍵注釋如下:
struct irq_chip {
struct device *parent_device; //指向父設(shè)備
const char *name; // /proc/interrupts中顯示的名字
unsigned int (*irq_startup)(struct irq_data *data); //啟動中斷,如果設(shè)置成NULL,則默認(rèn)為enable
void (*irq_shutdown)(struct irq_data *data); //關(guān)閉中斷,如果設(shè)置成NULL,則默認(rèn)為disable
void (*irq_enable)(struct irq_data *data); //中斷使能,如果設(shè)置成NULL,則默認(rèn)為chip->unmask
void (*irq_disable)(struct irq_data *data); //中斷禁止
void (*irq_ack)(struct irq_data *data); //開始新的中斷
void (*irq_mask)(struct irq_data *data); //中斷源屏蔽
void (*irq_mask_ack)(struct irq_data *data); //應(yīng)答并屏蔽中斷
void (*irq_unmask)(struct irq_data *data); //解除中斷屏蔽
void (*irq_eoi)(struct irq_data *data); //中斷處理結(jié)束后調(diào)用
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); //在SMP中設(shè)置CPU親和力
int (*irq_retrigger)(struct irq_data *data); //重新發(fā)送中斷到CPU
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); //設(shè)置中斷觸發(fā)類型
int (*irq_set_wake)(struct irq_data *data, unsigned int on); //使能/禁止電源管理中的喚醒功能
void (*irq_bus_lock)(struct irq_data *data); //慢速芯片總線上的鎖
void (*irq_bus_sync_unlock)(struct irq_data *data); //同步釋放慢速總線芯片的鎖
void (*irq_cpu_online)(struct irq_data *data);
void (*irq_cpu_offline)(struct irq_data *data);
void (*irq_suspend)(struct irq_data *data);
void (*irq_resume)(struct irq_data *data);
void (*irq_pm_shutdown)(struct irq_data *data);
void (*irq_calc_mask)(struct irq_data *data);
void (*irq_print_chip)(struct irq_data *data, struct seq_file *p);
int (*irq_request_resources)(struct irq_data *data);
void (*irq_release_resources)(struct irq_data *data);
void (*irq_compose_msi_msg)(struct irq_data *data, struct msi_msg *msg);
void (*irq_write_msi_msg)(struct irq_data *data, struct msi_msg *msg);
int (*irq_get_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool *state);
int (*irq_set_irqchip_state)(struct irq_data *data, enum irqchip_irq_state which, bool state);
int (*irq_set_vcpu_affinity)(struct irq_data *data, void *vcpu_info);
void (*ipi_send_single)(struct irq_data *data, unsigned int cpu);
void (*ipi_send_mask)(struct irq_data *data, const struct cpumask *dest);
unsigned long flags;
};
struct irq_domain {
struct list_head link; //用于添加到全局鏈表irq_domain_list中
const char *name; //IRQ domain的名字
const struct irq_domain_ops *ops; //IRQ domain映射操作函數(shù)集
void *host_data; //在GIC驅(qū)動中,指向了irq_gic_data
unsigned int flags;
unsigned int mapcount; //映射中斷的個數(shù)
/* Optional data */
struct fwnode_handle *fwnode;
enum irq_domain_bus_token bus_token;
struct irq_domain_chip_generic *gc;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_domain *parent; //支持級聯(lián)的話,指向父設(shè)備
#endif
#ifdef CONFIG_GENERIC_IRQ_DEBUGFS
struct dentry *debugfs_file;
#endif
/* reverse map data. The linear map gets appended to the irq_domain */
irq_hw_number_t hwirq_max; //IRQ domain支持中斷數(shù)量的最大值
unsigned int revmap_direct_max_irq;
unsigned int revmap_size; //線性映射的大小
struct radix_tree_root revmap_tree; //Radix Tree映射的根節(jié)點
unsigned int linear_revmap[]; //線性映射用到的查找表
};
struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token); // 用于中斷控制器設(shè)備與IRQ domain的匹配
int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token);
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw); //用于硬件中斷號與Linux中斷號的映射
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type); //通過device_node,解析硬件中斷號和觸發(fā)方式
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
/* extended V2 interfaces to support hierarchy irq_domains */
int (*alloc)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs, void *arg);
void (*free)(struct irq_domain *d, unsigned int virq,
unsigned int nr_irqs);
void (*activate)(struct irq_domain *d, struct irq_data *irq_data);
void (*deactivate)(struct irq_domain *d, struct irq_data *irq_data);
int (*translate)(struct irq_domain *d, struct irq_fwspec *fwspec,
unsigned long *out_hwirq, unsigned int *out_type);
#endif
};
3.3.1 IRQ domain
IRQ domain用于將硬件的中斷號,轉(zhuǎn)換成Linux系統(tǒng)中的中斷號(virtual irq, virq
),來張圖:
- 每個中斷控制器都對應(yīng)一個IRQ Domain;
- 中斷控制器驅(qū)動通過
irq_domain_add_*()
接口來創(chuàng)建IRQ Domain; - IRQ Domain支持三種映射方式:linear map(線性映射),tree map(樹映射),no map(不映射);
- linear map:維護固定大小的表,索引是硬件中斷號,如果硬件中斷最大數(shù)量固定,并且數(shù)值不大,可以選擇線性映射;
- tree map:硬件中斷號可能很大,可以選擇樹映射;
- no map:硬件中斷號直接就是Linux的中斷號;
三種映射的方式如下圖:
- 圖中描述了三個中斷控制器,對應(yīng)到三種不同的映射方式;
- 各個控制器的硬件中斷號可以一樣,最終在Linux內(nèi)核中映射的中斷號是唯一的;
4. Arch-speicific代碼分析
- 中斷也是異常模式的一種,當(dāng)外設(shè)觸發(fā)中斷時,處理器會切換到特定的異常模式進行處理,而這部分代碼都是架構(gòu)相關(guān)的;ARM64的代碼位于
arch/arm64/kernel/entry.S
。 - ARM64處理器有四個異常級別Exception Level:0~3,EL0級對應(yīng)用戶態(tài)程序,EL1級對應(yīng)操作系統(tǒng)內(nèi)核態(tài),EL2級對應(yīng)Hypervisor,EL3級對應(yīng)Secure Monitor;
- 異常觸發(fā)時,處理器進行切換,并且跳轉(zhuǎn)到異常向量表開始執(zhí)行,針對中斷異常,最終會跳轉(zhuǎn)到
irq_handler
中;
代碼比較簡單,如下:
/*
* Interrupt handling.
*/
.macro irq_handler
ldr_l x1, handle_arch_irq
mov x0, sp
irq_stack_entry
blr x1
irq_stack_exit
.endm
來張圖:
- 中斷觸發(fā),處理器去異常向量表找到對應(yīng)的入口,比如EL0的中斷跳轉(zhuǎn)到
el0_irq
處,EL1則跳轉(zhuǎn)到el1_irq
處; - 在GIC驅(qū)動中,會調(diào)用
set_handle_irq
接口來設(shè)置handle_arch_irq
的函數(shù)指針,讓它指向gic_handle_irq
,因此中斷觸發(fā)的時候會跳轉(zhuǎn)到gic_handle_irq
處執(zhí)行; gic_handle_irq
函數(shù)處理時,分為兩種情況,一種是外設(shè)觸發(fā)的中斷,硬件中斷號在16 ~ 1020
之間,一種是軟件觸發(fā)的中斷,用于處理器之間的交互,硬件中斷號在16以內(nèi);- 外設(shè)觸發(fā)中斷后,根據(jù)
irq domain
去查找對應(yīng)的Linux IRQ中斷號,進而得到中斷描述符irq_desc
,最終也就能調(diào)用到外設(shè)的中斷處理函數(shù)了;
GIC和Arch相關(guān)的介紹就此打住,下一篇文章會接著介紹通用的中斷處理框架,敬請期待。
參考
ARM Generic Interrupt Controller Architecture version 2.0