天天看點

linux中斷系統那些事之----中斷處理過程【轉】

轉自:http://blog.csdn.net/xiaojsj111/article/details/14129661

以外部中斷irq為例來說明,當外部硬體産生中斷時,linux的處理過程。首先先說明當外部中斷産生時,硬體處理器所做的工作如下:

R14_irq = address of next instruction to be executed + 4/*将寄存器lr_mode設定成傳回位址,即為目前pc的值,因為pc是目前執行指令的下兩條指令*/

       SPSR_irq = CPSR                /*儲存處理器目前狀态、中斷屏蔽位以及各條件标志位*/

       CPSR[4:0] = 0b10010         /*設定目前程式狀态寄存器CPSR中相應的位進入IRQ模式,注意cpsr是所有模式共享的*/

       CPSR[5] = 0                        /*在ARM狀态執行*/

                                          /*CPSR[6] 不變*/

       CPSR[7] = 1                       /*禁止正常中斷*/

       If high vectors configured then

              PC=0xFFFF0018          /*将程式計數器(PC)值設定成該異常中斷的中斷向量位址,進而跳轉到相應的異常中斷處理程式處執行,對于ARMv7向量表普遍中斷是0xFFFF0018*/

       else

              PC=0x00000018     /*對于低向量*/

假設在使用者空間時,産生了外部硬體中斷,則這個時候的指令跳轉流程如下:

