Linux的通用中斷子系統的一個設計原則就是把底層的硬件實現盡可能地隱藏起來,使得驅動程序的開發人員不用關注底層的實現,要實現這個目標,內核的開發者們必須把硬件相關的內容剝離出來,然后定義一些列標準的接口供上層訪問,上層的開發人員只要知道這些接口即可完成對中斷的進一步處理和控制。對底層的封裝主要包括兩部分:
實現不同體系結構中斷入口,這部分代碼通常用asm實現;
中斷控制器進行封裝和實現;
本文的內容正是要討論硬件封裝層的實現細節。我將以ARM體系進行介紹,大部分的代碼位于內核代碼樹的arch/arm/目錄內。
1.? CPU的中斷入口
我們知道,arm的異常和復位向量表有兩種選擇,一種是低端向量,向量地址位于0x00000000,另一種是高端向量,向量地址位于0xffff0000,Linux選擇使用高端向量模式,也就是說,當異常發生時,CPU會把PC指針自動跳轉到始于0xffff0000開始的某一個地址上:
ARM的異常向量表地址異常種類FFFF0000復位FFFF0004未定義指令FFFF0008軟中斷(swi)FFFF000CPrefetch abortFFFF0010Data abortFFFF0014保留FFFF0018IRQFFFF001CFIQ
中斷向量表在arch/arm/kernel/entry_armv.S中定義,為了方便討論,下面只列出部分關鍵的代碼:
[plain]?view plain?copy
.globl??__stubs_start??
__stubs_start:??
vector_stub?irq,?IRQ_MODE,?4??
.long???__irq_usr???????????@??0??(USR_26?/?USR_32)??
.long???__irq_invalid???????????@??1??(FIQ_26?/?FIQ_32)??
.long???__irq_invalid???????????@??2??(IRQ_26?/?IRQ_32)??
.long???__irq_svc???????????@??3??(SVC_26?/?SVC_32)??
vector_stub?dabt,?ABT_MODE,?8??
.long???__dabt_usr??????????@??0??(USR_26?/?USR_32)??
.long???__dabt_invalid??????????@??1??(FIQ_26?/?FIQ_32)??
.long???__dabt_invalid??????????@??2??(IRQ_26?/?IRQ_32)??
.long???__dabt_svc??????????@??3??(SVC_26?/?SVC_32)??
vector_fiq:??
disable_fiq??
subs????pc,?lr,?#4??
......??
.globl??__stubs_end??
__stubs_end:??
.equ????stubs_offset,?__vectors_start?+?0x200?-?__stubs_start??
.globl??__vectors_start??
__vectors_start:??
ARM(???swi?SYS_ERROR0??)??
THUMB(?svc?#0??????)??
THUMB(?nop?????????)??
W(b)????vector_und?+?stubs_offset??
W(ldr)??pc,?.LCvswi?+?stubs_offset??
W(b)????vector_pabt?+?stubs_offset??
W(b)????vector_dabt?+?stubs_offset??
W(b)????vector_addrexcptn?+?stubs_offset??
W(b)????vector_irq?+?stubs_offset??
W(b)????vector_fiq?+?stubs_offset??
.globl??__vectors_end??
__vectors_end:??
代碼被分為兩部分:
第一部分是真正的向量跳轉表,位于__vectors_start和__vectors_end之間;
第二部分是處理跳轉的部分,位于__stubs_start和__stubs_end之間;
[plain]?view plain?copy
vector_stub?irq,?IRQ_MODE,?4??
以上這一句把宏展開后實際上就是定義了vector_irq,根據進入中斷前的cpu模式,分別跳轉到__irq_usr或__irq_svc。
[plain]?view plain?copy
vector_stub?dabt,?ABT_MODE,?8??
以上這一句把宏展開后實際上就是定義了vector_dabt,根據進入中斷前的cpu模式,分別跳轉到__dabt_usr或__dabt_svc。
系統啟動階段,位于arch/arm/kernel/traps.c中的early_trap_init()被調用:
[cpp]?view plain?copy
void?__init?early_trap_init(void)??
{??
......??
/*?
*?Copy?the?vectors,?stubs?and?kuser?helpers?(in?entry-armv.S)?
*?into?the?vector?page,?mapped?at?0xffff0000,?and?ensure?these?
*?are?visible?to?the?instruction?stream.?
*/??
memcpy((void?*)vectors,?__vectors_start,?__vectors_end?-?__vectors_start);??
memcpy((void?*)vectors?+?0x200,?__stubs_start,?__stubs_end?-?__stubs_start);??
......??
}??
以上兩個memcpy會把__vectors_start開始的代碼拷貝到0xffff0000處,把__stubs_start開始的代碼拷貝到0xFFFF0000+0x200處,這樣,異常中斷到來時,CPU就可以正確地跳轉到相應中斷向量入口并執行他們。
圖1.1 ?Linux中ARM體系的中斷向量拷貝過程
對于系統的外部設備來說,通常都是使用IRQ中斷,所以我們只關注__irq_usr和__irq_svc,兩者的區別是進入和退出中斷時是否進行用戶棧和內核棧之間的切換,還有進程調度和搶占的處理等,這些細節不在這里討論。兩個函數最終都會進入irq_handler這個宏:
[plain]?view plain?copy
.macro??irq_handler??
#ifdef?CONFIG_MULTI_IRQ_HANDLER??
ldr?r1,?=handle_arch_irq??
mov?r0,?sp??
adr?lr,?BSYM(9997f)??
ldr?pc,?[r1]??
#else??
arch_irq_handler_default??
#endif??
9997:??
.endm??
如果選擇了MULTI_IRQ_HANDLER配置項,則意味著允許平臺的代碼可以動態設置irq處理程序,平臺代碼可以修改全局變量:handle_arch_irq,從而可以修改irq的處理程序。這里我們討論默認的實現:arch_irq_handler_default,它位于arch/arm/include/asm/entry_macro_multi.S中:
[plain]?view plain?copy
.macro??arch_irq_handler_default??
get_irqnr_preamble?r6,?lr??
1:??get_irqnr_and_base?r0,?r2,?r6,?lr??
movne???r1,?sp??
@??
@?routine?called?with?r0?=?irq?number,?r1?=?struct?pt_regs?*??
@??
adrne???lr,?BSYM(1b)??
bne?asm_do_IRQ??
......??
get_irqnr_preamble和get_irqnr_and_base兩個宏由machine級的代碼定義,目的就是從中斷控制器中獲得IRQ編號,緊接著就調用asm_do_IRQ,從這個函數開始,中斷程序進入C代碼中,傳入的參數是IRQ編號和寄存器結構指針,這個函數在arch/arm/kernel/irq.c中實現:
[cpp]?view plain?copy
/*?
*?asm_do_IRQ?is?the?interface?to?be?used?from?assembly?code.?
*/??
asmlinkage?void?__exception_irq_entry??
asm_do_IRQ(unsigned?int?irq,?struct?pt_regs?*regs)??
{??
handle_IRQ(irq,?regs);??
}??
到這里,中斷程序完成了從asm代碼到C代碼的傳遞,并且獲得了引起中斷的IRQ編號。
2.? 初始化
與通用中斷子系統相關的初始化由start_kernel()函數發起,調用流程如下圖所視:
圖2.1 ?通用中斷子系統的初始化
首先,在setup_arch函數中,early_trap_init被調用,其中完成了第1節所說的中斷向量的拷貝和重定位工作。
然后,start_kernel發出early_irq_init調用,early_irq_init屬于與硬件和平臺無關的通用邏輯層,它完成irq_desc結構的內存申請,為它們其中某些字段填充默認值,完成后調用體系相關的arch_early_irq_init函數完成進一步的初始化工作,不過ARM體系沒有實現arch_early_irq_init。
接著,start_kernel發出init_IRQ調用,它會直接調用所屬板子machine_desc結構體中的init_irq回調。machine_desc通常在板子的特定代碼中,使用MACHINE_START和MACHINE_END宏進行定義。
machine_desc->init_irq()完成對中斷控制器的初始化,為每個irq_desc結構安裝合適的流控handler,為每個irq_desc結構安裝irq_chip指針,使他指向正確的中斷控制器所對應的irq_chip結構的實例,同時,如果該平臺中的中斷線有多路復用(多個中斷公用一個irq中斷線)的情況,還應該初始化irq_desc中相應的字段和標志,以便實現中斷控制器的級聯。
3.? 中斷控制器的軟件抽象:struct irq_chip
正如上一篇文章Linux中斷(interrupt)子系統之一:中斷系統基本原理所述,所有的硬件中斷在到達CPU之前,都要先經過中斷控制器進行匯集,合乎要求的中斷請求才會通知cpu進行處理,中斷控制器主要完成以下這些功能:
對各個irq的優先級進行控制;
向CPU發出中斷請求后,提供某種機制讓CPU獲得實際的中斷源(irq編號);
控制各個irq的電氣觸發條件,例如邊緣觸發或者是電平觸發;
使能(enable)或者屏蔽(mask)某一個irq;
提供嵌套中斷請求的能力;
提供清除中斷請求的機制(ack);
有些控制器還需要CPU在處理完irq后對控制器發出eoi指令(end of interrupt);
在smp系統中,控制各個irq與cpu之間的親緣關系(affinity);
通用中斷子系統把中斷控制器抽象為一個數據結構:struct irq_chip,其中定義了一系列的操作函數,大部分多對應于上面所列的某個功能:
[cpp]?view plain?copy
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);??
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_print_chip)(struct?irq_data?*data,?struct?seq_file?*p);??
unsigned?long???flags;??
/*?Currently?used?only?by?UML,?might?disappear?one?day.*/??
#ifdef?CONFIG_IRQ_RELEASE_METHOD??
void????????(*release)(unsigned?int?irq,?void?*dev_id);??
#endif??
};??
各個字段解釋如下:
name??中斷控制器的名字,會出現在 /proc/interrupts中。
irq_startup? 第一次開啟一個irq時使用。
irq_shutdown? 與irq_starup相對應。
irq_enable? 使能該irq,通常是直接調用irq_unmask()。
irq_disable? 禁止該irq,通常是直接調用irq_mask,嚴格意義上,他倆其實代表不同的意義,disable表示中斷控制器根本就不響應該irq,而mask時,中斷控制器可能響應該irq,只是不通知CPU,這時,該irq處于pending狀態。類似的區別也適用于enable和unmask。
irq_ack? 用于CPU對該irq的回應,通常表示cpu希望要清除該irq的pending狀態,準備接受下一個irq請求。
irq_mask? 屏蔽該irq。
irq_unmask? 取消屏蔽該irq。
irq_mask_ack? 相當于irq_mask + irq_ack。
irq_eoi? 有些中斷控制器需要在cpu處理完該irq后發出eoi信號,該回調就是用于這個目的。
irq_set_affinity? 用于設置該irq和cpu之間的親緣關系,就是通知中斷控制器,該irq發生時,那些cpu有權響應該irq。當然,中斷控制器會在軟件的配合下,最終只會讓一個cpu處理本次請求。
irq_set_type? 設置irq的電氣觸發條件,例如IRQ_TYPE_LEVEL_HIGH或IRQ_TYPE_EDGE_RISING。
irq_set_wake? 通知電源管理子系統,該irq是否可以用作系統的喚醒源。
以上大部分的函數接口的參數都是irq_data結構指針,irq_data結構的由來在上一篇文章已經說過,這里僅貼出它的定義,各字段的意義請參考注釋:
[cpp]?view plain?copy
/**?
*?struct?irq_data?-?per?irq?and?irq?chip?data?passed?down?to?chip?functions?
*?@irq:????????interrupt?number?
*?@hwirq:??????hardware?interrupt?number,?local?to?the?interrupt?domain?
*?@node:???????node?index?useful?for?balancing?
*?@state_use_accessors:?status?information?for?irq?chip?functions.?
*??????????Use?accessor?functions?to?deal?with?it?
*?@chip:???????low?level?interrupt?hardware?access?
*?@domain:?????Interrupt?translation?domain;?responsible?for?mapping?
*??????????between?hwirq?number?and?linux?irq?number.?
*?@handler_data:???per-IRQ?data?for?the?irq_chip?methods?
*?@chip_data:??????platform-specific?per-chip?private?data?for?the?chip?
*??????????methods,?to?allow?shared?chip?implementations?
*?@msi_desc:???????MSI?descriptor?
*?@affinity:???????IRQ?affinity?on?SMP?
*?
*?The?fields?here?need?to?overlay?the?ones?in?irq_desc?until?we?
*?cleaned?up?the?direct?references?and?switched?everything?over?to?
*?irq_data.?
*/??
struct?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??
};??
根據設備使用的中斷控制器的類型,體系架構的底層的開發只要實現上述接口中的各個回調函數,然后把它們填充到irq_chip結構的實例中,最終把該irq_chip實例注冊到irq_desc.irq_data.chip字段中,這樣各個irq和中斷控制器就進行了關聯,只要知道irq編號,即可得到對應到irq_desc結構,進而可以通過chip指針訪問中斷控制器。?
4.? 進入流控處理層
進入C代碼的第一個函數是asm_do_IRQ,在ARM體系中,這個函數只是簡單地調用handle_IRQ:
[cpp]?view plain?copy
/*?
*?asm_do_IRQ?is?the?interface?to?be?used?from?assembly?code.?
*/??
asmlinkage?void?__exception_irq_entry??
asm_do_IRQ(unsigned?int?irq,?struct?pt_regs?*regs)??
{??
handle_IRQ(irq,?regs);??
}??
handle_IRQ本身也不是很復雜:
[cpp]?view plain?copy
void?handle_IRQ(unsigned?int?irq,?struct?pt_regs?*regs)??
{??
struct?pt_regs?*old_regs?=?set_irq_regs(regs);??
irq_enter();??
/*?
*?Some?hardware?gives?randomly?wrong?interrupts.??Rather?
*?than?crashing,?do?something?sensible.?
*/??
if?(unlikely(irq?>=?nr_irqs))?{??
if?(printk_ratelimit())??
printk(KERN_WARNING?"Bad?IRQ%u ",?irq);??
ack_bad_irq(irq);??
}?else?{??
generic_handle_irq(irq);??
}??
/*?AT91?specific?workaround?*/??
irq_finish(irq);??
irq_exit();??
set_irq_regs(old_regs);??
}??
irq_enter主要是更新一些系統的統計信息,同時在__irq_enter宏中禁止了進程的搶占:
[cpp]?view plain?copy
#define?__irq_enter()?????????????????????
do?{??????????????????????????
account_system_vtime(current);????????
add_preempt_count(HARDIRQ_OFFSET);????
trace_hardirq_enter();????????????
}?while?(0)??
CPU一旦響應IRQ中斷后,ARM會自動把CPSR中的I位置位,表明禁止新的IRQ請求,直到中斷控制轉到相應的流控層后才通過local_irq_enable()打開。你可能會奇怪,既然此時的irq中斷都是都是被禁止的,為何還要禁止搶占?這是因為要考慮中斷嵌套的問題,一旦流控層或驅動程序主動通過local_irq_enable打開了IRQ,而此時該中斷還沒處理完成,新的irq請求到達,這時代碼會再次進入irq_enter,在本次嵌套中斷返回時,內核不希望進行搶占調度,而是要等到最外層的中斷處理完成后才做出調度動作,所以才有了禁止搶占這一處理。
下一步,generic_handle_irq被調用,generic_handle_irq是通用邏輯層提供的API,通過該API,中斷的控制被傳遞到了與體系結構無關的中斷流控層:
[cpp]?view plain?copy
int?generic_handle_irq(unsigned?int?irq)??
{??
struct?irq_desc?*desc?=?irq_to_desc(irq);??
if?(!desc)??
return?-EINVAL;??
generic_handle_irq_desc(irq,?desc);??
return?0;??
}??
最終會進入該irq注冊的流控處理回調中:
[cpp]?view plain?copy
static?inline?void?generic_handle_irq_desc(unsigned?int?irq,?struct?irq_desc?*desc)??
{??
desc->handle_irq(irq,?desc);??
}??
5. ?中斷控制器的級聯
在實際的設備中,經常存在多個中斷控制器,有時多個中斷控制器還會進行所謂的級聯。為了方便討論,我們把直接和CPU相連的中斷控制器叫做根控制器,另外一些和跟控制器相連的叫子控制器。根據子控制器的位置,我們把它們分為兩種類型:
機器級別的級聯 ?子控制器位于SOC內部,或者子控制器在SOC的外部,但是是某個板子系列的標準配置,如圖5.1的左邊所示;
設備級別的級聯 ?子控制器位于某個外部設備中,用于匯集該設備發出的多個中斷,如圖5.1的右邊所示;
圖5.1 ?中斷控制器的級聯類型
對于機器級別的級聯,級聯的初始化代碼理所當然地位于板子的初始化代碼中(arch/xxx/mach-xxx),因為只要是使用這個板子或SOC的設備,必然要使用這個子控制器。而對于設備級別的級聯,因為該設備并不一定是系統的標配設備,所以中斷控制器的級聯操作應該在該設備的驅動程序中實現。機器設備的級聯,因為得益于事先已經知道子控制器的硬件連接信息,內核可以方便地為子控制器保留相應的irq_desc結構和irq編號,處理起來相對簡單。設備級別的級聯則不一樣,驅動程序必須動態地決定組合設備中各個子設備的irq編號和irq_desc結構。本章我只討論機器級別的級聯,設備級別的關聯可以使用同樣的原理,也可以實現為共享中斷,我會在本系列接下來的文章中討論。
要實現中斷控制器的級聯,要使用以下幾個的關鍵數據結構字段和通用中斷邏輯層的API:
irq_desc.handle_irq? irq的流控處理回調函數,子控制器在把多個irq匯集起來后,輸出端連接到根控制器的其中一個irq中斷線輸入腳,這意味著,每個子控制器的中斷發生時,CPU一開始只會得到根控制器的irq編號,然后進入該irq編號對應的irq_desc.handle_irq回調,該回調我們不能使用流控層定義好的幾個流控函數,而是要自己實現一個函數,該函數負責從子控制器中獲得irq的中斷源,并計算出對應新的irq編號,然后調用新irq所對應的irq_desc.handle_irq回調,這個回調使用流控層的標準實現。
irq_set_chained_handler()? 該API用于設置根控制器與子控制器相連的irq所對應的irq_desc.handle_irq回調函數,并且設置IRQ_NOPROBE和IRQ_NOTHREAD以及IRQ_NOREQUEST標志,這幾個標志保證驅動程序不會錯誤地申請該irq,因為該irq已經被作為級聯irq使用。
irq_set_chip_and_handler()? 該API同時設置irq_desc中的handle_irq回調和irq_chip指針。
以下例子代碼位于:/arch/arm/plat-s5p/irq-eint.c:
[cpp]?view plain?copy
int?__init?s5p_init_irq_eint(void)??
{??
int?irq;??
for?(irq?=?IRQ_EINT(0);?irq?<=?IRQ_EINT(15);?irq++)??
irq_set_chip(irq,?&s5p_irq_vic_eint);??
for?(irq?=?IRQ_EINT(16);?irq?<=?IRQ_EINT(31);?irq++)?{??
irq_set_chip_and_handler(irq,?&s5p_irq_eint,?handle_level_irq);??
set_irq_flags(irq,?IRQF_VALID);??
}??
irq_set_chained_handler(IRQ_EINT16_31,?s5p_irq_demux_eint16_31);??
return?0;??
}??
該SOC芯片的外部中斷:IRQ_EINT(0)到IRQ_EINT(15),每個引腳對應一個根控制器的irq中斷線,它們是正常的irq,無需級聯。IRQ_EINT(16)到IRQ_EINT(31)經過子控制器匯集后,統一連接到根控制器編號為IRQ_EINT16_31這個中斷線上。可以看到,子控制器對應的irq_chip是s5p_irq_eint,子控制器的irq默認設置為電平中斷的流控處理函數handle_level_irq,它們通過API:irq_set_chained_handler進行設置。如果根控制器有128個中斷線,IRQ_EINT0--IRQ_EINT15通常占據128內的某段連續范圍,這取決于實際的物理連接。IRQ_EINT16_31因為也屬于跟控制器,所以它的值也會位于128以內,但是IRQ_EINT16--IRQ_EINT31通常會在128以外的某段范圍,這時,代表irq數量的常量NR_IRQS,必須考慮這種情況,定義出超過128的某個足夠的數值。級聯的實現主要依靠編號為IRQ_EINT16_31的流控處理程序:s5p_irq_demux_eint16_31,它的最終實現類似于以下代碼:
[cpp]?view plain?copy
static?inline?void?s5p_irq_demux_eint(unsigned?int?start)??
{??
u32?status?=?__raw_readl(S5P_EINT_PEND(EINT_REG_NR(start)));??
u32?mask?=?__raw_readl(S5P_EINT_MASK(EINT_REG_NR(start)));??
unsigned?int?irq;??
status?&=?~mask;??
status?&=?0xff;??
while?(status)?{??
irq?=?fls(status)?-?1;??
generic_handle_irq(irq?+?start);??
status?&=?~(1?<
}??
}??
在獲得新的irq編號后,它的最關鍵的一句是調用了通用中斷邏輯層的API:generic_handle_irq,這時它才真正地把中斷控制權傳遞到中斷流控層中來。
?
評論
查看更多