背景
Read the fucking source code!--By 魯迅A picture is worth a thousand words.--By 高爾基
說明:
- Kernel版本:4.14
- ARM64處理器,Contex-A53,雙核
- 使用工具:Source Insight 3.5, Visio
1. 概述
從這篇文章開始,來聊一聊中斷子系統,
中斷是處理器用于異步處理外圍設備請求的一種機制,可以說中斷處理是作業系統管理外圍設備的基石,此外系統調度、核間互動等都離不開中斷,它的重要性不言而喻,
來一張概要的分層圖:

- 硬體層:最下層為硬體連接層,對應的是具體的外設與SoC的物理連接,中斷信號是從外設到中斷控制器,由中斷控制器統一管理,再路由到處理器上;
- 硬體相關層:這個層包括兩部分代碼,一部分是架構相關的,比如ARM64處理器處理中斷相關,另一部分是中斷控制器的驅動代碼;
- 通用層:這部分也可以認為是框架層,是硬體無關層,這部分代碼在所有硬體平臺上是通用的;
- 用戶層:這部分也就是中斷的使用者了,主要是各類設備驅動,通過中斷相關介面來進行申請和注冊,最終在外設觸發中斷時,進行相應的回呼處理;
中斷子系統系列文章,會包括硬體相關、中斷框架層、上半部與下半部、Softirq、Workqueue等機制的介紹,本文會先介紹硬體相關的原理及驅動,前戲結束,直奔主題,
2. GIC硬體原理
- ARM公司提供了一個通用的中斷控制器
GIC(Generic Interrupt Controller),GIC的版本包括V1 ~ V4,由于本人使用的SoC中的中斷控制器是V2版本,本文將圍繞GIC-V2來展開介紹;
來一張功能版的框圖:

GIC-V2從功能上說,除了常用的中斷使能、中斷屏蔽、優先級管理等功能外,還支持安全擴展、虛擬化等;GIC-V2從組成上說,主要分為Distributor和CPU Interface兩個模塊,Distributor主要負責中斷源的管理,包括優先級的處理,屏蔽、搶占等,并將最高優先級的中斷分發給CPU Interface,CPU Interface主要用于連接處理器,與處理器進行互動;Virtual Distributor和Virtual CPU Interface都與虛擬化相關,本文不深入分析;
再來一張細節圖看看Distributor和CPU Interface的功能:

-
GIC-V2支持三種型別的中斷:SGI(software-generated interrupts):軟體產生的中斷,主要用于核間互動,內核中的IPI:inter-processor interrupts就是基于SGI,中斷號ID0 - ID15用于SGI;PPI(Private Peripheral Interrupt):私有外設中斷,每個CPU都有自己的私有中斷,典型的應用有local timer,中斷號ID16 - ID31用于PPI;SPI(Shared Peripheral Interrupt):共享外設中斷,中斷產生后,可以分發到某一個CPU上,中斷號ID32 - ID1019用于SPI,ID1020 - ID1023保留用于特殊用途;
-
Distributor功能:- 全域開關控制
Distributor分發到CPU Interface; - 打開或關閉每個中斷;
- 設定每個中斷的優先級;
- 設定每個中斷將路由的CPU串列;
- 設定每個外設中斷的觸發方式:電平觸發、邊緣觸發;
- 設定每個中斷的Group:Group0或Group1,其中Group0用于安全中斷,支持FIQ和IRQ,Group1用于非安全中斷,只支持IRQ;
- 將
SGI中斷分發到目標CPU上; - 每個中斷的狀態可見;
- 提供軟體機制來設定和清除外設中斷的pending狀態;
- 全域開關控制
-
CPU Interface功能:- 使能中斷請求信號到CPU上;
- 中斷的確認;
- 標識中斷處理的完成;
- 為處理器設定中斷優先級掩碼;
- 設定處理器的中斷搶占策略;
- 確定處理器的最高優先級pending中斷;
中斷處理的狀態機如下圖:

Inactive:無中斷狀態;Pending:硬體或軟體觸發了中斷,但尚未傳遞到目標CPU,在電平觸發模式下,產生中斷的同時保持pending狀態;Active:發生了中斷并將其傳遞給目標CPU,并且目標CPU可以處理該中斷;Active and pending:發生了中斷并將其傳遞給目標CPU,同時發生了相同的中斷并且該中斷正在等待處理;
GIC檢測中斷流程如下:
- GIC捕獲中斷信號,中斷信號assert,標記為pending狀態;
Distributor確定好目標CPU后,將中斷信號發送到目標CPU上,同時,對于每個CPU,Distributor會從pending信號中選擇最高優先級中斷發送至CPU Interface;CPU Interface來決定是否將中斷信號發送至目標CPU;- CPU完成中斷處理后,發送一個完成信號
EOI(End of Interrupt)給GIC;
3. GIC驅動分析
3.1 設備資訊添加
ARM平臺的設備資訊,都是通過Device Tree設備樹來添加,設備樹資訊放置在arch/arm64/boot/dts/下
下圖就是一個中斷控制器的設備樹資訊:

compatible欄位:用于與具體的驅動來進行匹配,比如圖片中arm, gic-400,可以根據這個名字去匹配對應的驅動程式;interrupt-cells欄位:用于指定編碼一個中斷源所需要的單元個數,這個值為3,比如在外設在設備樹中添加中斷信號時,通常能看到類似interrupts = <0 23 4>;的資訊,第一個單元0,表示的是中斷型別(1:PPI,0:SPI),第二個單元23表示的是中斷號,第三個單元4表示的是中斷觸發的型別;reg欄位:描述中斷控制器的地址資訊以及地址范圍,比如圖片中分別制定了GIC Distributor(GICD)和GIC CPU Interface(GICC)的地址資訊;interrupt-controller欄位:表示該設備是一個中斷控制器,外設可以連接在該中斷控制器上;- 關于設備數的各個欄位含義,詳細可以參考
Documentation/devicetree/bindings下的對應資訊;
設備樹的資訊,是怎么添加到系統中的呢?Device Tree最侄訓編譯成dtb檔案,并通過Uboot傳遞給內核,在內核啟動后會將dtb檔案決議成device_node結構,關于設備樹的相關知識,本文先不展開,后續再找機會補充,來一張圖,先簡要介紹下關鍵路徑:

- 設備樹的節點資訊,最侄訓變成
device_node結構,在記憶體中維持一個樹狀結構; - 設備與驅動,會根據
compatible欄位進行匹配;
3.2 驅動流程分析
GIC驅動的執行流程如下圖所示:

- 首先需要了解一下鏈接腳本
vmlinux.lds,腳本中定義了一個__irqchip_of_table段,該段用于存放中斷控制器資訊,用于最終來匹配設備; - 在GIC驅動程式中,使用
IRQCHIP_DECLARE宏來宣告結構資訊,包括compatible欄位和回呼函式,該宏會將這個結構放置到__irqchip_of_table欄位中; - 在內核啟動初始化中斷的函式中,
of_irq_init函式會去查找設備節點資訊,該函式的傳入引數就是__irqchip_of_table段,由于IRQCHIP_DECLARE已經將資訊填充好了,of_irq_init函式會根據arm,gic-400去查找對應的設備節點,并獲取設備的資訊,中斷控制器也存在級聯的情況,of_irq_init函式中也處理了這種情況; or_irq_init函式中,最侄訓回呼IRQCHIP_DECLARE宣告的回呼函式,也就是gic_of_init,而這個函式就是GIC驅動的初始化入口函式了;- GIC的作業,本質上是由中斷信號來驅動,因此驅動本身的作業就是完成各類資訊的初始化,注冊好相應的回呼函式,以便能在信號到來之時去執行;
set_smp_process_call設定__smp_cross_call函式指向gic_raise_softirq,本質上就是通過軟體來觸發GIC的SGI中斷,用于核間互動;cpuhp_setup_state_nocalls函式,設定好CPU進行熱插拔時GIC的回呼函式,以便在CPU熱插拔時做相應處理;set_handle_irq函式的設定很關鍵,它將全域函式指標handle_arch_irq指向了gic_handle_irq,而處理器在進入中斷例外時,會跳轉到handle_arch_irq執行,所以,可以認為它就是中斷處理的入口函式了;- 驅動中完成了各類函式的注冊,此外還完成了
irq_chip,irq_domain等結構體的初始化,這些結構在下文會進一步分析; - 最后,完成GIC硬體模塊的初始化設定,以及電源管理相關的注冊等作業;
3.3 資料結構分析
先來張圖:

