linux中斷處理淺析

近在研究異步消息處理, 突然想起linux內核的中斷處理, 裏面由始至終都貫穿着"重要的事馬上做, 不重要的事推後做"的異步處理思想. 於是整理一下~


第一階段--獲取中斷號
每個CPU都有響應中斷的能力, 每個CPU響應中斷時都走相同的流程. 這個流程就是內核提供的中斷服務程序.
在進入中斷服務程序時, CPU已經自動禁止了本CPU上的中斷響應, 因爲CPU不能假定中斷服務程序是可重入的.
中斷處理程序的第一步要做兩件事情:
1. 將中斷號壓入棧中; (不同中斷號的中斷對應不同的中斷服務程序入口)
2. 將當前寄存器信息壓入棧中; (以便中斷退出時恢復)
顯然, 這兩步都是不可重入的(如果在保存寄存器值時被中斷了, 那麼另外的操作很可能就把寄存器給改寫了, 現場將無法恢復), 所以前面說到的CPU進入中斷服務程序時要自動禁止中斷.
棧上的信息被作爲函數參數, 調用do_IRQ函數.

第二階段--中斷串行化
進入do_IRQ函數, 第一步進行中斷的串行化處理, 將多個CPU同時產生的某一中斷進行串行化. 其方法是如果當前中斷處於"執行"狀態(表明另一個CPU正在處理相同的中斷), 則重新設置它的"觸發"標記, 然後立即返回. 正在處理同一中斷的那個CPU完成一次處理後, 會再次檢查"觸發"標記, 如果設置, 則再次觸發處理過程.
於是, 中斷的處理是一個循環過程, 每次循環調用handle_IRQ_event來處理中斷.

第三階段--關中斷條件下的中斷處理
進入handle_IRQ_event函數, 調用對應的內核或內核模塊通過request_irq函數註冊的中斷處理函數.
註冊的中斷處理函數有個中斷開關屬性, 一般情況下, 中斷處理函數總是在關中斷的情況下進行的. 而調用request_irq註冊中斷處理函數時也可以設置該中斷處理函數在開中斷的情況下進行, 這種情況比較少見, 因爲這要求中斷處理代碼必須是可重入的. (另外, 這裏如果開中斷, 正在處理的這個中斷一般也是會被阻塞的. 因爲正在處理某個中斷的時候, 硬件中斷控制器上的這個中斷並未被ack, 硬件不會發起下一次相同的中斷.)
中斷處理函數的過程可能會很長, 如果整個過程都在關中斷的情況下進行, 那麼後續的中斷將被阻塞很長的時間.
於是, 有了soft_irq. 把不可重入的一部分在中斷處理程序中(關中斷)去完成, 然後調用raise_softirq設置一個軟中斷, 中斷處理程序結束. 後面的工作將放在soft_irq裏面去做.

第四階段--開中斷條件下的軟中斷
上一階段循環調用完當前所有被觸發的中斷處理函數後, do_softirq函數被調用, 開始處理軟件中斷.
在軟中斷機制中, 爲每個CPU維護了一個若干位的掩碼集, 每位掩碼代表一箇中斷號. 在上一階段的中斷處理函數中, 調用raise_softirq設置了對應的軟中斷, 到了這裏, 軟中斷對應的處理函數就會被調用(處理函數由open_softirq函數來註冊).
可以看出, 軟中斷與中斷的模型很類似, 每個CPU有一組中斷號, 中斷有其對應的優先級, 每個CPU處理屬於自己的中斷. 最大的不同是開中斷與關中斷.
於是, 一箇中斷處理過程被分成了兩部分, 第一部分在中斷處理函數裏面關中斷的進行, 第二部分在軟中斷處理函數裏面開中斷的進行.

由於這一步是在開中斷條件下進行的,這裏還可能發生新的中斷(中斷嵌套),然後新中斷對應的中斷處理又將開始一個新的第一階段~第三階段。在新的這個第三階段中,可能又會觸發新的軟中斷。但是這個新的中斷處理過程並不會進入第四階段,而是當它發現自己是嵌套的中斷時,完成第三階段之後就會退出了。也就是說,只有第一層中斷處理過程會進入第四階段,嵌套發生的中斷處理過程只執行到第三階段。

