1. 程式人生 > >linux-2.6.18源碼分析筆記---中斷

linux-2.6.18源碼分析筆記---中斷

detect ready esp 標誌位 mas 處理程序 rri 查看 軟件

技術分享圖片一、中斷初始化

中斷的一些硬件機制不做過多的描述,只介紹一些和linux實現比較貼近的機制,便於理解代碼。

1.1 關於intel和linux幾種門的簡介

intel提供了4種門:系統門,中斷門,陷阱門,調用門。

調用門:不同特權級之間實現受控的程序控制轉移,它是放在GDT或LDT之中。使用調用門需要為CALL或JMP指令的操作數提供一個遠指針,該指針中的段選擇符用於指定調用門,指向的是GDT或LDT中的一個段,在低特權級代碼切換到高特權級代碼是會發生代碼段的轉移。(linux沒有使用這種門,感覺這是intel用來給操作系統實現系統調用的機制,但是linux沒有使用,linux使用陷阱門來實現系統調用,原因是軟件實現更加靈活,有優化空間,而且可以用來檢查一些硬件無法檢查的段寄存器數據的正確性)

任務門:用來處理中斷和異常。可以放在GDT、LDT、IDT中,任務門描述符中TSS選擇符字段指向GDT的一個TSS段描述符,在跳轉時必須跳轉到TSS選擇符指向的段,(linux在GDT中只定義了一個TSS,即每個CPU一個TSS),這也是linux中唯一使用調用門來處理的異常,其他異常都使用陷阱門來處理。

中斷門:處理中斷。放在IDT中,清空IF標誌,屏蔽將到來的中斷。linux在intel的基礎上將其中斷門分為如下兩類:

  • 中斷門:用戶態進程不能訪問,所有的中斷處理程序都通過中斷門激活,限制在內核態
  • 系統中斷門:能被用戶態程序訪問,與向量3相關的異常處理程序由系統中斷門來激活,在用戶態可以使用int3指令,該指令表示斷點,用來調試。

陷阱門:與中斷門類似,只是不修改IF標誌位。

  • 陷阱門:用戶態進程不能訪問,大部分的linux異常都由陷阱門激活。
  • 系統門:能被用戶態程序訪問,用戶態程序可以發布into、bound、int 0x80指令,其中int 0x80是系統中斷

門能否被用戶態訪問是有一套優先級判斷機制,這裏不做描述了。

1.2 幾種異常的初始化

arch\i386\kernel\trap.s中的trap_init函數

