中斷-整體流程

文章疏理自
<<深入Linux設備驅動程序內核機制>>
http://blog.csdn.net/droidphone/article/category/1118447
感謝以上兩位大俠的創作,讀者亦可查閱原文.

讀完此文你可以瞭解到:
1. 中斷的處理流程
2. 中斷在linux中的實現
3. arm架構對中斷做的處理
4. 電平/邊沿觸發時中斷控制器所做的動作

中斷控制器PIC,與CPU連接,然後產生中斷的外設與PIC連接.
一顆SoC芯片中集成了處理器和各種外設控制器,包括PIC.



中斷控制器的作用:
    對各個irq的優先級進行控制;
    向CPU發出中斷請求後,提供某種機制讓CPU獲得實際的中斷源(irq編號);
    控制各個irq的電氣觸發條件,例如邊緣觸發或者是電平觸發;
    使能(enable)或者屏蔽(mask)某一個irq;
    提供嵌套中斷請求的能力;
    提供清除中斷請求的機制(ack);
    有些控制器還需要CPU在處理完irq後對控制器發出eoi指令(end of interrupt);
    在smp系統中,控制各個irq與cpu之間的親緣關係(affinity);

中斷產生時處理器硬件邏輯會:
當前任務的上下文寄存器保存在一個特定的中斷棧中
屏蔽處理器響應外部中斷的能力(ARM是通過把CPSR中的I位置位,表明禁止新的IRQ請求)
硬件邏輯根據中斷向量表調用通用中斷處理函數

通用中斷處理函數
這部分代碼都是用匯編語言實現的,不同架構的平臺實現也不盡相同,但相同點是都會從PIC中得到導致此次中斷產生的中斷號irq,
然後調用一個C函數,即我們熟知的do_IRQ.
我們來簡單看下ARM架構是如何處理的.
我們知道,arm的異常和復位向量表有兩種選擇,一種是低端向量,向量地址位於0x00000000,另一種是高端向量,向量地址位於0xffff0000,
Linux選擇使用高端向量模式,也就是說,當異常發生時,CPU會把PC指針自動跳轉到始於0xffff0000開始的某一個地址上:

ARM的異常向量表 
地址         異常種類
FFFF0000     復位
FFFF0004     未定義指令
FFFF0008     軟中斷(swi)
FFFF000C     Prefetch abort
FFFF0010     Data abort
FFFF0014     保留
FFFF0018     IRQ
FFFF001C     FIQ

中斷向量表在arch/arm/kernel/entry_armv.S文件的結尾部分,不帖代碼了.
不過要注意位於__vectors_start和__vectors_end之間的是真正的向量跳轉表,位於__stubs_start和__stubs_end之間的
是處理跳轉的部分.
例如:
vector_stub irq, IRQ_MODE, 4  
以上這一句把宏展開後實際上就是定義了vector_irq,根據進入中斷前的cpu模式,分別跳轉到__irq_usr或__irq_svc

系統啓動階段,位於arch/arm/kernel/traps.c中的early_trap_init()被調用:
void __init early_trap_init(void)
{
    ......
    /*
     * Copy the vectors, stubs and kuser helpers (in entry-armv.S)
     * into the vector page, mapped at 0xffff0000, and ensure these
     * are visible to the instruction stream.
     */
    memcpy((void *)vectors, __vectors_start, __vectors_end - __vectors_start);
    memcpy((void *)vectors + 0x200, __stubs_start, __stubs_end - __stubs_start);
    ......
}
以上兩個memcpy會把__vectors_start開始的代碼拷貝到0xffff0000處,把__stubs_start開始的代碼拷貝到0xFFFF0000+0x200處,
這樣,異常中斷到來時,CPU就可以正確地跳轉到相應中斷向量入口並執行他們。

對於系統的外部設備來說,通常都是使用IRQ中斷,所以我們只關注__irq_usr和__irq_svc,兩個函數最終都會進入irq_handler這個宏:
    .macro    irq_handler
#ifdef CONFIG_MULTI_IRQ_HANDLER
    ldr    r1, =handle_arch_irq
    mov    r0, sp
    adr    lr, BSYM(9997f)
    ldr    pc, [r1]
#else
    arch_irq_handler_default
#endif
9997:
    .endm

代碼同樣位於arch/arm/kernel/entry_armv.S文件中

如果選擇了MULTI_IRQ_HANDLER配置項,則意味着允許平臺的代碼可以動態設置irq處理程序,平臺代碼可以修改全局變量
handle_arch_irq,從而可以修改irq的處理程序。我目前的代碼中,在板級相關的文件中
.handle_irq     = gic_handle_irq,

arch/arm/kernel/setup.c
handle_arch_irq = mdesc->handle_irq;

