這個中斷系列文章主要針對行動裝置中的Linux進行討論,文中的例子基本都是基於ARM這一體系架構,其他架構的原理其實也差不多,區別只是其中的硬體抽象層。核心版本基於3.3。雖然核心的版本不斷地提升,不過自從上一次變更到當前的通用中斷子系統後,大的架構性的東西並沒有太大的改變。
/*****************************************************************************************************/
聲明:本博內容均由http://blog.csdn.net/droidphone原創,轉載請註明出處,謝謝!
/*****************************************************************************************************/
1. 裝置、中斷控制器和CPU
一個完整的裝置中,與中斷相關的硬體可以劃分為3類,它們分別是:裝置、中斷控制器和CPU本身,展示了一個smp系統中的中斷硬體的組成結構:
圖 1.1 中斷系統的硬體組成
裝置 裝置是發起中斷的源,當裝置需要請求某種服務的時候,它會發起一個硬體中斷訊號,通常,該訊號會串連至中斷控制器,由中斷控制器做進一步的處理。在現代的行動裝置中,發起中斷的裝置可以位於soc(system-on-chip)晶片的外部,也可以位於soc的內部,因為目前大多數soc都整合了大量的硬體IP,例如I2C、SPI、Display Controller等等。
中斷控制器 中斷控制器負責收集所有中斷源發起的中斷,現有的中斷控制器幾乎都是可程式化的,通過對中斷控制器的編程,我們可以控制每個中斷源的優先順序、中斷的電器類型,還可以開啟和關閉某一個中斷源,在smp系統中,甚至可以控制某個中斷源發往哪一個CPU進行處理。對於ARM架構的soc,使用較多的中斷控制器是VIC(Vector Interrupt Controller),進入多核時代以後,GIC(General Interrupt Controller)的應用也開始逐漸層多。
CPU cpu是最終響應中斷的組件,它通過對可程式化插斷控制器的編程操作,控制和管理者系統中的每個中斷,當中斷控制器最終判定一個中斷可以被處理時,他會根據事先的設定,通知其中一個或者是某幾個cpu對該中斷進行處理,雖然中斷控制器可以同時通知數個cpu對某一個中斷進行處理,實際上,最後只會有一個cpu相應這個插斷要求,但具體是哪個cpu進行響應是可能是隨機的,中斷控制器在硬體上對這一特性進行了保證,不過這也依賴於作業系統對中斷系統的軟體實現。在smp系統中,cpu之間也通過IPI(inter
processor interrupt)中斷進行通訊。
2. IRQ編號
系統中每一個註冊的中斷源,都會分配一個唯一的編號用於識別該中斷,我們稱之為IRQ編號。IRQ編號貫穿在整個Linux的通用中斷子系統中。在行動裝置中,每個中斷源的IRQ編號都會在arch相關的一些標頭檔中,例如arch/xxx/mach-xxx/include/irqs.h。驅動程式在請求中斷服務時,它會使用IRQ編號註冊該中斷,中斷髮生時,cpu通常會從中斷控制器中擷取相關資訊,然後計算出相應的IRQ編號,然後把該IRQ編號傳遞到相應的驅動程式中。
3. 在驅動程式中申請中斷
Linux中斷子系統向驅動程式提供了一系列的API,其中的一個用於向系統申請中斷:
int 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是中斷處理服務函數,該函數工作在中斷上下文中,如果不需要,可以傳入NULL,但是不可以和thread_fn同時為NULL;
- thread_fn是中斷線程的回呼函數,工作在核心進程上下文中,如果不需要,可以傳入NULL,但是不可以和handler同時為NULL;
- irqflags是該中斷的一些標誌,可以指定該中斷的電氣類型,是否共用等資訊;
- devname指定該中斷的名稱;
- dev_id用於共用中斷時的cookie data,通常用於區分共用中斷具體由哪個裝置發起;
關於該API的詳細工作機理我們後面再討論。
4. 通用中斷子系統(Generic irq)的軟體抽象
在通用中斷子系統(generic irq)出現之前,核心使用__do_IRQ處理所有的中斷,這意味著__do_IRQ中要處理各種類型的中斷,這會導致軟體的複雜性增加,層次不分明,而且代碼的可重用性也不好。事實上,到了核心版本2.6.38,__do_IRQ這種方式已經徹底在核心的代碼中消失了。通用中斷子系統的原型最初出現於ARM體系中,一開始核心的開發人員們把3種中斷類型區分出來,他們是:
- 電平觸發中斷(level type)
- 邊緣觸發中斷(edge type)
- 簡易的中斷(simple type)
後來又針對某些需要回應eoi(end of interrupt)的中斷控制器,加入了fast eoi type,針對smp加入了per cpu type。把這些不同的中斷類型抽象出來後,成為了中斷子系統的流控層。要使所有的體系架構都可以重用這部分的代碼,中斷控制器也被進一步地封裝起來,形成了中斷子系統中的硬體封裝層。我們可以用下面的圖示表示通用中斷子系統的階層:
圖 4.1 通用中斷子系統的階層
硬體封裝層 它包含了體系架構相關的所有代碼,包括中斷控制器的抽象封裝,arch相關的中斷初始化,以及各個IRQ的相關資料結構的初始化工作,cpu的中斷入口也會在arch相關的代碼中實現。中斷通用邏輯層通過標準的封裝介面(實際上就是struct irq_chip定義的介面)訪問並控制中斷控制器的行為,體系相關的中斷入口函數在擷取IRQ編號後,通過中斷通用邏輯層提供的標準函數,把中斷調用傳遞到中斷流控層中。我們看看irq_chip的部分定義:
struct 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); ......};
看到上面的結構定義,很明顯,它實際上就是對中斷控制器的介面抽象,我們只要對每個中斷控制器實現以上介面(不必全部),並把它和相應的irq關聯起來,上層的實現即可通過這些介面訪問中斷控制器。而且,同一個中斷控制器的代碼可以方便地被不同的平台所重用。
中斷流控層 所謂中斷流控是指合理並正確地處理連續發生的中斷,比如一個中斷在處理中,同一個中斷再次到達時如何處理,何時應該屏蔽中斷,何時開啟中斷,何時回應中斷控制器等一系列的操作。該層實現了與體系和硬體無關的中斷流控處理操作,它針對不同的中斷電氣類型(level,edge......),實現了對應的標準中斷流控處理函數,在這些處理函數中,最終會把中斷控制權傳遞到驅動程式註冊中斷時傳入的處理函數或者是中斷線程中。目前核心提供了以下幾個主要的中斷流控函數的實現(只列出部分):
- handle_simple_irq();
- handle_level_irq(); 電平中斷流控處理常式
- handle_edge_irq(); 邊沿觸發中斷流控處理常式
- handle_fasteoi_irq(); 需要eoi的中斷處理器使用的中斷流控處理常式
- handle_percpu_irq(); 該irq只有單個cpu響應時使用的流控處理常式
中斷通用邏輯層 該層實現了對中斷系統幾個重要資料的管理,並提供了一系列的輔助管理函數。同時,該層還實現了中斷線程的實現和管理,共用中斷和嵌套中斷的實現和管理,另外它還提供了一些介面函數,它們將作為硬體封裝層和中斷流控層以及驅動程式API層之間的橋樑,例如以下API:
- generic_handle_irq();
- irq_to_desc();
- irq_set_chip();
- irq_set_chained_handler();
驅動程式API 該部分向驅動程式提供了一系列的API,用於向系統申請/釋放中斷,開啟/關閉中斷,設定中斷類型和中斷喚醒系統的特性等操作。驅動程式的開發人員通常只會使用到這一層提供的這些API即可完成驅動程式的開發工作,其他的細節都由另外幾個軟體層較好地“隱藏”起來了,驅動程式開發人員無需再關注底層的實現,這看起來確實是一件美妙的事情,不過我認為,要想寫出好的中斷代碼,還是花點時間瞭解一下其他幾層的實現吧。其中的一些API如下:
- enable_irq();
- disable_irq();
- disable_irq_nosync();
- request_threaded_irq();
- irq_set_affinity();
這裡不再對每一層做詳細的介紹,我將會在本系列的其他幾篇文章中做深入的探討。
5. irq描述結構:struct irq_desc
整個通用中斷子系統幾乎都是圍繞著irq_desc結構進行,系統中每一個irq都對應著一個irq_desc結構,所有的irq_desc結構的組織方式有兩種:
基於數組方式 平台相關板級代碼事先根據系統中的IRQ數量,定義常量:NR_IRQS,在kernel/irq/irqdesc.c中使用該常量定義irq_desc結構數組:
struct 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),}};
基於基數樹方式 當核心的配置項CONFIG_SPARSE_IRQ被選中時,核心使用基數樹(radix tree)來管理irq_desc結構,這一方式可以動態地分配irq_desc結構,對於那些具備大量IRQ數量或者IRQ編號不連續的系統,使用該方式管理irq_desc對記憶體的節省有好處,而且對那些內建中斷控制器管理裝置自身多個中斷源的外部裝置,它們可以在驅動程式中動態地申請這些中斷源所對應的irq_desc結構,而不必在系統的編譯階段保留irq_desc結構所需的記憶體。
下面我們看一看irq_desc的部分定義:
struct irq_data {unsigned intirq;unsigned longhwirq;unsigned intnode;unsigned intstate_use_accessors;struct irq_chip*chip;struct irq_domain*domain;void*handler_data;void*chip_data;struct msi_desc*msi_desc;#ifdef CONFIG_SMPcpumask_var_taffinity;#endif};
struct irq_desc {struct irq_datairq_data;unsigned int __percpu*kstat_irqs;irq_flow_handler_thandle_irq;#ifdef CONFIG_IRQ_PREFLOW_FASTEOIirq_preflow_handler_tpreflow_handler;#endifstruct irqaction*action;/* IRQ action list */unsigned intstatus_use_accessors;unsigned intdepth;/* nested irq disables */unsigned intwake_depth;/* nested wake enables */unsigned intirq_count;/* For detecting broken IRQs */raw_spinlock_tlock;struct cpumask*percpu_enabled;#ifdef CONFIG_SMPconst struct cpumask*affinity_hint;struct irq_affinity_notify *affinity_notify;#ifdef CONFIG_GENERIC_PENDING_IRQcpumask_var_tpending_mask;#endif#endifwait_queue_head_t wait_for_threads;const char*name;} ____cacheline_internodealigned_in_smp;
對於irq_desc中的主要欄位做一個解釋:
irq_data 這個內嵌結構在2.6.37版本引入,之前的核心版本的做法是直接把這個結構中的欄位直接放置在irq_desc結構體中,然後在調用硬體封裝層的chip->xxx()回調中傳入IRQ編號作為參數,但是底層的函數經常需要訪問->handler_data,->chip_data,->msi_desc等欄位,這需要利用irq_to_desc(irq)來獲得irq_desc結構的指標,然後才能訪問上述欄位,者帶來了效能的降低,尤其在配置為sparse
irq的系統中更是如此,因為這意味著基數樹的搜尋操作。為瞭解決這一問題,核心開發人員把幾個低層函數需要使用的欄位單獨封裝為一個結構,調用時的參數則改為傳入該結構的指標。實現同樣的目的,那為什麼不直接傳入irq_desc結構指標?因為這會破壞層次的封裝性,我們不希望低層代碼可以看到不應該看到的部分,僅此而已。
kstat_irqs 用於irq的一些統計資訊,這些統計資訊可以從proc檔案系統中查詢。
action 中斷響應鏈表,當一個irq被觸發時,核心會遍曆該鏈表,調用action結構中的回調handler或者啟用其中的中斷線程,之所以實現為一個鏈表,是為了實現中斷的共用,多個裝置共用同一個irq,這在外圍裝置中是普遍存在的。
status_use_accessors 記錄該irq的狀態資訊,核心提供了一系列irq_settings_xxx的輔助函數訪問該欄位,詳細請查看kernel/irq/settings.h
depth 用於管理enable_irq()/disable_irq()這兩個API的嵌套深度管理,每次enable_irq時該值減去1,每次disable_irq時該值加1,只有depth==0時才真正向硬體封裝層發出關閉irq的調用,只有depth==1時才會向硬體封裝層發出開啟irq的調用。disable的嵌套次數可以比enable的次數多,此時depth的值大於1,隨著enable的不斷調用,當depth的值為1時,在向硬體封裝層發出開啟irq的調用後,depth減去1後,此時depth為0,此時處於一個平衡狀態,我們只能調用disable_irq,如果此時enable_irq被調用,核心會報告一個irq失衡的警告,提醒驅動程式的開發人員檢查自己的代碼。
lock 用於保護irq_desc結構本身的自旋鎖。
affinity_hit 用於提示使用者空間,作為最佳化irq和cpu之間的親緣關係的依據。
pending_mask 用於調整irq在各個cpu之間的平衡。
wait_for_threads 用於synchronize_irq(),等待該irq所有線程完成。
irq_data結構中的各欄位:
irq 該結構所對應的IRQ編號。
hwirq 硬體irq編號,它不同於上面的irq;
node 通常用於hwirq和irq之間的映射操作;
state_use_accessors 硬體封裝層需要使用的狀態資訊,不要直接存取該欄位,核心定義了一組函數用於訪問該欄位:irqd_xxxx(),參見include/linux/irq.h。
chip 指向該irq所屬的中斷控制器的irq_chip結構指標
handler_data 每個irq的私人資料指標,該欄位由硬體封轉層使用,例如用作底層硬體的多工中斷。
chip_data 中斷控制器的私人資料,該欄位由硬體封轉層使用。
msi_desc 用於PCIe匯流排的MSI或MSI-X中斷機制。
affinity 記錄該irq與cpu之間的親緣關係,它其實是一個bit-mask,每一個bit代表一個cpu,置位後代表該cpu可能處理該irq。
這是通用中斷子系統系列文章的第一篇,這裡不會詳細介紹各個軟體層次的實現原理,但是有必要對整個架構做簡要的介紹:
- 系統啟動階段,取決於核心的配置,核心會通過數組或基數樹分配好足夠多的irq_desc結構;
- 根據不同的體繫結構,初始化中斷相關的硬體,尤其是中斷控制器;
- 為每個必要irq的irq_desc結構填充預設的欄位,例如irq編號,irq_chip指標,根據不同的中斷類型配置流控handler;
- 裝置驅動程式在初始化階段,利用request_threaded_irq() api申請中斷服務,兩個重要的參數是handler和thread_fn;
- 當裝置觸發一個中斷後,cpu會進入事先設定好的中斷入口,它屬於底層體系相關的代碼,它通過中斷控制器獲得irq編號,在對irq_data結構中的某些欄位進行處理後,會將控制權傳遞到中斷流控層(通過irq_desc->handle_irq);
- 中斷流控處理代碼在作出必要的流控處理後,通過irq_desc->action鏈表,取出驅動程式申請中斷時註冊的handler和thread_fn,根據它們的賦值情況,或者只是調用handler回調,或者啟動一個線程執行thread_fn,又或者兩者都執行;
- 至此,中斷最終由驅動程式進行了響應和處理。
6. 中斷子系統的proc檔案介面
在/proc目錄下面,有兩個與中斷子系統相關的檔案和子目錄,它們是:
- /proc/interrupts:檔案
- /proc/irq:子目錄
讀取interrupts會依次顯示irq編號,每個cpu對該irq的處理次數,中斷控制器的名字,irq的名字,以及驅動程式註冊該irq時使用的名字,以下是一個例子:
/proc/irq目錄下面會為每個註冊的irq建立一個以irq編號為名字的子目錄,每個子目錄下分別有以下條目:
- smp_affinity irq和cpu之間的親緣綁定關係;
- smp_affinity_hint 唯讀條目,用於使用者空間做irq平衡只用;
- spurious 可以獲得該irq被處理和未被處理的次數的統計資訊;
- handler_name 驅動程式註冊該irq時傳入的處理常式的名字;
根據irq的不同,以上條目不一定會全部都出現,以下是某個裝置的例子:
# 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
3
可見,以上裝置是一個使用雙核cpu的裝置,因為smp_affinity的值是3,系統預設每個中斷可以由兩個cpu進行處理。
本章內容結束。接下來的計劃:
Linux中斷(interrupt)子系統之二:arch相關的硬體封裝層
Linux中斷(interrupt)子系統之三:中斷流控處理層
Linux中斷(interrupt)子系統之四:驅動程式介面層
Linux中斷(interrupt)子系統之五:軟體中斷(softirq)