技術分享圖片
void __init trap_init(void)
{
#ifdef CONFIG_EISA
    void __iomem *p = ioremap(0x0FFFD9
, 4); if (readl(p) == E+(I<<8)+(S<<16)+(A<<24)) { EISA_bus = 1; } iounmap(p); #endif #ifdef CONFIG_X86_LOCAL_APIC init_apic_mappings(); #endif set_trap_gate(0,&divide_error); set_intr_gate(1,&debug); set_intr_gate(2,&nmi); set_system_intr_gate(3, &int3); /* int3/4 can be called from all */ set_system_gate(4,&overflow); set_trap_gate(5,&bounds); set_trap_gate(6,&invalid_op); set_trap_gate(7,&device_not_available); set_task_gate(8,GDT_ENTRY_DOUBLEFAULT_TSS); set_trap_gate(9,&coprocessor_segment_overrun); set_trap_gate(10,&invalid_TSS); set_trap_gate(11,&segment_not_present); set_trap_gate(12,&stack_segment); set_trap_gate(13,&general_protection); set_intr_gate(14,&page_fault); set_trap_gate(15,&spurious_interrupt_bug); set_trap_gate(16,&coprocessor_error); set_trap_gate(17,&alignment_check); #ifdef CONFIG_X86_MCE set_trap_gate(18,&machine_check); #endif set_trap_gate(19,&simd_coprocessor_error); if (cpu_has_fxsr) { /* * Verify that the FXSAVE/FXRSTOR data will be 16-byte aligned. * Generates a compile-time "error: zero width for bit-field" if * the alignment is wrong. */ struct fxsrAlignAssert { int _:!(offsetof(struct task_struct, thread.i387.fxsave) & 15); }; printk(KERN_INFO "Enabling fast FPU save and restore... "); set_in_cr4(X86_CR4_OSFXSR); printk("done.\n"); } if (cpu_has_xmm) { printk(KERN_INFO "Enabling unmasked SIMD FPU exception " "support... "); set_in_cr4(X86_CR4_OSXMMEXCPT); printk("done.\n"); } set_system_gate(SYSCALL_VECTOR,&system_call); /* * Should be a barrier for any external CPU state. */ cpu_init(); trap_init_hook(); }
trap_init

根據前面對各種門的描述,可以知道如下函數的含義(門的含義參考linux劃分而不是intel劃分):

set_trap_gate(n,addr):在IDT的n項插入一個陷阱門

set_intr_gate(n,addr):在IDT的n項插入一個中斷門

set_system_intr_gate(n,addr):在IDT的n項插入一個系統中斷門

set_system_gate(n,addr):在IDT的n項插入一個系統門

set_task_gate(n,addr):在IDT的n項插入一個任務

可以看到先註冊了19個中斷向量的處理函數,函數具體實現在arch\i386\kernel\entry.S

SYSCALL_VECTOR是定義在include\asm-i386\mach-default\irq_vector.h中的宏,為0x80,可知註冊的系統調用處理函數為system_call,在entry.S中。

1.3 中斷和異常的硬件處理

假設所有的初始化已經結束,在發出一個中斷或是異常時,硬件會做一些工作,然後才會跳轉到IDT中的處理函數,為了理解處理函數的最開始一部分,我們有必要了解硬件做了什麽。

下面的步驟假設門是中斷門或是陷阱門,並且只關註特權級切換的情況,了解在linux系統中的用戶態棧切換到內核棧的過程(linux只使用了兩種特權級,0和3,3表示用戶態,0表示內核態)

a、根據tr寄存器找到TSS,然後根據TSS中的ESP0字段找到內核棧的位置。

b、依次向內核棧中保存ss、esp、eflags、cs、eip這幾個寄存器的值

c、如果產生了一個硬件出錯碼,則將它保存在棧中

d、然後根據中斷向量找到中斷或是異常處理程序,執行異常處理程序。

1.4 異常處理程序的一般流程

可以觀察entry.S中的幾個異常處理程序,它們都有一個比較通用的流程

先是調用了RING0_INT_FRAME或是RING0_EC_FRAME宏,該宏定義在entry.S頭部,看看實現

技術分享圖片
#define RING0_INT_FRAME \
    CFI_STARTPROC simple;    CFI_DEF_CFA esp, 3*4;    /*CFI_OFFSET cs, -2*4;*/    CFI_OFFSET eip, -3*4
RING0_INT_FRAME

而以CFI開頭的宏定義在include\asm-i386\dwarf2.h中,發現其實這段宏好像也並不涉及到實際的匯編代碼

技術分享圖片
#ifdef CONFIG_UNWIND_INFO

#define CFI_STARTPROC .cfi_startproc
#define CFI_ENDPROC .cfi_endproc
#define CFI_DEF_CFA .cfi_def_cfa
#define CFI_DEF_CFA_REGISTER .cfi_def_cfa_register
#define CFI_DEF_CFA_OFFSET .cfi_def_cfa_offset
#define CFI_ADJUST_CFA_OFFSET .cfi_adjust_cfa_offset
#define CFI_OFFSET .cfi_offset
#define CFI_REL_OFFSET .cfi_rel_offset
#define CFI_REGISTER .cfi_register
#define CFI_RESTORE .cfi_restore
#define CFI_REMEMBER_STATE .cfi_remember_state
#define CFI_RESTORE_STATE .cfi_restore_state

#else

/* Due to the structure of pre-exisiting code, don‘t use assembler line
   comment character # to ignore the arguments. Instead, use a dummy macro. */
.macro ignore a=0, b=0, c=0, d=0
.endm

#define CFI_STARTPROC    ignore
#define CFI_ENDPROC    ignore
#define CFI_DEF_CFA    ignore
#define CFI_DEF_CFA_REGISTER    ignore
#define CFI_DEF_CFA_OFFSET    ignore
#define CFI_ADJUST_CFA_OFFSET    ignore
#define CFI_OFFSET    ignore
#define CFI_REL_OFFSET    ignore
#define CFI_REGISTER    ignore
#define CFI_RESTORE    ignore
#define CFI_REMEMBER_STATE ignore
#define CFI_RESTORE_STATE ignore

#endif

#endif
dwarf2.h

查看了《深入理解linux內核》、《linux內核源代碼情景分析》,他們在講述這段代碼時都沒有涉及到相關宏的含義,而且邏輯也是完整的,這裏就以這種宏沒有實質代碼來分析,不知道他是處於什麽考慮才設計的這段代碼。

參考《深入理解linux內核》分析異常處理的通用流程,假設handler_name代表一個通用的異常處理程序的名字:

技術分享圖片
ENTRY(handler_name)
    pushl $0    /*只有有些異常處理程序有*/
    pushl $do_handler_name
    jmp error_code
異常處理程序通用流程

1、如果控制單元沒有把一個硬件出錯碼插入到棧中,相應的匯編程序語言會包含一條push $0指令。可以查看entry.S中的幾種異常處理程序,如果沒有push $0指令,則代表該異常發生時,硬件向棧中push了一個出錯碼。

2、push一個c語言函數,代表異常處理程序,以do_開頭,後面加異常處理的名稱

3、跳轉到一段稱為error_code的代碼

技術分享圖片
error_code:
    pushl %ds
    CFI_ADJUST_CFA_OFFSET 4
    /*CFI_REL_OFFSET ds, 0*/
    pushl %eax
    CFI_ADJUST_CFA_OFFSET 4
    CFI_REL_OFFSET eax, 0
    xorl %eax, %eax
    pushl %ebp
    CFI_ADJUST_CFA_OFFSET 4
    CFI_REL_OFFSET ebp, 0
    pushl %edi
    CFI_ADJUST_CFA_OFFSET 4
    CFI_REL_OFFSET edi, 0
    pushl %esi
    CFI_ADJUST_CFA_OFFSET 4
    CFI_REL_OFFSET esi, 0
    pushl %edx
    CFI_ADJUST_CFA_OFFSET 4
    CFI_REL_OFFSET edx, 0
    decl %eax            # eax = -1
    pushl %ecx
    CFI_ADJUST_CFA_OFFSET 4
    CFI_REL_OFFSET ecx, 0
    pushl %ebx
    CFI_ADJUST_CFA_OFFSET 4
    CFI_REL_OFFSET ebx, 0
    cld
    pushl %es
    CFI_ADJUST_CFA_OFFSET 4
    /*CFI_REL_OFFSET es, 0*/
    UNWIND_ESPFIX_STACK
    popl %ecx
    CFI_ADJUST_CFA_OFFSET -4
    /*CFI_REGISTER es, ecx*/
    movl ES(%esp), %edi        # get the function address
    movl ORIG_EAX(%esp), %edx    # get the error code
    movl %eax, ORIG_EAX(%esp)
    movl %ecx, ES(%esp)
    /*CFI_REL_OFFSET es, ES*/
    movl $(__USER_DS), %ecx
    movl %ecx, %ds
    movl %ecx, %es
    movl %esp,%eax            # pt_regs pointer
    call *%edi
    jmp ret_from_exception
    CFI_ENDPROC
error_code

error_code執行如下流程:

a、將ds、eax、edi、esi、edx、ecx、ebx保存到棧中,執行cld,清除方向標誌。

b、將es寄存器的值保存到ecx,將esp+0x20的值賦給edi(在棧中的寄存器還有ds-ebx,共28個字節,由於esp指向第一個空位,所以esp+0x20指向ds之前的4個字節,是異常處理通用流程的第二步push到棧中的c語言函數地址),將esp+0x24賦值給edx(esp+0x24為出錯碼),在原出錯碼的地址填上-1(用來隔開0x80異常)。c函數地址處填入es的值。將棧指針賦值給eax,然後調用異常處理通用流程的第二步push到棧中的c語言函數地址,該函數是通過寄存器eax、edx來傳遞參數而不是通過棧。

c、等到c語言的中斷異常處理函數執行完之後就跳轉到ret_from_exception,就像它的函數名字一樣,從異常中返回

二、中斷處理

在include\linux\irq.h中,定義了struct irq_desc,中斷描述符,status的狀態也在該文件中。

技術分享圖片
/**
 * struct irq_desc - interrupt descriptor
 *
 * @handle_irq:        highlevel irq-events handler [if NULL, __do_IRQ()]
 * @chip:        low level interrupt hardware access
 * @handler_data:    per-IRQ data for the irq_chip methods
 * @chip_data:        platform-specific per-chip private data for the chip
 *            methods, to allow shared chip implementations
 * @action:        the irq action chain
 * @status:        status information
 * @depth:        disable-depth, for nested irq_disable() calls
 * @wake_depth:        enable depth, for multiple set_irq_wake() callers
 * @irq_count:        stats field to detect stalled irqs
 * @irqs_unhandled:    stats field for spurious unhandled interrupts
 * @lock:        locking for SMP
 * @affinity:        IRQ affinity on SMP
 * @cpu:        cpu index useful for balancing
 * @pending_mask:    pending rebalanced interrupts
 * @move_irq:        need to re-target IRQ destination
 * @dir:        /proc/irq/ procfs entry
 * @affinity_entry:    /proc/irq/smp_affinity procfs entry on SMP
 *
 * Pad this out to 32 bytes for cache and indexing reasons.
 */
struct irq_desc {
    void fastcall        (*handle_irq)(unsigned int irq,
                          struct irq_desc *desc,
                          struct pt_regs *regs);
    struct irq_chip        *chip;
    void            *handler_data;
    void            *chip_data;
    struct irqaction    *action;    /* IRQ action list */
    unsigned int        status;        /* IRQ status */

    unsigned int        depth;        /* nested irq disables */
    unsigned int        wake_depth;    /* nested wake enables */
    unsigned int        irq_count;    /* For detecting broken IRQs */
    unsigned int        irqs_unhandled;
    spinlock_t        lock;
#ifdef CONFIG_SMP
    cpumask_t        affinity;
    unsigned int        cpu;
#endif
#if defined(CONFIG_GENERIC_PENDING_IRQ) || defined(CONFIG_IRQBALANCE)
    cpumask_t        pending_mask;
    unsigned int        move_irq;    /* need to re-target IRQ dest */
#endif
#ifdef CONFIG_PROC_FS
    struct proc_dir_entry *dir;
#endif
} ____cacheline_aligned;

extern struct irq_desc irq_desc[NR_IRQS];
irq_desc

註意上部分代碼最後還創建了一個irq_desc數組,表示所有中斷向量的處理方式。其中NR_IRQS定義在include\asm-i386\mach-default\irq_vectors_limits.h中,為224。

2.1 中斷向量初始化

在arch\i386\kernel\i8259.c中定義了init_IRQ函數用來設置大量用於外設的通用中斷門

技術分享圖片
void __init init_IRQ(void)
{
    int i;

    /* all the set up before the call gates are initialised */
    pre_intr_init_hook();

    /*
     * Cover the whole vector space, no vector can escape
     * us. (some of these will be overridden and become
     * ‘special‘ SMP interrupts)
     */
    for (i = 0; i < (NR_VECTORS - FIRST_EXTERNAL_VECTOR); i++) {
        int vector = FIRST_EXTERNAL_VECTOR + i;
        if (i >= NR_IRQS)
            break;
        if (vector != SYSCALL_VECTOR) 
            set_intr_gate(vector, interrupt[i]);
    }

    /* setup after call gates are initialised (usually add in
     * the architecture specific gates)
     */
    intr_init_hook();

    /*
     * Set the clock to HZ Hz, we already have a valid
     * vector now:
     */
    setup_pit_timer();

    /*
     * External FPU? Set up irq13 if so, for
     * original braindamaged IBM FERR coupling.
     */
    if (boot_cpu_data.hard_math && !cpu_has_fpu)
        setup_irq(FPU_IRQ, &fpu_irq);

    irq_ctx_init(smp_processor_id());
}
init_IRQ

在include\asm-i386\mach-default\irq_vectors.h中定義了一些宏,NR_VECTORS表示中斷向量的最大數,i386有256個,FIRST_EXTERNAL_VECTOR表示第一個用於外部中斷的中斷號,前19個中斷向量用於異常,20到31intel保留,所以第一個外部中斷號為32.

interrupt數組定義在arch\i386\kernel\entry.S中,由幾段匯編程序創建

2.2 IRQ共享和動態分配

IRQ共享表示一個IRQ線由多個設備共享,當一個IRQ線出現中斷時,每個中斷服務例程(ISA)都被執行。

IRQ動態分配指一條IRQ線只有到最後時刻才與一個設備驅動程序相關聯。

前面描述的irq_desc數組代表了所有中斷向量,既然一個IRQ線能被多個設備同時使用,那該向量應該以某種方式記錄共享該線的設備,irq_desc結構中有action字段,該字段為irqaction結構,定義在include\linux\interrupt.h中

技術分享圖片
struct irqaction {
    irqreturn_t (*handler)(int, void *, struct pt_regs *);
    unsigned long flags;
    cpumask_t mask;
    const char *name;
    void *dev_id;
    struct irqaction *next;
    int irq;
    struct proc_dir_entry *dir;
};
irqaction

該結構中還定義了一個next指針,指向下一個irqaction結構。

所以最終的結構是這樣:

技術分享圖片

linux-2.6.18源碼分析筆記---中斷