中斷和中斷處理程序


1. 中斷

        Linux內核要對連接到計算機上的所有硬件設備進行管理,首先要能和它們互相通信。從所周知,處理器的速度跟外圍硬件設備的速度往往不在一個數量級上。所以,需要一種機制,如果輪詢(polling)是一種解決辦法,可以讓內核定期對設備的狀態進行查詢,然后做出相應的處理,但這讓內核做了不少無用功。

    更好的辦法是由我們來提供一種機制,讓硬件在需要的時候再向內核發出信號。這就是中斷機制。中斷本質上是一種特殊的電信號,由硬件設備生成,並直接送入中斷控制器的輸入引腳上,再由中斷控制器向處理器發送相應的信號,處理器一經檢測到此信號,便中斷自己當前工作轉而處理中斷,最后由OS來負責處理新到來的數據。中斷是異步的。

 

什么是中斷?簡單地說就是CPU在忙着作自己的事情,這時候硬件(比如說鍵盤按了一下)觸發了一個電信號,這個信號通過中斷線到達中斷控制器i8259A,i8259A接受到這個信號后,向CPU發送INT信號申請CPU來執行剛才的硬件操作,並且將中斷類型號也發給CPU,此時CPU保存當前正在做的事情(REST指令把程序計數器PC中的下一條待執行的指令的內存地址保存到)的情景現場,然后去處理這個申請,根據中斷類型號找到它的中斷向量即中斷程序在內存中的地址),然后去執行這段程序(這段程序已經寫好,在內存中),執行完后再向i8259A發送一個INTA信號表示其已經處理完剛才的申請。此時CPU就可以繼續做它剛才被打斷做的事情了,將剛才保存的情景現場恢復出來,CPU繼續執行接下來下面的程序。

    不同的設備對應的中斷不同,而每個中斷都通過一個唯一的數字標識。這些中斷值通常被稱為中斷請求(IRQ)線。比如,IRQ0是時鍾中斷,而IRQ1是鍵盤中斷。並不是所有的中斷號都這樣嚴格定義,像PCI總線上的設備,中斷就是動態分配的。

 

1.1. 異常與中斷

    異常與中斷不同,它在產生時必須考慮與處理器時鍾同步。實際上,異常也稱為同步中斷。比如,在處理器執行到由於編程失誤而導致的錯誤指令的時候,或者在執行期間出現特殊情況(缺頁),必須靠內核來處理的,處理器就產生一個異常。

中斷的的工作方式類似,其差異只在於中斷是由硬件而不是軟件引起的。

 

2. 中斷處理程序

    在響應一個特定中斷的時候,內核會執行一個函數,該函數叫中斷處理程序(interrupt handler)或中斷服務例程(interrupt service routine,ISR)。產生中斷的每個設備都有一個相應的中斷處理程序。一個設備的中斷處理程序是它設備驅動程序的一部分。中斷處理程序與其他內核的真正區別在於:中斷處理程序是被內核調用來響應中斷的,而它們運行於我們稱之為中斷上下文的特殊上下文中。

 

2.1. 上半部與下半部的對比

    又想程序運行得快,又想程序完成的工作量太多,這兩個目的相互矛盾。鑒於兩個目的之間存在不可調和的矛盾,所以需要把中斷處理程序分成兩半或兩個部分。中斷處理程序是上半部(top half):接收到一個中斷,他就立即開始執行,但只做嚴格時限的工作,例如對接收的中斷進行應答或復位硬件,這些工作都是在所有中斷被禁止的情況下完成的。能夠被允許稍后完成的工作會推遲到下半部(bottom half)去。此后,在合適的時機,下半部被開中斷執行。

 

3. 注冊中斷處理程序

   驅動程序可以通過下面的函數注冊並激活一個中斷處理程序,以便處理中斷:

      int request_irq(unsigned int irq,

irqretrun_t (*handler)(int,void *, struct pt_regs *),

unsigned long irqflags,

const char *devname,

void *dev_id);

第一個參數>irq表示要分配的中斷號。對於大多數其他設備來說,這個值要么是可以通過探測獲取,要么可以通過編程動態確定。

第二個參數>hanlder是一個指針,指向處理這個中斷的實際中斷處理程序。hanhler函數的原型接收三個參數。

第三個參數>irqflags可以是0,也可以是多個標志的掩碼。如果是SA_INTERRUPT,表面給定的中斷處理程序是一個快速中斷處理程序(fast interrupt hanlder)。使用了該標志,快速中斷處理程序在禁止所有中斷的情況下的本地處理器上運行。除了時鍾中斷,絕大數中斷都不使用該標志。如果是SA_SAMPLE_RANDOM,表明這個設備產生的中斷對內核熵池(entropy pool)有貢獻。如果是SA_SHARE標志,表明可以在多個中斷處理程序之間共享中斷線。在同一個給定線上注冊的每個處理程序必須指定這個標志。

第四個參數>devname是與中斷相關設備的ASCII文本表示法。這些名字會被/proc//irq和/proc/inerrupt文件使用,以便於用戶通信。