所以最終調用到了gic_handle_irq(arch/arm/common/gic.c)
這個函數主要功能就是從中斷控制器中獲得irq號,緊接着就調用handle_IRQ,從這個函數開始,中斷程序進入C代碼中,
傳入的參數是IRQ編號和寄存器結構指針.
VIC : Vectored Interrupt Controller
GIC: Generic Interrupt Controller
網上有句是說:
兩者區別是,向量中斷就是不同的中斷有不同的入口地址,非向量中斷就只有一個入口地址,
進去了在判斷中斷標誌來識別具體是哪個中斷。向量中斷實時性好,非向量中斷簡單.
我感覺GIC主要是在smp系統中提供統一的訪問接口.

到這裏我們做個圖(引用自DroidPhone的),看下軟件上是如何初始化的:


/*
 * handle_IRQ handles all hardware IRQ's.  Decoded IRQs should
 * not come via this function.  Instead, they should provide their
 * own 'handler'.  Used by platform code implementing C-based 1st
 * level decoding.
 */
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
        struct pt_regs *old_regs = set_irq_regs(regs);

        perf_mon_interrupt_in();       
        irq_enter();

        /*
         * Some hardware gives randomly wrong interrupts.  Rather
         * than crashing, do something sensible.
         */
        if (unlikely(irq >= nr_irqs)) {
                if (printk_ratelimit())        
                        printk(KERN_WARNING "Bad IRQ%u\n", irq);
                ack_bad_irq(irq);              
        } else {
                generic_handle_irq(irq);       
        }

        /* AT91 specific workaround */
        irq_finish(irq);

        irq_exit();
        set_irq_regs(old_regs);
        perf_mon_interrupt_out();
}

首先看兩個參數,irq是do_IRQ的調用者通用中斷處理函數從PIC中得到的irq,regs是保存下來的
被中斷任務的執行上下文.

我們看set_irq_regs函數的實現:
static inline struct pt_regs *set_irq_regs(struct pt_regs *new_regs)
{                       
        struct pt_regs *old_regs;

        old_regs = __this_cpu_read(__irq_regs);
        __this_cpu_write(__irq_regs, new_regs);
        return old_regs;
}
這個函數的意思就是把變量__irq_regs賦於新值regs,把它原來的值保存在old_regs中.這樣的目的就是
系統中的每一個CPU都可以通過__irq_regs來訪問系統保存的中斷上下文.__irq_regs用來在調試時打印
當前棧的信息,也可以通過保存的中斷上下文寄存器來判斷被中斷的進程運行在用戶態還是內核態.

irq_enter的作用是禁止搶佔.
是通過把preempt_count加上HARDIRQ_OFFSET,HARDIRQ_OFFSET代表中斷的上半部,preempt_count
是進程調度時用到的.也就是系統會根據preempt_count的值來判斷是否可以調度.只有當preempt_count爲0時纔可以調度.
當調用preempt_disable或add_preempt_count函數時都不可以進行調度,因爲都會改變preempt_count的值爲非0.
所以irq_enter就是告訴系統,現在正在處理中斷的上半部分工作,不可以進行調度.
你可能會奇怪,既然此時的irq中斷都是都是被禁止的,爲何還要禁止搶佔?這是因爲要考慮中斷嵌套的問題,
一旦驅動程序主動通過local_irq_enable打開了IRQ,而此時該中斷還沒處理完成,新的irq請求到達,
這時代碼會再次進入irq_enter,在本次嵌套中斷返回時,內核不希望進行搶佔調度,而是要等到最外層的中斷處理完成後才做出調度動作,所以纔有了禁止搶佔這一處理。

到了最關鍵的部分generic_handle_irq,
/**
 * generic_handle_irq - Invoke the handler for a particular irq
 * @irq:        The irq number to handle
 *      
 */
int generic_handle_irq(unsigned int irq)
{
        struct irq_desc *desc = irq_to_desc(irq);

        if (!desc)
                return -EINVAL;
        generic_handle_irq_desc(irq, desc);
        return 0;
}       
EXPORT_SYMBOL_GPL(generic_handle_irq);

static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{       
        desc->handle_irq(irq, desc);
}

說到這裏要介紹幾個數據結構,struct irq_desc, struct irq_data, struct irq_chip, struct irqaction. 我們先用圖來表示下它們的關係:

這裏只貼關鍵的成員變量:
struct irq_desc {
        struct irq_data         irq_data;    //保存中斷請求irq和chip相關的數據

    irq_flow_handler_t      handle_irq;    //指向一個跟當前設備中斷觸發電信號類型相關的函數,比方說如果是邊沿觸發,
                        //那麼就指向一個邊沿觸發類型的函數.在handle_irq指向的函數內部會調用
                        //設備特定的中斷服務例程.特定平臺linux系統在初始化階段會提供handle_irq的具體實現    

