Linux中斷(interrupt)子系統之一:中斷系統基本原理
這個(gè)中斷系列文章主要針對移動(dòng)設備中的Linux進(jìn)行討論,文中的例子基本都是基于A(yíng)RM這一體系架構,其他架構的原理其實(shí)也差不多,區別只是其中的硬件抽象層。內核版本基于3.3。雖然內核的版本不斷地提升,不過(guò)自從上一次變更到當前的通用中斷子系統后,大的框架性的東西并沒(méi)有太大的改變。
本文引用地址:http://dyxdggzs.com/article/201612/341595.htm1. 設備、中斷控制器和CPU
一個(gè)完整的設備中,與中斷相關(guān)的硬件可以劃分為3類(lèi),它們分別是:設備、中斷控制器和CPU本身,下圖展示了一個(gè)smp系統中的中斷硬件的組成結構:
圖 1.1 中斷系統的硬件組成
設備 設備是發(fā)起中斷的源,當設備需要請求某種服務(wù)的時(shí)候,它會(huì )發(fā)起一個(gè)硬件中斷信號,通常,該信號會(huì )連接至中斷控制器,由中斷控制器做進(jìn)一步的處理。在現代的移動(dòng)設備中,發(fā)起中斷的設備可以位于soc(system-on-chip)芯片的外部,也可以位于soc的內部,因為目前大多數soc都集成了大量的硬件IP,例如I2C、SPI、Display Controller等等。
中斷控制器 中斷控制器負責收集所有中斷源發(fā)起的中斷,現有的中斷控制器幾乎都是可編程的,通過(guò)對中斷控制器的編程,我們可以控制每個(gè)中斷源的優(yōu)先級、中斷的電器類(lèi)型,還可以打開(kāi)和關(guān)閉某一個(gè)中斷源,在smp系統中,甚至可以控制某個(gè)中斷源發(fā)往哪一個(gè)CPU進(jìn)行處理。對于A(yíng)RM架構的soc,使用較多的中斷控制器是VIC(Vector Interrupt Controller),進(jìn)入多核時(shí)代以后,GIC(General Interrupt Controller)的應用也開(kāi)始逐漸變多。
CPU cpu是最終響應中斷的部件,它通過(guò)對可編程中斷控制器的編程操作,控制和管理者系統中的每個(gè)中斷,當中斷控制器最終判定一個(gè)中斷可以被處理時(shí),他會(huì )根據事先的設定,通知其中一個(gè)或者是某幾個(gè)cpu對該中斷進(jìn)行處理,雖然中斷控制器可以同時(shí)通知數個(gè)cpu對某一個(gè)中斷進(jìn)行處理,實(shí)際上,最后只會(huì )有一個(gè)cpu相應這個(gè)中斷請求,但具體是哪個(gè)cpu進(jìn)行響應是可能是隨機的,中斷控制器在硬件上對這一特性進(jìn)行了保證,不過(guò)這也依賴(lài)于操作系統對中斷系統的軟件實(shí)現。在smp系統中,cpu之間也通過(guò)IPI(inter processor interrupt)中斷進(jìn)行通信。
2. IRQ編號
系統中每一個(gè)注冊的中斷源,都會(huì )分配一個(gè)唯一的編號用于識別該中斷,我們稱(chēng)之為IRQ編號。IRQ編號貫穿在整個(gè)Linux的通用中斷子系統中。在移動(dòng)設備中,每個(gè)中斷源的IRQ編號都會(huì )在arch相關(guān)的一些頭文件中,例如arch/xxx/mach-xxx/include/irqs.h。驅動(dòng)程序在請求中斷服務(wù)時(shí),它會(huì )使用IRQ編號注冊該中斷,中斷發(fā)生時(shí),cpu通常會(huì )從中斷控制器中獲取相關(guān)信息,然后計算出相應的IRQ編號,然后把該IRQ編號傳遞到相應的驅動(dòng)程序中。
3. 在驅動(dòng)程序中申請中斷
Linux中斷子系統向驅動(dòng)程序提供了一系列的API,其中的一個(gè)用于向系統申請中斷:
[cpp] view plain copyint request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
其中,
irq是要申請的IRQ編號,
handler是中斷處理服務(wù)函數,該函數工作在中斷上下文中,如果不需要,可以傳入NULL,但是不可以和thread_fn同時(shí)為NULL;
thread_fn是中斷線(xiàn)程的回調函數,工作在內核進(jìn)程上下文中,如果不需要,可以傳入NULL,但是不可以和handler同時(shí)為NULL;
irqflags是該中斷的一些標志,可以指定該中斷的電氣類(lèi)型,是否共享等信息;
devname指定該中斷的名稱(chēng);
dev_id用于共享中斷時(shí)的cookie data,通常用于區分共享中斷具體由哪個(gè)設備發(fā)起;
關(guān)于該API的詳細工作機理我們后面再討論。
4. 通用中斷子系統(Generic irq)的軟件抽象
在通用中斷子系統(generic irq)出現之前,內核使用__do_IRQ處理所有的中斷,這意味著(zhù)__do_IRQ中要處理各種類(lèi)型的中斷,這會(huì )導致軟件的復雜性增加,層次不分明,而且代碼的可重用性也不好。事實(shí)上,到了內核版本2.6.38,__do_IRQ這種方式已經(jīng)徹底在內核的代碼中消失了。通用中斷子系統的原型最初出現于A(yíng)RM體系中,一開(kāi)始內核的開(kāi)發(fā)者們把3種中斷類(lèi)型區分出來(lái),他們是:
電平觸發(fā)中斷(level type)
邊緣觸發(fā)中斷(edge type)
簡(jiǎn)易的中斷(simple type)
后來(lái)又針對某些需要回應eoi(end of interrupt)的中斷控制器,加入了fast eoi type,針對smp加入了per cpu type。把這些不同的中斷類(lèi)型抽象出來(lái)后,成為了中斷子系統的流控層。要使所有的體系架構都可以重用這部分的代碼,中斷控制器也被進(jìn)一步地封裝起來(lái),形成了中斷子系統中的硬件封裝層。我們可以用下面的圖示表示通用中斷子系統的層次結構:
圖 4.1 通用中斷子系統的層次結構
硬件封裝層 它包含了體系架構相關(guān)的所有代碼,包括中斷控制器的抽象封裝,arch相關(guān)的中斷初始化,以及各個(gè)IRQ的相關(guān)數據結構的初始化工作,cpu的中斷入口也會(huì )在arch相關(guān)的代碼中實(shí)現。中斷通用邏輯層通過(guò)標準的封裝接口(實(shí)際上就是struct irq_chip定義的接口)訪(fǎng)問(wèn)并控制中斷控制器的行為,體系相關(guān)的中斷入口函數在獲取IRQ編號后,通過(guò)中斷通用邏輯層提供的標準函數,把中斷調用傳遞到中斷流控層中。我們看看irq_chip的部分定義:
[cpp] view plain copystruct irq_chip {
const char *name;
unsigned int (*irq_startup)(struct irq_data *data);
void (*irq_shutdown)(struct irq_data *data);
void (*irq_enable)(struct irq_data *data);
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);
int (*irq_retrigger)(struct irq_data *data);
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type);
int (*irq_set_wake)(struct irq_data *data, unsigned int on);
......
};
看到上面的結構定義,很明顯,它實(shí)際上就是對中斷控制器的接口抽象,我們只要對每個(gè)中斷控制器實(shí)現以上接口(不必全部),并把它和相應的irq關(guān)聯(lián)起來(lái),上層的實(shí)現即可通過(guò)這些接口訪(fǎng)問(wèn)中斷控制器。而且,同一個(gè)中斷控制器的代碼可以方便地被不同的平臺所重用。
中斷流控層 所謂中斷流控是指合理并正確地處理連續發(fā)生的中斷,比如一個(gè)中斷在處理中,同一個(gè)中斷再次到達時(shí)如何處理,何時(shí)應該屏蔽中斷,何時(shí)打開(kāi)中斷,何時(shí)回應中斷控制器等一系列的操作。該層實(shí)現了與體系和硬件無(wú)關(guān)的中斷流控處理操作,它針對不同的中斷電氣類(lèi)型(level,edge......),實(shí)現了對應的標準中斷流控處理函數,在這些處理函數中,最終會(huì )把中斷控制權傳遞到驅動(dòng)程序注冊中斷時(shí)傳入的處理函數或者是中斷線(xiàn)程中。目前內核提供了以下幾個(gè)主要的中斷流控函數的實(shí)現(只列出部分):
handle_simple_irq();
handle_level_irq(); 電平中斷流控處理程序
handle_edge_irq(); 邊沿觸發(fā)中斷流控處理程序
handle_fasteoi_irq(); 需要eoi的中斷處理器使用的中斷流控處理程序
handle_percpu_irq(); 該irq只有單個(gè)cpu響應時(shí)使用的流控處理程序
中斷通用邏輯層 該層實(shí)現了對中斷系統幾個(gè)重要數據的管理,并提供了一系列的輔助管理函數。同時(shí),該層還實(shí)現了中斷線(xiàn)程的實(shí)現和管理,共享中斷和嵌套中斷的實(shí)現和管理,另外它還提供了一些接口函數,它們將作為硬件封裝層和中斷流控層以及驅動(dòng)程序API層之間的橋梁,例如以下API:
generic_handle_irq();
irq_to_desc();
irq_set_chip();
irq_set_chained_handler();
驅動(dòng)程序API 該部分向驅動(dòng)程序提供了一系列的API,用于向系統申請/釋放中斷,打開(kāi)/關(guān)閉中斷,設置中斷類(lèi)型和中斷喚醒系統的特性等操作。驅動(dòng)程序的開(kāi)發(fā)者通常只會(huì )使用到這一層提供的這些API即可完成驅動(dòng)程序的開(kāi)發(fā)工作,其他的細節都由另外幾個(gè)軟件層較好地“隱藏”起來(lái)了,驅動(dòng)程序開(kāi)發(fā)者無(wú)需再關(guān)注底層的實(shí)現,這看起來(lái)確實(shí)是一件美妙的事情,不過(guò)我認為,要想寫(xiě)出好的中斷代碼,還是花點(diǎn)時(shí)間了解一下其他幾層的實(shí)現吧。其中的一些API如下:
enable_irq();
disable_irq();
disable_irq_nosync();
request_threaded_irq();
irq_set_affinity();
這里不再對每一層做詳細的介紹,我將會(huì )在本系列的其他幾篇文章中做深入的探討。
5. irq描述結構:struct irq_desc
整個(gè)通用中斷子系統幾乎都是圍繞著(zhù)irq_desc結構進(jìn)行,系統中每一個(gè)irq都對應著(zhù)一個(gè)irq_desc結構,所有的irq_desc結構的組織方式有兩種:
基于數組方式 平臺相關(guān)板級代碼事先根據系統中的IRQ數量,定義常量:NR_IRQS,在kernel/irq/irqdesc.c中使用該常量定義irq_desc結構數組:
[cpp] view plain copystruct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp = {
[0 ... NR_IRQS-1] = {
.handle_irq = handle_bad_irq,
.depth = 1,
.lock = __RAW_SPIN_LOCK_UNLOCKED(irq_desc->lock),
}
};
基于基數樹(shù)方式 當內核的配置項CONFIG_SPARSE_IRQ被選中時(shí),內核使用基數樹(shù)(radix tree)來(lái)管理irq_desc結構,這一方式可以動(dòng)態(tài)地分配irq_desc結構,對于那些具備大量IRQ數量或者IRQ編號不連續的系統,使用該方式管理irq_desc對內存的節省有好處,而且對那些自帶中斷控制器管理設備自身多個(gè)中斷源的外部設備,它們可以在驅動(dòng)程序中動(dòng)態(tài)地申請這些中斷源所對應的irq_desc結構,而不必在系統的編譯階段保留irq_desc結構所需的內存。
下面我們看一看irq_desc的部分定義:
[cpp] view plain copystruct irq_data {
unsigned int irq;
unsigned long hwirq;
unsigned int node;
unsigned int state_use_accessors;
struct irq_chip *chip;
struct irq_domain *domain;
void *handler_data;
void *chip_data;
struct msi_desc *msi_desc;
#ifdef CONFIG_SMP
cpumask_var_t affinity;
#endif
};
[cpp] view plain copystruct irq_desc {
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs;
irq_flow_handler_t handle_irq;
#ifdef CONFIG_IRQ_PREFLOW_FASTEOI
irq_preflow_handler_t preflow_handler;
#endif
struct irqaction *action; /* IRQ action list */
unsigned int status_use_accessors;
unsigned int depth; /* nested irq disables */
unsigned int wake_depth; /* nested wake enables */
unsigned int irq_count; /* For detecting broken IRQs */
raw_spinlock_t lock;
struct cpumask *percpu_enabled;
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint;
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
wait_queue_head_t wait_for_threads;
const char *name;
} ____cacheline_internodealigned_in_smp;
對于irq_desc中的主要字段做一個(gè)解釋?zhuān)?/p>
irq_data 這個(gè)內嵌結構在2.6.37版本引入,之前的內核版本的做法是直接把這個(gè)結構中的字段直接放置在irq_desc結構體中,然后在調用硬件封裝層的chip->xxx()回調中傳入IRQ編號作為參數,但是底層的函數經(jīng)常需要訪(fǎng)問(wèn)->handler_data,->chip_data,->msi_desc等字段,這需要利用irq_to_desc(irq)來(lái)獲得irq_desc結構的指針,然后才能訪(fǎng)問(wèn)上述字段,者帶來(lái)了性能的降低,尤其在配置為sparse irq的系統中更是如此,因為這意味著(zhù)基數樹(shù)的搜索操作。為了解決這一問(wèn)題,內核開(kāi)發(fā)者把幾個(gè)低層函數需要使用的字段單獨封裝為一個(gè)結構,調用時(shí)的參數則改為傳入該結構的指針。實(shí)現同樣的目的,那為什么不直接傳入irq_desc結構指針?因為這會(huì )破壞層次的封裝性,我們不希望低層代碼可以看到不應該看到的部分,僅此而已。
kstat_irqs 用于irq的一些統計信息,這些統計信息可以從proc文件系統中查詢(xún)。
action 中斷響應鏈表,當一個(gè)irq被觸發(fā)時(shí),內核會(huì )遍歷該鏈表,調用action結構中的回調handler或者激活其中的中斷線(xiàn)程,之所以實(shí)現為一個(gè)鏈表,是為了實(shí)現中斷的共享,多個(gè)設備共享同一個(gè)irq,這在外圍設備中是普遍存在的。
status_use_accessors 記錄該irq的狀態(tài)信息,內核提供了一系列irq_settings_xxx的輔助函數訪(fǎng)問(wèn)該字段,詳細請查看kernel/irq/settings.h
depth 用于管理enable_irq()/disable_irq()這兩個(gè)API的嵌套深度管理,每次enable_irq時(shí)該值減去1,每次disable_irq時(shí)該值加1,只有depth==0時(shí)才真正向硬件封裝層發(fā)出關(guān)閉irq的調用,只有depth==1時(shí)才會(huì )向硬件封裝層發(fā)出打開(kāi)irq的調用。disable的嵌套次數可以比enable的次數多,此時(shí)depth的值大于1,隨著(zhù)enable的不斷調用,當depth的值為1時(shí),在向硬件封裝層發(fā)出打開(kāi)irq的調用后,depth減去1后,此時(shí)depth為0,此時(shí)處于一個(gè)平衡狀態(tài),我們只能調用disable_irq,如果此時(shí)enable_irq被調用,內核會(huì )報告一個(gè)irq失衡的警告,提醒驅動(dòng)程序的開(kāi)發(fā)人員檢查自己的代碼。
lock 用于保護irq_desc結構本身的自旋鎖。
affinity_hit 用于提示用戶(hù)空間,作為優(yōu)化irq和cpu之間的親緣關(guān)系的依據。
pending_mask 用于調整irq在各個(gè)cpu之間的平衡。
wait_for_threads 用于synchronize_irq(),等待該irq所有線(xiàn)程完成。
irq_data結構中的各字段:
irq 該結構所對應的IRQ編號。
hwirq 硬件irq編號,它不同于上面的irq;
node 通常用于hwirq和irq之間的映射操作;
state_use_accessors 硬件封裝層需要使用的狀態(tài)信息,不要直接訪(fǎng)問(wèn)該字段,內核定義了一組函數用于訪(fǎng)問(wèn)該字段:irqd_xxxx(),參見(jiàn)include/linux/irq.h。
chip 指向該irq所屬的中斷控制器的irq_chip結構指針
handler_data 每個(gè)irq的私有數據指針,該字段由硬件封轉層使用,例如用作底層硬件的多路復用中斷。
chip_data 中斷控制器的私有數據,該字段由硬件封轉層使用。
msi_desc 用于PCIe總線(xiàn)的MSI或MSI-X中斷機制。
affinity 記錄該irq與cpu之間的親緣關(guān)系,它其實(shí)是一個(gè)bit-mask,每一個(gè)bit代表一個(gè)cpu,置位后代表該cpu可能處理該irq。
這是通用中斷子系統系列文章的第一篇,這里不會(huì )詳細介紹各個(gè)軟件層次的實(shí)現原理,但是有必要對整個(gè)架構做簡(jiǎn)要的介紹:
系統啟動(dòng)階段,取決于內核的配置,內核會(huì )通過(guò)數組或基數樹(shù)分配好足夠多的irq_desc結構;
根據不同的體系結構,初始化中斷相關(guān)的硬件,尤其是中斷控制器;
為每個(gè)必要irq的irq_desc結構填充默認的字段,例如irq編號,irq_chip指針,根據不同的中斷類(lèi)型配置流控handler;
設備驅動(dòng)程序在初始化階段,利用request_threaded_irq() api申請中斷服務(wù),兩個(gè)重要的參數是handler和thread_fn;
當設備觸發(fā)一個(gè)中斷后,cpu會(huì )進(jìn)入事先設定好的中斷入口,它屬于底層體系相關(guān)的代碼,它通過(guò)中斷控制器獲得irq編號,在對irq_data結構中的某些字段進(jìn)行處理后,會(huì )將控制權傳遞到中斷流控層(通過(guò)irq_desc->handle_irq);
中斷流控處理代碼在作出必要的流控處理后,通過(guò)irq_desc->action鏈表,取出驅動(dòng)程序申請中斷時(shí)注冊的handler和thread_fn,根據它們的賦值情況,或者只是調用handler回調,或者啟動(dòng)一個(gè)線(xiàn)程執行thread_fn,又或者兩者都執行;
至此,中斷最終由驅動(dòng)程序進(jìn)行了響應和處理。
6. 中斷子系統的proc文件接口
在/proc目錄下面,有兩個(gè)與中斷子系統相關(guān)的文件和子目錄,它們是:
/proc/interrupts:文件
/proc/irq:子目錄
讀取interrupts會(huì )依次顯示irq編號,每個(gè)cpu對該irq的處理次數,中斷控制器的名字,irq的名字,以及驅動(dòng)程序注冊該irq時(shí)使用的名字,以下是一個(gè)例子:

