ARM Linux中斷機(jī)制分析
以下代碼基于內(nèi)核linux2.6.38.3(trimslice官網(wǎng)下載)
本文引用地址:http://m.butianyuan.cn/article/201611/317921.htm本文主要分析ARM發(fā)生中斷時(shí)的處理流程,以在usr態(tài)發(fā)生IRQ為例,即usr—>irq為例討論。
1.內(nèi)核異常向量表的初始化
1.1初始化大致流程
ARM linux內(nèi)核啟動(dòng)時(shí),首先運(yùn)行的是linux/arch/arm/kernel/head.S,進(jìn)行一些初始化工作,然后調(diào)用main.c->start_kernel()函數(shù),進(jìn)而調(diào)用trap_init()(或者調(diào)用early_trap_init()函數(shù)進(jìn)行初始化)、init_IRQ()函數(shù)進(jìn)行中斷初始化、建立異常向量表.
1.2異常向量表的建立
異常向量表的建立過(guò)程就是拷貝過(guò)程,為了將內(nèi)核代碼寫(xiě)成位置無(wú)關(guān)的,有很多地方需要注意。
1.2.1異常向量表基地址確定
在ARM V4及V4T以后的大部分處理器中,中斷向量表的位置可以有兩個(gè)位置:一個(gè)是0x00000000,另一個(gè)是0xffff0000??梢酝ㄟ^(guò)CP15協(xié)處理器c1寄存器中V位(bit[13])控制。V和中斷向量表的對(duì)應(yīng)關(guān)系如下:
V=0 ~ 0x00000000~0x0000001C
V=1 ~ 0xffff0000~0xffff001C
注:CP15控制寄存器說(shuō)明詳見(jiàn)ARM ARMB4-1690.
在異常向量表初始化前運(yùn)行的文件linux/arch/arm/kernel/head.S中設(shè)置了CP15寄存器(在~/arch/arm/mm/proc-v7.S文件中的__v7_setup函數(shù)中設(shè)置),這里通過(guò)設(shè)置CP15的c1寄存器已經(jīng)確定了異常向量表的基地址(0xffff0000)。
1.2.2 異常向量表拷貝過(guò)程
內(nèi)核代碼編譯生成后,需要將異常向量表拷貝到指定位置(0x00000000 or 0xffff0000),這就需要將內(nèi)核中的異常向量表設(shè)計(jì)成與位置無(wú)關(guān)的。
本文所使用內(nèi)核版本使用了early_trap_init()代替trap_init()來(lái)初始化異常。
early_trap_init()在linux/arch/arm/kernel/traps.c中,代碼如下:
1、CONFIG_VECTORS_BASE在處理器型號(hào)確定后就已經(jīng)確定,其值在內(nèi)核配置完成后自動(dòng)生成,保存在.config文件中。本文使用內(nèi)核版本在maketrimslice_deconfig后自動(dòng)生成的.config中定義:CONFIG_VECTORS_BASE=0xffff0000,也就是說(shuō),異常向量表的基地址0xffff0000。
~/arch/arm/kernel/traps.c line783 void __init early_trap_init(void) { #if defined(CONFIG_CPU_USE_DOMAINS) unsigned longvectors = CONFIG_VECTORS_BASE; //vectors是中斷向量基地址 #else unsigned long vectors = (unsigned long)vectors_page; #endif /*以下這些都在arch/arm/kernel/entry-armv.S下定義*/ extern char __stubs_start[], __stubs_end[]; extern char __vectors_start[], __vectors_end[]; extern char __kuser_helper_start[], __kuser_helper_end[]; int kuser_sz = __kuser_helper_end - __kuser_helper_start; /* * 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. */ /*__vectors_end至__vectors_start之間為異常向量表。__stubs_end至__stubs_start之間是異常處理的位置。這些變量定義都在arch/arm/kernel/entry-armv.S中*/ 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); /* * Do processor specific fixups for the kuser helpers */ kuser_get_tls_init(vectors); /* * Copy signal return handlers into the vector page, and * set sigreturn to be a pointer to these. */ memcpy((void *)(vectors + KERN_SIGRETURN_CODE - CONFIG_VECTORS_BASE), sigreturn_codes, sizeof(sigreturn_codes)); memcpy((void *)(vectors + KERN_RESTART_CODE - CONFIG_VECTORS_BASE), syscall_restart_code, sizeof(syscall_restart_code)); flush_icache_range(vectors, vectors + PAGE_SIZE); modify_domain(DOMAIN_USER, DOMAIN_CLIENT); } |
以下是__vectors_start, __vectors_end,__stubs_end__stubs_start的定義。
arch/arm/kernel/entry-armv.S .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 .globl__stubs_start __stubs_start: /* * Interrupt dispatcher */ vector stub irq,IRQ_MODE,4 //vector_stub是一個(gè)宏,展開(kāi)后是一塊代碼,后面緊跟著跳轉(zhuǎn)表 .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) .long __irq_invalid @ 4 …… …… …… …… .globl__stubs_end __stubs_end: .equ stubs_offset, __vectors_start + 0x200 - __stubs_start vector_stub irq, IRQ_MODE, 4展開(kāi)后如下: // -------------------------------- begin展開(kāi) .align5//將異常入口強(qiáng)制進(jìn)行2^5字節(jié)對(duì)齊,即一個(gè)cache line大小對(duì)齊,出于性能考慮 vector_irq: sublr, lr, 4//需要調(diào)整pc返回值,對(duì)于irq異常,將lr減去4,對(duì)于其他異常需要作出不同調(diào)整 @ Save r0, lr_ @ (parent CPSR) @ stmiasp, {r0, lr}@ save r0, lr mrslr, spsr strlr, [sp, #8]@ save spsr @ Prepare for SVC32 mode.IRQs remain disabled. @ mrsr0, cpsr eorr0, r0, IRQ_MODE ^ SVC_MODE) msrspsr_cxsf, r0 @ the branch table must immediately follow this code @ andlr, lr, #0x0f movr0, sp ldrlr, [pc, lr, lsl #2] movspc, lr@ branch to handler in SVC mode // -------------------------------- end展開(kāi) |
異常向量表的拷貝過(guò)程用圖表示比較清晰,如下圖所示:
圖一 向量表搬移及offset偏移量計(jì)算示圖
圖一說(shuō)明:上面兩條有方向的橫線,橫線方向代表地址生長(zhǎng)方向,下面那個(gè)是Code/Load視圖,是搬移前的代碼在生成的二進(jìn)制內(nèi)核中的組織情況,上面的Exec view是代碼在內(nèi)存中開(kāi)始執(zhí)行后的分配情況。
2.linux對(duì)ARM異常、中斷的處理流程
2.1當(dāng)IRQ發(fā)生時(shí),硬件完成的操作
R14_irq= address of next instruction to be executed + 4/*將寄存器lr_mode設(shè)置成返回地址*/
SPSR_irq = CPSR /*保存處理器當(dāng)前狀態(tài)、中斷屏蔽位以及各條件標(biāo)志位*/
CPSR[4:0] = 0b10010 /*設(shè)置當(dāng)前程序狀態(tài)寄存器CPSR中相應(yīng)的位進(jìn)入IRQ模式*/
CPSR[5] = 0 /*在ARM狀態(tài)執(zhí)行*/
/*CPSR[6] 不變*/
CPSR[7] = 1 /*禁止正常中斷*/
If high vectors configured then
PC=0xFFFF0018 /*將程序計(jì)數(shù)器(PC)值設(shè)置成該異常中斷的中斷向量地址,從
*而跳轉(zhuǎn)到相應(yīng)的異常中斷處理程序處執(zhí)行,對(duì)于ARMv7向量表普遍是0xFFFF0018
*/
else
PC=0x00000018
2.2 指令流跳轉(zhuǎn)過(guò)程
以上CPU操作完成后,PC跳轉(zhuǎn)到0xFFFF0018,該地址就是指令W(b) vector_irq + stubs_offset所在地址。然后跳轉(zhuǎn)到vector_stub irq,IRQ_MODE, 4,去執(zhí)行相應(yīng)的異常、中斷處理函數(shù)。
接下來(lái)具體看代碼:
.globl __vectors_start //異常向量表開(kāi)始0xFFFF0000 __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 //中斷發(fā)生后的跳轉(zhuǎn)地址0xFFFF0018 W(b) vector_fiq + stubs_offset .globl __vectors_end __vectors_end: |
stubs_offset只是個(gè)偏移量,用來(lái)修正跳轉(zhuǎn)地址的,主要的操作是vector_irq執(zhí)行。vector_irq是由宏vector_stub irq,IRQ_MODE,4(IRQ_MODE在includeasmptrace.h中定義:0x12)生成。以下是vector_irq生成后的代碼(匯編代碼中,@開(kāi)始的語(yǔ)句、//、//都代表注釋):
.align 5 vector_irq: sub lr, lr, 4//因?yàn)楫惓0l(fā)生時(shí),cpu將pc地址+4賦值給lr,這里做修正。 @ Save r0, lr_ @ (parent CPSR) @ stmia sp, {r0, lr}//保存r0, lr,到irq堆棧中(每個(gè)異常都有屬于自己的堆棧) mrs lr, spsr //lr保存spsr_irq的值,即usr狀態(tài)的cpsr的值(見(jiàn)2.1) str lr, [sp, #8]//保存spsr到[sp+8]處 @ Prepare for SVC32 mode. IRQs remain disabled. @ mrs r0, cpsr eor r0, r0,#( IRQ_MODE ^ SVC_MODE| PSR_ISETSTATE) // PSR_ISETSTATE:選擇ARM/Thumb指令集 msr spsr_cxsf, r0//這里的cxsf表示從低到高分別占用的4個(gè)8bit的數(shù)據(jù)域 |
異或運(yùn)算是可以交換位置的,也即A^B^C等價(jià)于A^C^B。所以這里的r0^( IRQ_MODE ^ SVC_MODE| PSR_ISETSTATE)等價(jià)于r0^ IRQ_MODE ^SVC_MODE,由于r0的低5位模式位與IRQ_MODE相同,所以r0^ IRQ_MODE的運(yùn)算結(jié)果的低5位全被清零,然后再^SVC_MODE,也即低5位被設(shè)置為SVC_MODE,其它位保持不變。
@ the branch table must immediately follow this code and lr, lr, #0x0f//提取發(fā)生異常前的處理器模式,這里也就是usr模式 mov r0, sp ldr lr, [pc, lr, lsl #2] movs pc, lr |
sp是SVC32模式下的堆棧指針,這里將它移到r0中,就可以作為C函數(shù)的第一個(gè)參數(shù),即C函數(shù)中的pt_regs參數(shù)。
pc是當(dāng)前地址+8,也就是本段代碼后面緊跟的跳轉(zhuǎn)表的基地址,lr用于在跳轉(zhuǎn)表中索引,lr左移兩位等同于*4,因?yàn)槊總€(gè)條目是4字節(jié)。從usr模式進(jìn)入irq模式,則lr=pc+4*0,若從svc模式進(jìn)入irq,則lr=pc+4*3,即__irq_svc的地址,其他地址進(jìn)入__irq_invalid出錯(cuò)處理,因?yàn)椴荒軓钠渌J竭M(jìn)入irq異常。
假設(shè)這里是從usr進(jìn)入irq,則執(zhí)行跳轉(zhuǎn)表中的第一條指令。跳轉(zhuǎn)的基準(zhǔn)地址為當(dāng)前pc,因?yàn)锳RMv4是三級(jí)流水線結(jié)構(gòu)的,它總是指向當(dāng)前指令的下兩條指令的地址,盡管以后版本的指令流水線擴(kuò)展為5級(jí)和8級(jí),但是這一特性一直被兼容處理,也即pc(excute)=pc(fetch) + 8,其中:pc(fetch)是當(dāng)前正在執(zhí)行的指令,就是之前取該指令時(shí)候的PC的值;pc(execute):當(dāng)前指令執(zhí)行的,計(jì)算中如果用到pc,是指此時(shí)pc的值。
當(dāng)mov指令的目標(biāo)寄存器是PC,且指令以S結(jié)束,則它會(huì)把spsr的值恢復(fù)給cpsr,上面說(shuō)到當(dāng)前的spsr中保存的是r0的值,即svc模式。所以本條指令是跳轉(zhuǎn)到__irq_usr的同時(shí)將處理器模式轉(zhuǎn)為svc模式。異常處理一定要進(jìn)入svc模式的原因是:異常處理一定要進(jìn)入PL1特權(quán)級(jí);另一個(gè)原因是使能嵌套中斷。具體原因在問(wèn)題4中解釋。關(guān)于__irq_svc和__irq_invalid暫時(shí)不討論。
/* * Interrupt dispatcher以下跳轉(zhuǎn)表必須緊跟在vector_irq之后 */ vector_stub irq, IRQ_MODE, 4 //生成vector_irq /*從用戶態(tài)進(jìn)入中斷的處理函數(shù)*/ .long __irq_usr @ 0 (USR_26 / USR_32) .long __irq_invalid @ 1 (FIQ_26 / FIQ_32) .long __irq_invalid @ 2 (IRQ_26 / IRQ_32) /*從SVC進(jìn)入中斷的處理函數(shù)*/ .long __irq_svc @ 3 (SVC_26 / SVC_32) .long __irq_invalid @ 4 .long __irq_invalid @ 5 .long __irq_invalid @ 6 |
圖2IRQ中斷處理跳轉(zhuǎn)示意圖
注意,以下操作都是在svc模式中,因?yàn)橐栌肧VC模式進(jìn)行ISP處理,所以需要保存所有SVC模式下的寄存器到SVC堆棧中,最后才去調(diào)用中斷服務(wù)例程(ISP)irq_handler。
2.2.1 __irq_usr
.align 5 __irq_usr: usr_entry //用于用戶模式下發(fā)生中斷時(shí)初始化中斷處理堆棧,同時(shí)保存所有SVC態(tài)寄存器到堆棧。 kuser_cmpxchg_check //對(duì)低版本的ARM核來(lái)說(shuō),用戶態(tài)無(wú)法實(shí)現(xiàn)原子比較交換。如果用戶態(tài)在處理原 //子比較交換的過(guò)程中發(fā)生中斷,需要特殊處理,略過(guò) get_thread_info tsk //根據(jù)當(dāng)前sp指針,將該指針最右邊13位清0,獲得當(dāng)前任務(wù)的thread_info #ifdef CONFIG_PREEMPT//如果可以搶占,遞增任務(wù)的搶占計(jì)數(shù) ldr r8, [tsk, #TI_PREEMPT]//T被定義為offsetof(struct thread_info, preempt_count),顯然通過(guò)tsk就 可以很容易得到進(jìn)程preempt_count成員的地址了 add r7, r8, #1 @ increment it str r7, [tsk, #TI_PREEMPT] #endif irq_handler //中斷服務(wù)例程,后面分析 #ifdef CONFIG_PREEMPT ldr r0, [tsk, #TI_PREEMPT]//獲得當(dāng)前的搶占計(jì)數(shù) str r8, [tsk, #TI_PREEMPT]//并將r8中的值保存回去。相當(dāng)于將前一步遞增的搶占計(jì)數(shù)減回去了 teq r0, r7//r0,r7是調(diào)用irq_handler前后的搶占計(jì)數(shù),這里進(jìn)行比較,是防止驅(qū)動(dòng)的ISR //程序沒(méi)有配對(duì)操作搶占計(jì)數(shù)導(dǎo)致系統(tǒng)錯(cuò)誤。 ARM( strne r0, [r0, -r0] )//如果搶占計(jì)數(shù)被破壞,則強(qiáng)制寫(xiě)入0. THUMB( movne r0, #0 ) THUMB( strne r0, [r0] ) #endif mov why, #0 b ret_to_user //返回到用戶態(tài) UNWIND(.fnend ) ENDPROC(__irq_usr) |
接下來(lái)分別看各個(gè)函數(shù)的功能
arch/arm/include/asm/ptrace.h struct pt_regs { unsigned long uregs[18]; }; #endif /* __KERNEL__ */ #define ARM_cpsr uregs[16] #define ARM_pc uregs[15] #define ARM_lr uregs[14] #define ARM_sp uregs[13] #define ARM_ip uregs[12] #define ARM_fp uregs[11] #define ARM_r10 uregs[10] #define ARM_r9 uregs[9] …… #define ARM_ORIG_r0 uregs[17] |
pt_regs結(jié)構(gòu)體定義,后面要用到。
.macrousr_entry //usr_entry宏定義 UNWIND(.fnstart ) UNWIND(.cantunwind ) @ dont unwind the user space sub sp, sp, #S_FRAME_SIZE @ S_FRAME_SIZE定義在trimslice-kernelarcharmkernelarm-offsets.c中S_FRAME_SIZE被定義為sizeof(struct pt_regs)的大小=18*4=72字節(jié),將svc32堆棧指針向低地址方向移動(dòng)一個(gè)pt_regs結(jié)構(gòu)大小,用于保存svc模式下的寄存器現(xiàn)場(chǎng)。 ARM( stmib sp, {r1 - r12} )@向svc32堆棧中保存寄存器現(xiàn)場(chǎng) THUMB( stmia sp, {r0 - r12} ) ldmia r0, {r3 - r5}@前面r0存放的是irq模式下的棧指針sp的值,從棧中取出r0-r2存放到r3-r5中 add r0, sp, #S_PC@ here for interlock avoidance;S_PC定義為offsetof(struct pt_regs, ARM_pc),所 以這里通過(guò)add指令將r0指向ARM_pc mov r6, #-1 @r6中保存-1 str r3, [sp] @ save the "real" r0 copied從中斷棧中取出真實(shí)的r0存放到pt_regs->r0中。 @ from the exception stack |
@ We are now ready to fill in the remaining blanks on the stack: @ @ r4 - lr_ @ r5 - spsr_ @ r6 - orig_r0 (see pt_regs definition in ptrace.h) @ @ Also, separately save sp_usr and lr_usr @ stmia r0, {r4 - r6}//stmia將svc模式下的寄存器r4-r6保存到ARM_pc,ARM_cpsr和 ARM_ORIG_r0,顯然ARM_ORIG_r0保存了-1(r6)這個(gè)常量 ARM( stmdb r0, {sp, lr}^ )//stmdb指令的^標(biāo)志表示存儲(chǔ)發(fā)生中斷的模式下的sp,lr寄存器 到ARM_sp和ARM_lr中。 THUMB( store_user_sp_lr r0, r1, S_SP - S_PC ) @ Enable the alignment trap while in kernel mode alignment_trap r0//alignment_trap在配置CONFIG_ALIGNMENT_TRAP時(shí)有效,如果開(kāi)啟了該選 //項(xiàng),中斷處理中將支持對(duì)齊跟蹤 @ Clear FP to mark the first stack frame zero_fp//zero_fp用來(lái)設(shè)置fp棧幀寄存器為0 #ifdef CONFIG_IRQSOFF_TRACER bl trace_hardirqs_off #endif .endm@usr_entry宏定義結(jié)束 |
以上的指令的作用可以總結(jié)如下,其中將普通寄存器r1到r12保存到ARM_r1- ARM_r12,這相當(dāng)于把發(fā)生中斷時(shí)的寄存器r1-r12進(jìn)行了保存。接下來(lái)保存中斷發(fā)生時(shí)的r0,lr_irq和spsr_irq保存到r1-r3,r4賦值為-1,它們接下來(lái)將被使用。接下來(lái)保存r0到ARM_r0,lr_irq,spsr_irq和-1到ARM_pc ARM_cpsr ARM_ORIG_R0,注意到stmdb指令中的"^",它保存sp_usr和lr_usr分別到ARM_sp和ARM_lr,顯然這里將所有中斷發(fā)生時(shí)的寄存器都進(jìn)行了保存。
圖3 保存中斷堆棧
2.2.3 get_thread_info
get_thread_info宏用來(lái)根據(jù)當(dāng)前的sp值,通過(guò)lsr和lsl分別右移左移13位,相當(dāng)于對(duì)sp向下圓整到8K對(duì)齊。這里也就是thread_info所在的地址。
arch/arm/kernel/entry-header.S .macroget_thread_info, rd mov rd, sp, lsr #13 mov rd, rd, lsl #13 .endm |
linux/arch/arm/kernel/entry-armv.S /* * Interrupt handling. Preserves r7, r8, r9 */ .macroirq_handler #ifdef CONFIG_MULTI_IRQ_HANDLER ldr r5, =handle_arch_irq mov r0, sp ldr r5, [r5] adr lr, BSYM(9997f) teq r5, #0 movne pc, r5 #endif arch_irq_handler_default 9997: .endm |
linux/arch/arm/kernel/entry-armv.S
/*
* Interrupt handling. Preserves r7, r8, r9
*/
.macroirq_handler
#ifdefCONFIG_MULTI_IRQ_HANDLER
ldr r5,=handle_arch_irq
mov r0,sp
ldr r5,[r5]
adr lr,BSYM(9997f)
teq r5,#0
movne pc,r5
#endif
arch_irq_handler_default
9997:
.endm
2.2.5 arch_irq_handler_default
irq_handler是真正的IRQ中斷處理入口,在中斷處理中需要預(yù)留r7,r8和r9寄存器。它們被用來(lái)處理內(nèi)核搶占。在沒(méi)有配置MULTI_IRQ_HANDLER 的情況下,irq_handler的邏輯很簡(jiǎn)單,就是簡(jiǎn)單的調(diào)用arch_irq_handler_default。
如果配置了該選項(xiàng),平臺(tái)代碼可以修改全局變量:handle_arch_irq,這里只討論默認(rèn)實(shí)現(xiàn).
arch/arm/include/asm/entry_macro_multi.S /* * Interrupt handling. Preserves r7, r8, r9 */ .macroarch_irq_handler_default //get_irqnr_preamble用來(lái)獲取中斷狀態(tài)寄存器基地址 get_irqnr_preamble r5, lr//將中斷控制器的狀態(tài)寄存器的地址存儲(chǔ)到r5 1: get_irqnr_and_base r0, r6, r5, lr//判斷中斷號(hào),通過(guò)r0返回 movne r1, sp//如果還存在中斷,就將sp作為第二個(gè)參數(shù),調(diào)用asm_do_IRQ。sp目前指向pt_regs @ @ routine called with r0 = irq number, r1 = struct pt_regs * @ adrne lr, BSYM(1b)//這里將lr設(shè)置為get_irqnr_and_base的第二條指令,因?yàn)榈诙窝h(huán)時(shí),不必執(zhí)行其第一條指令(加載寄存器基址) bne asm_do_IRQ //將中斷號(hào)、pt_regs(中斷前的寄存器現(xiàn)場(chǎng))傳遞給asm_do_IRQ。asm_do_IRQ返回時(shí), //會(huì)返回到get_irqnr_and_base處,直到所有中斷都已經(jīng)處理完畢才退出循環(huán)。 #ifdef CONFIG_SMP//針對(duì)SMP系統(tǒng)的處理 /* * this macro assumes that irqstat (r6) and base (r5) are * preserved from get_irqnr_and_base above */ ALT_SMP(test_for_ipi r0, r6, r5, lr)//這里是從寄存器中讀取ipi標(biāo)志 ALT_UP_B(9997f) movne r1, sp adrne lr, BSYM(1b)//同理,這里也是將返回地址設(shè)置為ALT_SMP的第二條指令,構(gòu)造成一個(gè)循環(huán) bne do_IPI//只要存在IPI就調(diào)用do_IPI,并循環(huán)直到處理完所有IPI #ifdef CONFIG_LOCAL_TIMERS//同理,這里循環(huán)處理多核系統(tǒng)中的本地時(shí)鐘中斷。 test_for_ltirq r0, r6, r5, lr movne r0, sp adrne lr, BSYM(1b) bne do_local_timer #endif #endif 9997: .endm |
2.2.6 get_irqnr_preamble
get_irqnr_preamble用于獲得中斷狀態(tài)寄存器基地址,特定于CPU,這里CPU用的是tegra,其定義如下
/* arch/arm/mach-tegra/include/mach/entry-macro.S /* Uses the GIC interrupt controller built into the cpu */ #define ICTRL_BASE (IO_CPU_VIRT + 0x40100)// #define IO_CPU_VIRT 0xFE000000 .macroget_irqnr_preamble, base, tmp movw base, #(ICTRL_BASE & 0x0000ffff) movt base, #((ICTRL_BASE & 0xffff0000) >> 16) .endm |
2.2.7 get_irqnr_and_base
get_irqnr_and_base用來(lái)獲取中斷號(hào)。
/* arch/arm/mach-tegra/include/mach/entry-macro.S .macro get_irqnr_and_base, irqnr, irqstat, base, tmp ldr irqnr, [base, #0x20] @ EVT_IRQ_STS cmp irqnr, #0x80 .endm |
get_irqnr_preamble和get_irqnr_and_base兩個(gè)宏由machine級(jí)的代碼定義,目的就是從中斷控制器中獲得IRQ編號(hào),緊接著就調(diào)用asm_do_IRQ,從這個(gè)函數(shù)開(kāi)始,中斷程序進(jìn)入C代碼中,傳入的參數(shù)是IRQ編號(hào)和寄存器結(jié)構(gòu)指針,
2.2.8 asm_do_IRQ
圖4 asm_do_IRQ流程圖
asm_do_IRQ是ARM處理硬件中斷的核心函數(shù),第一個(gè)參數(shù)指定了硬中斷的中斷號(hào),第二個(gè)參數(shù)是寄存器備份組成的一個(gè)結(jié)構(gòu)體,保存了中斷發(fā)生時(shí)的模式對(duì)應(yīng)的寄存器的值,在中斷返回時(shí)使用。
linux/arch/arm/kernel/irq.c asmlinkage void __exception_irq_entry asm_do_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%un", irq); ack_bad_irq(irq); } else { generic_handle_irq(irq); } /* AT91 specific workaround */ irq_finish(irq); irq_exit(); set_irq_regs(old_regs); } |
2.2.9 irq_enter
irq_enter是更新一些系統(tǒng)的統(tǒng)計(jì)信息,同時(shí)在__irq_enter宏中禁止了進(jìn)程的搶占。雖然在產(chǎn)生IRQ時(shí),ARM會(huì)自動(dòng)把CPSR中的I位置位,禁止新的IRQ請(qǐng)求,直到中斷控制轉(zhuǎn)到相應(yīng)的流控層后才通過(guò)local_irq_enable()打開(kāi)。那為何還要禁止搶占?這是因?yàn)橐紤]中斷嵌套的問(wèn)題,一旦流控層或驅(qū)動(dòng)程序主動(dòng)通過(guò)local_irq_enable打開(kāi)了IRQ,而此時(shí)該中斷還沒(méi)處理完成,新的irq請(qǐng)求到達(dá),這時(shí)代碼會(huì)再次進(jìn)入irq_enter,在本次嵌套中斷返回時(shí),內(nèi)核不希望進(jìn)行搶占調(diào)度,而是要等到最外層的中斷處理完成后才做出調(diào)度動(dòng)作,所以才有了禁止搶占這一處理。
linux/kernel/softirq.c voidirq_enter(void) { int cpu = smp_processor_id(); rcu_irq_enter(); if (idle_cpu(cpu) && !in_interrupt()) { /* Prevent raise_softirq from needlessly waking up ksoftirqd * here, as softirq will be serviced on return from interrupt.*/ local_bh_disable(); tick_check_idle(cpu); _local_bh_enable(); } __irq_enter(); } #define __irq_enter() do { account_system_vtime(current); add_preempt_count(HARDIRQ_OFFSET); trace_hardirq_enter(); } while (0) |
2.2.10 generic_handle_irq
~/include /linux/Irqdesc.h /* * Architectures call this to let the generic IRQ layer * handle an interrupt. If the descriptor is attached to an * irqchip-style controller then we call the ->handle_irq() handler, * and it calls __do_IRQ() if its attached to an irqtype-style controller. */ static inline void generic_handle_irq_desc(unsigned int irq,struct irq_desc *desc) { desc->handle_irq(irq, desc);//調(diào)用該irq注冊(cè)的函數(shù)處理,該函數(shù)在注冊(cè)中斷時(shí)填寫(xiě)irq_desc結(jié)構(gòu)體時(shí)指定。 }// handle_irq是個(gè)函數(shù)指針,它用來(lái)實(shí)現(xiàn)中斷處理器的電流處理。電流處理分為邊 //沿跳變處理和電平處理。 static inline void generic_handle_irq(unsigned int irq)//該函數(shù)是與體系結(jié)構(gòu)無(wú)關(guān)的通用邏輯層API { generic_handle_irq_desc(irq, irq_to_desc(irq)); } |
2.2.11 ret_to_user
以上內(nèi)容處理結(jié)束后,退回用戶層。
arch/arm/kernel/entry-common.S /* * "slow" syscall return path. "why" tells us if this was a real syscall. */ ENTRY(ret_to_user) ret_slow_syscall: disable_irq @ disable interrupts ldr r1, [tsk, #TI_FLAGS]//從任務(wù)的TI_FLAGS標(biāo)志判斷是否需要處理?yè)屨蓟蛘咝盘?hào)。 tst r1, #_TIF_WORK_MASK bne work_pending//處理?yè)屨蓟蛘咝盘?hào) no_work_pending: //沒(méi)有搶占或者信號(hào)需要處理,或者已經(jīng)處理完畢,開(kāi)始退回用戶態(tài) #if defined(CONFIG_IRQSOFF_TRACER)//退回用戶態(tài)必然會(huì)打開(kāi)中斷,這里記錄下打開(kāi)中斷的事實(shí),供調(diào)試用。 asm_trace_hardirqs_on #endif /* perform architecture specific actions before user return */ arch_ret_to_user r1, lr//在返回用戶態(tài)前,處理各個(gè)體系結(jié)構(gòu)的鉤子 restore_user_regs fast = 0, offset = 0//恢復(fù)寄存器現(xiàn)場(chǎng),并切回用戶態(tài)。這里不再具體分析恢復(fù)方式。 ENDPROC(ret_to_user) |
3.問(wèn)題及解答
問(wèn)題1:vector_irq已經(jīng)是異常、中斷處理的入口函數(shù)了,為什么還要加stubs_offset?( b vector_irq + stubs_offset)
答:(1)內(nèi)核剛啟動(dòng)時(shí)(head.S文件)通過(guò)設(shè)置CP15的c1寄存器已經(jīng)確定了異常向量表的起始地址(例如0xffff0000),因此需要把已經(jīng)寫(xiě)好的內(nèi)核代碼中的異常向量表考到0xffff0000處,只有這樣在發(fā)生異常時(shí)內(nèi)核才能正確的處理異常。
(2)從上面代碼看出向量表和stubs(中斷處理函數(shù))都發(fā)生了搬移,如果還用bvector_irq,那么實(shí)際執(zhí)行的時(shí)候就無(wú)法跳轉(zhuǎn)到搬移后的vector_irq處,因?yàn)橹噶畲a里寫(xiě)的是原來(lái)的偏移量,所以需要把指令碼中的偏移量寫(xiě)成搬移后的。至于為什么搬移后的地址是vector_irq+stubs_offset,如圖一所示。下圖是搬移示意圖更加清晰說(shuō)明了搬移過(guò)程。。
問(wèn)題2:為什么在異常向量表中,用b指令跳轉(zhuǎn)而不是用ldr絕對(duì)跳轉(zhuǎn)?
答:因?yàn)槭褂胋指令跳轉(zhuǎn)比絕對(duì)跳轉(zhuǎn)(ldr pc,XXXX)效率高,正因?yàn)樾矢撸园裚_stubs_start~__stubs_end之間的代碼考到了0xffff0200起始處。
注意:
因?yàn)閎跳轉(zhuǎn)指令只能在+/-32MB之內(nèi)跳轉(zhuǎn),所以必須拷貝到0xffff0000附近。
b指令是相對(duì)于當(dāng)前PC的跳轉(zhuǎn),當(dāng)匯編器看到 B 指令后會(huì)把要跳轉(zhuǎn)的標(biāo)簽轉(zhuǎn)化為相對(duì)于當(dāng)前PC的偏移量寫(xiě)入指令碼。
經(jīng)過(guò)Uboot的啟動(dòng),內(nèi)核跳入linux/arch/arm/kernel/head.S開(kāi)始執(zhí)行。
問(wèn)題1:為什么首先進(jìn)入head.S開(kāi)始執(zhí)行?
問(wèn)題3:為什么首先進(jìn)入head.S開(kāi)始執(zhí)行?
答:內(nèi)核源代碼頂層目錄下的Makefile制定了vmlinux生成規(guī)則:
# vmlinux image - includingupdated kernel symbols
vmlinux: $(vmlinux-lds)$(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o)FORCE
其中$(vmlinux-lds)是編譯連接腳本,對(duì)于ARM平臺(tái),就是arch/arm/kernel/vmlinux-lds文件。vmlinux-init也在頂層Makefile中定義:
vmlinux-init := $(head-y)$(init-y)
head-y 在arch/arm/Makefile中定義:
head-y:=arch/arm/kernel/head$(MMUEX T).o arch/arm/kernel/init_task.o
…
ifeq ($(CONFIG_MMU),)
MMUEXT := -nommu
endif
對(duì)于有MMU的處理器,MMUEXT為空白字符串,所以arch/arm/kernel/head.O 是第一個(gè)連接的文件,而這個(gè)文件是由arch/arm/kernel/head.S編譯產(chǎn)生成的。
綜合以上分析,可以得出結(jié)論,非壓縮ARM Linux內(nèi)核的入口點(diǎn)在arch/arm/kernel/head.s中。
問(wèn)題4: 中斷為什么必須進(jìn)入svc模式?
一個(gè)最重要原因是:
如果一個(gè)中斷模式(例如從usr進(jìn)入irq模式,在irq模式中)中重新允許了中斷,并且在這個(gè)中斷例程中使用了BL指令調(diào)用子程序,BL指令會(huì)自動(dòng)將子程序返回地址保存到當(dāng)前模式的lr(即r14_irq)中,這個(gè)地址隨后會(huì)被在當(dāng)前模式下產(chǎn)生的中斷所破壞,因?yàn)楫a(chǎn)生中斷時(shí)CPU會(huì)將當(dāng)前模式的PC保存到r14_irq,這樣就把剛剛保存的子程序返回地址沖掉。為了避免這種情況,中斷例程應(yīng)該切換到SVC或者系統(tǒng)模式,這樣的話,BL指令可以使用r14_svc來(lái)保存子程序的返回地址。
問(wèn)題5:為什么跳轉(zhuǎn)表中有的用了b指令跳轉(zhuǎn),而有的用了ldr px,xxxx?
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
.LCvswi:
.word vector_swi
由于系統(tǒng)調(diào)用異常的代碼編譯在其他文件中,其入口地址與異常向量相隔較遠(yuǎn),使用b指令無(wú)法跳轉(zhuǎn)過(guò)去(b指令只能相對(duì)當(dāng)前pc跳轉(zhuǎn)32M范圍)。因此將其地址存放到LCvswi中,并從內(nèi)存地址中加載其入口地址,原理與其他調(diào)用是一樣的。這也就是為什么系統(tǒng)調(diào)用的速度稍微慢一點(diǎn)的原因。
問(wèn)題6:為什么ARM能處理中斷?
因?yàn)锳RM架構(gòu)的CPU有一個(gè)機(jī)制,只要中斷產(chǎn)生了,CPU就會(huì)根據(jù)中斷類型自動(dòng)跳轉(zhuǎn)到某個(gè)特定的地址(即中斷向量表中的某個(gè)地址)。如下表所示,既是中斷向量表。
ARM中斷向量表及地址
問(wèn)題7:什么是High vector?
A:在Linux3.1.0,arch/arm/include/asm/system.hline121 有定義如下:
#if __LINUX_ARM_ARCH__ >=4
#define vectors_high() (cr_alignment & CR_V)
#else
#define vectors_high() (0)
#endif
意思就是,如果使用的ARM架構(gòu)大于等于4,則定義vectors_high()=cr_alignment&CR_V,該值就等于0xffff0000
在Linux3.1.0,arch/arm/include/asm/system.hline33有定義如下:
#define CR_V (1 << 13) /* Vectors relocated to 0xffff0000 */
arm下規(guī)定,在0x00000000或0xffff0000的地址處必須存放一張?zhí)D(zhuǎn)表。
問(wèn)題8:中斷向量表是如何存放到0x00000000或0xffff0000地址的?
A:Uboot執(zhí)行結(jié)束后會(huì)把Linux內(nèi)核拷貝到內(nèi)存中開(kāi)始執(zhí)行,linux內(nèi)核執(zhí)行的第一條指令是linux/arch/arm/kernel/head.S,此文件中執(zhí)行一些參數(shù)設(shè)置等操作后跳入linux/init/main.c文件的start_kernel函數(shù),此函數(shù)調(diào)用一系列初始化函數(shù),其中trip_init()函數(shù)實(shí)現(xiàn)向量表的設(shè)定操作。
參考文獻(xiàn)
1. ARM Linux中斷向量表搬移設(shè)計(jì)過(guò)程http://blog.chinaunix.net/uid-361890-id-175347.html
2. 《LINUX3.0內(nèi)核源代碼分析》第二章:中斷和異常 http://blog.chinaunix.net/uid-25845340-id-2982887.html
3. Kernel Memory Layout on ARM Linuxhttp://www.arm.linux.org.uk/developer/memory.txt
4.http://emblinux.sinaapp.com/ar01s16.html#id3603818
5. Linux中斷(interrupt)子系統(tǒng)之二:arch相關(guān)的硬件封裝層http://blog.csdn.net/droidphone/article/details/7467436
附錄1
Kernel Memory Layout on ARM Linux
Start End Use
--------------------------------------------------------------------------
ffff8000 ffffffff copy_user_page / clear_user_page use.
ForSA11xx and Xscale, this is used to
setupa minicache mapping.
ffff1000 ffff7fff Reserved.
Platformsmust not use this address range.
ffff0000 ffff0fff CPUvector page.
The CPU vectors are mapped here ifthe
CPU supports vector relocation(control
register V bit.)
ffc00000 fffeffff DMA memory mapping region. Memory returned
bythe dma_alloc_xxx functions will be
dynamicallymapped here.
ff000000 ffbfffff Reserved for future expansion of DMA
mappingregion.
VMALLOC_END feffffff Free for platform use, recommended.
VMALLOC_ENDmust be aligned to a 2MB
boundary.
VMALLOC_START VMALLOC_END-1 vmalloc() /ioremap() space.
Memoryreturned by vmalloc/ioremap will
bedynamically placed in this region.
VMALLOC_STARTmay be based upon the value
ofthe high_memory variable.
PAGE_OFFSET high_memory-1 Kernel direct-mapped RAM region.
Thismaps the platforms RAM, and typically
mapsall platform RAM in a 1:1 relationship.
TASK_SIZE PAGE_OFFSET-1 Kernel module space
Kernelmodules inserted via insmod are
placedhere using dynamic mappings.
00001000 TASK_SIZE-1 User space mappings
Per-threadmappings are placed here via
themmap() system call.
00000000 00000fff CPU vector page / null pointer trap
CPUswhich do not support vector remapping
placetheir vector page here. NULL pointer
dereferencesby both the kernel and user
spaceare also caught via this mapping.
Please note that mappings which collidewith the above areas may result
in a non-bootable kernel, or may cause thekernel to (eventually) panic
at run time.
Since future CPUs may impact the kernelmapping layout, user programs
must not access any memory which is notmapped inside their 0x0001000
to TASK_SIZE address range. If they wish to access these areas, they
must set up their own mappings using open()and mmap().
評(píng)論