    struct irqaction        *action;        /* IRQ action list */ irq action鏈表,是對某一具體設備的中斷處理的抽象,設備驅動程序會通過
                        //request_irq來向這個鏈表中註冊自己的中斷處理函數,這個action結構體中有個next成員變量,會把
                        //註冊的ISR串連起來,當然如果此irq line上只有一個設備,那麼這個action就對應這個設備的中斷處理程序

        unsigned int            status_use_accessors;//處理中斷時的irq狀態都保存在這個成員變量中

        const char              *name;        //會顯示在/proc/interrupts中
    ......
};

struct irq_data {
        unsigned int            irq;        //中斷請求號

        struct irq_chip         *chip;        //當前中斷來自的PIC,chip是對PIC的一個抽象,屏蔽硬件平臺上PIC的差異,
                        //給軟件提供統一的對PIC操作的接口.這些函數接口主要用來屏蔽或啓用當前
                        //中斷,設定外部設備中斷觸發電信號的類型,向發出中斷請求的設備發送中斷響應信號
                        //平臺初始化函數負責實現該平臺對應的PIC函數,並安裝對irq_desc數組中
    ......
};

struct irqaction {
        irq_handler_t           handler;    //中斷服務例程,即我們熟知的ISR,當我們調用request_irq時會把我們實現的ISR註冊到這裏

        void                    *dev_id;    //調用handler時傳遞的參數,在多個設備共享一個irq號時特別重要,設備驅動程序就是通過此
                        //成員變量來標識自己,在free_irq時用到

        struct irqaction        *next;        //串連下一個action

    ......
};

通過以上的介紹,我們可以知道,一箇中斷對應兩個層次,一個是handle_irq,一個是action.前者對應irq line上的處理動作,後者對應設備相關的中斷處理.
也就是說,一條irq line對應一個handle_irq,而這條irq line上可以掛載多個設備,即多個設備都可以通過同一個irq line產生中斷,action成員變量用來
串連掛載在此irq line上的各個設備驅動的ISR.如果irq line上只有一個設備驅動註冊,那麼這個action成員變量即是此設備驅動的ISR.用圖來表示就是:


接着desc->handle_irq(irq, desc)這個調用走,handle_irq是在irq_set_handler裏賦的值.
void __irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained,
                  const char *name)
{
    ......
        desc->handle_irq = handle;
        desc->name = name;
    ......
}
接下來就到了具體的handler, 如果是電平觸發那麼會調用到handle_level_irq,邊沿觸發的中斷會調用到handle_edge_irq,在這兩個函數中
做了一些器件相關的工作,可以查看中斷-電平/邊沿觸發時要做的具體工作,(這篇文章很好的說明了中斷在什麼時候是屏蔽的,什麼時候是打開的)
或<<深入Linux設備驅動程序內核機制>>的5.7~5.8節.
一點小提示,mask_ack_irq就是屏蔽中斷線
irq_ack用來向設備發送一箇中斷響應信號,從硬件角度講就是讓發出中斷的設備產生一個信號電平的轉換,防止該設備不停的發出同一中斷信號.
kernel/irq/chip.c的handle_edge_irq函數有段註釋,我們從註釋中可以瞭解到:

After the ack another
interrupt can happen on the same source even before the first one
is handled by the associated event handler. If this happens it
might be necessary to disable (mask) the interrupt depending on the
controller hardware. 

mask_irq 禁止中斷
ack_irq 復位設備的中斷請求引腳,ack後中斷控制器纔會再次向處理器發中斷請求
mask_ack_irq 是以上兩者的結合

同時引出個問題:在SMP系統中,我們知道在中斷上下文會禁止CPU響應中斷並禁止搶佔,那麼當一個CPU處理IRQ時,此中斷線若再次來中斷會不會在另一個CPU上處理?
答:要分是什麼觸發,邊沿還是電平觸發。
電平觸發:來中斷時會先mask然後ack,mask是禁止產生中斷,ack就是復位設備中斷腳,在ack發出之後,設備就可以再次來中斷,否則會一直保持高電平狀態。
雖說在ack後可以再次來中斷,但是由於之前執行了mask,禁止中斷,所以直到執行unmask另一個CPU纔會收到中斷。
邊沿觸發:不像電平觸發那樣,如果不ack會一直保持高電平,邊沿觸發只是在電平跳變時才觸發IRQ,所以處理不當就容易丟失。也正因爲這樣,
邊沿觸發不會mask irq,只是ack,以便復位引腳,在這之後再次產生中斷另一個CPU可以做處理,因爲之前並沒有mask。

無論是邊沿觸發還是電平觸發,最終都會調用到handle_irq_event.它爲調用設備驅動程序安裝的中斷處理程序做最後的準備.
handle_irq_event最終會調用handle_irq_event_percpu,然後通過一個do {} while 循環 action->handler(irq, action->dev_id)
來調用中斷處理程序.

