新聞中心

EEPW首頁(yè) > 嵌入式系統(tǒng) > 設(shè)計(jì)應(yīng)用 > ARM linux的中斷處理過(guò)程

ARM linux的中斷處理過(guò)程

作者: 時(shí)間:2016-11-09 來(lái)源:網(wǎng)絡(luò) 收藏
一、前言

本文主要以ARM體系結(jié)構(gòu)下的中斷處理為例,講述整個(gè)中斷處理過(guò)程中的硬件行為和軟件動(dòng)作。具體整個(gè)處理過(guò)程分成三個(gè)步驟來(lái)描述:

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

1、第二章描述了中斷處理的準(zhǔn)備過(guò)程

2、第三章描述了當(dāng)發(fā)生中的時(shí)候,ARM硬件的行為

3、第四章描述了ARM的中斷進(jìn)入過(guò)程

4、第五章描述了ARM的中斷退出過(guò)程

本文涉及的代碼來(lái)自3.14內(nèi)核。另外,本文注意描述ARM指令集的內(nèi)容,有些source code為了簡(jiǎn)短一些,刪除了THUMB相關(guān)的代碼,除此之外,有些debug相關(guān)的內(nèi)容也會(huì)刪除。

二、中斷處理的準(zhǔn)備過(guò)程

1、中斷模式的stack準(zhǔn)備

ARM處理器有多種process mode,例如user mode(用戶(hù)空間的AP所處于的模式)、supervisor mode(即SVC mode,大部分的內(nèi)核態(tài)代碼都處于這種mode)、IRQ mode(發(fā)生中斷后,處理器會(huì)切入到該mode)等。對(duì)于linux kernel,其中斷處理處理過(guò)程中,ARM 處理器大部分都是處于SVC mode。但是,實(shí)際上產(chǎn)生中斷的時(shí)候,ARM處理器實(shí)際上是進(jìn)入IRQ mode,因此在進(jìn)入真正的IRQ異常處理之前會(huì)有一小段IRQ mode的操作,之后會(huì)進(jìn)入SVC mode進(jìn)行真正的IRQ異常處理。由于IRQ mode只是一個(gè)過(guò)度,因此IRQ mode的棧很小,只有12個(gè)字節(jié),具體如下:

struct stack {
u32 irq[3];
u32 abt[3];
u32 und[3];
} ____cacheline_aligned;

static struct stack stacks[NR_CPUS];

除了irq mode,linux kernel在處理abt mode(當(dāng)發(fā)生data abort exception或者prefetch abort exception的時(shí)候進(jìn)入的模式)和und mode(處理器遇到一個(gè)未定義的指令的時(shí)候進(jìn)入的異常模式)的時(shí)候也是采用了相同的策略。也就是經(jīng)過(guò)一個(gè)簡(jiǎn)短的abt或者und mode之后,stack切換到svc mode的棧上,這個(gè)棧就是發(fā)生異常那個(gè)時(shí)間點(diǎn)current thread的內(nèi)核棧。anyway,在irq mode和svc mode之間總是需要一個(gè)stack保存數(shù)據(jù),這就是中斷模式的stack,系統(tǒng)初始化的時(shí)候,cpu_init函數(shù)中會(huì)進(jìn)行中斷模式stack的設(shè)定:

void notrace cpu_init(void)
{

unsigned int cpu = smp_processor_id();------獲取CPU ID
struct stack *stk = &stacks[cpu];---------獲取該CPU對(duì)于的irq abt和und的stack指針

……

#ifdef CONFIG_THUMB2_KERNEL
#define PLC "r"------Thumb-2下,msr指令不允許使用立即數(shù),只能使用寄存器。
#else
#define PLC "I"
#endif


__asm__ (
"msr cpsr_c, %1nt"------讓CPU進(jìn)入IRQ mode
"add r14, %0, %2nt"------r14寄存器保存stk->irq
"mov sp, r14nt"--------設(shè)定IRQ mode的stack為stk->irq
"msr cpsr_c, %3nt"
"add r14, %0, %4nt"
"mov sp, r14nt"--------設(shè)定abt mode的stack為stk->abt
"msr cpsr_c, %5nt"
"add r14, %0, %6nt"
"mov sp, r14nt"--------設(shè)定und mode的stack為stk->und
"msr cpsr_c, %7"--------回到SVC mode
:--------------------上面是code,下面的output部分是空的
: "r" (stk),----------------------對(duì)應(yīng)上面代碼中的%0
PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),------對(duì)應(yīng)上面代碼中的%1
"I" (offsetof(struct stack, irq[0])),------------對(duì)應(yīng)上面代碼中的%2
PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),------以此類(lèi)推,下面不贅述
"I" (offsetof(struct stack, abt[0])),
PLC (PSR_F_BIT | PSR_I_BIT | UND_MODE),
"I" (offsetof(struct stack, und[0])),
PLC (PSR_F_BIT | PSR_I_BIT | SVC_MODE)
: "r14");--------上面是input操作數(shù)列表,r14是要clobbered register列表
}

