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。