Linux 核心的軟中斷深入解析

NP等不等於P發表於2015-05-12

軟中斷介紹

把可以延遲的處理從硬中斷處理程式獨立出來,這樣這個處理可以在開中斷的情況下執行,這個處理就是軟中斷。可見,軟中斷的這種脫離可以大大縮短硬中斷的響應時間,對於很多實時應用來說及其重要。

我們本文只談軟中斷,至於tasklet、workqueue等我們以後再談。我們在講述軟中斷流程(參考linux kernel 4.0)時會嘗試深入理解其中的各個細節之處,分享我們自己的理解(如果不正,還望指出,謝謝)。

理解 linux 核心的軟中斷

(題圖來自:techvark.com)

軟中斷資料結構的定義

軟中斷目前有10(由NR_SOFTIRQS定義)個,通過softirq_vec[NR_SOFTIRQS]陣列來管理這些軟中斷,全部cpu共用。

軟中斷的註冊

通過open_softirq()將具體的軟中斷處理函式和軟中斷編號繫結。如網路系統註冊了收發包的軟中斷處理函式:

open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);

軟中斷的啟用

每個cpu都有一個32bit的點陣圖(即__softirq_pending)來維護本cpu上的軟中斷是否啟用。

typedef struct {
    unsigned int __softirq_pending;
    #ifdef CONFIG_SMP
    unsigned int ipi_irqs[NR_IPI];
    #endif
} ____cacheline_aligned irq_cpustat_

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

軟中斷的啟用時機之一:irq_exit

irq_exit函式裡可能會啟用軟中斷,啟用條件是:

不在硬中斷裡並且不在軟中斷裡並且本cpu的__softirq_pending中有置位。

if (!in_interrupt() && local_softirq_pending())
    invoke_softirq();

由這個條件,我們可以知道,軟中斷和硬中斷在這裡是同等對待(在in_interrupt裡)的,體現都是中斷處理這一個本質。不能在硬中斷裡的條件,表明必須優先性,必須硬中斷全部處理完,才考慮軟中斷;不能在軟中斷裡的條件,表明遮蔽了軟中斷的巢狀。

invoke_softirq函式的處理是,要麼(先喚醒ksoftirqd)將軟中斷交由ksoftirqd專門執行緒處理,要麼直接呼叫__do_softirq即時處理(當然,即時處理要區分是在哪個棧上:是當前棧上還是在獨立的軟中斷棧上)。

我們看看即時處理這個流程。local_softirq_pending前肯定會清除preempt_count中的硬中斷位,如果此時preempt_count裡沒有軟中斷位則可以被搶佔(即時關閉硬中斷)。在進入到__do_softirq處理各個軟中斷期間,肯定是禁止搶佔了。在硬(軟)中斷上下文裡的搶佔是眾所周知不被允許的:會讓被中斷的程式執行時間不確定,也是不公平的(也就是說,不要在硬中斷和軟中斷的處理中有排程離開的意向)。

軟中斷的啟用時機之二:raise_softirq

網路卡收包方式從非NAPI進化到NAPI方式,就充分展示了軟中斷的優點:把收報任務最大程度地交給軟中斷處理,最大程度簡化硬中斷處理。這種進化,我們以後再講。

raise_softirq函式會呼叫__raise_softirq_irqoff函式,在指定cpu的__softirq_pending點陣圖上置位相應的軟中斷。raise_softirq_irqoff函式和raise_softirq函式的區別是關中斷的操作是否已經完成了。置位點陣圖是一個競爭操作,所有硬中斷裡都可能做,所以得保證在關中斷的情況下完成。

軟中斷的啟用之三:ksoftirqd

每個cpu都有一個ksoftirqd執行緒在軟中斷量大時專門處理軟中斷:

DEFINE_PER_CPU(struct task_struct *, ksoftirqd);

ksoftirqd執行緒的核心函式run_ksoftirqd的(迴圈)處理是:關中斷看本cpu的__softirq_pending的置位情況,如有則執行__do_softirqd(),執行完開中斷)。這個執行很順暢,因為是在該執行緒自己的棧上,不會有影響使用者程式的問題。

這裡有個疑問,此處以前是關搶佔保護,現在是關中斷的保護了(參考2012年的patch 3e339b,softirq: Use hotplugthread infrastructure)?我們的理解是:關搶佔的保護方式,會讓後續更多的軟中斷由ksoftirqd處理,不符合ksoftirqd的輔助地位。就處理軟中斷的地位而言,應該是irq_exit的為主,ksoftirqd的為輔。)

ksoftirqd裡也可以看到,在執行軟中斷前是可以被搶佔的,但是一旦開始執行就不能被搶佔了(和上面的排程之一:irq_exit中的講述的思想是一致的)。就是說,軟中斷和硬中斷的處理思想是一致的:執行期間不允許發生排程!

上述不能搶佔的原因其實就是類似事務性的一個原則:一旦開始不能停止。另外一個原因是,執行的是使用者自定義的硬(軟)中斷程式,操作具有不確定性,如果讓這些操作期間具有排程可能,則會脫離核心的控制範圍。

軟中斷的啟用之四:其他地方

比如netif_rx_ni(),執行do_softirq前關搶佔,不能在執行軟中斷期間排程。

軟中斷的啟用之五:local_bh_enable

if (unlikely(!in_interrupt() && local_softirq_pending()))
    do_softirq();

