硬中斷的完整處理過程

一個硬中斷的完整處理過程

★ CPU作的工做:
CPU收到中斷/異常信號;
CPU判斷當前CPL級別若是等於3,則致使堆棧切換3->0,堆棧切換過程:
a. CPU從當前TR指向的TSS中讀取SS0和ESP0;
b. CPU將當前的【SS:ESP】寄存器內容臨時保存起來,假設爲SSt和ESPt;
c. CPU將SS0和ESP0恢復到【SS:ESP】寄存器中;
d. CPU將臨時保存的SSt和ESPt壓入當前的堆棧【SS:ESP】中(其實就是SS0和ESP0指向的堆棧);
CPU判斷當前CPL級別若是等於0,則不會有2中的步驟;
CPU將EFLAGS、CS、EIP依次壓入當前的堆棧【SS:ESP】中;
若是當前是異常,則CPU將異常碼error code壓入當前的堆棧【SS:ESP】中,不然會省略該步驟;
對於中斷,CPU清零當前EFLAGS->IF位,即關閉CPU中斷使能,對於異常,CPU則不會清零該位;
執行對中斷/異常處理程序的調用;

注:對中斷/異常處理程序的要求,爲了正常的從中斷/異常處理程序中返回,中斷/異常處理程序必須使用IRET指令返回,該指令會依次出棧EIP、CS和 EFLAGS,比RET多一個EFLAGS,當EFLAGS恢復後,因爲原來保存時IF位爲1,所以這裏至關於CPU中斷使能又打開了;
★ Linux內核作的工做 (2.4 kernel):
1. 中斷向量表-->common_interrupt:
common_interrupt:
SAVE_ALL
pushl $ret_from_intr
SYMBOL_NAME_STR(call_do_IRQ):
jmp SYMBOL_NAME_STR(do_IRQ);

SAVE_ALL保存全部CPU沒有保存的寄存器,因爲do_IRQ是函數,這裏直接調用jmp,(通常用call來調用函數,call會致使 push EIP,但jmp不會)這樣當do_IRQ返回調用ret(ret至關於pop EIP)時,會彈出棧中最後一個元素到EIP,很顯然這裏就是 ret_from_intr,也就是說,從do_IRQ中返回後,會跳轉到ret_from_intr處繼續執行;
2. 來到do_IRQ:
a. 首先給硬中斷計數加1,irq_enter(cpu, irq)也就是:++local_irq_count(cpu);每進入一個硬中斷處理函數前,local_irq_count(cpu)計數便被加1,處理完畢後減1;
b. 若是當前設備中斷處理函數能夠在中斷打開的狀況下運行,則調用sti將EFLAGS.IF置位,打開硬中斷使能;
c. 調用request_irq註冊的設備硬中斷處理函數;
d. 不管EFLAGS.IF是否爲0,都調用cli將EFLAGS.IF清零,將硬中斷使能關閉;
e. 給硬中斷計數減1,irq_enter(cpu, irq);該函數其實就是:--local_irq_count(cpu);
f. 若是此時有軟中斷須要運行(如在前面的硬中斷處理函數中調用__cpu_raise_softirq),則進入do_softirq中處理軟中斷,關於do_softirq中的處理步驟見3;
e. do_IRQ執行ret,返回到ret_from_intr。
3. do_softirq:
a. 首先判斷當前是否還有沒有處理完畢的硬中斷處理程序或軟中斷處理程序,若是有,則直接退出該函數。
b.將軟中斷處理計數加1,local_bh_disable()也就是local_bh_count(cpu)++;每進入do_softirq準備進 行處理軟中斷前,local_bh_count(cpu)計數便被加1,軟中斷處理函數處理完畢後,在do_softirq返回前,將其值減1;
c. 不管EFLAGS.IF是否爲0,都調用cli將EFLAGS.IF清零,將硬中斷使能關閉,處理些軟中斷標誌位的問題,隨後調用sti將EFLAGS.IF置位,打開硬中斷使能;
d. 執行軟中斷處理函數;
e. 調用cli將EFLAGS.IF清零,將硬中斷使能關閉,處理些軟中斷標誌位的問題;
f. 將軟中斷處理計數減1,local_bh_enable()也就是local_bh_count(cpu)--;
g. 返回到2.e中;
4. ret_from_intr:
ENTRY(ret_from_intr)
GET_CURRENT(%ebx)
movl EFLAGS(%esp),%eax
movb CS(%esp),%al
testl $(VM_MASK | 3),%eax
jne ret_with_reschedule
jmp restore_all

在這段代碼中,經過"testl $(VM_MASK |3),%eax"檢測中斷前夕寄存器EFLAGS的高6位和代碼段寄存器CS的內容,來判斷中斷前夕CPU是否運行於VM86模式、用戶空間仍是系統空 間,對VM86這裏不講了,可是咱們知道CS最低兩位表明着中斷髮生時CPU的運行級別CPL,咱們知道Linux只採用兩種運行級別,系統爲0,用戶爲 3,因此,若是CS的最低兩位爲非0,那就說明中斷髮生於用戶空間。若是中斷髮生於系統空間,控制就直接轉移到restore_all,而若是發生於用戶 空間,則轉移到ret_with_reschedule。在restore_all中恢復1中保存的寄存器,隨後調用iret恢復EIP、CS、 EFLAGS返回到中斷髮生時的狀態。
5. ret_with_reschedule:
a. 若是發現當前進程的need_resched==1,則會調用schedule;
b. 若是發現還有待須要處理的軟中斷,則會調用do_softirq;