[cpp] view plain copy

  1. __vectors_start:---------------〉在中斷向量表被拷貝後,該位址就是0xffff0000.  
  2.  ARM( swi SYS_ERROR0 )  
  3.  THUMB( svc #0 )  
  4.  THUMB( nop )  
  5. W(b) vector_und + stubs_offset  
  6. W(ldr) pc, .LCvswi + stubs_offset  
  7. W(b) vector_pabt + stubs_offset  
  8. W(b) vector_dabt + stubs_offset  
  9. W(b) vector_addrexcptn + stubs_offset  
  10. W(b)vector_irq + stubs_offset----------〉當外部中斷産生時,pc直接指向這個位址。  
  11. W(b) vector_fiq + stubs_offset  
  12. .globl __vectors_end  

下面的vector_stubirq, IRQ_MODE, 4語句,展開就是vector_irq,是以上述語句跳轉到如下語句執行:

  1. __stubs_start:  
  2. /* 
  3.  * Interrupt dispatcher 
  4.  */  
  5. vector_stub irq, IRQ_MODE, 4  
  6. .long __irq_usr@  0  (USR_26 / USR_32)  
  7. .long __irq_invalid@  1  (FIQ_26 / FIQ_32)  
  8. .long __irq_invalid@  2  (IRQ_26 / IRQ_32)  
  9. .long __irq_svc@  3  (SVC_26 / SVC_32)  
  10. .long __irq_invalid@  4  
  11. .long __irq_invalid@  5  
  12. .long __irq_invalid@  6  
  13. .long __irq_invalid@  7  
  14. .long __irq_invalid@  8  
  15. .long __irq_invalid@  9  
  16. .long __irq_invalid@  a  
  17. .long __irq_invalid@  b  
  18. .long __irq_invalid@  c  
  19. .long __irq_invalid@  d  
  20. .long __irq_invalid@  e  
  21. .long __irq_invalid@  f  

vector_stubirq, IRQ_MODE, 4語句展開如下:

  1. <span style="font-size:18px">/* 
  2.  * Vector stubs. 
  3.  * 
  4.  * This code is copied to 0xffff0200 so we can use branches in the 
  5.  * vectors, rather than ldr's.  Note that this code must not 
  6.  * exceed 0x300 bytes. 
  7.  * Common stub entry macro: 
  8.  *   Enter in IRQ mode, spsr = SVC/USR CPSR, lr = SVC/USR PC 
  9.  * SP points to a minimal amount of processor-private memory, the address 
  10.  * of which is copied into r0 for the mode specific abort handler. 
  11.     .macro  vector_stub, name, mode, correction=0  
  12.     .align  5  
  13. vector_\name:  
  14.     .if \correction  
  15.     sub lr, lr, #\correction  //因為硬體處理器是将目前指令的下兩條指令的位址存儲在lr寄存器中,是以這裡需要減4,讓他指向被中斷指令的下一條,這樣當中斷被恢複時,可以繼續被中斷的指令繼續執行。  
  16.     .endif<span style="white-space:pre">            </span>      //需要注意的是,這個時候的lr寄存器,已經是irq模式下的私有寄存器了,在中斷産生時,硬體處理器已經自動為他賦了值。  
  17.     @  
  18.     @ Save r0, lr_<exception> (parent PC) and spsr_<exception>  
  19.     @ (parent CPSR)  
  20.     stmia   sp, {r0, lr}        @ save r0, lr//儲存r0和lr寄存器,即被中斷的下一條指令  
  21.     mrs lr, spsr  
  22.     str lr, [sp, #8]        @ save spsr  
  23.     @ Prepare for SVC32 mode.  IRQs remain disabled.//準備從中斷模式切換到管理模式,不同的模式,對應各自不同的堆棧。  
  24.     mrs r0, cpsr      
  25.     eor r0, r0, #(\mode ^ SVC_MODE | PSR_ISETSTATE)  
  26.     msr spsr_cxsf, r0  
  27.     @ the branch table must immediately follow this code  
  28.     and lr, lr, #0x0f           //擷取被中斷前,處理器所處的模式  
  29.  THUMB( adr r0, 1f          )  
  30.  THUMB( ldr lr, [r0, lr, lsl #2]    )  
  31.     mov r0, sp<span style="white-space:pre">            </span>//讓r0寄存器指向中斷模式下堆棧的基位址  
  32.  ARM(   ldr lr, [pc, lr, lsl #2]    )  
  33.     movs    pc, lr          @ branch to handler in SVC mode,同時将中斷模式下的spsr_irq(irq私有的)指派給cpsr(該寄存器所有模式共享)  
  34. ENDPROC(vector_\name)</span>  

此時中斷模式下的私有棧sp的存儲情況如下:(注意這個時候的sp是中斷模式下的堆棧sp),并且這個時候r0寄存器中,儲存有sp的指針值,由于r0已經被儲存到堆棧,是以可以放心被使用

根據被中斷時,處理器模式的不同,分别跳轉到__irq_usr和__irq_svc兩個分支。

在這裡我們以__irq_usr為例來說明:

  1. <span style="font-size:18px">__irq_usr:  
  2.     usr_entry       //進行中斷前的硬體上下文的儲存  
  3.     kuser_cmpxchg_check  
  4.     irq_handler  
  5.     get_thread_info tsk//擷取被中斷的使用者程序或核心線程所對應的核心棧所對應的thread info結構。  
  6.     mov why, #0  
  7.     b   ret_to_user_from_irq//恢複被中斷時的上下文,然後繼續被中斷的程序或線程的執行  
  8.  UNWIND(.fnend      )  
  9. ENDPROC(__irq_usr)</span>  

usr_entry展開如下:

  1. .macro  usr_entry  
  2. UNWIND(.fnstart )  
  3. UNWIND(.cantunwind  )   @ don't unwind the user space  
  4. sub sp, sp, #S_FRAME_SIZE   // #S_FRAME_SIZE的值為72  
  5. ARM(    stmib   sp, {r1 - r12}  )      //盡管目前是處于管理模式,但由于svc和usr的r0-r12是公共的,是以相當于儲存使用者模式的r1-r12寄存器  
  6. THUMB(  stmia   sp, {r0 - r12}  )  
  7. ldmia   r0, {r3 - r5}          //将之前儲存在中斷模式堆棧中的r0_usr,lr,spsr分别存儲到r3-r5中  
  8. add r0, sp, #S_PC       @ here for interlock avoidance #S_PC=60  
  9. mov r6, #-1         @  ""  ""     ""        ""  
  10. str r3, [sp]        @ save the "real" r0 copied  
  11.                 @ from the exception stack  
  12. @  
  13. @ We are now ready to fill in the remaining blanks on the stack:  
  14. @  r4 - lr_<exception>, already fixed up for correct return/restart  
  15. @  r5 - spsr_<exception>  
  16. @  r6 - orig_r0 (see pt_regs definition in ptrace.h)  
  17. @ Also, separately save sp_usr and lr_usr  
  18. stmia   r0, {r4 - r6}  
  19. ARM(    stmdb   r0, {sp, lr}^           )//儲存使用者模式下的sp_usr,lr_usr  
  20. THUMB(  store_user_sp_lr r0, r1, S_SP - S_PC    )  
  21. @ Enable the alignment trap while in kernel mode  
  22. alignment_trap r0  
  23. @ Clear FP to mark the first stack frame  
  24. zero_fp  
  25. ifdef CONFIG_IRQSOFF_TRACER  
  26. bl  trace_hardirqs_off  
  27. endif  
  28. .endm  

至此,使用者模式下所有的寄存器都被正确儲存了,并且處理器模式中irq模式成功切換到管理模式,并且sp這個時候是指向儲存r0_usr寄存器值得地方。此時的管理模式的核心棧分布如下:

需要說明的是:上圖中的lr_irq即為使用者模式下被中斷指令的下一條指令,spsr_irq即為使用者模式下被中斷時的cpsr寄存器。

在這裡說明下,中斷時寄存器的儲存是有固定的順序的,他們順序即如下所示:

cpsr(r16)
pc(r15)
lr(r14)
sp(r13)
r12(ip)
r11(fp)
r10
r9
r8
r7
r6
r5
r4
r3
r2
r1
r0

上圖中的S_FRAME_SIZE, S_PC在arch/arm/kernel/Asm-offsets.c:中定義

  DEFINE(S_FRAME_SIZE,     sizeof(struct pt_regs));

  DEFINE(S_PC,         offsetof(struct pt_regs, ARM_pc));

include/asm-arm/Ptrace.h:

struct pt_regs {

    long uregs[18];

};

#define ARM_pc     uregs[15]

呵呵,pt_regs中對應的就是上面棧上的18個寄存器,ARM_pc是pc寄存器存放在這個數組中的偏移。

接着看get_thread_info, 它也是個宏,用來擷取目前線程的位址。他的結構體定義如下:

include/linux/Sched.h:

union thread_union {

    struct thread_info thread_info;  /*線程屬性*/

    unsigned long stack[THREAD_SIZE/sizeof(long)];  /*棧*/

由它定義的線程是8K位元組對齊的, 并且在這8K的最低位址處存放的就是thread_info對象,即該棧擁有者線程的對象,而get_thread_info就是通過把sp低13位清0(8K邊界)來擷取目前thread_info對象的位址。

   arch/arm/kernel/entry-armv.S:

    .macro  get_thread_info, rd

    mov /rd, sp, lsr #13

    mov /rd, /rd, lsl #13

    .endm

調用該宏後寄存器tsk裡存放的就是目前線程的位址了, tsk是哪個寄存器呢,呵呵我們在看:

arch/arm/kernel/entry-header.S:

tsk .req    r9      @ current thread_info

呵呵,tsk隻是r9的别名而已, 是以這時r9裡儲存的就是目前線程的位址。

為了将彙編部分講完,我們繼續研究ret_to_user_from_irq函數,該函數展開後,如下:

  1. <span style="font-size:18px">ENTRY(ret_to_user_from_irq)  
  2.     ldr r1, [tsk, #TI_FLAGS] //tsk如上所述,是r9寄存器的别名,并且是指向thread_info結構體的  
  3.     tst r1, #_TIF_WORK_MASK  //檢測是否有待處理的任務  
  4.     bne work_pending  
  5. no_work_pending:  
  6. #if defined(CONFIG_IRQSOFF_TRACER)  
  7.     asm_trace_hardirqs_on  
  8. #endif  
  9.     /* perform architecture specific actions before user return */  
  10.     arch_ret_to_user r1, lr    //針對arm,是dummy的  
  11.     restore_user_regs fast = 0, offset = 0//恢複之前使用者模式時被中斷時所儲存的寄存器上下文  
  12. ENDPROC(ret_to_user_from_irq)</span>  

restore_user_regs展開如下: 

  1. <span style="font-size:18px">   .macro  restore_user_regs, fast = 0, offset = 0  
  2.     ldr r1, [sp, #\offset + S_PSR]  @ get calling cpsr 即為被中斷時,處理器的cpsr值  
  3.     ldr lr, [sp, #\offset + S_PC]!  @ get pc         即為被中斷指令的,下一條指令  
  4.     msr spsr_cxsf, r1           @ save in spsr_svc  //将r1指派給管理模式下的spsr_svc,這樣在movs時,會自動将該值指派為cpsr  
  5. #if defined(CONFIG_CPU_V6)  
  6.     strex   r1, r2, [sp]            @ clear the exclusive monitor  
  7. #elif defined(CONFIG_CPU_32v6K)  
  8.     clrex                   @ clear the exclusive monitor  
  9.     .if \fast  
  10.     ldmdb   sp, {r1 - lr}^          @ get calling r1 - lr  
  11.     .else  
  12.     ldmdb   sp, {r0 - lr}^          @ get calling r0 - lr,将儲存在核心棧中的r0到r14恢複到使用者模式中的寄存器  
  13.     .endif  
  14.     mov r0, r0              @ ARMv5T and earlier require a nop  
  15.                         @ after ldm {}^  
  16.     add sp, sp, #S_FRAME_SIZE - S_PC    //恢複核心棧到中斷産生之前的位置。  
  17.     movs    pc, lr              @ return & move spsr_svc into cpsr  
  18.     .endm  
  19. </span>  

至此中斷彙編部分已經全部處理完成。

最後摘錄部門網上經典的問題解答:

問題1:vector_irq已經是異常、中斷處理的入口函數了,為什麼還要加stubs_offset?(  b    vector_irq + stubs_offset)

答:(1)核心剛啟動時(head.S檔案)通過設定CP15的c1寄存器已經确定了異常向量表的起始位址(例如0xffff0000),是以需要把已經寫好的核心代碼中的異常向量表考到0xffff0000處,隻有這樣在發生異常時核心才能正确的處理異常。

(2)從上面代碼看出向量表和stubs(中斷處理函數)都發生了搬移,如果還用b vector_irq,那麼實際執行的時候就無法跳轉到搬移後的vector_irq處,因為指令碼裡寫的是原來的偏移量,是以需要把指令碼中的偏移量寫成搬移後的。至于為什麼搬移後的位址是vector_irq+stubs_offset,請參考我的上篇blog:linux中斷系統那些事之----中斷初始化過程

問題2:為什麼在異常向量表中,用b指令跳轉而不是用ldr絕對跳轉?

答:因為使用b指令跳轉比絕對跳轉(ldr pc,XXXX)效率高,正因為效率高,是以把__stubs_start~__stubs_end之間的代碼考到了0xffff0200起始處。

注意:

因為b跳轉指令隻能在+/-32MB之内跳轉,是以必須拷貝到0xffff0000附近。

b指令是相對于目前PC的跳轉,當彙編器看到 B 指令後會把要跳轉的标簽轉化為相對于目前PC的偏移量寫入指令碼。

問題3:為什麼首先進入head.S開始執行?

答:核心源代碼頂層目錄下的Makefile制定了vmlinux生成規則:

# vmlinux image - includingupdated kernel symbols

vmlinux: $(vmlinux-lds)$(vmlinux-init) $(vmlinux-main) vmlinux.o $(kallsyms.o)FORCE

其中$(vmlinux-lds)是編譯連接配接腳本,對于ARM平台,就是arch/arm/kernel/vmlinux-lds檔案。vmlinux-init也在頂層Makefile中定義:

vmlinux-init := $(head-y)$(init-y)

head-y 在arch/arm/Makefile中定義:

head-y:=arch/arm/kernel/head$(MMUEX T).o arch/arm/kernel/init_task.o

ifeq ($(CONFIG_MMU),)

MMUEXT := -nommu

endif

對于有MMU的處理器,MMUEXT為空白字元串,是以arch/arm/kernel/head.O 是第一個連接配接的檔案,而這個檔案是由arch/arm/kernel/head.S編譯産生成的。

綜合以上分析,可以得出結論,非壓縮ARM Linux核心的入口點在arch/arm/kernel/head.s中。

問題4: 中斷為什麼必須進入svc模式?

一個最重要原因是:

如果一個中斷模式(例如從usr進入irq模式,在irq模式中)中重新允許了中斷,并且在這個中斷例程中使用了BL指令調用子程式,BL指令會自動将子程式傳回位址儲存到目前模式的sp(即r14_irq)中,這個位址随後會被在目前模式下産生的中斷所破壞,因為産生中斷時CPU會将目前模式的PC儲存到r14_irq,這樣就把剛剛儲存的子程式傳回位址沖掉。為了避免這種情況,中斷例程應該切換到SVC或者系統模式,這樣的話,BL指令可以使用r14_svc來儲存子程式的傳回位址。

問題5:為什麼跳轉表中有的用了b指令跳轉,而有的用了ldr  px,xxxx?

         W(b)         vector_und+ stubs_offset

         W(ldr)      pc, .LCvswi + stubs_offset

         W(b)         vector_pabt+ stubs_offset

         W(b)         vector_dabt+ stubs_offset

         W(b)         vector_addrexcptn+ stubs_offset

         W(b)         vector_irq+ stubs_offset      

         W(b)         vector_fiq+ stubs_offset

.LCvswi:

         .word       vector_swi

由于系統調用異常的代碼編譯在其他檔案中,其入口位址與異常向量相隔較遠,使用b指令無法跳轉過去(b指令隻能相對目前pc跳轉32M範圍)。是以将其位址存放到LCvswi中,并從記憶體位址中加載其入口位址,原理與其他調用是一樣的。這也就是為什麼系統調用的速度稍微慢一點的原因。

問題6:為什麼ARM能進行中斷?

因為ARM架構的CPU有一個機制,隻要中斷産生了,CPU就會根據中斷類型自動跳轉到某個特定的位址(即中斷向量表中的某個位址)。如下表所示,既是中斷向量表。

 ARM中斷向量表及位址

問題7:什麼是High vector?

A:在Linux3.1.0,arch/arm/include/asm/system.hline121 有定義如下:

#if __LINUX_ARM_ARCH__ >=4

#define vectors_high()  (cr_alignment & CR_V)

#else

#define vectors_high()  (0)

#endif

意思就是,如果使用的ARM架構大于等于4,則定義vectors_high()=cr_alignment&CR_V,該值就等于0xffff0000

在Linux3.1.0,arch/arm/include/asm/system.hline33有定義如下:

#define CR_V   (1 << 13)       /* Vectors relocated to 0xffff0000 */

arm下規定,在0x00000000或0xffff0000的位址處必須存放一張跳轉表。

問題8:中斷向量表是如何存放到0x00000000或0xffff0000位址的?

A:Uboot執行結束後會把Linux核心拷貝到記憶體中開始執行,linux核心執行的第一條指令是linux/arch/arm/kernel/head.S,此檔案中執行一些參數設定等操作後跳入linux/init/main.c檔案的start_kernel函數,此函數調用一系列初始化函數,其中trip_init()函數實作向量表的設定操作。

【作者】sky

【出處】http://www.cnblogs.com/sky-heaven/

【部落格園】 http://www.cnblogs.com/sky-heaven/

【知乎】 http://www.zhihu.com/people/zhang-bing-hua

【我的作品---旋轉倒立擺】 http://v.youku.com/v_show/id_XODM5NDAzNjQw.html?spm=a2hzp.8253869.0.0&from=y1.7-2

【我的作品---自平衡自動循迹車】 http://v.youku.com/v_show/id_XODM5MzYyNTIw.html?spm=a2hzp.8253869.0.0&from=y1.7-2

【大餅教你學系列】https://edu.csdn.net/course/detail/10393

本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.

繼續閱讀