/proc/irq目錄下面會(huì )為每個(gè)注冊的irq創(chuàng )建一個(gè)以irq編號為名字的子目錄,每個(gè)子目錄下分別有以下條目:
smp_affinity irq和cpu之間的親緣綁定關(guān)系;
smp_affinity_hint 只讀條目,用于用戶(hù)空間做irq平衡只用;
spurious 可以獲得該irq被處理和未被處理的次數的統計信息;
handler_name 驅動(dòng)程序注冊該irq時(shí)傳入的處理程序的名字;
根據irq的不同,以上條目不一定會(huì )全部都出現,以下是某個(gè)設備的例子:
# cd /proc/irq
# ls
ls
332
248
......
......
12
11
default_smp_affinity
# ls 332
bcmsdh_sdmmc
spurious
node
affinity_hint
smp_affinity
# cat 332/smp_affinity
可見(jiàn),以上設備是一個(gè)使用雙核cpu的設備,因為smp_affinity的值是3,系統默認每個(gè)中斷可以由兩個(gè)cpu進(jìn)行處理。
本章內容結束。接下來(lái)的計劃:
Linux中斷(interrupt)子系統之二:arch相關(guān)的硬件封裝層
Linux中斷(interrupt)子系統之三:中斷流控處理層
Linux中斷(interrupt)子系統之四:驅動(dòng)程序接口層
Linux中斷(interrupt)子系統之五:軟件中斷(softirq)
評論