說明:可以走到ret_with_reschedule處,從4中可知,該中斷前必定位於用戶層,並且不可能有可中斷的硬中斷或軟中斷沒有執行,也就是說 到達這裏in_interrupt必然爲0。爲何這裏要說不可能有可中斷的硬中斷或軟中斷沒有執行呢?可中斷的硬中斷或軟中斷必然是被中斷或者異常所打 斷的,當後者處理完畢後,從4中可知,因爲後者發生前位於內核態(也就是可中斷的硬中斷或軟中斷處理過程當中的那個點),故控制就直接轉移到 restore_all,即返回到了可中斷的硬中斷或軟中斷被打斷時的那個點,繼續處理完畢,可見,到達這裏,必然不存在可中斷的硬中斷或軟中斷未被執 行。
附 注:關於in_interrupt宏,也就是(local_irq_count(__cpu) +local_bh_count(__cpu) !=0)。什麼狀況下會致使進入do_softriq時,in_interrupt不爲0呢?第一種,若是當前正在處理可中斷的硬中斷處理函數,假設此時 來了另外一個通道的硬中斷,將致使當前硬中斷處理函數被中斷,進入do_IRQ,隨後處理新來的硬中斷處理函數,當處理完畢後,到達do_softirq, 由2中可知,此時local_irq_count(__cpu)被原先的硬中斷加1,因爲其尚未處理完畢,故--local_irq_count (cpu)還沒來得及執行,所以local_irq_count(__cpu)>0,也就是in_interrupt!=0;第二種,若是當前正在 do_softirq中處理軟中斷處理函數,如今來了個硬中斷,將致使當前軟中斷處理函數被中斷,進入do_IRQ,隨後處理新來的硬中斷處理函數,當處 理完畢後,又來帶到了do_softirq,由3中可知,此時local_bh_count(cpu)被前一個do_softirq加1了,可是因爲其是 中途被中斷的,故local_bh_count(cpu)--還沒來得及執行,所以local_bh_count(__cpu)>0,也就是 in_interrupt!=0;第三種就是綜合第一種和第二種兩種狀況。

★ Linux內核作的工做 (2.6 kernel):
首先介紹中斷向量的範圍:
0   -   19 (0x0   -   0x13) 非屏蔽中斷和異常
20  -   31 (0x14  -   0x1f) Intel 保留
32  -  127 (0x20  -   0x7f) 外部中斷 (IRQ)
128         (0x80)            系統調用
129 -  238 (0x81  -  0xee) 外部中斷 (IRQ)
239         (0xef)          本地APIC時鐘中斷
240         (0xf0)          本地APIC高溫中斷 (P4模型中引人)
241 -  250 (0xf1  -  0xfa) linux 未來使用
251 -  253 (0xfb  -  0xfd) 處理器間中斷
254         (0xfe)          本地APIC錯誤中斷
255         (0xff)          本地APIC僞中斷 (CPU屏蔽某個中斷是產生)

在init_IRQ中
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]);
}
設置了中斷函數的入口地址. 那麼 interrupt 在哪那?
在entry.S中有
    .data
ENTRY(interrupt)
    .text
    vector=0
ENTRY(irq_entries_start)
    RING0_INT_FRAME
    .rept NR_IRQS
    ALIGN
    .if vector
    CFI_ADJUST_CFA_OFFSET -4
    .endif
1:      pushl $~(vector)
    CFI_ADJUST_CFA_OFFSET 4
    jmp common_interrupt
    .data
    .long 1b
    .text
    vector=vector+1
    .endr
    ALIGN
    //看interrupt 在此初始化,全部中斷都會調用到下面的標記
    common_interrupt:
    SAVE_ALL
    TRACE_IRQS_OFF
    movl %esp,%eax //棧頂地址被放到eax中
    call do_IRQ
    jmp ret_from_intr
    CFI_ENDPROC

    如今來看 do_IRQ -> __do_IRQ
    首先: struct irq_desc *desc = irq_desc + irq;
    struct irq_desc {
        ......
        struct irqaction  * action; //IRQ action list
        ......
    };
函數中
......
status |= IRQ_PENDING; /* we _want_ to handle it */

action = NULL; // action  NULL
//若是沒有任何cpu正在處理irq且irq線沒有被關閉,action才被賦值
if (likely(!(status & (IRQ_DISABLED | IRQ_INPROGRESS)))) {
    action = desc->action;
    status &= ~IRQ_PENDING; /* we commit to handling */
    status |= IRQ_INPROGRESS; /* we are handling it */
}
desc->status = status;
......
而後無論怎樣若是要處理中斷會調用 handle_IRQ_event
其中主循環爲
do {
    ret = action->handler(irq, action->dev_id, regs); //調用具體中斷處理函數
    if (ret == IRQ_HANDLED)
        status |= action->flags;
    retval |= ret;
    action = action->next;
} while (action);

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;
};

下面講一下怎樣安裝的中斷處理函數.
安裝中斷處理程序主要的函數是
int request_irq(unsigned int irq, irqreturn_t (*handler)(int, void *, struct pt_regs *), unsigned long irqflags, const char *devname, void *dev_id)
函數中主要會調用 setup_irq 會把分配的 struct irqaction 加入到 struct irq_desc *desc = irq_desc + irq; 中.
其中
......
#if defined(CONFIG_IRQ_PER_CPU)
if (new->flags & IRQF_PERCPU) //能夠在cpu上嵌套執行中斷處理函數
    desc->status |= IRQ_PER_CPU;
#endif
......
相關文章
相關標籤/搜索