1. 程式人生 > >Linux kernel的中斷子系統之(六):ARM中斷處理過程

Linux kernel的中斷子系統之(六):ARM中斷處理過程

總結:二中斷處理經過兩種模式:IRQ模式和SVC模式,這兩種模式都有自己的stack,同時涉及到異常向量表中的中斷向量。

三ARM處理器在感知到中斷之後,切換CPSR暫存器模式到IRQ;儲存CPSR和PC;mask irq;PC指向irq vector。

四進入中斷的IRQ模式相關處理,然後根據當前處於使用者還是核心空間分別處理。

五是在中斷例程處理完之後退出流程,同樣根據進入中斷前處於使用者還是核心不同分別處理。

一、前言

本文主要以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、中斷控制器的初始化

三、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)需要儲存嗎,當然需要,稍後再儲存。執行到這裡,核心棧的佈局如下圖所示:

ir1

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。執行到這裡,核心棧的佈局如下圖所示:

ir2

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

附錄

change log-2014-10-20,自己又重新閱讀了一遍,做了一些修改,如下:

1、“ARM處理器有多種process mode”修改為“ARM處理器有多種processor mode”
2、增加cpu_init的呼叫場景說明:
    (1)bootstrap CPU initialize
    (2)secondary CPUs initialize
    (3)CPU resume from sleep
3、增加對核心中斷處理的描述
4、增加對搶佔相關的描述

change log-2014-11-20,根據zuoertu網友的提問,做了一些修改,如下:

1、增加對orig_r0的描述

2、增加對why的描述

相關推薦

Linux kernel中斷子系統ARM中斷處理過程

總結:二中斷處理經過兩種模式:IRQ模式和SVC模式,這兩種模式都有自己的stack,同時涉及到異常向量表中的中斷向量。 三ARM處理器在感知到中斷之後,切換CPSR暫存器模式到IRQ;儲存CPSR和PC;mask irq;PC指向irq vector。 四進入中斷的IRQ模式相關處理,然後根據當前處於使用

Linux kernel中斷子系統驅動申請中斷API