- GIC驅動中,使用
struct gic_chip_data結構體來描述GIC控制器的資訊,整個驅動都是圍繞著該結構體的初始化,驅動中將函式指標都初始化好,實際的作業是由中斷信號觸發,也就是在中斷來臨的時候去進行回呼; struct irq_chip結構,描述的是中斷控制器的底層操作函式集,這些函式集最終完成對控制器硬體的操作;struct irq_domain結構,用于硬體中斷號和Linux IRQ中斷號(virq,虛擬中斷號)之間的映射;
還是上一下具體的資料結構代碼吧,關鍵注釋如下:
struct irq_chip {
struct device *parent_device; //指向父設備
const char *name; // /proc/interrupts中顯示的名字
unsigned int (*irq_startup)(struct irq_data *data); //啟動中斷,如果設定成NULL,則默認為enable
void (*irq_shutdown)(struct irq_data *data); //關閉中斷,如果設定成NULL,則默認為disable
void (*irq_enable)(struct irq_data *data); //中斷使能,如果設定成NULL,則默認為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); //應答并屏蔽中斷
void (*irq_unmask)(struct irq_data *data); //解除中斷屏蔽
void (*irq_eoi)(struct irq_data *data); //中斷處理結束后呼叫
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force); //在SMP中設定CPU親和力
int (*irq_retrigger)(struct irq_data *data); //重新發送中斷到CPU
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); //設定中斷觸發型別
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映射操作函式集
void *host_data; //在GIC驅動中,指向了irq_gic_data
unsigned int flags;
unsigned int mapcount; //映射中斷的個數
/* 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; //支持級聯的話,指向父設備
#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支持中斷數量的最大值
unsigned int revmap_direct_max_irq;
unsigned int revmap_size; //線性映射的大小
struct radix_tree_root revmap_tree; //Radix Tree映射的根節點
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); // 用于中斷控制器設備與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,決議硬體中斷號和觸發方式
#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用于將硬體的中斷號,轉換成Linux系統中的中斷號(virtual irq, virq),來張圖:

- 每個中斷控制器都對應一個IRQ Domain;
- 中斷控制器驅動通過
irq_domain_add_*()介面來創建IRQ Domain; - IRQ Domain支持三種映射方式:linear map(線性映射),tree map(樹映射),no map(不映射);
- linear map:維護固定大小的表,索引是硬體中斷號,如果硬體中斷最大數量固定,并且數值不大,可以選擇線性映射;
- tree map:硬體中斷號可能很大,可以選擇樹映射;
- no map:硬體中斷號直接就是Linux的中斷號;
三種映射的方式如下圖:

- 圖中描述了三個中斷控制器,對應到三種不同的映射方式;
- 各個控制器的硬體中斷號可以一樣,最終在Linux內核中映射的中斷號是唯一的;
4. Arch-speicific代碼分析
- 中斷也是例外模式的一種,當外設觸發中斷時,處理器會切換到特定的例外模式進行處理,而這部分代碼都是架構相關的;ARM64的代碼位于
arch/arm64/kernel/entry.S, - ARM64處理器有四個例外級別Exception Level:0~3,EL0級對應用戶態程式,EL1級對應作業系統內核態,EL2級對應Hypervisor,EL3級對應Secure Monitor;
- 例外觸發時,處理器進行切換,并且跳轉到例外向量表開始執行,針對中斷例外,最侄訓跳轉到
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
來張圖:

- 中斷觸發,處理器去例外向量表找到對應的入口,比如EL0的中斷跳轉到
el0_irq處,EL1則跳轉到el1_irq處; - 在GIC驅動中,會呼叫
set_handle_irq介面來設定handle_arch_irq的函式指標,讓它指向gic_handle_irq,因此中斷觸發的時候會跳轉到gic_handle_irq處執行; gic_handle_irq函式處理時,分為兩種情況,一種是外設觸發的中斷,硬體中斷號在16 ~ 1020之間,一種是軟體觸發的中斷,用于處理器之間的互動,硬體中斷號在16以內;- 外設觸發中斷后,根據
irq domain去查找對應的Linux IRQ中斷號,進而得到中斷描述符irq_desc,最終也就能呼叫到外設的中斷處理函式了;
GIC和Arch相關的介紹就此打住,下一篇文章會接著介紹通用的中斷處理框架,敬請期待,
參考
ARM Generic Interrupt Controller Architecture version 2.0
歡迎關注公眾號,不定期更新Linux內核機制相關文章,謝謝,

轉載請註明出處,本文鏈接:https://www.uj5u.com/caozuo/67281.html
標籤:Linux
