Read the fucking source code!
--By 魯迅A picture is worth a thousand words.
--By 高爾基說明:node
從這篇文章開始,來聊一聊中斷子系統。
中斷是處理器用於異步處理外圍設備請求的一種機制,能夠說中斷處理是操做系統管理外圍設備的基石,此外系統調度、核間交互等都離不開中斷,它的重要性不言而喻。linux
來一張概要的分層圖:安全
中斷子系統系列文章,會包括硬件相關、中斷框架層、上半部與下半部、Softirq、Workqueue等機制的介紹,本文會先介紹硬件相關的原理及驅動,前戲結束,直奔主題。數據結構
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
的功能:app
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
功能:dom
Distributor
分發到CPU Interface
;SGI
中斷分發到目標CPU上;CPU Interface
功能:異步
中斷處理的狀態機以下圖:函數
Inactive
:無中斷狀態;Pending
:硬件或軟件觸發了中斷,但還沒有傳遞到目標CPU,在電平觸發模式下,產生中斷的同時保持pending
狀態;Active
:發生了中斷並將其傳遞給目標CPU,而且目標CPU能夠處理該中斷;Active and pending
:發生了中斷並將其傳遞給目標CPU,同時發生了相同的中斷而且該中斷正在等待處理;GIC檢測中斷流程以下:
Distributor
肯定好目標CPU後,將中斷信號發送到目標CPU上,同時,對於每一個CPU,Distributor
會從pending信號中選擇最高優先級中斷髮送至CPU Interface
;CPU Interface
來決定是否將中斷信號發送至目標CPU;EOI(End of Interrupt)
給GIC;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
字段進行匹配;GIC驅動的執行流程以下圖所示:
vmlinux.lds
,腳本中定義了一個__irqchip_of_table
段,該段用於存放中斷控制器信息,用於最終來匹配設備;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驅動的初始化入口函數了;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
等結構體的初始化,這些結構在下文會進一步分析;先來張圖:
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 };
IRQ domain用於將硬件的中斷號,轉換成Linux系統中的中斷號(virtual irq, virq
),來張圖:
irq_domain_add_*()
接口來建立IRQ Domain;三種映射的方式以下圖:
arch/arm64/kernel/entry.S
。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_irq
處,EL1則跳轉到el1_irq
處;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內核機制相關文章,謝謝。