第五個參數>dev_id主要用戶共享中斷線。當一個中斷處理程序需要釋放時,dev_id將提供唯一的標志信息,以便從共享中斷線的諸多中斷處理程序中刪除指定的那一個。如果無需共享中斷線,那么將該參數賦為空值(NULL)就可以了。

該函數執行成功會返回0。如果返回非0值,就表示有錯誤發生。

   注意:request_irq函數可能會睡眠,因此,不能在中斷上下文或其他不允許阻塞的代碼中使用該函數。在注冊的過程中,內核需要在/proc/irq文件中創建一個與中斷對應的項。函數proc_mkdir就是用來創建這個新的procfs項的。函數proc_mkdir通過調用函數proc_mkdir通過調用proc_create對這個profs項進行設置,而proc_create會調用函數kmalloc函數請求分配內存。函數kmalloc是可以睡眠的。

 

3.1. 釋放中斷處理程序

    卸載驅動程序時,需要注銷相應的中斷處理程序,並釋放中斷線。可以調用void_free_irq(unsigned int irq, void * dev_id)來釋放中斷線。

如果指定的中斷線不是共享的,那么該函數刪除處理程序的同時將禁用這條中斷線。如果中斷線是共享的,則僅刪除dev_di對應的處理程序,而這條中斷線只有在刪除了最后一個處理程序時才會被禁用。

 

4. 編寫中斷處理程序

    以下是一個典型的中斷處理程序聲明:

       static irqreturn_t intr_handler(int irq, void *dev_id, struct pt_regs *regs);

    第一個參數irq就是這個處理程序要響應的中斷的中斷線號。

    第二個參數dev_id是一個通用指針,它與在中斷處理程序注冊時傳遞request_irq的參數的dve_id必須一致。另外dev_id也可能指向中斷處理程序使用的一個數據結構。因為,對於每個設備而言,設備結構是唯一的。

    第三個參數regs是一個指向結構的指針,該結構包含處理中斷之前處理器的寄存器和狀態。考慮到現有的中斷處理程序很少使用該參數,因此可以忽略它。

    中斷處理程序的返回值是一個特殊類型:irqreturn_t。中斷處理程序可能會返回兩個特殊的值:IRQ_NONE和IRQ_HANDLED。當中斷處理程序檢測到一個中斷,但該中斷對應的設備並不是在注冊處理函數期間指定的產生源時,返回IRQ_NONE;當中斷處理程序被正確調用,且確實是它所對應的設備產生了中斷,返回IRQ_HANDLED。而實際上irqreturn_t就是一個int類型。

    中斷處理程序通常會標記為static,因為它從來不會被別人的文件中的代碼直接調用。

 

4.1. 重入和中斷處理程序

        Linux中的中斷處理程序是無需重入的。當一個給定的中斷處理程序正在執行時,相應的中斷線在所有處理器上都會被屏蔽掉,以防止在同一中斷線上接收另一個新的中斷。

 

4.2. 共享的中斷處理程序

    共享的處理程序與非共享的處理程序在注冊和運行方式上比較類似,但差異如下:

    1) request_irq的參數flags必須設置SA_SHARE標志

    2) 對每個注冊的中斷處理程序來說,dev_id參數必須唯一

    3) 中斷處理程序必須能夠區分它的設備是否真的產生了中斷。這既需要硬件的支持,耶需要處理程序有相關的處理邏輯。

 

    指定SA_SHARE標志以調用request_irq時,只有在以下兩種情況下才可能成功:

    1) 中斷線當前未被注冊

    2) 在該線上的所有已經注冊處理程序都指定了SA_SHARE。

 

    內核接收一個中斷后,它將依次調用在該中斷線上注冊的每一個處理程序。因此,一個處理程序應該必須知道它是否應該為這個負責。如果與它相關的設備並沒有產生中斷,那么處理器應該立即退出。

 

5. 中斷上下文

    當執行一個中斷處理程序或下半部時,內核處於中斷上下文(interrupt context)中。中斷上下文和進程沒有關系,不可以睡眠。中斷上下文具有嚴格的時間限制,因為它打斷了其他代碼。

    而進程上下文是一種內核所處的操作模式,此時內核代表進程執行,比如執行系統調用或運行內核線程。在進程上下文中,可以通過current宏關聯當前進程,可以睡眠。

    中斷處理程序打斷了其他代碼,正是因為這種異步執行的特性,所以所有的中斷處理程序必須盡可能的迅速、簡潔。盡量把工作從中斷處理程序中分離出來,交給下半部。

    中斷處理程序棧的設置是一個配置選項,決定中斷處理程序是否共享中斷進程的內核棧。內核棧的大小是兩頁。在2.6的內核中,增加一個選項,把棧的大小兩頁減到一頁,這就減輕了內存的壓力,因為系統中每個進程僅需要一頁內核棧了。但是,為了應對棧大小的減少,中斷處理程序擁有了自己的棧,每個處理器一個,大小為一頁。這個棧稱為中斷棧。

 