嵌入式匯編的語(yǔ)法格式是:asm(code : output operand list : input operand list : clobber list);大家對(duì)著上面的code就可以分開(kāi)各段內(nèi)容了。在input operand list中,有兩種限制符(constraint),"r"或者"I","I"表示立即數(shù)(Immediate operands),"r"表示用通用寄存器傳遞參數(shù)。clobber list中有一個(gè)r14,表示在匯編代碼中修改了r14的值,這些信息是編譯器需要的內(nèi)容。

2、SVC模式的stack準(zhǔn)備

我們經(jīng)常說(shuō)進(jìn)程的用戶(hù)空間和內(nèi)核空間,對(duì)于一個(gè)應(yīng)用程序而言,可以運(yùn)行在用戶(hù)空間,也可以通過(guò)系統(tǒng)調(diào)用進(jìn)入內(nèi)核空間。在用戶(hù)空間,使用的是用戶(hù)棧,也就是我們軟件工程師編寫(xiě)用戶(hù)空間程序的時(shí)候,保存局部變量的stack。陷入內(nèi)核后,當(dāng)然不能用用戶(hù)棧了,這時(shí)候就需要使用到內(nèi)核棧。所謂內(nèi)核棧其實(shí)就是處于SVC mode時(shí)候使用的棧。

Linux kernel在創(chuàng)建進(jìn)程(包括用戶(hù)進(jìn)程和內(nèi)核線程)的時(shí)候都會(huì)分配一個(gè)(或者兩個(gè),和配置相關(guān))page frame,底部是struct thread_info數(shù)據(jù)結(jié)構(gòu),頂部(高地址)就是該進(jìn)程的內(nèi)核棧。當(dāng)進(jìn)程切換的時(shí)候,整個(gè)硬件和軟件的上下文都會(huì)進(jìn)行切換,這里就包括了svc mode的sp寄存器的值被切換到調(diào)度算法選定的新的進(jìn)程的內(nèi)核棧上來(lái)。

3、異常向量表的準(zhǔn)備

對(duì)于ARM處理器而言,當(dāng)發(fā)生異常的時(shí)候,處理器會(huì)暫停當(dāng)前指令的執(zhí)行,保存現(xiàn)場(chǎng),轉(zhuǎn)而去執(zhí)行對(duì)應(yīng)的異常向量處的指令,當(dāng)處理完該異常的時(shí)候,恢復(fù)現(xiàn)場(chǎng),回到原來(lái)的那點(diǎn)去繼續(xù)執(zhí)行程序。系統(tǒng)所有的異常向量(共計(jì)8個(gè))組成了異常向量表。向量表(vector table)的代碼如下:

.section .vectors, "ax", %progbits
__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, __vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq ---------------------------IRQ Vector
W(b) vector_fiq

對(duì)于本文而言,我們重點(diǎn)關(guān)注vector_irq這個(gè)exception vector。異常向量表可能被安放在兩個(gè)位置上:

(1)異常向量表位于0x0的地址。這種設(shè)置叫做Normal vectors或者Low vectors。

(2)異常向量表位于0xffff0000的地址。這種設(shè)置叫做high vectors

