1. 程式人生 > >Linux中斷 - ARM中斷處理過程

Linux中斷 - ARM中斷處理過程

thum nio cti abort 兩個 alloc pos 不同 eve

一、前言

本文主要以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 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的原因了。

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 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,從而實現了模式的切換。

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} -------------------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:代碼執行到這裏的時候,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
#endif

svc_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中斷處理過程