Linux 中斷處理淺析

發表於2016-10-01

最近在研究非同步訊息處理, 突然想起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個). 驅動程式可以直接向這個工作佇列新增任務. 某些驅動程式還可能會建立並使用屬於自己的工作佇列.

相關文章