Linux中斷 - ARM中斷處理過程
一、前言
本文主要以ARM體系結構下的中斷處理為例,講述整個中斷處理過程中的硬件行為和軟件動作。具體整個處理過程分成三個步驟來描述:
1、第二章描述了中斷處理的準備過程
2、第三章描述了當發生中的時候,ARM硬件的行為
3、第四章描述了ARM的中斷進入過程
4、第五章描述了ARM的中斷退出過程
本文涉及的代碼來自3.14內核。另外,本文註意描述ARM指令集的內容,有些source code為了簡短一些,刪除了THUMB相關的代碼,除此之外,有些debug相關的內容也會刪除。
二、中斷處理的準備過程
1、中斷模式的stack準備
ARM處理器有多種processor mode,例如user mode(用戶空間的AP所處於的模式)、supervisor mode(即SVC mode,大部分的內核態代碼都處於這種mode)、IRQ mode(發生中斷後,處理器會切入到該mode)等。對於linux kernel,其中斷處理處理過程中,ARM 處理器大部分都是處於SVC mode。但是,實際上產生中斷的時候,ARM處理器實際上是進入IRQ mode,因此在進入真正的IRQ異常處理之前會有一小段IRQ mode的操作,之後會進入SVC mode進行真正的IRQ異常處理。由於IRQ mode只是一個過度,因此IRQ mode的棧很小,只有12個字節,具體如下:
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(當發生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的設定:
void notrace cpu_init(void)
{unsigned int cpu = smp_processor_id();------獲取CPU ID
struct stack *stk = &stacks[cpu];---------獲取該CPU對於的irq abt和und的stack指針……
#ifdef CONFIG_THUMB2_KERNEL
#define PLC "r"------Thumb-2下,msr指令不允許使用立即數,只能使用寄存器。
#else
#define PLC "I"
#endif
__asm__ (
"msr cpsr_c, %1\n\t"------讓CPU進入IRQ mode
"add r14, %0, %2\n\t"------r14寄存器保存stk->irq
"mov sp, r14\n\t"--------設定IRQ mode的stack為stk->irq
"msr cpsr_c, %3\n\t"
"add r14, %0, %4\n\t"
"mov sp, r14\n\t"--------設定abt mode的stack為stk->abt
"msr cpsr_c, %5\n\t"
"add r14, %0, %6\n\t"
"mov sp, r14\n\t"--------設定und mode的stack為stk->und
"msr cpsr_c, %7"--------回到SVC mode
:--------------------上面是code,下面的output部分是空的
: "r" (stk),----------------------對應上面代碼中的%0
PLC (PSR_F_BIT | PSR_I_BIT | IRQ_MODE),------對應上面代碼中的%1
"I" (offsetof(struct stack, irq[0])),------------對應上面代碼中的%2
PLC (PSR_F_BIT | PSR_I_BIT | ABT_MODE),------以此類推,下面不贅述
"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操作數列表,r14是要clobbered register列表
}
嵌入式匯編的語法格式是: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來重新設定這些值。
2、SVC模式的stack準備
我們經常說進程的用戶空間和內核空間,對於一個應用程序而言,可以運行在用戶空間,也可以通過系統調用進入內核空間。在用戶空間,使用的是用戶棧,也就是我們軟件工程師編寫用戶空間程序的時候,保存局部變量的stack。陷入內核後,當然不能用用戶棧了,這時候就需要使用到內核棧。所謂內核棧其實就是處於SVC mode時候使用的棧。
在linux最開始啟動的時候,系統只有一個進程(更準確的說是kernel thread),就是PID等於0的那個進程,叫做swapper進程(或者叫做idle進程)。該進程的內核棧是靜態定義的,如下:
union thread_union init_thread_union __init_task_data =
{ INIT_THREAD_INFO(init_task) };union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
對於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寄存器的值被切換到調度算法選定的新的進程的內核棧上來。
3、異常向量表的準備
對於ARM處理器而言,當發生異常的時候,處理器會暫停當前指令的執行,保存現場,轉而去執行對應的異常向量處的指令,當處理完該異常的時候,恢復現場,回到原來的那點去繼續執行程序。系統所有的異常向量(共計8個)組成了異常向量表。向量表(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
對於本文而言,我們重點關註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了,具體代碼如下:
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 frameif (!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的原因了。
4、中斷控制器的初始化
具體可以參考GIC代碼分析。
三、ARM HW對中斷事件的處理
當一切準備好之後,一旦打開處理器的全局中斷就可以處理來自外設的各種中斷事件了。
當外設(SOC內部或者外部都可以)檢測到了中斷事件,就會通過interrupt requestion line上的電平或者邊沿(上升沿或者下降沿或者both)通知到該外設連接到的那個中斷控制器,而中斷控制器就會在多個處理器中選擇一個,並把該中斷通過IRQ(或者FIQ,本文不討論FIQ的情況)分發給該processor。ARM處理器感知到了中斷事件後,會進行下面一系列的動作:
1、修改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。
2、保存發生中斷那一點的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,剩下的交給軟件去調整。
3、mask IRQ exception。也就是設定CPSR.I = 1
4、設定PC值為IRQ exception vector。基本上,ARM處理器的硬件就只能幫你幫到這裏了,一旦設定PC值,ARM處理器就會跳轉到IRQ的exception vector地址了,後續的動作都是軟件行為了。
四、如何進入ARM中斷處理
1、IRQ mode中的處理
IRQ mode的處理都在vector_irq中,vector_stub是一個宏,定義如下:
.macro vector_stub, name, mode, correction=0
.align 5vector_\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,從而實現了模式的切換。
2、當發生中斷的時候,代碼運行在用戶空間
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提供有用的信息。
.align 5
__irq_usr:
usr_entry---------請參考本章第一節(1)保存用戶現場的描述
kuser_cmpxchg_check---和本文描述的內容無關,這些不就介紹了
irq_handler----------核心處理內容,請參考本章第二節的描述
get_thread_info tsk------tsk是r9,指向當前的thread info數據結構
mov why, #0--------why是r8
b ret_to_user_from_irq----中斷返回,下一章會詳細描述
why其實就是r8寄存器,用來傳遞參數的,表示本次放回用戶空間相關的系統調用是哪個?中斷處理這個場景和系統調用無關,因此設定為0。
(1)保存發生中斷時候的現場。所謂保存現場其實就是把發生中斷那一刻的硬件上下文(各個寄存器)保存在了SVC mode的stack上。
.macro usr_entry
sub sp, sp, #S_FRAME_SIZE--------------A
stmib sp, {r1 - r12} -------------------Bldmia 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:代碼執行到這裏的時候,ARM處理已經切換到了SVC mode。一旦進入SVC mode,ARM處理器看到的寄存器已經發生變化,這裏的sp已經變成了sp_svc了。因此,後續的壓棧操作都是壓入了發生中斷那一刻的進程的(或者內核線程)內核棧(svc mode棧)。具體保存多少個寄存器值?S_FRAME_SIZE已經給出了答案,這個值是18個寄存器。r0~r15再加上CPSR也只有17個而已。先保留這個疑問,我們稍後回答。
B:壓棧首先壓入了r1~r12,這裏為何不處理r0?因為r0在irq mode切到svc mode的時候被汙染了,不過,原始的r0被保存的irq mode的stack上了。r13(sp)和r14(lr)需要保存嗎,當然需要,稍後再保存。執行到這裏,內核棧的布局如下圖所示:
stmib中的ib表示increment before,因此,在壓入R1的時候,stack pointer會先增加4,重要是預留r0的位置。stmib sp, {r1 - r12}指令中的sp沒有“!”的修飾符,表示壓棧完成後並不會真正更新stack pointer,因此sp保持原來的值。
C:註意,這裏r0指向了irq stack,因此,r3是中斷時候的r0值,r4是中斷現場的PC值,r5是中斷現場的CPSR值。
D:把r0賦值為S_PC的值。根據struct pt_regs的定義(這個數據結構反應了內核棧上的保存的寄存器的排列信息),從低地址到高地址依次為:
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指向了這個位置
ARM_cpsr
ARM_ORIG_r0
為什麽要給r0賦值?因此kernel不想修改sp的值,保持sp指向棧頂。
E:在內核棧上保存剩余的寄存器的值,根據代碼,依次是r0,PC,CPSR和orig r0。執行到這裏,內核棧的布局如下圖所示:
R0,PC和CPSR來自IRQ mode的stack。實際上這段操作就是從irq stack就中斷現場搬移到內核棧上。
F:內核棧上還有兩個寄存器沒有保持,分別是發生中斷時候sp和lr這兩個寄存器。這時候,r0指向了保存PC寄存器那個地址(add r0, sp, #S_PC),stmdb r0, {sp, lr}^中的“db”是decrement before,因此,將sp和lr壓入stack中的剩余的兩個位置。需要註意的是,我們保存的是發生中斷那一刻(對於本節,這是當時user mode的sp和lr),指令中的“^”符號表示訪問user mode的寄存器。
(2)核心處理
irq_handler的處理有兩種配置。一種是配置了CONFIG_MULTI_IRQ_HANDLER。這種情況下,linux kernel允許run time設定irq handler。如果我們需要一個linux kernel image支持多個平臺,這是就需要配置這個選項。另外一種是傳統的linux的做法,irq_handler實際上就是arch_irq_handler_default,具體代碼如下:
.macro irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
ldr r1, =handle_arch_irq
mov r0, sp--------設定傳遞給machine定義的handle_arch_irq的參數
adr lr, BSYM(9997f)----設定返回地址
ldr pc, [r1]
#else
arch_irq_handler_default
#endif
9997:
.endm
對於情況一,machine相關代碼需要設定handle_arch_irq函數指針,這裏的匯編指令只需要調用這個machine代碼提供的irq handler即可(當然,要準備好參數傳遞和返回地址設定)。
情況二要稍微復雜一些(而且,看起來kernel中使用的越來越少),代碼如下:
.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 需要兩個參數,一個是 irq number(保存在r0)
@ 另一個是 struct pt_regs *(保存在r1中)
adrne lr, BSYM(1b)-------返回地址設定為符號1,也就是說要不斷的解析irq狀態寄存器的內容,得到IRQ number,直到所有的irq number處理完畢
bne asm_do_IRQ
.endm
這裏的代碼已經是和machine相關的代碼了,我們這裏只是簡短描述一下。所謂machine相關也就是說和系統中的中斷控制器相關了。get_irqnr_preamble是為中斷處理做準備,有些平臺根本不需要這個步驟,直接定義為空即可。get_irqnr_and_base 有四個參數,分別是:r0保存了本次解析的irq number,r2是irq狀態寄存器的值,r6是irq controller的base address,lr是scratch register。
對於ARM平臺而言,我們推薦使用第一種方法,因為從邏輯上講,中斷處理就是需要根據當前的硬件中斷系統的狀態,轉換成一個IRQ number,然後調用該IRQ number的處理函數即可。通過get_irqnr_and_base這樣的宏定義來獲取IRQ是舊的ARM SOC系統使用的方法,它是假設SOC上有一個中斷控制器,硬件狀態和IRQ number之間的關系非常簡單。但是實際上,ARM平臺上的硬件中斷系統已經是越來越復雜了,需要引入interrupt controller級聯,irq domain等等概念,因此,使用第一種方法優點更多。
3、當發生中斷的時候,代碼運行在內核空間
如果中斷發生在內核空間,代碼會跳轉到__irq_svc處執行:
.align 5
__irq_svc:
svc_entry----保存發生中斷那一刻的現場保存在內核棧上
irq_handler ----具體的中斷處理,同user mode的處理。#ifdef CONFIG_PREEMPT--------和preempt相關的處理
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
#endifsvc_exit r5, irq = 1 @ return from exception
一個task的thread info數據結構定義如下(只保留和本場景相關的內容):
struct thread_info {
unsigned long flags; /* low level flags */
int preempt_count; /* 0 => preemptable, <0 => bug */
……
};
flag成員用來標記一些low level的flag,而preempt_count用來判斷當前是否可以發生搶占,如果preempt_count不等於0(可能是代碼調用preempt_disable顯式的禁止了搶占,也可能是處於中斷上下文等),說明當前不能進行搶占,直接進入恢復現場的工作。如果preempt_count等於0,說明已經具備了搶占的條件,當然具體是否要搶占當前進程還是要看看thread info中的flag成員是否設定了_TIF_NEED_RESCHED這個標記(可能是當前的進程的時間片用完了,也可能是由於中斷喚醒了優先級更高的進程)。
保存現場的代碼和user mode下的現場保存是類似的,因此這裏不再詳細描述,只是在下面的代碼中內嵌一些註釋。
.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設為-1
add r2, sp, #(S_FRAME_SIZE + \stack_hole - 4)----r2是發現中斷那一刻stack的現場
str r3, [sp, #-4]! ----保存r0,註意有一個!,sp會加上4,這時候sp就指向棧頂的r0位置了mov r3, lr ----保存svc mode的lr到r3
stmia r7, {r2 - r6} ---------壓棧,在棧上形成形成struct pt_regs
.endm
至此,在內核棧上保存了完整的硬件上下文。實際上不但完整,而且還有些冗余,因為其中有一個orig_r0的成員。所謂original r0就是發生中斷那一刻的r0值,按理說,ARM_r0和ARM_ORIG_r0都應該是用戶空間的那個r0。 為何要保存兩個r0值呢?為何中斷將-1保存到了ARM_ORIG_r0位置呢?理解這個問題需要跳脫中斷處理這個主題,我們來看ARM的系統調用。對於系統調用,它 和中斷處理雖然都是cpu異常處理範疇,但是一個明顯的不同是系統調用需要傳遞參數,返回結果。如果進行這樣的參數傳遞呢?對於ARM,當然是寄存器了, 特別是返回結果,保存在了r0中。對於ARM,r0~r7是各種cpu mode都相同的,用於傳遞參數還是很方便的。因此,進入系統調用的時候,在內核棧上保存了發生系統調用現場的所有寄存器,一方面保存了hardware context,另外一方面,也就是獲取了系統調用的參數。返回的時候,將返回值放到r0就OK了。
根據上面的描述,r0有兩個作用,傳遞參數,返回結果。當把系統調用的結果放到r0的時候,通過r0傳遞的參數值就被覆蓋了。本來,這也沒有什麽,但是有些場合是需要需要這兩個值的:
1、ptrace (和debugger相關,這裏就不再詳細描述了)
2、system call restart (和signal相關,這裏就不再詳細描述了)
正因為如此,硬件上下文的寄存器中r0有兩份,ARM_r0是傳遞的參數,並復制一份到ARM_ORIG_r0,當系統調用返回的時候,ARM_r0是系統調用的返回值。
OK,我們再回到中斷這個主題,其實在中斷處理過程中,沒有使用ARM_ORIG_r0這個值,但是,為了防止system call restart,可以賦值為非系統調用號的值(例如-1)。
五、中斷退出過程
無論是在內核態(包括系統調用和中斷上下文)還是用戶態,發生了中斷後都會調用irq_handler進行處理,這裏會調用對應的irq number的handler,處理softirq、tasklet、workqueue等(這些內容另開一個文檔描述),但無論如何,最終都是要返回發生中斷的現場。
1、中斷發生在user mode下的退出過程,代碼如下:
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相關,暫且略過/* perform architecture specific actions before user return */
arch_ret_to_user r1, lr----有些硬件平臺需要在中斷返回用戶空間做一些特別處理
ct_user_enter save = 0 ----和trace context相關,暫且略過restore_user_regs fast = 0, offset = 0------------B
ENDPROC(ret_to_user_from_irq)
ENDPROC(ret_to_user)
A:thread_info中的flags成員中有一些low level的標識,如果這些標識設定了就需要進行一些特別的處理,這裏檢測的flag主要包括:
#define _TIF_WORK_MASK (_TIF_NEED_RESCHED | _TIF_SIGPENDING | _TIF_NOTIFY_RESUME)
這三個flag分別表示是否需要調度、是否有信號處理、返回用戶空間之前是否需要調用callback函數。只要有一個flag被設定了,程序就進入work_pending這個分支(work_pending函數需要傳遞三個參數,第三個是參數why是標識哪一個系統調用,當然,我們這裏傳遞的是0)。
B:從字面的意思也可以看成,這部分的代碼就是將進入中斷的時候保存的現場(寄存器值)恢復到實際的ARM的各個寄存器中,從而完全返回到了中斷發生的那一點。具體的代碼如下:
.macro restore_user_regs, fast = 0, offset = 0
ldr r1, [sp, #\offset + S_PSR] ----r1保存了pt_regs中的spsr,也就是發生中斷時的CPSR
ldr lr, [sp, #\offset + S_PC]! ----lr保存了PC值,同時sp移動到了pt_regs中PC的位置
msr spsr_cxsf, r1 ---------賦值給spsr,進行返回用戶空間的準備
clrex @ clear the exclusive monitor
.if \fast
ldmdb sp, {r1 - lr}^ @ get calling r1 - lr
.else
ldmdb sp, {r0 - lr}^ ------將保存在內核棧上的數據保存到用戶態的r0~r14寄存器
.endif
mov r0, r0 ---------NOP操作,ARMv5T之前的需要這個操作
add sp, sp, #S_FRAME_SIZE - S_PC----現場已經恢復,移動svc mode的sp到原來的位置
movs pc, lr --------返回用戶空間
.endm
2、中斷發生在svc mode下的退出過程。具體代碼如下:
.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-------將中斷現場的cpsr值保存到spsr中,準備返回中斷發生的現場
ldmia sp, {r0 - pc}^ -----這條指令是ldm異常返回指令,這條指令除了字面上的操作,還包括了將spsr copy到cpsr中。
.endm
Linux中斷 - ARM中斷處理過程