想想,如果異常和軟中斷有共享資料的話,異常處理走到此共享資料的臨界區時需要關軟中斷,但不需要關硬中斷。那麼當走完臨界區時,需要開軟中斷,此時就是一個啟用時機(看preempt_count了,其實可能也是一個搶佔時機)。

用“啟用”而不是“呼叫”的原因是外圍處理僅修改本cpu的__softirq_pending點陣圖,最後由核心機制(比如ksoftirqd、能通過in_interrupt檢查的軟中斷處理)真正處理,而這就是軟中斷的理念:讓硬中斷(或者其它)更快執行,所以不會採用直接呼叫的方式。

“啟用”的原則是誰啟用,誰處理,哪個cpu上的硬中斷帶來的軟中斷就由哪個cpu處理(或者說,歸屬cpu是軟中斷跟著硬中斷走)。這樣,充分發揮smp的優勢,均衡到各個cpu上。至於硬中斷和cpu之間的關係,我們以後講到硬中斷時再討論。每個cpu維護自己的軟中斷機制就行了,各個cpu是互不相關的。注意,還是有相關性的:各個cpu並行處理同一型別的軟中斷時,該型別軟中斷處理需要為共享資料做保護,這是軟中斷可重入性需要付出的代價。

軟中斷核心函式處理之do_softirq

do_softirq先檢查軟中斷重入條件:必須不在硬中斷裡並且不在軟中斷裡,符合條件之後就可以開始做如下的軟中斷處理了:

pending = local_softirq_pending();
if (pending)
    __do_softirq();

這個處理是在關中斷的保護下完成的,畢竟軟中斷和硬中斷本質上是一樣的,都是中斷體系的(當然,進入到硬/軟中斷內部再開則另當別論了)。也可以看到,區域性變數pending沒有傳入__do_softirq內部,所以此處僅是判斷,不是使用,此處判斷值和內部使用值可能有差異,點陣圖中置位位數會少一些。

我們再深究一下這個檢查條件。我們的理解是:

這個條件達到了兩個效果:同一個cpu上的軟中斷不巢狀;巢狀硬中斷中不處理軟中斷。就同一個cpu而言,__do_softirq函式的執行是序列的,非重入的(do_softirq函式可以說是可重入的);就多個cpu而言,__do_softirq函式是可重入的,即使是同一個型別的軟中斷。也就是說,軟中斷通過這個檢查條件做到了本cpu上的軟中斷處理序列化,當然,多cpu之間的還是並行的,所以同一型別軟中斷處理還是需要保護自己的相關共享資料結構的。

軟中斷核心函式處理之__do_softirq

__do_softirq函式處理是儘量(雖然可能還是執行不完)執行所有被啟用的軟中斷(由本cpu上的__softirq_pending點陣圖標識)處理。我們分三個階段分析。

準備處理階段:關閉軟中斷(效果是讓上面提到的檢查條件為真,從而達到禁止本cpu上的軟中斷巢狀的目的)。

核心處理階段:關硬中斷,獲得本cpu的__softirq_pending點陣圖並儲存起來,清空點陣圖,開硬中斷(僅在讀寫點陣圖時需要關硬中斷,防止其它硬中斷同時操作)。執行本cpu的所有軟中斷(由儲存起來的點陣圖獲得)。這個核心處理是個迴圈,最多10次(MAX_SOFTIRQ_RESTART),畢竟此時用的是使用者程式的棧,不能借用太久。退出迴圈的條件是:總時間超出或者被搶佔(開中斷就會有被搶佔)或者達到10次了。

結尾處理階段:關硬中斷,開軟中斷。

另外,如果10次迴圈都解決不完軟中斷,說明期間發生的硬中斷很多,帶來的額外的軟中斷也很多。那麼就不繼續影響借用的使用者程式棧了,直接交給專門的ksoftirqd核心執行緒處理。這也就說明了迴圈的含義:處理軟中斷期間時還會進入新的硬中斷,從而帶進新的軟中斷(當然,僅僅是在本cpu的__softirq_pending上置位,不會有實際處理),所以需要反覆去處理(處理的目標很明確,就是要清空本cpu上的__softirq_pending點陣圖)。

再看看那個防止軟中斷巢狀的流程。關軟中斷中肯定有一句原子地加1的關鍵語句,如果當前核心路徑A在該原子操作之前被另一個核心路徑B打斷,則B執行完硬中斷和軟中斷後,返回到A的此處,A接著執行該原子操作,之後的軟中斷處理應該是空轉,因為肯定已經被B處理完了。如果在該原子操作之後被B打斷,則B執行完硬中斷,不會執行自己的軟中斷而是會直接退出(因為軟中斷巢狀了),返回到A的此處,A接著執行,這次A除了處理自己軟中斷,還會額外地處理B的軟中斷。

對於preempt_count中的軟中斷位,由上述可以知道,它的作用有兩個:防止軟中斷在單cpu上巢狀;保證了在執行軟中斷期間不被搶佔。

最後,還得重複一句:這裡講的__do_softirq函式都是在一個cpu上的處理,多個cpu上的並行是不受任何控制的。

總結

關於中斷的時序貌似很複雜,但其實都逃不過兩個原則:硬中斷會打斷硬中斷(當然是不同型別的);硬中斷會打斷軟中斷(同樣地:軟中斷不會打斷硬中斷,軟中斷也不會打斷軟中斷)。所有貌似複雜的時序其實都只是這兩個的疊加而已。

相關文章