轉自:http://www.wowotech.net/irq_handler.htmlhtml
1、前言node
本文主要以ARM體系結構下的中斷處理爲例,講述整個中斷處理過程當中的硬件行爲和軟件動做。具體整個處理過程分紅三個步驟來描述:linux
一、第二章描述了中斷處理的準備過程算法
二、第三章描述了當發生中的時候,ARM硬件的行爲bootstrap
三、第四章描述了ARM的中斷進入過程數據結構
四、第五章描述了ARM的中斷退出過程app
2、中斷處理的準備過程dom
ARM處理器有多種processor mode,例如user mode(用戶空間的AP所處於的模式)、supervisor mode(即SVC mode,大部分的內核態代碼都處於這種mode)、IRQ mode(發生中斷後,處理器會切入到該mode)等。函數
對於linux kernel,其中斷處理處理過程當中,ARM 處理器大部分都是處於SVC mode。fetch
可是,實際上產生中斷的時候,ARM處理器其實是先進入IRQ mode,所以在進入真正的IRQ異常處理以前會有一小段IRQ mode的操做,以後會進入SVC mode進行真正的IRQ異常處理。因爲IRQ mode只是一個過分,所以IRQ mode的棧很小,只有12個字節,具體以下:
sprdroid9.0_trunk/kernel4.4/arch/arm/kernel/setup.c 132/* 133 * Cached cpu_architecture() result for use by assembler code. 134 * C code should use the cpu_architecture() function instead of accessing this 135 * variable directly. 136 */ 137int __cpu_architecture __read_mostly = CPU_ARCH_UNKNOWN; 138 139struct stack { 140 u32 irq[3]; 141 u32 abt[3]; 142 u32 und[3]; 143 u32 fiq[3]; 144} ____cacheline_aligned;
除了irq mode,linux kernel在處理abt mode(當發生data abort exception或者prefetch abort exception的時候進入的模式)和und mode(處理器遇到一個未定義的指令的時候進入的異常模式)的時候也是採用了相同的策略。
也就是通過一個簡短的abt或者und mode以後,stack切換到svc mode的棧上,這個棧就是發生異常那個時間點current thread的內核棧
anyway,在irq mode和svc mode之間老是須要一個stack保存數據,這就是中斷模式的stack,系統初始化的時候,cpu_init函數中會進行中斷模式stack的設定:
/* 518 * cpu_init - initialise one CPU. 519 * 520 * cpu_init sets up the per-CPU stacks. 521 */ 522void notrace cpu_init(void) 523{ 524#ifndef CONFIG_CPU_V7M 525 unsigned int cpu = smp_processor_id();------獲取CPU ID 526 struct stack *stk = &stacks[cpu];---------獲取該CPU對於的irq abt和und的stack指針 527 528 if (cpu >= NR_CPUS) { 529 pr_crit("CPU%u: bad primary CPU number\n", cpu); 530 BUG(); 531 } 532 533 /* 534 * This only works on resume and secondary cores. For booting on the 535 * boot cpu, smp_prepare_boot_cpu is called after percpu area setup. 536 */ 537 set_my_cpu_offset(per_cpu_offset(cpu)); 538 539 cpu_proc_init(); 540 541 /* 542 * Define the placement constraint for the inline asm directive below. 543 * In Thumb-2, msr with an immediate value is not allowed. 544 */ 545#ifdef CONFIG_THUMB2_KERNEL 546#define PLC "r"------Thumb-2下,msr指令不容許使用當即數,只能使用寄存器。 547#else 548#define PLC "I" 549#endif 550 551 /* 552 * setup stacks for re-entrant exception handlers 553 */ 554 __asm__ ( 555 "msr cpsr_c, %1\n\t"------讓CPU進入IRQ mode 556 "add r14, %0, %2\n\t"------r14寄存器保存stk->irq 557 "mov sp, r14\n\t"--------設定IRQ mode的stack爲stk->irq 558 "msr cpsr_c, %3\n\t" 559 "add r14, %0, %4\n\t" 560 "mov sp, r14\n\t"--------設定abt mode的stack爲stk->abt 561 "msr cpsr_c, %5\n\t" 562 "add r14, %0, %6\n\t" 563 "mov sp, r14\n\t"--------設定und mode的stack爲stk->und 564 "msr cpsr_c, %7\n\t" 565 "add r14, %0, %8\n\t" 566 "mov sp, r14\n\t"--------設定fiq mode的stack爲stk->fiq 567 "msr cpsr_c, %9"--------回到SVC mode 568 :--------------------上面是code,下面的output部分是空的 569 : "r" (stk),----------------------對應上面代碼中的%0 570 PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),------對應上面代碼中的%1 571 "I" (offsetof(struct stack, irq[0])),------------對應上面代碼中的%2 572 PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),------以此類推,下面不贅述 573 "I" (offsetof(struct stack, abt[0])), 574 PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE), 575 "I" (offsetof(struct stack, und[0])), 576 PLC (PSR_F_BIT | PSR_I_BIT | FIQ_MODE), 577 "I" (offsetof(struct stack, fiq[0])), 578 PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE) 579 : "r14");--------上面是input操做數列表,r14是要clobbered register列表 580#endif 581}
嵌入式彙編的語法格式是:
asm(code
: output operand list
: input operand list
: clobber list);
你們對着上面的code就能夠分開各段內容了。在input operand list中,有兩種限制符(constraint),"r"或者"I","I"表示當即數(Immediate operands),"r"表示用通用寄存器傳遞參數。clobber list中有一個r14,表示在彙編代碼中修改了r14的值,這些信息是編譯器須要的內容。
對於SMP,bootstrap CPU會在系統初始化的時候執行cpu_init函數,進行本CPU的irq、abt和und三種模式的內核棧的設定,具體調用序列是:start_kernel--->setup_arch--->setup_processor--->cpu_init。
對於系統中其餘的CPU,bootstrap CPU會在系統初始化的最後,對每個online的CPU進行初始化,具體的調用序列是:start_kernel--->rest_init--->kernel_init--->kernel_init_freeable--->kernel_init_freeable--->smp_init--->cpu_up--->_cpu_up--->__cpu_up。__cpu_up函數是和CPU architecture相關的。
對於ARM,其調用序列是__cpu_up--->boot_secondary--->smp_ops.smp_boot_secondary(SOC相關代碼)--->secondary_startup--->__secondary_switched--->secondary_start_kernel--->cpu_init。
除了初始化,系統電源管理也須要irq、abt和und stack的設定。若是咱們設定的電源管理狀態在進入sleep的時候,CPU會丟失irq、abt和und stack point寄存器的值,那麼在CPU resume的過程當中,要調用cpu_init來從新設定這些值。
二、SVC模式的stack準備
咱們常常說進程的用戶空間和內核空間,對於一個應用程序而言,能夠運行在用戶空間,也能夠經過系統調用進入內核空間。在用戶空間,使用的是用戶棧,也就是咱們軟件工程師編寫用戶空間程序的時候,保存局部變量的stack。陷入內核後,固然不能用用戶棧了,這時候就須要使用到內核棧。所謂內核棧其實就是處於SVC mode時候使用的棧。
在linux最開始啓動的時候,系統只有一個進程(更準確的說是kernel thread),就是PID等於0的那個進程,叫作swapper進程(或者叫作idle進程)。該進程的內核棧是靜態定義的,以下:
/sprdroid9.0_trunk/kernel4.4/init/init_task.c 21/* 22 * Initial thread structure. Alignment of this is handled by a special 23 * linker map entry. 24 */ 25union thread_union init_thread_union __init_task_data = { 26#ifndef CONFIG_THREAD_INFO_IN_TASK 27 INIT_THREAD_INFO(init_task) 28#endif 29}; 2633union thread_union { 2634#ifndef CONFIG_THREAD_INFO_IN_TASK 2635 struct thread_info thread_info; 2636#endif 2637 unsigned long stack[THREAD_SIZE/sizeof(long)]; 2638};
對於ARM平臺,THREAD_SIZE是8192個byte,所以佔據兩個page frame。
隨着初始化的進行,Linux kernel會建立若干的內核線程,而在進入用戶空間後,user space的進程也會建立進程或者線程。
Linux kernel在建立進程(包括用戶進程和內核線程)的時候都會分配一個(或者兩個,和配置相關)page frame,具體代碼以下:
static struct task_struct *dup_task_struct(struct task_struct *orig) { ...... ti = alloc_thread_info_node(tsk, node); if (!ti) goto free_tsk; ...... }
底部是struct thread_info數據結構,頂部(高地址)就是該進程的內核棧。當進程切換的時候,整個硬件和軟件的上下文都會進行切換,這裏就包括了svc mode的sp寄存器的值被切換到調度算法選定的新的進程的內核棧上來。
三、異常向量表的準備
對於ARM處理器而言,當發生異常的時候,處理器會暫停當前指令的執行,保存現場,轉而去執行對應的異常向量處的指令,當處理完該異常的時候,
恢復現場,回到原來的那點去繼續執行程序。系統全部的異常向量(共計8個)組成了異常向量表。向量表(vector table)的代碼以下:
/sprdroid9.0_trunk/kernel4.4/arch/arm/kernel/entry-armv.S 208 .section .vectors, "ax", %progbits 1209__vectors_start: 1210 W(b) vector_rst 1211 W(b) vector_und 1212 W(ldr) pc, __vectors_start + 0x1000 1213 W(b) vector_pabt 1214 W(b) vector_dabt 1215 W(b) vector_addrexcptn 1216 W(b) vector_irq---------------------------IRQ Vector 1217 W(b) vector_fiq 1218
對於本文而言,咱們重點關注vector_irq這個exception vector。異常向量表可能被安放在兩個位置上:
(1)異常向量表位於0x0的地址。這種設置叫作Normal vectors或者Low vectors。
(2)異常向量表位於0xffff0000的地址。這種設置叫作high vectors
具體是low vectors仍是high vectors是由ARM的一個叫作的SCTLR寄存器的第13個bit (vector bit)控制的。對於啓用MMU的ARM Linux而言,系統使用了high vectors。爲何不用low vector呢?對於linux而言,0~3G的空間是用戶空間,若是使用low vector,那麼異常向量表在0地址,那麼則是用戶空間的位置,所以linux選用high vector。固然,使用Low vector也能夠,這樣Low vector所在的空間則屬於kernel space了(也就是說,3G~4G的空間加上Low vector所佔的空間屬於kernel space),不過這時候要注意一點,由於全部的進程共享kernel space,而用戶空間的程序常常會發生空指針訪問,這時候,內存保護機制應該能夠捕獲這種錯誤(大部分的MMU均可以作到,例如:禁止userspace訪問kernel space的地址空間),防止vector table被訪問到。對於內核中因爲程序錯誤致使的空指針訪問,內存保護機制也須要控制vector table被修改,所以vector table所在的空間被設置成read only的。在使用了MMU以後,具體異常向量表放在那個物理地址已經不重要了,重要的是把它映射到0xffff0000的虛擬地址就OK了,具體代碼以下:
/sprdroid9.0_trunk/kernel4.4/arch/arm/mm/mmu.c static void __init devicemaps_init(const struct machine_desc *mdesc) { …… vectors = early_alloc(PAGE_SIZE * 2); -----分配兩個page的物理頁幀 early_trap_init(vectors); -------copy向量表以及相關help function到該區域 …… map.pfn = __phys_to_pfn(virt_to_phys(vectors)); map.virtual = 0xffff0000; map.length = PAGE_SIZE; #ifdef CONFIG_KUSER_HELPERS map.type = MT_HIGH_VECTORS; #else map.type = MT_LOW_VECTORS; #endif create_mapping(&map); ----------映射0xffff0000的那個page frame if (!vectors_high()) {---若是SCTLR.V的值設定爲low vectors,那麼還要映射0地址開始的memory map.virtual = 0; map.length = PAGE_SIZE * 2; map.type = MT_LOW_VECTORS; create_mapping(&map); } map.pfn += 1; map.virtual = 0xffff0000 + PAGE_SIZE; map.length = PAGE_SIZE; map.type = MT_LOW_VECTORS; create_mapping(&map); ----------映射high vecotr開始的第二個page frame …… }
爲何要分配兩個page frame呢?這裏vectors table和kuser helper函數(內核空間提供的函數,可是用戶空間使用)佔用了一個page frame,另外異常處理的stub函數佔用了另一個page frame。爲何會有stub函數呢?稍後會講到。
在early_trap_init函數中會初始化異常向量表,具體代碼以下:
void __init early_trap_init(void *vectors_base) { unsigned long vectors = (unsigned long)vectors_base; extern char __stubs_start[], __stubs_end[]; extern char __vectors_start[], __vectors_end[]; unsigned i; vectors_page = vectors_base; 將整個vector table那個page frame填充成未定義的指令。起始vector table加上kuser helper函數並不能徹底的充滿這個page,有些縫隙。若是不這麼處理,當極端狀況下(程序錯誤或者HW的issue),CPU可能從這些縫隙中取指執行,從而致使不可知的後果。若是將這些縫隙填充未定義指令,那麼CPU能夠捕獲這種異常。 for (i = 0; i < PAGE_SIZE / sizeof(u32); i++) ((u32 *)vectors_base)[i] = 0xe7fddef1; 拷貝vector table,拷貝stub function memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start); memcpy((void *)vectors + 0x1000, __stubs_start, __stubs_end - __stubs_start); kuser_init(vectors_base); ----copy kuser helper function flush_icache_range(vectors, vectors + PAGE_SIZE * 2); modify_domain(DOMAIN_USER, DOMAIN_CLIENT); }
一旦涉及代碼的拷貝,咱們就須要關心其編譯鏈接時地址(link-time address)和運行時地址(run-time address)。在kernel完成連接後,__vectors_start有了其link-time address,若是link-time address和run-time address一致,那麼這段代碼運行時毫無壓力。可是,目前對於vector table而言,其被copy到其餘的地址上(對於High vector,這是地址就是0xffff00000),也就是說,link-time address和run-time address不同了,若是仍然想要這些代碼能夠正確運行,那麼須要這些代碼是位置無關的代碼。對於vector table而言,必需要位置無關。B這個branch instruction自己就是位置無關的,它能夠跳轉到一個當前位置的offset。不過並不是全部的vector都是使用了branch instruction,對於軟中斷,其vector地址上指令是「W(ldr) pc, __vectors_start + 0x1000 」,這條指令被編譯器編譯成ldr pc, [pc, #4080],這種狀況下,該指令也是位置無關的,可是有個限制,offset必須在4K的範圍內,這也是爲什麼存在stub section的緣由了。
四、中斷控制器的初始化
3、ARM HW對中斷事件的處理
當一切準備好以後,一旦打開處理器的全局中斷就能夠處理來自外設的各類中斷事件了。
當外設(SOC內部或者外部均可以)檢測到了中斷事件,就會經過interrupt requestion line上的電平或者邊沿(上升沿或者降低沿或者both)通知到該外設鏈接到的那個中斷控制器,而中斷控制器就會在多個處理器中選擇一個,並把該中斷經過IRQ(或者FIQ,本文不討論FIQ的狀況)分發給該processor。ARM處理器感知到了中斷事件後,會進行下面一系列的動做:
一、修改CPSR(Current Program Status Register)寄存器中的M[4:0]。M[4:0]表示了ARM處理器當前處於的模式( processor modes)。ARM定義的mode包括:
處理器模式 | 縮寫 | 對應的M[4:0]編碼 | Privilege level |
User | usr | 10000 | PL0 |
FIQ | fiq | 10001 | PL1 |
IRQ | irq | 10010 | PL1 |
Supervisor | svc | 10011 | PL1 |
Monitor | mon | 10110 | PL1 |
Abort | abt | 10111 | PL1 |
Hyp | hyp | 11010 | PL2 |
Undefined | und | 11011 | PL1 |
System | sys | 11111 | PL1 |
一旦設定了CPSR.M,ARM處理器就會將processor mode切換到IRQ mode。
二、保存發生中斷那一點的CPSR值(step 1以前的狀態)和PC值
ARM處理器支持9種processor mode,每種mode看到的ARM core register(R0~R15,共計16個)都是不一樣的。每種mode都是從一個包括全部的Banked ARM core register中選取。所有Banked ARM core register包括:
Usr | System | Hyp | Supervisor | abort | undefined | Monitor | IRQ | FIQ |
R0_usr | ||||||||
R1_usr | ||||||||
R2_usr | ||||||||
R3_usr | ||||||||
R4_usr | ||||||||
R5_usr | ||||||||
R6_usr | ||||||||
R7_usr | ||||||||
R8_usr | R8_fiq | |||||||
R9_usr | R9_fiq | |||||||
R10_usr | R10_fiq | |||||||
R11_usr | R11_fiq | |||||||
R12_usr | R12_fiq | |||||||
SP_usr | SP_hyp | SP_svc | SP_abt | SP_und | SP_mon | SP_irq | SP_fiq | |
LR_usr | LR_svc | LR_abt | LR_und | LR_mon | LR_irq | LR_fiq | ||
PC | ||||||||
CPSR | ||||||||
SPSR_hyp | SPSR_svc | SPSR_abt | SPSR_und | SPSR_mon | SPSR_irq | SPSR_fiq | ||
ELR_hyp |
在IRQ mode下,CPU看到的R0~R12寄存器、PC以及CPSR是和usr mode(userspace)或者svc mode(kernel space)是同樣的。不一樣的是IRQ mode下,有本身的R13(SP,stack pointer)、R14(LR,link register)和SPSR(Saved Program Status Register)。
CPSR是共用的,雖然中斷可能發生在usr mode(用戶空間),也多是svc mode(內核空間),不過這些信息都是體如今CPSR寄存器中。硬件會將發生中斷那一刻的CPSR保存在SPSR寄存器中(因爲不一樣的mode下有不一樣的SPSR寄存器,所以更準確的說應該是SPSR-irq,也就是IRQ mode中的SPSR寄存器)。
PC也是共用的,因爲後續PC會被修改成irq exception vector,所以有必要保存PC值。固然,與其說保存PC值,不如說是保存返回執行的地址。對於IRQ而言,咱們指望返回地址是發生中斷那一點執行指令的下一條指令。具體的返回地址保存在lr寄存器中(注意:這個lr寄存器是IRQ mode的lr寄存器,能夠表示爲lr_irq):
(1)對於thumb state,lr_irq = PC
(2)對於ARM state,lr_irq = PC - 4
爲什麼要減去4?個人理解是這樣的(不必定對)。因爲ARM採用流水線結構,當CPU正在執行某一條指令的時候,其實取指的動做早就執行了,這時候PC值=正在執行的指令地址 + 8,以下所示:
----> 發生中斷的指令
發生中斷的指令+4
-PC-->發生中斷的指令+8
發生中斷的指令+12
一旦發生了中斷,當前正在執行的指令固然要執行完畢,可是已經完成取指、譯碼的指令則終止執行。當發生中斷的指令執行完畢以後,原來指向(發生中斷的指令+8)的PC會繼續增長4,所以發生中斷後,ARM core的硬件着手處理該中斷的時候,硬件現場以下圖所示:
----> 發生中斷的指令
發生中斷的指令+4 <-------中斷返回的指令是這條指令
發生中斷的指令+8
-PC-->發生中斷的指令+12
這時候的PC值實際上是比發生中斷時候的指令超前12。減去4以後,lr_irq中保存了(發生中斷的指令+8)的地址。爲何HW不幫忙直接減去8呢?這樣,後續軟件不就不用再減去4了。這裏咱們不能孤立的看待問題,實際上ARM的異常處理的硬件邏輯不只僅處理IRQ的exception,還要處理各類exception,很遺憾,不一樣的exception指望的返回地址不統一,所以,硬件只是幫忙減去4,剩下的交給軟件去調整。
三、mask IRQ exception。也就是設定CPSR.I = 1
四、設定PC值爲IRQ exception vector。基本上,ARM處理器的硬件就只能幫你幫到這裏了,一旦設定PC值,ARM處理器就會跳轉到IRQ的exception vector地址了,後續的動做都是軟件行爲了。
4、如何進入ARM中斷處理
一、IRQ mode中的處理
IRQ mode的處理都在vector_irq中,vector_stub是一個宏,定義以下:
.macro vector_stub, name, mode, correction=0 .align 5 vector_\name: .if \correction sub lr, lr, #\correction-------------(1) .endif @ @ Save r0, lr_ (parent PC) and spsr_ @ (parent CPSR) @ stmia sp, {r0, lr} @ save r0, lr--------(2) mrs lr, spsr str lr, [sp, #8] @ save spsr @ @ Prepare for SVC32 mode. IRQs remain disabled. @ mrs r0, cpsr-----------------------(3) eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE) msr spsr_cxsf, r0 @ @ the branch table must immediately follow this code @ and lr, lr, #0x0f---lr保存了發生IRQ時候的CPSR,經過and操做,能夠獲取CPSR.M[3:0]的值 這時候,若是中斷髮生在用戶空間,lr=0,若是是內核空間,lr=3 THUMB( adr r0, 1f )----根據當前PC值,獲取lable 1的地址 THUMB( ldr lr, [r0, lr, lsl #2] )-lr根據當前mode,要麼是__irq_usr的地址 ,要麼是__irq_svc的地址 mov r0, sp------將irq mode的stack point經過r0傳遞給即將跳轉的函數 ARM( ldr lr, [pc, lr, lsl #2] )---根據mode,給lr賦值,__irq_usr或者__irq_svc movs pc, lr @ branch to handler in SVC mode-----(4) ENDPROC(vector_\name) .align 2 @ handler addresses follow this label 1: .endm
(1)咱們指望在棧上保存發生中斷時候的硬件現場(HW context),這裏就包括ARM的core register。上一章咱們已經瞭解到,當發生IRQ中斷的時候,lr中保存了發生中斷的PC+4,若是減去4的話,獲得的就是發生中斷那一點的PC值。
(2)當前是IRQ mode,SP_irq在初始化的時候已經設定(12個字節)。在irq mode的stack上,依次保存了發生中斷那一點的r0值、PC值以及CPSR值(具體操做是經過spsr進行的,其實硬件已經幫咱們保存了CPSR到SPSR中了)。爲什麼要保存r0值?由於隨後的代碼要使用r0寄存器,所以咱們要把r0放到棧上,只有這樣才能完徹底全恢復硬件現場。
(3)可憐的IRQ mode稍縱即逝,這段代碼就是準備將ARM推送到SVC mode。如何準備?其實就是修改SPSR的值,SPSR不是CPSR,不會引發processor mode的切換(畢竟這一步只是準備而已)。
(4)不少異常處理的代碼返回的時候都是使用了stack相關的操做,這裏沒有。「movs pc, lr 」指令除了字面上意思(把lr的值付給pc),還有一個隱含的操做(movs中‘s’的含義):把SPSR copy到CPSR,從而實現了模式的切換。
二、當發生中斷的時候,代碼運行在用戶空間
Interrupt dispatcher的代碼以下:
vector_stub irq, IRQ_MODE, 4 -----減去4,確保返回發生中斷以後的那條指令
.long __irq_usr @ 0 (USR_26 / USR_32) <---------------------> base address + 0
.long __irq_invalid @ 1 (FIQ_26 / FIQ_32)
.long __irq_invalid @ 2 (IRQ_26 / IRQ_32)
.long __irq_svc @ 3 (SVC_26 / SVC_32)<---------------------> base address + 12
.long __irq_invalid @ 4
.long __irq_invalid @ 5
.long __irq_invalid @ 6
.long __irq_invalid @ 7
.long __irq_invalid @ 8
.long __irq_invalid @ 9
.long __irq_invalid @ a
.long __irq_invalid @ b
.long __irq_invalid @ c
.long __irq_invalid @ d
.long __irq_invalid @ e
.long __irq_invalid @ f
這其實就是一個lookup table,根據CPSR.M[3:0]的值進行跳轉(參考上一節的代碼:and lr, lr, #0x0f)。所以,該lookup table共設定了16個入口,固然只有兩項有效,分別對應user mode和svc mode的跳轉地址。其餘入口的__irq_invalid也是很是關鍵的,這保證了在其模式下發生了中斷,系統能夠捕獲到這樣的錯誤,爲debug提供有用的信息。