新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 設(shè)計應(yīng)用 > ARM Linux中斷機制分析

ARM Linux中斷機制分析

作者: 時間:2016-11-09 來源:網(wǎng)絡(luò) 收藏
——以用戶模式產(chǎn)生irq中斷為例

以下代碼基于內(nèi)核linux2.6.38.3(trimslice官網(wǎng)下載)

本文引用地址:http://m.butianyuan.cn/article/201611/317921.htm

本文主要分析ARM發(fā)生中斷時的處理流程,以在usr態(tài)發(fā)生IRQ為例,即usr—>irq為例討論。

1.內(nèi)核異常向量表的初始化

1.1初始化大致流程

ARM linux內(nèi)核啟動時,首先運行的是linux/arch/arm/kernel/head.S,進行一些初始化工作,然后調(diào)用main.c->start_kernel()函數(shù),進而調(diào)用trap_init()(或者調(diào)用early_trap_init()函數(shù)進行初始化)、init_IRQ()函數(shù)進行中斷初始化、建立異常向量表.

1.2異常向量表的建立

異常向量表的建立過程就是拷貝過程,為了將內(nèi)核代碼寫成位置無關(guān)的,有很多地方需要注意。

1.2.1異常向量表基地址確定

在ARM V4及V4T以后的大部分處理器中,中斷向量表的位置可以有兩個位置:一個是0x00000000,另一個是0xffff0000??梢酝ㄟ^CP15協(xié)處理器c1寄存器中V位(bit[13])控制。V和中斷向量表的對應(yīng)關(guān)系如下:

V=0 ~ 0x00000000~0x0000001C

V=1 ~ 0xffff0000~0xffff001C

注:CP15控制寄存器說明詳見ARM ARMB4-1690.

在異常向量表初始化前運行的文件linux/arch/arm/kernel/head.S中設(shè)置了CP15寄存器(在~/arch/arm/mm/proc-v7.S文件中的__v7_setup函數(shù)中設(shè)置),這里通過設(shè)置CP15的c1寄存器已經(jīng)確定了異常向量表的基地址(0xffff0000)。

1.2.2 異常向量表拷貝過程

內(nèi)核代碼編譯生成后,需要將異常向量表拷貝到指定位置(0x00000000 or 0xffff0000),這就需要將內(nèi)核中的異常向量表設(shè)計成與位置無關(guān)的。

本文所使用內(nèi)核版本使用了early_trap_init()代替trap_init()來初始化異常。

early_trap_init()在linux/arch/arm/kernel/traps.c中,代碼如下:

