ARM linux的中斷處理過程 (轉)


一、前言

本文主要以ARM體系結構下的中斷處理為例,講述整個中斷處理過程中的硬件行為和軟件動作。具體整個處理過程分成三個步驟來描述:

1、第二章描述了中斷處理的准備過程

2、第三章描述了當發生中的時候,ARM硬件的行為

3、第四章描述了ARM的中斷進入過程

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

本文涉及的代碼來自3.14內核。另外,本文注意描述ARM指令集的內容,有些source code為了簡短一些,刪除了THUMB相關的代碼,除此之外,有些debug相關的內容也會刪除。

 

二、中斷處理的准備過程

1、中斷模式的stack准備

ARM處理器有多種process 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 abtundstack指針

……

#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 modestackstk->irq 
    "msr    cpsr_c, %3\n\t" 
    "add    r14, %0, %4\n\t" 
    "mov    sp, r14\n\t"
--------設定abt modestackstk->abt 
    "msr    cpsr_c, %5\n\t" 
    "add    r14, %0, %6\n\t" 
    "mov    sp, r14\n\t"
--------設定und modestackstk->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的值,這些信息是編譯器需要的內容。

2、SVC模式的stack准備

我們經常說進程的用戶空間和內核空間,對於一個應用程序而言,可以運行在用戶空間,也可以通過系統調用進入內核空間。在用戶空間,使用的是用戶棧,也就是我們軟件工程師編寫用戶空間程序的時候,保存局部變量的stack。陷入內核后,當然不能用用戶棧了,這時候就需要使用到內核棧。所謂內核棧其實就是處於SVC mode時候使用的棧。

Linux kernel在創建進程(包括用戶進程和內核線程)的時候都會分配一個(或者兩個,和配置相關)page frame,底部是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可以捕獲這種異常。 

or (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的情況)分發給process。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 modestack 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
------tskr9,指向當前的thread info數據結構 
    mov    why, #0
--------whyr8 
    b    ret_to_user_from_irq
----中斷返回,下一章會詳細描述

(1)保存發生中斷時候的現場。所謂保存現場其實就是把發生中斷那一刻的硬件上下文(各個寄存器)保存在了SVC mode的stack上。

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

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

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


    stmia    r0, {r4 - r6}
--------------------
    stmdb    r0, {sp, lr}^
-------------------
    .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。

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

保存現場的代碼和user mode下的現場保存是類似的,因此這里不再詳細描述,只是在下面的代碼中內嵌一些注釋。

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

    ldmia    r0, {r3 - r5} 
    add    r7, sp, #S_SP - 4
------r7指向struct pt_regsr12的位置 
    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 modelrr3 
    stmia    r7, {r2 - r6}
---------壓棧,在棧上形成形成struct pt_regs 
    .endm

五、中斷退出過程

1、中斷發生在user mode下的退出過程,代碼如下:

ENTRY(ret_to_user_from_irq) 
    ldr    r1, [tsk, #TI_FLAGS] 
    tst    r1, #_TIF_WORK_MASK
---------------
    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------------
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這個分支。

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_regsPC的位置 
    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}^
------將保存在內核棧上的數據保存到用戶態的r0r14寄存器 
    .endif 
    mov    r0, r0  
---------NOP操作,ARMv5T之前的需要這個操作 
    add    sp, sp, #S_FRAME_SIZE - S_PC
----現場已經恢復,移動svc modesp到原來的位置 
    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 copycpsr中。 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM