1. 程式人生 > >【原創】Linux中斷子系統(一)-中斷控制器及驅動分析

【原創】Linux中斷子系統(一)-中斷控制器及驅動分析

# 背景 - `Read the fucking source code!` --By 魯迅 - `A picture is worth a thousand words.` --By 高爾基 說明: 1. Kernel版本:4.14 2. ARM64處理器,Contex-A53,雙核 3. 使用工具:Source Insight 3.5, Visio # 1. 概述 從這篇文章開始,來聊一聊中斷子系統。 中斷是處理器用於非同步處理外圍裝置請求的一種機制,可以說中斷處理是作業系統管理外圍裝置的基石,此外系統排程、核間互動等都離不開中斷,它的重要性不言而喻。 來一張概要的分層圖: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531110656598-1083949797.png) - 硬體層:最下層為硬體連線層,對應的是具體的外設與SoC的物理連線,中斷訊號是從外設到中斷控制器,由中斷控制器統一管理,再路由到處理器上; - 硬體相關層:這個層包括兩部分程式碼,一部分是架構相關的,比如ARM64處理器處理中斷相關,另一部分是中斷控制器的驅動程式碼; - 通用層:這部分也可以認為是框架層,是硬體無關層,這部分程式碼在所有硬體平臺上是通用的; - 使用者層:這部分也就是中斷的使用者了,主要是各類裝置驅動,通過中斷相關介面來進行申請和註冊,最終在外設觸發中斷時,進行相應的回撥處理; 中斷子系統系列文章,會包括硬體相關、中斷框架層、上半部與下半部、Softirq、Workqueue等機制的介紹,本文會先介紹硬體相關的原理及驅動,前戲結束,直奔主題。 # 2. GIC硬體原理 - ARM公司提供了一個通用的中斷控制器`GIC(Generic Interrupt Controller)`,`GIC`的版本包括`V1 ~ V4`,由於本人使用的SoC中的中斷控制器是`V2`版本,本文將圍繞`GIC-V2`來展開介紹; 來一張功能版的框圖: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531110816618-151629003.png) - `GIC-V2`從功能上說,除了常用的中斷使能、中斷遮蔽、優先順序管理等功能外,還支援安全擴充套件、虛擬化等; - `GIC-V2`從組成上說,主要分為`Distributor`和`CPU Interface`兩個模組,`Distributor`主要負責中斷源的管理,包括優先順序的處理,遮蔽、搶佔等,並將最高優先順序的中斷分發給`CPU Interface`,`CPU Interface`主要用於連線處理器,與處理器進行互動; - `Virtual Distributor`和`Virtual CPU Interface`都與虛擬化相關,本文不深入分析; 再來一張細節圖看看`Distributor`和`CPU Interface`的功能: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531110908676-1953288373.png) - `GIC-V2`支援三種類型的中斷: 1. `SGI(software-generated interrupts)`:軟體產生的中斷,主要用於核間互動,核心中的`IPI:inter-processor interrupts`就是基於`SGI`,中斷號`ID0 - ID15`用於`SGI`; 2. `PPI(Private Peripheral Interrupt)`:私有外設中斷,每個CPU都有自己的私有中斷,典型的應用有`local timer`,中斷號`ID16 - ID31`用於`PPI`; 3. `SPI(Shared Peripheral Interrupt)`:共享外設中斷,中斷產生後,可以分發到某一個CPU上,中斷號`ID32 - ID1019`用於`SPI`,`ID1020 - ID1023`保留用於特殊用途; - `Distributor`功能: 1. 全域性開關控制`Distributor`分發到`CPU Interface`; 2. 開啟或關閉每個中斷; 3. 設定每個中斷的優先順序; 4. 設定每個中斷將路由的CPU列表; 5. 設定每個外設中斷的觸發方式:電平觸發、邊緣觸發; 6. 設定每個中斷的Group:Group0或Group1,其中Group0用於安全中斷,支援FIQ和IRQ,Group1用於非安全中斷,只支援IRQ; 7. 將`SGI`中斷分發到目標CPU上; 8. 每個中斷的狀態可見; 9. 提供軟體機制來設定和清除外設中斷的pending狀態; - `CPU Interface`功能: 1. 使能中斷請求訊號到CPU上; 2. 中斷的確認; 3. 標識中斷處理的完成; 4. 為處理器設定中斷優先順序掩碼; 5. 設定處理器的中斷搶佔策略; 6. 確定處理器的最高優先順序pending中斷; 中斷處理的狀態機如下圖: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111054566-721772228.png) - `Inactive`:無中斷狀態; - `Pending`:硬體或軟體觸發了中斷,但尚未傳遞到目標CPU,在電平觸發模式下,產生中斷的同時保持`pending`狀態; - `Active`:發生了中斷並將其傳遞給目標CPU,並且目標CPU可以處理該中斷; - `Active and pending`:發生了中斷並將其傳遞給目標CPU,同時發生了相同的中斷並且該中斷正在等待處理; GIC檢測中斷流程如下: 1. GIC捕獲中斷訊號,中斷訊號assert,標記為pending狀態; 2. `Distributor`確定好目標CPU後,將中斷訊號傳送到目標CPU上,同時,對於每個CPU,`Distributor`會從pending訊號中選擇最高優先順序中斷髮送至`CPU Interface`; 3. `CPU Interface`來決定是否將中斷訊號傳送至目標CPU; 4. CPU完成中斷處理後,傳送一個完成訊號`EOI(End of Interrupt)`給GIC; # 3. GIC驅動分析 ## 3.1 裝置資訊新增 ARM平臺的裝置資訊,都是通過`Device Tree`裝置樹來新增,裝置樹資訊放置在`arch/arm64/boot/dts/`下 下圖就是一箇中斷控制器的裝置樹資訊: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111143314-993344319.png) - `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`結構。關於裝置樹的相關知識,本文先不展開,後續再找機會補充。來一張圖,先簡要介紹下關鍵路徑: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111239115-662907709.png) - 裝置樹的節點資訊,最終會變成`device_node`結構,在記憶體中維持一個樹狀結構; - 裝置與驅動,會根據`compatible`欄位進行匹配; ## 3.2 驅動流程分析 GIC驅動的執行流程如下圖所示: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111308070-739998455.png) - 首先需要了解一下連結指令碼`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 資料結構分析 先來張圖: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111554895-528341955.png) - GIC驅動中,使用`struct gic_chip_data`結構體來描述GIC控制器的資訊,整個驅動都是圍繞著該結構體的初始化,驅動中將函式指標都初始化好,實際的工作是由中斷訊號觸發,也就是在中斷來臨的時候去進行回撥; - `struct irq_chip`結構,描述的是中斷控制器的底層操作函式集,這些函式集最終完成對控制器硬體的操作; - `struct irq_domain`結構,用於硬體中斷號和Linux IRQ中斷號(virq,虛擬中斷號)之間的對映; 還是上一下具體的資料結構程式碼吧,關鍵註釋如下: ```c 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`),來張圖: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111647851-1005315068.png) - 每個中斷控制器都對應一個IRQ Domain; - 中斷控制器驅動通過`irq_domain_add_*()`介面來建立IRQ Domain; - IRQ Domain支援三種對映方式:linear map(線性對映),tree map(樹對映),no map(不對映); 1. linear map:維護固定大小的表,索引是硬體中斷號,如果硬體中斷最大數量固定,並且數值不大,可以選擇線性對映; 2. tree map:硬體中斷號可能很大,可以選擇樹對映; 3. no map:硬體中斷號直接就是Linux的中斷號; 三種對映的方式如下圖: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111718514-879227841.png) - 圖中描述了三個中斷控制器,對應到三種不同的對映方式; - 各個控制器的硬體中斷號可以一樣,最終在Linux核心中對映的中斷號是唯一的; # 4. Arch-speicific程式碼分析 - 中斷也是異常模式的一種,當外設觸發中斷時,處理器會切換到特定的異常模式進行處理,而這部分程式碼都是架構相關的;ARM64的程式碼位於`arch/arm64/kernel/entry.S`。 - ARM64處理器有四個異常級別Exception Level:0~3,EL0級對應使用者態程式,EL1級對應作業系統核心態,EL2級對應Hypervisor,EL3級對應Secure Monitor; - 異常觸發時,處理器進行切換,並且跳轉到異常向量表開始執行,針對中斷異常,最終會跳轉到`irq_handler`中; 程式碼比較簡單,如下: ```c /* * Interrupt handling. */ .macro irq_handler ldr_l x1, handle_arch_irq mov x0, sp irq_stack_entry blr x1 irq_stack_exit .endm ``` 來張圖: ![](https://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111755704-1231972965.png) - 中斷觸發,處理器去異常向量表找到對應的入口,比如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://img2020.cnblogs.com/blog/1771657/202005/1771657-20200531111839327-10947968