1、CONFIG_VECTORS_BASE在處理器型號確定后就已經(jīng)確定,其值在內(nèi)核配置完成后自動生成,保存在.config文件中。本文使用內(nèi)核版本在maketrimslice_deconfig后自動生成的.config中定義:CONFIG_VECTORS_BASE=0xffff0000,也就是說,異常向量表的基地址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是一個宏,展開后是一塊代碼,后面緊跟著跳轉(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展開后如下:

// -------------------------------- begin展開

.align5//將異常入口強制進行2^5字節(jié)對齊,即一個cache line大小對齊,出于性能考慮

vector_irq:

sublr, lr, 4//需要調(diào)整pc返回值,對于irq異常,將lr減去4,對于其他異常需要作出不同調(diào)整

@ Save r0, lr_ (parent PC) and spsr_

@ (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展開


異常向量表的拷貝過程用圖表示比較清晰,如下圖所示:

圖一 向量表搬移及offset偏移量計算示圖

圖一說明:上面兩條有方向的橫線,橫線方向代表地址生長方向,下面那個是Code/Load視圖,是搬移前的代碼在生成的二進制內(nèi)核中的組織情況,上面的Exec view是代碼在內(nèi)存中開始執(zhí)行后的分配情況。

2.linux對ARM異常、中斷的處理流程

2.1當IRQ發(fā)生時,硬件完成的操作

R14_irq= address of next instruction to be executed + 4/*將寄存器lr_mode設(shè)置成返回地址*/

SPSR_irq = CPSR /*保存處理器當前狀態(tài)、中斷屏蔽位以及各條件標志位*/

CPSR[4:0] = 0b10010 /*設(shè)置當前程序狀態(tài)寄存器CPSR中相應(yīng)的位進入IRQ模式*/

CPSR[5] = 0 /*在ARM狀態(tài)執(zhí)行*/

/*CPSR[6] 不變*/

CPSR[7] = 1 /*禁止正常中斷*/

If high vectors configured then

PC=0xFFFF0018 /*將程序計數(shù)器(PC)值設(shè)置成該異常中斷的中斷向量地址,從

*而跳轉(zhuǎn)到相應(yīng)的異常中斷處理程序處執(zhí)行,對于ARMv7向量表普遍是0xFFFF0018

*/

else

PC=0x00000018

2.2 指令流跳轉(zhuǎn)過程

以上CPU操作完成后,PC跳轉(zhuǎn)到0xFFFF0018,該地址就是指令W(b) vector_irq + stubs_offset所在地址。然后跳轉(zhuǎn)到vector_stub irq,IRQ_MODE, 4,去執(zhí)行相應(yīng)的異常、中斷處理函數(shù)。

接下來具體看代碼:

.globl __vectors_start //異常向量表開始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只是個偏移量,用來修正跳轉(zhuǎn)地址的,主要的操作是vector_irq執(zhí)行。vector_irq是由宏vector_stub irq,IRQ_MODE,4(IRQ_MODE在includeasmptrace.h中定義:0x12)生成。以下是vector_irq生成后的代碼(匯編代碼中,@開始的語句、//、//都代表注釋):

.align 5

vector_irq:

sub lr, lr, 4//因為異常發(fā)生時,cpu將pc地址+4賦值給lr,這里做修正。

@ Save r0, lr_ (parent PC) and spsr_

@ (parent CPSR)

@

stmia sp, {r0, lr}//保存r0, lr,到irq堆棧中(每個異常都有屬于自己的堆棧)

mrs lr, spsr //lr保存spsr_irq的值,即usr狀態(tài)的cpsr的值(見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個8bit的數(shù)據(jù)域

異或運算是可以交換位置的,也即A^B^C等價于A^C^B。所以這里的r0^( IRQ_MODE ^ SVC_MODE| PSR_ISETSTATE)等價于r0^ IRQ_MODE ^SVC_MODE,由于r0的低5位模式位與IRQ_MODE相同,所以r0^ IRQ_MODE的運算結(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ù)的第一個參數(shù),即C函數(shù)中的pt_regs參數(shù)。

pc是當前地址+8,也就是本段代碼后面緊跟的跳轉(zhuǎn)表的基地址,lr用于在跳轉(zhuǎn)表中索引,lr左移兩位等同于*4,因為每個條目是4字節(jié)。從usr模式進入irq模式,則lr=pc+4*0,若從svc模式進入irq,則lr=pc+4*3,即__irq_svc的地址,其他地址進入__irq_invalid出錯處理,因為不能從其他模式進入irq異常。

假設(shè)這里是從usr進入irq,則執(zhí)行跳轉(zhuǎn)表中的第一條指令。跳轉(zhuǎn)的基準地址為當前pc,因為ARMv4是三級流水線結(jié)構(gòu)的,它總是指向當前指令的下兩條指令的地址,盡管以后版本的指令流水線擴展為5級和8級,但是這一特性一直被兼容處理,也即pc(excute)=pc(fetch) + 8,其中:pc(fetch)是當前正在執(zhí)行的指令,就是之前取該指令時候的PC的值;pc(execute):當前指令執(zhí)行的,計算中如果用到pc,是指此時pc的值。

當mov指令的目標寄存器是PC,且指令以S結(jié)束,則它會把spsr的值恢復給cpsr,上面說到當前的spsr中保存的是r0的值,即svc模式。所以本條指令是跳轉(zhuǎn)到__irq_usr的同時將處理器模式轉(zhuǎn)為svc模式。異常處理一定要進入svc模式的原因是:異常處理一定要進入PL1特權(quán)級;另一個原因是使能嵌套中斷。具體原因在問題4中解釋。關(guān)于__irq_svc和__irq_invalid暫時不討論。

/*

* Interrupt dispatcher以下跳轉(zhuǎn)表必須緊跟在vector_irq之后

*/

vector_stub irq, IRQ_MODE, 4 //生成vector_irq

/*從用戶態(tài)進入中斷的處理函數(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進入中斷的處理函數(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模式中,因為要借用SVC模式進行ISP處理,所以需要保存所有SVC模式下的寄存器到SVC堆棧中,最后才去調(diào)用中斷服務(wù)例程(ISP)irq_handler。

2.2.1 __irq_usr

.align 5

__irq_usr:

usr_entry //用于用戶模式下發(fā)生中斷時初始化中斷處理堆棧,同時保存所有SVC態(tài)寄存器到堆棧。

kuser_cmpxchg_check //對低版本的ARM核來說,用戶態(tài)無法實現(xiàn)原子比較交換。如果用戶態(tài)在處理原

//子比較交換的過程中發(fā)生中斷,需要特殊處理,略過

get_thread_info tsk //根據(jù)當前sp指針,將該指針最右邊13位清0,獲得當前任務(wù)的thread_info

#ifdef CONFIG_PREEMPT//如果可以搶占,遞增任務(wù)的搶占計數(shù)

ldr r8, [tsk, #TI_PREEMPT]//T被定義為offsetof(struct thread_info, preempt_count),顯然通過tsk就

可以很容易得到進程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]//獲得當前的搶占計數(shù)

str r8, [tsk, #TI_PREEMPT]//并將r8中的值保存回去。相當于將前一步遞增的搶占計數(shù)減回去了

teq r0, r7//r0,r7是調(diào)用irq_handler前后的搶占計數(shù),這里進行比較,是防止驅(qū)動的ISR

//程序沒有配對操作搶占計數(shù)導致系統(tǒng)錯誤。

ARM( strne r0, [r0, -r0] )//如果搶占計數(shù)被破壞,則強制寫入0.

THUMB( movne r0, #0 )

THUMB( strne r0, [r0] )

#endif

mov why, #0

b ret_to_user //返回到用戶態(tài)

UNWIND(.fnend )

ENDPROC(__irq_usr)


接下來分別看各個函數(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堆棧指針向低地址方向移動一個pt_regs結(jié)構(gòu)大小,用于保存svc模式下的寄存器現(xiàn)場。

ARM( stmib sp, {r1 - r12} )@向svc32堆棧中保存寄存器現(xiàn)場

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),所

以這里通過add指令將r0指向ARM_pc

mov r6, #-1 @r6中保存-1

str r3, [sp] @ save the "real" r0 copied從中斷棧中取出真實的r0存放到pt_regs->r0中。

@ from the exception stack

2.2.2 usr_entry

@ We are now ready to fill in the remaining blanks on the stack:

@

@ r4 - lr_, already fixed up for correct return/restart

@ 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)這個常量

ARM( stmdb r0, {sp, lr}^ )//stmdb指令的^標志表示存儲發(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時有效,如果開啟了該選

//項,中斷處理中將支持對齊跟蹤

@ Clear FP to mark the first stack frame

zero_fp//zero_fp用來設(shè)置fp棧幀寄存器為0

#ifdef CONFIG_IRQSOFF_TRACER

bl trace_hardirqs_off

#endif

.endm@usr_entry宏定義結(jié)束

以上的指令的作用可以總結(jié)如下,其中將普通寄存器r1到r12保存到ARM_r1- ARM_r12,這相當于把發(fā)生中斷時的寄存器r1-r12進行了保存。接下來保存中斷發(fā)生時的r0,lr_irq和spsr_irq保存到r1-r3,r4賦值為-1,它們接下來將被使用。接下來保存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ā)生時的寄存器都進行了保存。

圖3 保存中斷堆棧

2.2.3 get_thread_info

get_thread_info宏用來根據(jù)當前的sp值,通過lsr和lsl分別右移左移13位,相當于對sp向下圓整到8K對齊。這里也就是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

2.2.4 irq_handler

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中斷處理入口,在中斷處理中需要預留r7,r8和r9寄存器。它們被用來處理內(nèi)核搶占。在沒有配置MULTI_IRQ_HANDLER 的情況下,irq_handler的邏輯很簡單,就是簡單的調(diào)用arch_irq_handler_default。

如果配置了該選項,平臺代碼可以修改全局變量:handle_arch_irq,這里只討論默認實現(xiàn).

arch/arm/include/asm/entry_macro_multi.S

/*

* Interrupt handling. Preserves r7, r8, r9

*/

.macroarch_irq_handler_default //get_irqnr_preamble用來獲取中斷狀態(tài)寄存器基地址

get_irqnr_preamble r5, lr//將中斷控制器的狀態(tài)寄存器的地址存儲到r5

1: get_irqnr_and_base r0, r6, r5, lr//判斷中斷號,通過r0返回

movne r1, sp//如果還存在中斷,就將sp作為第二個參數(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的第二條指令,因為第二次循環(huán)時,不必執(zhí)行其第一條指令(加載寄存器基址)

bne asm_do_IRQ //將中斷號、pt_regs(中斷前的寄存器現(xiàn)場)傳遞給asm_do_IRQ。asm_do_IRQ返回時,

//會返回到get_irqnr_and_base處,直到所有中斷都已經(jīng)處理完畢才退出循環(huán)。

#ifdef CONFIG_SMP//針對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標志

ALT_UP_B(9997f)

movne r1, sp

adrne lr, BSYM(1b)//同理,這里也是將返回地址設(shè)置為ALT_SMP的第二條指令,構(gòu)造成一個循環(huán)

bne do_IPI//只要存在IPI就調(diào)用do_IPI,并循環(huán)直到處理完所有IPI

#ifdef CONFIG_LOCAL_TIMERS//同理,這里循環(huán)處理多核系統(tǒng)中的本地時鐘中斷。

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用來獲取中斷號。

/* 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兩個宏由machine級的代碼定義,目的就是從中斷控制器中獲得IRQ編號,緊接著就調(diào)用asm_do_IRQ,從這個函數(shù)開始,中斷程序進入C代碼中,傳入的參數(shù)是IRQ編號和寄存器結(jié)構(gòu)指針,

2.2.8 asm_do_IRQ

圖4 asm_do_IRQ流程圖

asm_do_IRQ是ARM處理硬件中斷的核心函數(shù),第一個參數(shù)指定了硬中斷的中斷號,第二個參數(shù)是寄存器備份組成的一個結(jié)構(gòu)體,保存了中斷發(fā)生時的模式對應(yīng)的寄存器的值,在中斷返回時使用。

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)計信息,同時在__irq_enter宏中禁止了進程的搶占。雖然在產(chǎn)生IRQ時,ARM會自動把CPSR中的I位置位,禁止新的IRQ請求,直到中斷控制轉(zhuǎn)到相應(yīng)的流控層后才通過local_irq_enable()打開。那為何還要禁止搶占?這是因為要考慮中斷嵌套的問題,一旦流控層或驅(qū)動程序主動通過local_irq_enable打開了IRQ,而此時該中斷還沒處理完成,新的irq請求到達,這時代碼會再次進入irq_enter,在本次嵌套中斷返回時,內(nèi)核不希望進行搶占調(diào)度,而是要等到最外層的中斷處理完成后才做出調(diào)度動作,所以才有了禁止搶占這一處理。

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注冊的函數(shù)處理,該函數(shù)在注冊中斷時填寫irq_desc結(jié)構(gòu)體時指定。

}// handle_irq是個函數(shù)指針,它用來實現(xiàn)中斷處理器的電流處理。電流處理分為邊

//沿跳變處理和電平處理。

static inline void generic_handle_irq(unsigned int irq)//該函數(shù)是與體系結(jié)構(gòu)無關(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標志判斷是否需要處理搶占或者信號。

tst r1, #_TIF_WORK_MASK

bne work_pending//處理搶占或者信號

no_work_pending: //沒有搶占或者信號需要處理,或者已經(jīng)處理完畢,開始退回用戶態(tài)

#if defined(CONFIG_IRQSOFF_TRACER)//退回用戶態(tài)必然會打開中斷,這里記錄下打開中斷的事實,供調(diào)試用。

asm_trace_hardirqs_on

#endif

/* perform architecture specific actions before user return */

arch_ret_to_user r1, lr//在返回用戶態(tài)前,處理各個體系結(jié)構(gòu)的鉤子

restore_user_regs fast = 0, offset = 0//恢復寄存器現(xiàn)場,并切回用戶態(tài)。這里不再具體分析恢復方式。

ENDPROC(ret_to_user)


3.問題及解答

問題1:vector_irq已經(jīng)是異常、中斷處理的入口函數(shù)了,為什么還要加stubs_offset?( b vector_irq + stubs_offset)

答:(1)內(nèi)核剛啟動時(head.S文件)通過設(shè)置CP15的c1寄存器已經(jīng)確定了異常向量表的起始地址(例如0xffff0000),因此需要把已經(jīng)寫好的內(nèi)核代碼中的異常向量表考到0xffff0000處,只有這樣在發(fā)生異常時內(nèi)核才能正確的處理異常。

(2)從上面代碼看出向量表和stubs(中斷處理函數(shù))都發(fā)生了搬移,如果還用bvector_irq,那么實際執(zhí)行的時候就無法跳轉(zhuǎn)到搬移后的vector_irq處,因為指令碼里寫的是原來的偏移量,所以需要把指令碼中的偏移量寫成搬移后的。至于為什么搬移后的地址是vector_irq+stubs_offset,如圖一所示。下圖是搬移示意圖更加清晰說明了搬移過程。。

問題2:為什么在異常向量表中,用b指令跳轉(zhuǎn)而不是用ldr絕對跳轉(zhuǎn)?

答:因為使用b指令跳轉(zhuǎn)比絕對跳轉(zhuǎn)(ldr pc,XXXX)效率高,正因為效率高,所以把__stubs_start~__stubs_end之間的代碼考到了0xffff0200起始處。

注意:

因為b跳轉(zhuǎn)指令只能在+/-32MB之內(nèi)跳轉(zhuǎn),所以必須拷貝到0xffff0000附近。

b指令是相對于當前PC的跳轉(zhuǎn),當匯編器看到 B 指令后會把要跳轉(zhuǎn)的標簽轉(zhuǎn)化為相對于當前PC的偏移量寫入指令碼。

經(jīng)過Uboot的啟動,內(nèi)核跳入linux/arch/arm/kernel/head.S開始執(zhí)行。

問題1:為什么首先進入head.S開始執(zhí)行?

問題3:為什么首先進入head.S開始執(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)是編譯連接腳本,對于ARM平臺,就是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

對于有MMU的處理器,MMUEXT為空白字符串,所以arch/arm/kernel/head.O 是第一個連接的文件,而這個文件是由arch/arm/kernel/head.S編譯產(chǎn)生成的。

綜合以上分析,可以得出結(jié)論,非壓縮ARM Linux內(nèi)核的入口點在arch/arm/kernel/head.s中。

問題4: 中斷為什么必須進入svc模式?

一個最重要原因是:

如果一個中斷模式(例如從usr進入irq模式,在irq模式中)中重新允許了中斷,并且在這個中斷例程中使用了BL指令調(diào)用子程序,BL指令會自動將子程序返回地址保存到當前模式的lr(即r14_irq)中,這個地址隨后會被在當前模式下產(chǎn)生的中斷所破壞,因為產(chǎn)生中斷時CPU會將當前模式的PC保存到r14_irq,這樣就把剛剛保存的子程序返回地址沖掉。為了避免這種情況,中斷例程應(yīng)該切換到SVC或者系統(tǒng)模式,這樣的話,BL指令可以使用r14_svc來保存子程序的返回地址。

問題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)用異常的代碼編譯在其他文件中,其入口地址與異常向量相隔較遠,使用b指令無法跳轉(zhuǎn)過去(b指令只能相對當前pc跳轉(zhuǎn)32M范圍)。因此將其地址存放到LCvswi中,并從內(nèi)存地址中加載其入口地址,原理與其他調(diào)用是一樣的。這也就是為什么系統(tǒng)調(diào)用的速度稍微慢一點的原因。

問題6:為什么ARM能處理中斷?

因為ARM架構(gòu)的CPU有一個機制,只要中斷產(chǎn)生了,CPU就會根據(jù)中斷類型自動跳轉(zhuǎn)到某個特定的地址(即中斷向量表中的某個地址)。如下表所示,既是中斷向量表。



ARM中斷向量表及地址

問題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)表。

問題8:中斷向量表是如何存放到0x00000000或0xffff0000地址的?

A:Uboot執(zhí)行結(jié)束后會把Linux內(nèi)核拷貝到內(nè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ù)實現(xiàn)向量表的設(shè)定操作。

參考文獻

1. ARM Linux中斷向量表搬移設(shè)計過程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().



關(guān)鍵詞: ARMLinux中斷機

評論


技術(shù)專區(qū)

關(guān)閉