思路 esc 設計師 數組 還需 申請 進一步 time num 一、前言本文主要的議題是作為一個普通的驅動工程師,在撰寫自己負責的驅動的時候,如何向Linux Kernel中的中斷子系統註冊中斷處理函數?為了理解註冊中斷的接口,必須了解一些中斷線程化(threaded i

Linux kernel中斷子系統綜述

lock www. api cdc 電平 還需 結構 現在 ces 一、前言一個合格的linux驅動工程師需要對kernel中的中斷子系統有深刻的理解,只有這樣,在寫具體driver的時候才能:1、正確的使用linux kernel提供的的API,例如最著名的request

Linux kernel中斷子系統High level irq event handler

總結:從架構相關的彙編處理跳轉到Machine/控制器相關的handle_arch_irq,generic_handle_irq作為High level irq event handler入口。 一介紹了進入High level irq event handler的路徑__irq_svc-->irq_

Linux kernel中斷子系統IRQ number和中斷描述符

總結: 二描述了中斷處理示意圖,以及關中斷、開中斷,和IRQ number重要概念。 三介紹了三個重要的結構體,irq_desc、irq_data、irq_chip及其之間關係。 四介紹了irq_desc這個全域性變數的初始化,五是操作中斷描述符相關結構體的API介面介紹。 一、前言 本文主要圍繞IRQ

Linux kernel中斷子系統驅動申請中斷API

總結:二重點區分了搶佔式核心和非搶佔式核心的區別:搶佔式核心可以在核心空間進行搶佔,通過對中斷處理進行執行緒化可以提高Linux核心實時性。 三介紹了Linux中斷註冊函式request_threaded_irq,其實request_irq也是對request_threaded_irq的封裝。 四對requ

linux裝置驅動歸納總結3.中斷下半部tasklet

linux裝置驅動歸納總結(六):3.中斷的上半部和下半部——tasklet xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 一、什麼是下半部 中斷是一個很霸道的東西,處理

Linux小小白入門教程建立和刪除資料夾

以下操作在Linux終端進行。Linux因為許可權非常嚴格,所以暫時所有的命令操作全部是在/home資料夾下的/yangjw資料夾下進行。/yangjw資料夾就是登入使用者名稱所在的資料夾,出了此資料

SQL Server2012 學習 檢視的建立、修改等基本操作

前面幾篇部落格對資料表的建立,修改等操作進行了分析。資料表中為了避免冗餘,只儲存最基本的資訊,例如身高、體重、年齡等。如果想檢視一個人的所有資訊,可能要涉及多個數據表(比如有3個數據表分別儲存身高、體重和年齡),這時使用檢視就可以起到很好的效果。1.建立檢視1.1視覺化介面中

Linux核心同步機制spin lock[轉]

內容轉自蝸窩科技-http://www.wowotech.net/kernel_synchronization/spinlock.html,並進行適當地排版。 0 前言 在linux kernel的實現中,經常會遇到這樣的場景:共享資料被中斷上下文和程序上下文訪問,該如何保

Linux vm執行引數OOM相關的引數

一、前言 本文是描述Linux virtual memory執行引數的第二篇,主要是講OOM相關的引數的。為了理解OOM引數,第二章簡單的描述什麼是OOM。如果這個名詞對你毫無壓力,你可以直接進入第三章,這一章是描述具體的引數的,除了描述具體的引數,我們引用了一些具體的

白話Spring原始碼BeanDefinition的註冊過程

上一篇部落格講了bean的建立過程。這次跟大家分享BeanDefinition的註冊過程。 一、什麼是BeanDefinition BeanDefinition:就是bean的定義資訊,比如bean的名稱,對應的class,bean的屬性值,bean是否是單列等等,一般是通過xml來定義的,

python實戰筆記6使用代理處理反爬抓取微信文章

搜狗(http://weixin.sogou.com/)已經為我們做了一層微信文章的爬取,通過它我們可以獲取一些微信文章的列表以及微信公眾號的一些資訊,但是它有很多反爬蟲的措施,可以檢測到你的IP異常,然後把你封掉。本文采用代理的方法處理反爬來抓取微信文章。 (1)目標站點

【原創】Linux虛擬化KVM-Qemu分析中斷虛擬化

# 背景 - `Read the fucking source code!` --By 魯迅 - `A picture is worth a thousand words.` --By 高爾基 說明: 1. KVM版本:5.9.1 2. QEMU版本:5.0.0 3. 工具:Source Insight

嵌入式核心及驅動開發學習筆記 驅動層中斷實現

由於中斷訊號的突發性,CPU要捕獲中斷訊號,有兩種方式。一是不斷輪詢是否有中斷髮生,這樣有點傻;二是通過中斷機制,過程如下: 中斷源 ---> 中斷訊號  --->  中斷控制器 --->  CPU  中斷源有很多,CPU拿

Linux 學習bash指令碼編寫

bash指令碼程式設計:整數測試及特殊變數 exit:退出指令碼 exit # 如果指令碼沒有明確定義退出狀態碼,那麼,最後執行的一條命令的退出碼即為指令碼的退出狀態碼。 bash中常用的條件測試有三種: 測試方法: 命令測試法 [ expression ] 關

Linux基礎檔案內容的關鍵詞匹配

檔案內容的關鍵詞匹配 對於一個內容很多的檔案,如果需要查詢某個關鍵詞及其所在的位置,可以使用grep命令進行搜尋。grep命令是一個非常強大的文字處理命令,主要功能是根據關鍵詞對文字進行篩選,查詢匹配的關鍵詞並輸出位置,grep命令提供了許多選項,常用選項如下所

把握linux核心設計思想核心時鐘中斷

(位於檔案kernel/time/tick-common.c)void __init tick_init(void) { clockevents_register_notifier(&tick_notifier); } tick_notifier定義如下:static struct notif

物聯網平臺構架系列 Amazon, Microsoft, IBM IoT 解決方案導論 結語

物聯網; iot; aws; 亞馬遜; greengrass;microsoft; azure;ibm; watson; bluemix最近研究了一些物聯網平臺技術資料,以做選型參考。腦子裏積累大量信息,便想寫出來做一些普及。作為科普文章,力爭通俗易懂,不確保概念嚴謹性。我會給考據癖者提供相關英文鏈接,以便深

linux操作系統基礎篇

linux操作系統 linux服務 images without 重新 修改 文件內容 請求 用戶訪問 linux服務篇 1.samba服務的搭建 samba的功能: samba是一個網絡服務器,用於Linux和Windows之間共享文件。2. samba服務的啟動、停止、