具體是low vectors還是high vectors是由ARM的一個(gè)叫做的SCTLR寄存器的第13個(gè)bit (vector bit)控制的。對(duì)于啟用MMU的ARM Linux而言,系統(tǒng)使用了high vectors。為什么不用low vector呢?對(duì)于linux而言,0~3G的空間是用戶(hù)空間,如果使用low vector,那么異常向量表在0地址,那么則是用戶(hù)空間的位置,因此linux選用high vector。當(dāng)然,使用Low vector也可以,這樣Low vector所在的空間則屬于kernel space了(也就是說(shuō),3G~4G的空間加上Low vector所占的空間屬于kernel space),不過(guò)這時(shí)候要注意一點(diǎn),因?yàn)樗械倪M(jìn)程共享kernel space,而用戶(hù)空間的程序經(jīng)常會(huì)發(fā)生空指針訪問(wèn),這時(shí)候,內(nèi)存保護(hù)機(jī)制應(yīng)該可以捕獲這種錯(cuò)誤(大部分的MMU都可以做到,例如:禁止userspace訪問(wèn)kernel space的地址空間),防止vector table被訪問(wèn)到。對(duì)于內(nèi)核中由于程序錯(cuò)誤導(dǎo)致的空指針訪問(wèn),內(nèi)存保護(hù)機(jī)制也需要控制vector table被修改,因此vector table所在的空間被設(shè)置成read only的。在使用了MMU之后,具體異常向量表放在那個(gè)物理地址已經(jīng)不重要了,重要的是把它映射到0xffff0000的虛擬地址就OK了,具體代碼如下:

static void __init devicemaps_init(const struct machine_desc *mdesc)
{
……
vectors = early_alloc(PAGE_SIZE * 2); -----分配兩個(gè)page的物理頁(yè)幀

early_trap_init(vectors); -------copy向量表以及相關(guān)help function到該區(qū)域

……
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的那個(gè)page frame

if (!vectors_high()) {---如果SCTLR.V的值設(shè)定為low vectors,那么還要映射0地址開(kāi)始的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開(kāi)始的第二個(gè)page frame

……
}

為什么要分配兩個(gè)page frame呢?這里vectors table和kuser helper函數(shù)(內(nèi)核空間提供的函數(shù),但是用戶(hù)空間使用)占用了一個(gè)page frame,另外異常處理的stub函數(shù)占用了另外一個(gè)page frame。為什么會(huì)有stub函數(shù)呢?稍后會(huì)講到。

在early_trap_init函數(shù)中會(huì)初始化異常向量表,具體代碼如下:

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;

將整個(gè)vector table那個(gè)page frame填充成未定義的指令。起始vector table加上kuser helper函數(shù)并不能完全的充滿(mǎn)這個(gè)page,有些縫隙。如果不這么處理,當(dāng)極端情況下(程序錯(cuò)誤或者HW的issue),CPU可能從這些縫隙中取指執(zhí)行,從而導(dǎo)致不可知的后果。如果將這些縫隙填充未定義指令,那么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);
}

一旦涉及代碼的拷貝,我們就需要關(guān)心其編譯連接時(shí)地址(link-time address)和運(yùn)行時(shí)地址(run-time address)。在kernel完成鏈接后,__vectors_start有了其link-time address,如果link-time address和run-time address一致,那么這段代碼運(yùn)行時(shí)毫無(wú)壓力。但是,目前對(duì)于vector table而言,其被copy到其他的地址上(對(duì)于High vector,這是地址就是0xffff00000),也就是說(shuō),link-time address和run-time address不一樣了,如果仍然想要這些代碼可以正確運(yùn)行,那么需要這些代碼是位置無(wú)關(guān)的代碼。對(duì)于vector table而言,必須要位置無(wú)關(guān)。B這個(gè)branch instruction本身就是位置無(wú)關(guān)的,它可以跳轉(zhuǎn)到一個(gè)當(dāng)前位置的offset。不過(guò)并非所有的vector都是使用了branch instruction,對(duì)于軟中斷,其vector地址上指令是“W(ldr) pc, __vectors_start + 0x1000 ”,這條指令被編譯器編譯成ldr pc, [pc, #4080],這種情況下,該指令也是位置無(wú)關(guān)的,但是有個(gè)限制,offset必須在4K的范圍內(nèi),這也是為何存在stub section的原因了。

4、中斷控制器的初始化

具體可以參考GIC代碼分析。

三、ARM HW對(duì)中斷事件的處理

當(dāng)一切準(zhǔn)備好之后,一旦打開(kāi)處理器的全局中斷就可以處理來(lái)自外設(shè)的各種中斷事件了。

當(dāng)外設(shè)(SOC內(nèi)部或者外部都可以)檢測(cè)到了中斷事件,就會(huì)通過(guò)interrupt requestion line上的電平或者邊沿(上升沿或者下降沿或者both)通知到該外設(shè)連接到的那個(gè)中斷控制器,而中斷控制器就會(huì)在多個(gè)處理器中選擇一個(gè),并把該中斷通過(guò)IRQ(或者FIQ,本文不討論FIQ的情況)分發(fā)給process。ARM處理器感知到了中斷事件后,會(huì)進(jìn)行下面一系列的動(dòng)作:

1、修改CPSR(Current Program Status Register)寄存器中的M[4:0]。M[4:0]表示了ARM處理器當(dāng)前處于的模式( processor modes)。ARM定義的mode包括:

處理器模式縮寫(xiě)對(duì)應(yīng)的M[4:0]編碼Privilege level
Userusr10000PL0
FIQfiq10001PL1
IRQirq10010PL1
Supervisorsvc10011PL1
Monitormon10110PL1
Abortabt10111PL1
Hyphyp11010PL2
Undefinedund11011PL1
Systemsys11111PL1

一旦設(shè)定了CPSR.M,ARM處理器就會(huì)將processor mode切換到IRQ mode。

2、保存發(fā)生中斷那一點(diǎn)的CPSR值(step 1之前的狀態(tài))和PC值

ARM處理器支持9種processor mode,每種mode看到的ARM core register(R0~R15,共計(jì)16個(gè))都是不同的。每種mode都是從一個(gè)包括所有的Banked ARM core register中選取。全部Banked ARM core register包括:

UsrSystemHypSupervisorabortundefinedMonitorIRQFIQ
R0_usr
R1_usr
R2_usr
R3_usr
R4_usr
R5_usr
R6_usr
R7_usr
R8_usrR8_fiq
R9_usrR9_fiq
R10_usrR10_fiq
R11_usrR11_fiq
R12_usrR12_fiq
SP_usrSP_hypSP_svcSP_abtSP_undSP_monSP_irqSP_fiq
LR_usrLR_svcLR_abtLR_undLR_monLR_irqLR_fiq
PC
CPSR
SPSR_hypSPSR_svcSPSR_abtSPSR_undSPSR_monSPSR_irqSPSR_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是共用的,雖然中斷可能發(fā)生在usr mode(用戶(hù)空間),也可能是svc mode(內(nèi)核空間),不過(guò)這些信息都是體現(xiàn)在CPSR寄存器中。硬件會(huì)將發(fā)生中斷那一刻的CPSR保存在SPSR寄存器中(由于不同的mode下有不同的SPSR寄存器,因此更準(zhǔn)確的說(shuō)應(yīng)該是SPSR-irq,也就是IRQ mode中的SPSR寄存器)。

PC也是共用的,由于后續(xù)PC會(huì)被修改為irq exception vector,因此有必要保存PC值。當(dāng)然,與其說(shuō)保存PC值,不如說(shuō)是保存返回執(zhí)行的地址。對(duì)于IRQ而言,我們期望返回地址是發(fā)生中斷那一點(diǎn)執(zhí)行指令的下一條指令。具體的返回地址保存在lr寄存器中(注意:這個(gè)lr寄存器是IRQ mode的lr寄存器,可以表示為lr_irq):

(1)對(duì)于thumb state,lr_irq = PC

(2)對(duì)于ARM state,lr_irq = PC - 4

為何要減去4?我的理解是這樣的(不一定對(duì))。由于ARM采用流水線結(jié)構(gòu),當(dāng)CPU正在執(zhí)行某一條指令的時(shí)候,其實(shí)取指的動(dòng)作早就執(zhí)行了,這時(shí)候PC值=正在執(zhí)行的指令地址 + 8,如下所示:

----> 發(fā)生中斷的指令

發(fā)生中斷的指令+4

-PC-->發(fā)生中斷的指令+8

發(fā)生中斷的指令+12

一旦發(fā)生了中斷,當(dāng)前正在執(zhí)行的指令當(dāng)然要執(zhí)行完畢,但是已經(jīng)完成取指、譯碼的指令則終止執(zhí)行。當(dāng)發(fā)生中斷的指令執(zhí)行完畢之后,原來(lái)指向(發(fā)生中斷的指令+8)的PC會(huì)繼續(xù)增加4,因此發(fā)生中斷后,ARM core的硬件著手處理該中斷的時(shí)候,硬件現(xiàn)場(chǎng)如下圖所示:

----> 發(fā)生中斷的指令

發(fā)生中斷的指令+4 <-------中斷返回的指令是這條指令

發(fā)生中斷的指令+8

-PC-->發(fā)生中斷的指令+12

這時(shí)候的PC值其實(shí)是比發(fā)生中斷時(shí)候的指令超前12。減去4之后,lr_irq中保存了(發(fā)生中斷的指令+8)的地址。為什么HW不幫忙直接減去8呢?這樣,后續(xù)軟件不就不用再減去4了。這里我們不能孤立的看待問(wèn)題,實(shí)際上ARM的異常處理的硬件邏輯不僅僅處理IRQ的exception,還要處理各種exception,很遺憾,不同的exception期望的返回地址不統(tǒng)一,因此,硬件只是幫忙減去4,剩下的交給軟件去調(diào)整。

3、mask IRQ exception。也就是設(shè)定CPSR.I = 1

4、設(shè)定PC值為IRQ exception vector?;旧?,ARM處理器的硬件就只能幫你幫到這里了,一旦設(shè)定PC值,ARM處理器就會(huì)跳轉(zhuǎn)到IRQ的exception vector地址了,后續(xù)的動(dòng)作都是軟件行為了。

四、如何進(jìn)入ARM中斷處理

1、IRQ mode中的處理

IRQ mode的處理都在vector_irq中,vector_stub是一個(gè)宏,定義如下:

.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保存了發(fā)生IRQ時(shí)候的CPSR,通過(guò)and操作,可以獲取CPSR.M[3:0]的值

這時(shí)候,如果中斷發(fā)生在用戶(hù)空間,lr=0,如果是內(nèi)核空間,lr=3
THUMB( adr r0, 1f )----根據(jù)當(dāng)前PC值,獲取lable 1的地址
THUMB( ldr lr, [r0, lr, lsl #2] )-lr根據(jù)當(dāng)前mode,要么是__irq_usr的地址 ,要么是__irq_svc的地址
mov r0, sp------將irq mode的stack point通過(guò)r0傳遞給即將跳轉(zhuǎn)的函數(shù)
ARM( ldr lr, [pc, lr, lsl #2] )---根據(jù)mode,給lr賦值,__irq_usr或者_(dá)_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)我們期望在棧上保存發(fā)生中斷時(shí)候的硬件現(xiàn)場(chǎng)(HW context),這里就包括ARM的core register。上一章我們已經(jīng)了解到,當(dāng)發(fā)生IRQ中斷的時(shí)候,lr中保存了發(fā)生中斷的PC+4,如果減去4的話(huà),得到的就是發(fā)生中斷那一點(diǎn)的PC值。

(2)當(dāng)前是IRQ mode,SP_irq在初始化的時(shí)候已經(jīng)設(shè)定(12個(gè)字節(jié))。在irq mode的stack上,依次保存了發(fā)生中斷那一點(diǎn)的r0值、PC值以及CPSR值(具體操作是通過(guò)spsr進(jìn)行的,其實(shí)硬件已經(jīng)幫我們保存了CPSR到SPSR中了)。為何要保存r0值?因?yàn)殡S后的代碼要使用r0寄存器,因此我們要把r0放到棧上,只有這樣才能完完全全恢復(fù)硬件現(xiàn)場(chǎng)。

(3)可憐的IRQ mode稍縱即逝,這段代碼就是準(zhǔn)備將ARM推送到SVC mode。如何準(zhǔn)備?其實(shí)就是修改SPSR的值,SPSR不是CPSR,不會(huì)引起processor mode的切換(畢竟這一步只是準(zhǔn)備而已)。

(4)很多異常處理的代碼返回的時(shí)候都是使用了stack相關(guān)的操作,這里沒(méi)有。“movs pc, lr ”指令除了字面上意思(把lr的值付給pc),還有一個(gè)隱含的操作(movs中‘s’的含義):把SPSR copy到CPSR,從而實(shí)現(xiàn)了模式的切換。

2、當(dāng)發(fā)生中斷的時(shí)候,代碼運(yùn)行在用戶(hù)空間

Interrupt dispatcher的代碼如下:

vector_stub irq, IRQ_MODE, 4 -----減去4,確保返回發(fā)生中斷之后的那條指令

.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

這其實(shí)就是一個(gè)lookup table,根據(jù)CPSR.M[3:0]的值進(jìn)行跳轉(zhuǎn)(參考上一節(jié)的代碼:and lr, lr, #0x0f)。因此,該lookup table共設(shè)定了16個(gè)入口,當(dāng)然只有兩項(xiàng)有效,分別對(duì)應(yīng)user mode和svc mode的跳轉(zhuǎn)地址。其他入口的__irq_invalid也是非常關(guān)鍵的,這保證了在其模式下發(fā)生了中斷,系統(tǒng)可以捕獲到這樣的錯(cuò)誤,為debug提供有用的信息。

.align 5
__irq_usr:
usr_entry---------請(qǐng)參考本章第一節(jié)(1)保存用戶(hù)現(xiàn)場(chǎng)的描述
kuser_cmpxchg_check---和本文描述的內(nèi)容無(wú)關(guān),這些不就介紹了
irq_handler----------核心處理內(nèi)容,請(qǐng)參考本章第二節(jié)的描述
get_thread_info tsk------tsk是r9,指向當(dāng)前的thread info數(shù)據(jù)結(jié)構(gòu)
mov why, #0--------why是r8
b ret_to_user_from_irq----中斷返回,下一章會(huì)詳細(xì)描述

(1)保存發(fā)生中斷時(shí)候的現(xiàn)場(chǎng)。所謂保存現(xiàn)場(chǎng)其實(shí)就是把發(fā)生中斷那一刻的硬件上下文(各個(gè)寄存器)保存在了SVC mode的stack上。

.macro usr_entry
sub sp, sp, #S_FRAME_SIZE--------------A
stmib sp, {r1 - r12} -------------------B

ldmia r0, {r3 - r5}--------------------C
add r0, sp, #S_PC-------------------D
mov r6, #-1----orig_r0的值

str r3, [sp] ----保存中斷那一刻的r0


stmia r0, {r4 - r6}--------------------E
stmdb r0, {sp, lr}^-------------------F
.endm

A:代碼執(zhí)行到這里的時(shí)候,ARM處理已經(jīng)切換到了SVC mode。一旦進(jìn)入SVC mode,ARM處理器看到的寄存器已經(jīng)發(fā)生變化,這里的sp已經(jīng)變成了sp_svc了。因此,后續(xù)的壓棧操作都是壓入了發(fā)生中斷那一刻的進(jìn)程的(或者內(nèi)核線程)內(nèi)核棧(svc mode棧)。具體保存多少個(gè)寄存器值?S_FRAME_SIZE已經(jīng)給出了答案,這個(gè)值是18個(gè)寄存器。r0~r15再加上CPSR也只有17個(gè)而已。先保留這個(gè)疑問(wèn),我們稍后回答。

B:壓棧首先壓入了r1~r12,這里為何不處理r0?因?yàn)閞0在irq mode切到svc mode的時(shí)候被污染了,不過(guò),原始的r0被保存的irq mode的stack上了。r13(sp)和r14(lr)需要保存嗎,當(dāng)然需要,稍后再保存。執(zhí)行到這里,內(nèi)核棧的布局如下圖所示:

stmib中的ib表示increment before,因此,在壓入R1的時(shí)候,stack pointer會(huì)先增加4,重要是預(yù)留r0的位置。stmib sp, {r1 - r12}指令中的sp沒(méi)有“!”的修飾符,表示壓棧完成后并不會(huì)真正更新stack pointer,因此sp保持原來(lái)的值。

C:注意,這里r0指向了irq stack,因此,r3是中斷時(shí)候的r0值,r4是中斷現(xiàn)場(chǎng)的PC值,r5是中斷現(xiàn)場(chǎng)的CPSR值。

D:把r0賦值為S_PC的值。根據(jù)struct pt_regs的定義(這個(gè)數(shù)據(jù)結(jié)構(gòu)反應(yīng)了內(nèi)核棧上的保存的寄存器的排列信息),從低地址到高地址依次為:

ARM_r0
ARM_r1
ARM_r2
ARM_r3
ARM_r4
ARM_r5
ARM_r6
ARM_r7
ARM_r8
ARM_r9
ARM_r10
ARM_fp
ARM_ip
ARM_sp
ARM_lr
ARM_pc<---------add r0, sp, #S_PC指令使得r0指向了這個(gè)位置
ARM_cpsr
ARM_ORIG_r0

為什么要給r0賦值?因此kernel不想修改sp的值,保持sp指向棧頂。

E:在內(nèi)核棧上保存剩余的寄存器的值,根據(jù)代碼,依次是r0,PC,CPSR和orig r0。執(zhí)行到這里,內(nèi)核棧的布局如下圖所示:

R0,PC和CPSR來(lái)自IRQ mode的stack。實(shí)際上這段操作就是從irq stack就中斷現(xiàn)場(chǎng)搬移到內(nèi)核棧上。

F:內(nèi)核棧上還有兩個(gè)寄存器沒(méi)有保持,分別是發(fā)生中斷時(shí)候sp和lr這兩個(gè)寄存器。這時(shí)候,r0指向了保存PC寄存器那個(gè)地址(add r0, sp, #S_PC),stmdb r0, {sp, lr}^中的“db”是decrement before,因此,將sp和lr壓入stack中的剩余的兩個(gè)位置。需要注意的是,我們保存的是發(fā)生中斷那一刻(對(duì)于本節(jié),這是當(dāng)時(shí)user mode的sp和lr),指令中的“^”符號(hào)表示訪問(wèn)user mode的寄存器。

(2)核心處理

irq_handler的處理有兩種配置。一種是配置了CONFIG_MULTI_IRQ_HANDLER。這種情況下,linux kernel允許run time設(shè)定irq handler。如果我們需要一個(gè)linux kernel image支持多個(gè)平臺(tái),這是就需要配置這個(gè)選項(xiàng)。另外一種是傳統(tǒng)的linux的做法,irq_handler實(shí)際上就是arch_irq_handler_default,具體代碼如下:

.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp--------設(shè)定傳遞給machine定義的handle_arch_irq的參數(shù)
adr lr, BSYM(9997f)----設(shè)定返回地址
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm

對(duì)于情況一,machine相關(guān)代碼需要設(shè)定handle_arch_irq函數(shù)指針,這里的匯編指令只需要調(diào)用這個(gè)machine代碼提供的irq handler即可(當(dāng)然,要準(zhǔn)備好參數(shù)傳遞和返回地址設(shè)定)。

情況二要稍微復(fù)雜一些(而且,看起來(lái)kernel中使用的越來(lái)越少),代碼如下:

.macro arch_irq_handler_default
get_irqnr_preamble r6, lr
1: get_irqnr_and_base r0, r2, r6, lr
movne r1, sp
@
@ asm_do_IRQ 需要兩個(gè)參數(shù),一個(gè)是 irq number(保存在r0)
@ 另一個(gè)是 struct pt_regs *(保存在r1中)
adrne lr, BSYM(1b)-------返回地址設(shè)定為符號(hào)1,也就是說(shuō)要不斷的解析irq狀態(tài)寄存器

的內(nèi)容,得到IRQ number,直到所有的irq number處理完畢
bne asm_do_IRQ
.endm

這里的代碼已經(jīng)是和machine相關(guān)的代碼了,我們這里只是簡(jiǎn)短描述一下。所謂machine相關(guān)也就是說(shuō)和系統(tǒng)中的中斷控制器相關(guān)了。get_irqnr_preamble是為中斷處理做準(zhǔn)備,有些平臺(tái)根本不需要這個(gè)步驟,直接定義為空即可。get_irqnr_and_base 有四個(gè)參數(shù),分別是:r0保存了本次解析的irq number,r2是irq狀態(tài)寄存器的值,r6是irq controller的base address,lr是scratch register。

3、當(dāng)發(fā)生中斷的時(shí)候,代碼運(yùn)行在內(nèi)核空間

如果中斷發(fā)生在內(nèi)核空間,代碼會(huì)跳轉(zhuǎn)到__irq_svc處執(zhí)行:

.align 5
__irq_svc:
svc_entry----保存發(fā)生中斷那一刻的現(xiàn)場(chǎng)保存在內(nèi)核棧上
irq_handler ----具體的中斷處理,同user mode的處理。

#ifdef CONFIG_PREEMPT--------和preempt相關(guān)的處理,本文不進(jìn)行描述
get_thread_info tsk
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif

svc_exit r5, irq = 1 @ return from exception

保存現(xiàn)場(chǎng)的代碼和user mode下的現(xiàn)場(chǎng)保存是類(lèi)似的,因此這里不再詳細(xì)描述,只是在下面的代碼中內(nèi)嵌一些注釋。

.macro svc_entry, stack_hole=0
sub sp, sp, #(S_FRAME_SIZE + stack_hole - 4)----sp指向struct pt_regs中r1的位置
stmia sp, {r1 - r12} ------寄存器入棧。

ldmia r0, {r3 - r5}
add r7, sp, #S_SP - 4 ------r7指向struct pt_regs中r12的位置
mov r6, #-1 ----------orig r0設(shè)為-1
add r2, sp, #(S_FRAME_SIZE + stack_hole - 4)----r2是發(fā)現(xiàn)中斷那一刻stack的現(xiàn)場(chǎng)
str r3, [sp, #-4]! ----保存r0,注意有一個(gè)!,sp會(huì)加上4,這時(shí)候sp就指向棧頂?shù)膔0位置了

mov r3, lr ----保存svc mode的lr到r3
stmia r7, {r2 - r6} ---------壓棧,在棧上形成形成struct pt_regs
.endm

五、中斷退出過(guò)程

1、中斷發(fā)生在user mode下的退出過(guò)程,代碼如下:

ENTRY(ret_to_user_from_irq)
ldr r1, [tsk, #TI_FLAGS]
tst r1, #_TIF_WORK_MASK---------------A
bne work_pending
no_work_pending:
asm_trace_hardirqs_on ------和irq flag trace相關(guān),暫且略過(guò)

/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr----有些硬件平臺(tái)需要在中斷返回用戶(hù)空間做一些特別處理
ct_user_enter save = 0 ----和trace context相關(guān),暫且略過(guò)

restore_user_regs fast = 0, offset = 0------------B
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)

A:thread_info中的flags成員中有一些low level的標(biāo)識(shí),如果這些標(biāo)識(shí)設(shè)定了就需要進(jìn)行一些特別的處理,這里檢測(cè)的flag主要包括:

#define _TIF_WORK_MASK (_TIF_NEED_RESCHED | _TIF_SIGPENDING | _TIF_NOTIFY_RESUME)

這三個(gè)flag分別表示是否需要調(diào)度、是否有信號(hào)處理、返回用戶(hù)空間之前是否需要調(diào)用callback函數(shù)。只要有一個(gè)flag被設(shè)定了,程序就進(jìn)入work_pending這個(gè)分支。

B:從字面的意思也可以看成,這部分的代碼就是將進(jìn)入中斷的時(shí)候保存的現(xiàn)場(chǎng)(寄存器值)恢復(fù)到實(shí)際的ARM的各個(gè)寄存器中,從而完全返回到了中斷發(fā)生的那一點(diǎn)。具體的代碼如下:

.macro restore_user_regs, fast = 0, offset = 0
ldr r1, [sp, #offset + S_PSR] ----r1保存了pt_regs中的spsr,也就是發(fā)生中斷時(shí)的CPSR
ldr lr, [sp, #offset + S_PC]! ----lr保存了PC值,同時(shí)sp移動(dòng)到了pt_regs中PC的位置
msr spsr_cxsf, r1 ---------賦值給spsr,進(jìn)行返回用戶(hù)空間的準(zhǔn)備
clrex @ clear the exclusive monitor

.if fast
ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
.else
ldmdb sp, {r0 - lr}^ ------將保存在內(nèi)核棧上的數(shù)據(jù)保存到用戶(hù)態(tài)的r0~r14寄存器
.endif
mov r0, r0 ---------NOP操作,ARMv5T之前的需要這個(gè)操作
add sp, sp, #S_FRAME_SIZE - S_PC----現(xiàn)場(chǎng)已經(jīng)恢復(fù),移動(dòng)svc mode的sp到原來(lái)的位置
movs pc, lr --------返回用戶(hù)空間
.endm

2、中斷發(fā)生在svc mode下的退出過(guò)程,代碼如下:

.macro svc_exit, rpsr, irq = 0
.if irq != 0
@ IRQs already off
.else
@ IRQs off again before pulling preserved data off the stack
disable_irq_notrace
.endif
msr spsr_cxsf, rpsr-----將中斷現(xiàn)場(chǎng)的cpsr值保存到spsr中,準(zhǔn)備返回中斷發(fā)生的現(xiàn)場(chǎng)

ldmia sp, {r0 - pc}^ -----這條指令是ldm異常返回指令,這條指令除了字面上的操作,

還包括了將spsr copy到cpsr中。
.endm



關(guān)鍵詞: ARMlinux中斷處

評(píng)論


技術(shù)專(zhuān)區(qū)

關(guān)閉