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

背景

  • Read the fucking source code! --By 魯迅
  • A picture is worth a thousand words. --By 高爾基

說明:node

  1. Kernel版本:4.14
  2. ARM64處理器,Contex-A53,雙核
  3. 使用工具:Source Insight 3.5, Visio

1. 概述

從這篇文章開始,來聊一聊中斷子系統。
中斷是處理器用於異步處理外圍設備請求的一種機制,能夠說中斷處理是操做系統管理外圍設備的基石,此外系統調度、核間交互等都離不開中斷,它的重要性不言而喻。linux

來一張概要的分層圖:安全

  • 硬件層:最下層爲硬件鏈接層,對應的是具體的外設與SoC的物理鏈接,中斷信號是從外設到中斷控制器,由中斷控制器統一管理,再路由處處理器上;
  • 硬件相關層:這個層包括兩部分代碼,一部分是架構相關的,好比ARM64處理器處理中斷相關,另外一部分是中斷控制器的驅動代碼;
  • 通用層:這部分也能夠認爲是框架層,是硬件無關層,這部分代碼在全部硬件平臺上是通用的;
  • 用戶層:這部分也就是中斷的使用者了,主要是各種設備驅動,經過中斷相關接口來進行申請和註冊,最終在外設觸發中斷時,進行相應的回調處理;

中斷子系統系列文章,會包括硬件相關、中斷框架層、上半部與下半部、Softirq、Workqueue等機制的介紹,本文會先介紹硬件相關的原理及驅動,前戲結束,直奔主題。數據結構

2. GIC硬件原理

  • ARM公司提供了一個通用的中斷控制器GIC(Generic Interrupt Controller)GIC的版本包括V1 ~ V4,因爲本人使用的SoC中的中斷控制器是V2版本,本文將圍繞GIC-V2來展開介紹;

來一張功能版的框圖:架構

  • GIC-V2從功能上說,除了經常使用的中斷使能、中斷屏蔽、優先級管理等功能外,還支持安全擴展、虛擬化等;
  • GIC-V2從組成上說,主要分爲DistributorCPU Interface兩個模塊,Distributor主要負責中斷源的管理,包括優先級的處理,屏蔽、搶佔等,並將最高優先級的中斷分發給CPU InterfaceCPU Interface主要用於鏈接處理器,與處理器進行交互;
  • Virtual DistributorVirtual CPU Interface都與虛擬化相關,本文不深刻分析;

再來一張細節圖看看DistributorCPU Interface的功能:app

  • 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用於SPIID1020 - ID1023保留用於特殊用途;
  • Distributor功能:dom

    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中斷;

中斷處理的狀態機以下圖:函數

  • 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/

下圖就是一箇中斷控制器的設備樹信息:

  • 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(不映射);
    1. linear map:維護固定大小的表,索引是硬件中斷號,若是硬件中斷最大數量固定,而且數值不大,能夠選擇線性映射;
    2. tree map:硬件中斷號可能很大,能夠選擇樹映射;
    3. 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內核機制相關文章,謝謝。

相關文章
相關標籤/搜索