有一點要注意,就是在中斷共享的情況下,也就是一條中斷線上註冊了多箇中斷處理程序,那麼在某一個設備觸發中斷時,這條中斷線上的
所有中斷處理程序都會被調用到,因此共享中斷時ISR要判斷是否是自己的設備產生的中斷,這主要靠讀取自己設備的中斷狀態寄存器完成.
如果發現不是自己的設備產生的中斷,那麼返回一個IRQ_NONE就好了.

這裏再做個圖,以表中斷處理的整個大致流程:


還有些要注意的:
對一些共享資源,如果中斷例程有可能用到,那麼要加自旋鎖.
中斷中只能用
spin_lock_irq/spin_unlock_irq
spin_lock_irqsave/spin_unlock_irqrestore
千萬不能用
spin_lock/spin_unlock
後者有可能會導致死鎖.比方進程A跑在CPU0上,正在使用某個共享資源C,突然來一中斷,CPU0去處理中斷,正好中斷也要用C,如果用spin_lock,那麼中斷會自旋,等待進程A的釋放,而進程A被中斷,等待中斷執行完,所以就形成了死鎖.
用前者的自旋鎖接口就不會出現此問題,因爲前者是禁止搶佔,禁止中斷的情況下訪問共享資源.
但有一點,就是用自旋鎖保護的代碼要儘可能的在最短的時間內執行完,因爲前者雖然不會出現死鎖,但是其他的進程也會爲了等待資源的釋放而自旋,這樣會影響系統的性能,所以要臨界區的代碼要儘快執行完.能保證這一點有時候並不容易,因爲我們必須清楚的知道自己在做什麼,就是你調用的函數你要清楚的瞭解它是否會睡眠,還有在某些情況下進程在臨界區中可能被換出處理器.所以我們要勞記一條準則:任何擁有自旋鎖的代碼必須是原子的,不能睡眠.



中斷相關補充關於SOFTIRQ:

中斷分爲HARDIRQ和SOFTIRQ兩部分,耗時的工作將會延遲到SOFTIRQ去做。

對於中斷後半部的延遲操作可以說內核提供了三種機制:softirq、tasklet、work queue。

軟中斷處理程序執行的時候,允許響應中斷,但它自己不能休眠。
當一個軟中斷處理程序執行的時候,當前處理器上的軟中斷被禁止,但其他處理器仍可以執行軟中斷。就是說,如果一個軟中斷處理程序在它執行的同時被再次觸發了,那麼另外一個處理器可以同時運行其處理程序。所以任何共享數據都需要用鎖來保護。但是內核並未對軟中斷加鎖,因爲這樣會影響性能,這樣使用軟中斷就沒有任何意義了(這句話不太理解,爲什麼就沒有意義了呢?難道是因爲性能的影響?)。因此,大部分軟中斷處理程序都採取單處理器數據(即per-CPU數據)或其他一些技巧來避免顯式地加鎖。
緊接着還有一句話:
引入軟中斷的主要原因是其可擴展性。就是可以擴展到多個處理器上,即可以併發執行。如果不需要擴展到多處理器,那麼就使用tasklet吧。(那應該可以回答上面的疑問,就是性能的影響)

軟中斷一般在下列地方執行:
   A、從一個硬件中斷代碼處返回時;
   B、在ksoftirqd內核線程中;
   C、顯式檢查和執行待處理的軟中斷代碼中,如網絡子系統。

一般軟中斷都用在那些執行頻率很高和連續性要求很高的情況下。Linux只有兩處用了軟中斷,網絡子系統和SCSI子系統。

爲了避免在一箇中斷的softirq部分耗太多時間而對系統性能造成影響,便引入了ksoftirqd。它的主要任務就是處理softirq,如果沒有softirq需要處理,該進程將進入睡眠。關於ksoftirqd在LKD的8.3.2節的第4小部分會有詳細的介紹。

------------------------------------
附:
tasklet是用軟中斷實現的一種下半部機制。

tasklet和軟中斷執行的時機一般都是在HARDIRQ處理程序返回時。



per-CPU變量即每個CPU維護一個本地變量,我們知道用per-CPU變量就不需要加鎖來保護了。因爲per-CPU變量是用禁止搶佔來實現。
用per-CPU變量的好處就是:
1、減少數據鎖定。
2、減少緩存失效。什麼是緩存失效?就是如果一個處理器操作一個數據,而這個數據存放在其他處理器緩存中,那麼存放這個數據的處理器就要不斷的清除或刷新自己的緩存。持續不斷的緩存失效稱爲緩存抖動,這樣對系統性能影響很大。而使用per-CPU變量將使緩存影響降至最低,因爲理想情況下只會訪問自己的數據。

搶佔即一個高優先級的搶佔低優先級的執行,並不是只針對SMP系統中,別把概念搞錯了!!!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章