然而嵌套發生的中斷處理過程也可能會觸發軟中斷,所以第一層中斷處理過程在第四階段需要是一個循環的過程,需要循環處理嵌套發生的所有軟中斷。爲什麼要這樣做呢?因爲這樣可以按軟中斷觸發的順序來執行這些軟中斷,否則後來的軟中斷可能就會先執行完成了。

極端情況下,嵌套發生的軟中斷可能非常多,全部處理完可能需要很長的時間,於是內核會在處理完一定數量的軟中斷後,將剩下未處理的軟中斷推給一個叫ksoftirqd的內核線程來處理,然後結束本次中斷處理過程。


第五階段--開中斷條件下的tasklet
實際上, 軟中斷很少直接被使用. 而第二部分開中斷情況下的進行的處理過程一般是由tasklet機制來完成的.
tasklet是由軟中斷引出的, 內核定義了兩個軟中斷掩碼HI_SOFTIRQ和TASKLET_SOFTIRQ(兩者優先級不同), 這兩個掩碼對應的軟中斷處理函數作爲入口, 進入tasklet處理過程.
於是, 在第三階段的中斷處理函數中, 完成關中斷的部分後, 然後調用tasklet_schedule/tasklet_hi_schedule標記一個tasklet, 然後中斷處理程序結束. 後面的工作由HI_SOFTIRQ/TASKLET_SOFTIRQ對應的軟中斷處理程序去處理被標記的tasklet(每個tasklet在其初始化時都設置了處理函數).
看上去, tasklet只不過是在softirq的基礎上多了一層調用, 其作用是什麼呢? 前面說過, softirq是與CPU相對應的, 每個CPU處理自己的softirq. 這些softirq的處理函數需要設計爲可重入的, 因爲它們可能在多個CPU上同時運行. 而tasklet則是在多個CPU間被串行化執行的, 其處理函數不必考慮可重入的事情.
然而, softirq畢竟還是要比tasklet少繞點彎路, 所以少數實時性要求相對較高的處理過程還是在精心設計之後, 直接使用softirq了. 比如: 時鐘中斷處理過程, 網絡發送/接收處理過程.

結尾階段
CPU接收到中斷以後, 以歷以上五個階段, 中斷處理完成. 最後需要恢復第一階段中被保存在棧上的寄存器信息. 中斷處理結束.

關於調度
上面的流程中, 還隱含了一個問題, 整個處理過程是持續佔有CPU的(除了開中斷情況下可能被新的中斷打斷以外). 並且, 中斷處理的這幾個階段中, 程序不能夠讓出CPU!
這是由內核的設計決定的, 中斷服務程序沒有自己的task結構(即操作系統教科書上說的進程控制塊), 所以它不能被內核調度. 通常說一個進程讓出CPU, 在之後如果滿足某種條件, 內核會通過它的task結構找到它, 並調度其運行.
這裏可能存在兩方面的問題:
1. 連續的低優先的中斷可能持續佔有CPU, 而高優先的某些進程則無法獲得CPU;
2. 中斷處理的這幾個階段中不能調用可能導致睡眠的函數(包括分配內存);
對於第一個問題, 較新的linux內核增加了ksoftirqd內核線程, 如果持續處理的softirq超過一定數量, 則結束中斷處理過程, 然後喚醒ksoftirqd, 讓它來繼續處理. 雖然softirq可能被推後到ksoftirqd內核線程去處理, 但是還是不能在softirq處理過程中睡眠, 因爲不能保證softirq一定在ksoftirqd內核線程中被處理.
據說在montavista(一種嵌入式實時linux)中, 將內核的中斷機制做了修改. (某些中斷的)中斷處理過程被賦予了task結構, 能夠被內核調度. 解決了上述兩個問題. (montavista的目標是實時性, 這樣的做法犧牲了一定的整體性能.)

工作隊列
linux基線版本的內核在解決上述問題上, 提供了workqueue機制.
定義一個work結構(包含了處理函數), 然後在上述的中斷處理的幾個階段的某一步中調用schedule_work函數, work便被添加到workqueue中, 等待處理.
工作隊列有着自己的處理線程, 這些work被推遲到這些線程中去處理. 處理過程只可能發生在這些工作線程中, 所以這裏可以睡眠.
內核默認啓動了一個工作隊列, 對應一組工作線程events/n(n代表處理器編號, 這樣的線程有n個). 驅動程序可以直接向這個工作隊列添加任務. 某些驅動程序還可能會創建並使用屬於自己的工作隊列.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章