ULK第四章裡明确講到“Linux實作了一種沒有優先級的中斷模型”,并且“Linux中斷和異常都支援嵌套”。這個我不太了解了,這兩種說法都與我以前的了解剛好相反,核對了原書,翻譯沒有錯。
Linux中斷系統到底是否支援優先級,可否嵌套,中斷号又是怎麼來确定的,中斷産生時又是如何一步步執行到中斷處理函數的。為了徹底搞懂Linux中斷系統,我決定從最原始材料出發,一探究竟。(s3c2440+linux2.6.21)
先來看看ARM的硬體執行流程
異常是ARM處理器模式分類,ARM有七種運作模式USR,SYS,SVC,IRQ,FIQ,UND,ABT
五種異常模式:SVC,IRQ,FIQ,UND,ABT

中斷模式是ARM異常模式之一(IRQ模式,FIQ模式),是一種異步事件,如外部按鍵産生中斷,内部定時器産生中斷,通信資料口資料收發産生中斷等。
1.當一個異常産生時,以FIQ為例,CPU切入FIQ模式時,
①将原來執行程式的下一條指令位址儲存到LR中,就是将R14儲存到R14_fiq裡面。
②拷貝CPSR到SPSR_fiq。
③改變CPSR模式位的值,改到FIQ模式。
④改變PC值,将其指向相應的異常處理向量表。
離開異常處理的時候,
①将LR(R14_fiq)賦給PC。
②将SPSR(SPSR_fiq)拷貝到CPSR。
③清除中斷禁止标志(如果開始時置位了)。
ARM一般在某個固定位址中有一個異常向量表,比如0x0
當一個外部IRQ中斷産生時
①處理器切換到IRQ模式
②PC跳到0x18處運作,因為這是IRQ的中斷入口。
③通過0x18:LDR PC, IRQ_ADDR,跳轉到相應的中斷服務程式。這個中斷服務程式就要确定中斷源,每個中斷源會有自己獨立的中斷服務程式。
④得到中斷源,然後執行相應中斷服務程式
⑤清除中斷标志,傳回
這就是一個外部中斷完整的執行流程了,下面以具體寄存器來更具體的了解ARM的中斷機制。
假設ARM核有兩個中斷引腳,一根是irq pin,一根是fiq pin,正常情況下,ARM核隻是機械地随着PC訓示去執行,當CPSR中的I位和F位都為1時,IRQ和FIQ都處于禁止狀态,這時候無論發什麼信号,ARM都不會理睬。
當I位或F位為0時,irq pin有中斷信号過來時,ARM目前工作就會被打斷,切換到IRQ模式,并且跳轉到異常向量表的中斷入口0x18,SRCPND中相應位置1,經過檢查中斷優先級寄存器以及屏蔽寄存器,确定中斷源,INTPND相應位置1(經過仲裁,隻有一位置1),這過程由ARM自動完成。0x18存放的是總的中斷處理函數,在這個函數裡,可以建立一個二級中斷向量表,先清除SRCPND相應位,然後根據中斷源執行相應中斷服務程式,清除INTPND,傳回。
及時清除中斷 Pending 寄存器的标志位是為了避免兩個問題:①發生中斷傳回後,立即又被中斷,不斷的重複響應②丢失中斷處理過程中發生的中斷,傳回後不響應。
在某個IRQ中斷程式執行過程中,有另外一個外部IRQ中斷産生,會将SRCPND相應位置1,等該中斷服務執行完,經過仲裁決定下一個要響應的中斷。但是假如當産生的是FIQ,則儲存目前IRQ的現場,嵌套響應FIQ,FIQ服務程式執行完,再繼續執行IRQ服務。那麼當一個FIQ正在服務,産生另外一個FIQ,會怎樣呢,答案是不會被打斷,跟IRQ一樣等目前中斷服務完成,再仲裁剩餘需要相應的中斷。
是以得出這樣的結論:
①關于中斷嵌套:IRQ模式隻能被FIQ模式打斷,FIQ模式下誰也打不斷。
②關于優先級:ARM核對中斷優先級,有明确的可程式設計管理。
下面再來看看Linux對ARM是怎麼處理的,記住一個前提:Linux對ARM的硬體特性可以取舍,但不可更改。
1.建立異常向量表:
系統從arch/arm/kernel/head.S的ENTRY(stext)開始執行,__lookup_processor_type檢查處理器ID,__lookup_machine_type檢查機器ID,__create_page_tables建立頁表,啟動MMU,然後由arch/arm/kernel/head_common.S 跳到start_kernel()->trap_init()
點選(此處)折疊或打開
void __init trap_init(void)
{
unsigned long vectors = CONFIG_VECTORS_BASE;
…
memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
memcpy((void *)vectors + 0x1000 - kuser_sz, __kuser_helper_start, kuser_sz);
}
#define CONFIG_VECTORS_BASE 0xffff0000
CONFIG_VECTORS_BASE在autoconf.h定義,在ARM V4及V4T以後的大部分處理器中,中斷向量表的位置可以有兩個位置:一個是0,另一個是0xffff0000。可以通過CP15協處理器c1寄存器中V位(bit[13])控制。V和中斷向量表的對應關系如下:
V=0 ~ 0x00000000~0x0000001C
V=1 ~ 0xffff0000~0xffff001C
__vectors_end 至 __vectors_start之間為異常向量表,位于arch/arm/kernel/entry-armv.S
.globl __vectors_start
__vectors_start:
swi SYS_ERROR0
b vector_und + stubs_offset//複位異常
ldr pc, .LCvswi + stubs_offset //未定義異常
b vector_pabt + stubs_offset//軟體中斷異常
b vector_dabt + stubs_offset//資料異常
b vector_addrexcptn + stubs_offset//保留
b vector_irq + stubs_offset //普通中斷異常
b vector_fiq + stubs_offset//快速中斷異常
.globl __vectors_end
__vectors_end:
stubs_offset值如下:
.equ stubs_offset, __vectors_start + 0x200 - __stubs_start
stubs_offset是如何确定的呢?(引用網絡上的一段比較詳細的解釋)
當彙編器看到B指令後會把要跳轉的标簽轉化為相對于目前PC的偏移量(±32M)寫入指令碼。從上面的代碼可以看到中斷向量表和stubs都發生了代碼搬移,是以如果中斷向量表中仍然寫成b vector_irq,那麼實際執行的時候就無法跳轉到搬移後的vector_irq處,因為指令碼裡寫的是原來的偏移量,是以需要把指令碼中的偏移量寫成搬移後的。我們把搬移前的中斷向量表中的irq入口位址記irq_PC,它在中斷向量表的偏移量就是irq_PC-vectors_start, vector_irq在stubs中的偏移量是vector_irq-stubs_start,這兩個偏移量在搬移前後是不變的。搬移後 vectors_start在0xffff0000處,而stubs_start在0xffff0200處,是以搬移後的vector_irq相對于中斷向量中的中斷入口位址的偏移量就是,200+vector_irq在stubs中的偏移量再減去中斷入口在向量表中的偏移量,即200+ vector_irq-stubs_start-irq_PC+vectors_start = (vector_irq-irq_PC) + vectors_start+200-stubs_start,對于括号内的值實際上就是中斷向量表中寫的vector_irq,減去irq_PC是由彙編器完成的,而後面的 vectors_start+200-stubs_start就應該是stubs_offset,實際上在entry-armv.S中也是這樣定義的。
2.中斷響應
當有外部中斷産生時,跳轉到異常向量表的“b vector_irq + stubs_offset //普通中斷異常”
進入異常處理函數,跳轉的入口位置 arch\arm\kernel\entry-armv.S 代碼簡略如下
.globl __stubs_start
__stubs_start:
/*
* Interrupt dispatcher
*/
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
vector_stub pabt, ABT_MODE, 4
vector_stub und, UND_MODE
* Undefined FIQs
vector_fiq:
disable_fiq
subs pc, lr, #4
vector_addrexcptn:
b vector_addrexcptn
vector_stub是個函數調用宏,根據中斷前的工作模式決定進入__irq_usr,__irq_svc。這裡入__irq_svc,同時看到這裡FIQ産生時,系統未做任何處理,直接傳回,即Linux沒有提供對FIQ的支援,繼續跟進代碼
__irq_svc:
svc_entry
…
irq_handler
.macro irq_handler
get_irqnr_preamble r5, lr
1: get_irqnr_and_base r0, r6, r5, lr @判斷中斷号,通過R0傳回,3.5節有實作過程
movne r1, sp
@
@ routine called with r0 = irq number, r1 = struct pt_regs *
adrne lr, 1b
bne asm_do_IRQ @進入中斷處理。
……
.endm
get_irqnr_and_base中斷号判斷過程,include/asm/arch-s3c2410/entry-macro.s
.macro get_irqnr_and_base, irqnr, irqstat, base, tmp
mov \base, #S3C24XX_VA_IRQ
@@ try the interrupt offset register, since it is there
ldr \irqstat, [ \base, #INTPND ]
teq \irqstat, #0
beq 1002f
ldr \irqnr, [ \base, #INTOFFSET ] @通過判斷INTOFFSET寄存器得到中斷位置
@@ we have the value
1001:
adds \irqnr, \irqnr, #IRQ_EINT0 @加上中斷号的基準數值,得到最終的中斷号,注意:此時沒有考慮子中斷的具體情況。IRQ_EINT0在include/asm/arch-s3c2410/irqs.h中定義.從這裡可以看出,中斷号的具體值是有平台相關的代碼決定的,和硬體中斷挂起寄存器中的中斷号是不等的。
1002:
@@ exit here, Z flag unset if IRQ
asm_do_IRQ實作過程,arch/arm/kernel/irq.c
asmlinkage void asm_do_IRQ(unsigned int irq, struct pt_regs *regs)
struct pt_regs *old_regs = set_irq_regs(regs);
struct irq_desc *desc = irq_desc + irq;//根據中斷号,找到響應的irq_desc
/*
* Some hardware gives randomly wrong interrupts. Rather
* than crashing, do something sensible.
*/
if (irq >= NR_IRQS)
desc = &bad_irq_desc;
irq_enter();
desc_handle_irq(irq, desc);//根據irq和desc進入中斷處理
/* AT91 specific workaround */
irq_finish(irq);
irq_exit();
set_irq_regs(old_regs);
static inline void desc_handle_irq(unsigned int irq, struct irq_desc *desc)
desc->handle_irq(irq, desc);//中斷處理
上述asmlinkage void __exception asm_do_IRQ(unsigned int irq, struct pt_regs *regs)使用了asmlinkage辨別。那麼這個辨別的含義如何了解呢?
該符号定義在kernel/include/linux/linkage.h中,如下所示:
#include //各個具體處理器在此檔案中定義asmlinkage
#ifdef __cplusplus
#define CPP_ASMLINKAGE extern "C"
#else
#define CPP_ASMLINKAGE
#endif
#ifndef asmlinkage//如果以前沒有定義asmlinkage
#define asmlinkage CPP_ASMLINKAGE
對于ARM處理器的,沒有定義asmlinkage,是以沒有意義(不要以為參數是從堆棧傳遞的,對于ARM平台來說還是符合ATPCS過程調用标準,通過寄存器傳遞的)。
但對于X86處理器的中是這樣定義的:
#define asmlinkage CPP_ASMLINKAGE __attribute__((regparm(0)))
表示函數的參數傳遞是通過堆棧完成的。
中斷處理過程代碼就跟到這了,那麼最後一個問題desc->handle_irq(irq, desc);是怎麼跟我們注冊的中斷函數相關聯的呢?再從中斷模型注冊入手:
中斷相關的資料結構:在include/asm/arch/irq.h中定義。
irq_desc[]是一個指向irq_desc_t結構的數組, irq_desc_t結構是各個裝置中斷服務例程的描述符。Irq_desc_t結構體中的成員action指向該中斷号對應的irqaction結構體連結清單。Irqaction結構體定義在include/linux/interrupt.h中,如下:
truct irqaction {
irq_handler_t handler; //中斷處理函數,注冊時提供
unsigned long flags; //中斷标志,注冊時提供
cpumask_t mask; //中斷掩碼
const char *name; //中斷名稱
void *dev_id; //裝置id,本文後面部分介紹中斷共享時會詳細說明這個參數的作用
struct irqaction *next; //如果有中斷共享,則繼續執行,
int irq; //中斷号,注冊時提供
struct proc_dir_entry *dir; //指向IRQn相關的/proc/irq/n目錄的描述符
};
在注冊中斷号為irq的中斷服務程式時,系統會根據注冊參數封裝相應的irqaction結構體。并把中斷号為irq的irqaction結構體寫入irq_desc [irq]->action。這樣就把裝置的中斷請求号與該裝置的中斷服務例程irqaction聯系在一起了。當CPU接收到中斷請求後,就可以根據中斷号通過irq_desc []找到該裝置的中斷服務程式。
3. 中斷共享的處理模型
共享中斷的不同裝置的iqraction結構體都會添加進該中斷号對應的irq_desc結構體的action成員所指向的irqaction連結清單内。當核心發生中斷時,它會依次調用該連結清單内所有的handler函數。是以,若驅動程式需要使用共享中斷機制,其中斷處理函數必須有能力識别是否是自己的硬體産生了中斷。通常是通過讀取該硬體裝置提供的中斷flag标志位進行判斷。也就是說不是任何裝置都可以做為中斷共享源的,它必須能夠通過的它的中斷flag判斷出是否發生了中斷。
中斷共享的注冊方法是:
int request_irq(unsigned int irq, irq_handler_t handler,IRQF_SHARED, const char *devname, void *dev_id)
很多權威資料中都提到,中斷共享注冊時的注冊函數中的dev_id參數是必不可少的,并且dev_id的值必須唯一。那麼這裡提供唯一的dev_id值的究竟是做什麼用的?
根據我們前面中斷模型的知識,可以看出發生中斷時,核心并不判斷究竟是共享中斷線上的哪個裝置産生了中斷,它會循環執行所有該中斷線上注冊的中斷處理函數(即irqaction->handler函數)。是以irqaction->handler函數有責任識别出是否是自己的硬體裝置産生了中斷,然後再執行該中斷處理函數。通常是通過讀取該硬體裝置提供的中斷flag标志位進行判斷。那既然kernel循環執行該中斷線上注冊的所有irqaction->handler函數,把識别究竟是哪個硬體裝置産生了中斷這件事交給中斷處理函數本身去做,那request_irq的dev_id參數究竟是做什麼用的?
很多資料中都建議将裝置結構指針作為dev_id參數。在中斷到來時,迅速地根據硬體寄存器中的資訊比照傳入的dev_id參數判斷是否是本裝置的中斷,若不是,應迅速傳回。這樣的說法沒有問題,也是我們程式設計時都遵循的方法。但事實上并不能夠說明為什麼中斷共享必須要設定dev_id。
下面解釋一下dev_id參數為什麼必須的,而且是必須唯一的。
當調用free_irq登出中斷處理函數時(通常解除安裝驅動時其中斷處理函數也會被登出掉),因為dev_id是唯一的,是以可以通過它來判斷從共享中斷線上的多個中斷處理程式中删除指定的一個。如果沒有這個參數,那麼kernel不可能知道給定的中斷線上到底要删除哪一個處理程式。
登出函數定義在Kernel/irq/manage.c中定義:
void free_irq(unsigned int irq, void *dev_id)
4.S3C2410子中斷的注冊的實作
前面判斷中斷号的方法,可以看到隻是通過S3C2410中斷控制器中的INTOFFSET寄存器來判斷的。對于INTPND中的EINT4_7、EINT8_23、INT_UART0、INT_ADC 等帶有子中斷的向量,INTOFFSET無法判斷出具體的中斷号。平台留給我們的注冊方法如下:
在include/asm/arch/irqs.h中有類似如下定義:
/* interrupts generated from the external interrupts sources */
#define IRQ_EINT4 S3C2410_IRQ(32) /* 48 */
#define IRQ_EINT5 S3C2410_IRQ(33)
#define IRQ_EINT6 S3C2410_IRQ(34)
#define IRQ_EINT7 S3C2410_IRQ(35)
#define IRQ_EINT8 S3C2410_IRQ(36)
#define IRQ_EINT9 S3C2410_IRQ(37)
#define IRQ_EINT10 S3C2410_IRQ(38)
#define IRQ_EINT11 S3C2410_IRQ(39)
#define IRQ_EINT12 S3C2410_IRQ(40)
#define IRQ_EINT13 S3C2410_IRQ(41)
#define IRQ_EINT14 S3C2410_IRQ(42)
#define IRQ_EINT15 S3C2410_IRQ(43)
#define IRQ_EINT16 S3C2410_IRQ(44)
#define IRQ_EINT17 S3C2410_IRQ(45)
#define IRQ_EINT18 S3C2410_IRQ(46)
#define IRQ_EINT19 S3C2410_IRQ(47)
#define IRQ_EINT20 S3C2410_IRQ(48) /* 64 */
#define IRQ_EINT21 S3C2410_IRQ(49)
#define IRQ_EINT22 S3C2410_IRQ(50)
#define IRQ_EINT23 S3C2410_IRQ(51)
可以看到平台為每種子中斷都定義了中斷号,如果你想實作EINT10的中斷注冊,直接按照IRQ_EINT10這個中斷号注冊都可以了。那麼平台代碼是如何實作這部分中斷注冊的呢?
5.S3C2410子中斷注冊問題的解決
/*arch/arm/plat-s3c24xx/irq.c*/
void __init s3c24xx_init_irq(void)
{……
set_irq_chained_handler(IRQ_EINT4t7, s3c_irq_demux_extint4t7);
set_irq_chained_handler(IRQ_EINT8t23, s3c_irq_demux_extint8);
set_irq_chained_handler(IRQ_UART0, s3c_irq_demux_uart0);
set_irq_chained_handler(IRQ_UART1, s3c_irq_demux_uart1);
set_irq_chained_handler(IRQ_UART2, s3c_irq_demux_uart2);
set_irq_chained_handler(IRQ_ADCPARENT, s3c_irq_demux_adc);
平台在初始化時會調用到s3c24xx_init_irq,在此函數中實作了對EINT4_7、EINT8_23、INT_UART0、INT_ADC等中斷的注冊。下面看看這些帶有子中斷的中斷号對應的處理函數的内容。以IRQ_EINT4t7為例,其它情況類似。
s3c_irq_demux_extint4t7(unsigned int irq,
struct irq_desc *desc)
unsigned long eintpnd = __raw_readl(S3C24XX_EINTPEND);
unsigned long eintmsk = __raw_readl(S3C24XX_EINTMASK);
eintpnd &= ~eintmsk;
eintpnd &= 0xff; /* only lower irqs */
/* eintpnd中可以有多個位同時置1,這一點和intpnd的隻能有1個位置1是不一樣的 */
while (eintpnd) { //循環執行所有置位的子中斷
irq = __ffs(eintpnd); //算出第一個不為0的位,類似arm v5後的clz前導0的作用
eintpnd &= ~(1irq);//清除相應的位
irq += (IRQ_EINT4 - 4);//算出對應的中斷号
desc_handle_irq(irq, irq_desc + irq);//執行對應子中斷的注冊函數
從上面的函數可以看出子中斷是如何注冊及被調用到的。有人可能會問為何不在include/asm/arch-s3c2410/entry-macro.s 檔案中get_irqnr_and_base函數判斷中斷号時,直接算出對應的子中斷号,就可以直接找到子中斷處理了呢?
原因是: get_irqnr_and_base是平台給系統提供的函數,對于多個子中斷同時置位的情況無法通過一個值傳回(因為子中斷中,如eintpnd是可以多個位同時置位的))。而intpnd則沒有這個問題。
至此,對于s3c2440/10+linux2.6得出以下結論:
①不支援中斷嵌套(因為FIQ不支援)
②有明确中斷優先級(可程式設計)
③中斷号是根據硬體特性固定的,riq号通過某種轉換得到與寄存器相應位,一般在irqs.h檔案定義