6. 中斷處理機制的實現

    設備產生中斷,通過總線把電信號發送給中斷控制器,處理器會立即停止它正在做的事,關閉中斷系統,然后跳到內存中預定義的位置開始執行那里的代碼。這個預定義的位置是由內核設置的,是中斷處理程序的入口點。

    在內核棧,中斷的旅程開始於預定義入口點,這類似於系統調用通過預定義的異常句柄進入內核。對於每條中斷線,處理器都會跳到對應的一個唯一的位置。初始入口點只是在棧中保存這個號,並存放當前寄存器的值;然后,內核調用do_IRQ函數。

        unsigned int do_IRQ(struct pt_regs regs);

    該函數計算出中斷號后,對所接收的中斷進行應答,禁止這條線上的中斷傳遞。在普通的PC機器上,這些操作由mask_and_ack_8259A來完成的。

    接着,該函數需要確保在這條中斷線上有個有效的處理程序,而且這個程序已經啟動,但是當前並沒有執行。do_IRQ就調用handle_IRQ_event來運行為這條中斷線安裝的中斷處理程序。

    最后,函數返回,回到do_IRQ。而do_IRQ做清理工作並返回到初始入口點,然后再從這個入口點跳到函數ret_from_intr函數。這個例程會檢查重新調度是否正在掛起。如果重新調度正在掛起,而且內核正在返回用戶空間(也就是中斷了用戶進程),那么schedule被調用。如果內核正在返回內核空間(也就是中斷了內核本身),只有在preempt_count為0,schedule才會被調用。在schedule返回之后,或者沒有掛起的工作,那么,原來的寄存器被恢復,內核恢復到曾經中斷的點。

    在x86上,初始的匯編例程位於arch/i386/kernel/entry.S,C方法在arch/i386/kernel/irq.c中。

 

6.1. 文件/proc/interrupts

        procfs是一個虛擬文件系統,它只存於內核內存,一般安裝與/proc目錄下。在procfs中讀寫都要調用內核函數,這些函數模擬從真實文件中讀或寫。

 

7. 中斷控制

        Linux內核提供了一組接口用於操作機器上的中斷狀態。可以在<asm/system.h>和<asm/irq.h>中找到。一般來說,控制中斷系統的原因是需要提供同步。通過禁止中斷,可以確保某個中斷處理程序不會搶占當前的代碼。此外,禁止中斷還可以禁止內核搶占。

 

7.1. 禁止和激活中斷

    用於禁止和激活當前處理器上的本地中斷:

         local_irq_disable();

         local_irq_enable();

         local_irq_save(unsigned long flags);

         local_irq_restore(unsigned long flags);

    前兩個函數通常調用單個匯編指令來實現。實際上,在x86中它們分別使用cli指令和sti指令。如果在調用local_irq_disable例程之前已經禁止了中斷,那么該例程往往帶來潛在的危險;同樣相應的local_irq_enable例程耶存在危險,因為他將無條件地激活中斷,盡管這些中斷可能在開始時就是關閉的。后兩個函數可以保存現場,是系統更加安全。

    內核2.5版本不再使用全局的cli,相應地,所有中斷同步現在必須結合使用本地中斷控制器和自旋鎖。也就是說,為了確保對共享數據的互斥訪問,現在需要做更多的工作。取消全局cli的優點:一是強制驅動程序編寫實現真正的加鎖,具有特定的細粒度比全局鎖快許多;二是這使得很多代碼更具流線型,避免了代碼的成簇布局。

前面的所有函數既可以在中斷中調用,也可以在進程上下文中調用。

 

7.2. 禁止指定中斷線

    在某些情況下,只禁止整個系統中一條特定的中斷線就夠了。

         void disable_irq(unsigned int irq);

         void disable_irq_nosync(unsigned int irq);

         void enable_irq(unsigned int irq);

         void synchronize_irq(unsigned int irq);

    前兩個函數禁止中斷控制器上指定的中斷線。另外函數只有在當前正在執行的所有處理程序完成后,disable_irq才能返回。因此。調用者不僅確保不在指定中斷線上傳遞新的中斷,同時還有確保所有已經開始執行的處理程序已經全部退出。

    函數disable_irq_nosync不會等待當前中斷處理程序執行完畢。

函數synchronize_irq等待一個特定的中斷處理程序的退出。如果該處理程序正在執行,那么該函數必須退出后才能返回。

    對於這些函數的調用可以嵌套。其中有三個函數可以從中斷或進程上下文中調用,而且不會睡眠。禁止多個中斷處理程序共享的中斷線是不合適的,禁止中斷線也就禁止了這條線上所有設備的中斷傳遞。因此,用於新設備的驅動程序應該傾向於不使用這些接口。

 

7.3. 中斷系統的狀態

    宏irqs_disable定義在<asm/system.h>中。如果本地處理器上的中斷系統被禁止,則它返回非0,否則返回0。

    在<asm/hardirq.h>中定義的兩個宏提供一個用來檢測內核的當前上下文的接口:

        int_interrupt()

        int_irq()

    第一個宏in_interrup最有用:如果內核處於中斷上下文中,返回非0。說明內核此刻正在執行中斷處理程序,或者正在執行下半部處理程序。宏in_irq只有在內核確實正在執行中斷處理程序時返回非0。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM