1、前言linux
對於中斷處理而言,linux將其分紅了兩個部分,一個叫作中斷handler(top half),是全程關閉中斷的,另一部分是deferable task(bottom half),屬於不那麼緊急須要處理的事情。在執行bottom half的時候,是開中斷的。有多種bottom half的機制,例如:softirq、tasklet、workqueue或是直接建立一個kernel thread來執行bottom half(這在舊的kernel驅動中常見,如今,一個理智的driver廠商是不會這麼作的)。本文主要討論softirq機制。因爲tasklet是基於softirq的,所以本文也會說起tasklet,但主要是從需求層面考慮,不會涉及其具體的代碼實現。數組
在普通的驅動中通常是不會用到softirq,可是因爲驅動常用的tasklet是基於softirq的,所以,瞭解softirq機制有助於撰寫更優雅的driver。softirq不能動態分配,都是靜態定義的。內核已經定義了若干種softirq number,例如網絡數據的收發、block設備的數據訪問(數據量大,通訊帶寬高),timer的deferable task(時間方面要求高)。本文的第二章討論了softirq和tasklet這兩種機制有何不一樣,分別適用於什麼樣的場景。第三章描述了一些context的概念,這是要理解後續內容的基礎。第四章是進入softirq的實現,對比hard irq來解析soft irq的註冊、觸發,調度的過程。網絡
注:本文中的linux kernel的版本是3.14數據結構
2、爲什麼有softirq和tasklet併發
一、爲什麼有top half和bottom half負載均衡
中斷處理模塊是任何OS中最重要的一個模塊,對系統的性能會有直接的影響。想像一下:若是在經過U盤進行大量數據拷貝的時候,你按下一個key,須要半秒的時間才顯示出來,這個場景是否讓你崩潰?所以,對於那些複雜的、須要大量數據處理的硬件中斷,咱們不能讓handler中處理完一切再恢復現場(handler是全程關閉中斷的),而是僅僅在handler中處理一部分,具體包括:函數
(1)有實時性要求的oop
(2)和硬件相關的。例如ack中斷,read HW FIFO to ram等性能
(3)若是是共享中斷,那麼獲取硬件中斷狀態以便判斷是不是本中斷髮生優化
除此以外,其餘的內容都是放到bottom half中處理。在把中斷處理過程劃分紅top half和bottom half以後,關中斷的top half被瘦身,能夠很是快速的執行完畢,大大減小了系統關中斷的時間,提升了系統的性能。
咱們能夠基於下面的系統進一步的進行討論:
當網卡控制器的FIFO收到的來自以太網的數據的時候(例如半滿的時候,能夠軟件設定),能夠將該事件經過irq signal送達Interrupt Controller。Interrupt Controller能夠把中斷分發給系統中的Processor A or B。
NIC的中斷處理過程大概包括:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>handle Data in the ram----------->unmask interrupt controller
咱們先假設Processor A處理了這個網卡中斷事件,因而NIC的中斷handler在Processor A上歡快的執行,這時候,Processor A的本地中斷是disable的。NIC的中斷handler在執行的過程當中,網絡數據仍然源源不斷的到來,可是,若是NIC的中斷handler不操做NIC的寄存器來ack這個中斷的話,NIC是不會觸發下一次中斷的。還好,咱們的NIC interrupt handler老是在最開始就會ack,所以,這不會致使性能問題。ack以後,NIC已經具體再次trigger中斷的能力。當Processor A上的handler 在處理接收來自網絡的數據的時候,NIC的FIFO極可能又收到新的數據,並trigger了中斷,這時候,Interrupt controller尚未umask,所以,即使還有Processor B(也就是說有處理器資源),中斷控制器也沒法把這個中斷送達處理器系統。所以,只能眼睜睜的看着NIC FIFO填滿數據,數據溢出,或者向對端發出擁塞信號,不管如何,總體的系統性能是受到嚴重的影響。
注意:對於新的interrupt controller,可能沒有mask和umask操做,可是原理是同樣的,只不過NIC的handler執行完畢要發生EOI而已。
要解決上面的問題,最重要的是儘快的執行完中斷handler,打開中斷,unmask IRQ(或者發送EOI),方法就是把耗時的handle Data in the ram這個步驟踢出handler,讓其在bottom half中執行。
二、爲什麼有softirq和tasklet
OK,linux kernel已經把中斷處理分紅了top half和bottom half,看起來已經不錯了,那爲什麼還要提供softirq、tasklet和workqueue這些bottom half機制,linux kernel原本就夠複雜了,bottom half還來添亂。實際上,在早期的linux kernel還真是隻有一個bottom half機制,簡稱BH,簡單好用,可是性能不佳。後來,linux kernel的開發者開發了task queue機制,試圖來替代BH,固然,最後task queue也消失在內核代碼中了。如今的linux kernel提供了三種bottom half的機制,來應對不一樣的需求。
workqueue和softirq、tasklet有本質的區別:workqueue運行在process context,而softirq和tasklet運行在interrupt context。所以,出現workqueue是不奇怪的,在有sleep需求的場景中,defering task必須延遲到kernel thread中執行,也就是說必須使用workqueue機制。softirq和tasklet是怎麼回事呢?從本質上將,bottom half機制的設計有兩方面的需求,一個是性能,一個是易用性。設計一個通用的bottom half機制來知足這兩個需求很是的困難,所以,內核提供了softirq和tasklet兩種機制。softirq更傾向於性能,而tasklet更傾向於易用性。
咱們仍是進入實際的例子吧,仍是使用上一節的系統圖。在引入softirq以後,網絡數據的處理以下:
關中斷:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>raise softirq------>unmask interrupt controller
開中斷:在softirq上下文中進行handle Data in the ram的動做
一樣的,咱們先假設Processor A處理了這個網卡中斷事件,很快的完成了基本的HW操做後,raise softirq。在返回中斷現場前,會檢查softirq的觸發狀況,所以,後續網絡數據處理的softirq在processor A上執行。在執行過程當中,NIC硬件再次觸發中斷,Interrupt controller將該中斷分發給processor B,執行動做和Processor A是相似的,所以,最後,網絡數據處理的softirq在processor B上執行。
爲了性能,同一類型的softirq有可能在不一樣的CPU上併發執行,這給使用者帶來了極大的痛苦,由於驅動工程師在撰寫softirq的回調函數的時候要考慮重入,考慮併發,要引入同步機制。可是,爲了性能,咱們必須如此。
當網絡數據處理的softirq同時在Processor A和B上運行的時候,網卡中斷又來了(多是10G的網卡吧)。這時候,中斷分發給processor A,這時候,processor A上的handler仍然會raise softirq,可是並不會調度該softirq。也就是說,softirq在一個CPU上是串行執行的。這種狀況下,系統性能瓶頸是CPU資源,須要增長更多的CPU來解決該問題。
若是是tasklet的狀況會如何呢?爲什麼tasklet性能不如softirq呢?若是一個tasklet在processor A上被調度執行,那麼它永遠也不會同時在processor B上執行,也就是說,tasklet是串行執行的(注:不一樣的tasklet仍是會併發的),不須要考慮重入的問題。咱們仍是用網卡這個例子吧(注意:這個例子僅僅是用來對比,實際上,網絡數據是使用softirq機制的),一樣是上面的系統結構圖。假設使用tasklet,網絡數據的處理以下:
關中斷:mask and ack interrupt controller-------->ack NIC-------->copy FIFO to ram------>schedule tasklet------>unmask interrupt controller
開中斷:在softirq上下文中(通常使用TASKLET_SOFTIRQ這個softirq)進行handle Data in the ram的動做
一樣的,咱們先假設Processor A處理了這個網卡中斷事件,很快的完成了基本的HW操做後,schedule tasklet(同時也就raise TASKLET_SOFTIRQ softirq)。在返回中斷現場前,會檢查softirq的觸發狀況,所以,在TASKLET_SOFTIRQ softirq的handler中,獲取tasklet相關信息並在processor A上執行該tasklet的handler。在執行過程當中,NIC硬件再次觸發中斷,Interrupt controller將該中斷分發給processor B,執行動做和Processor A是相似的,雖然TASKLET_SOFTIRQ softirq在processor B上能夠執行,可是,在檢查tasklet的狀態的時候,若是發現該tasklet在其餘processor上已經正在運行,那麼該tasklet不會被處理,一直等到在processor A上的tasklet處理完,在processor B上的這個tasklet才能被執行。這樣的串行化操做雖然對驅動工程師是一個福利,可是對性能而言是極大的損傷。
3、理解softirq須要的基礎知識(各類context)
一、preempt_count
爲了更好的理解下面的內容,咱們須要先看看一些基礎知識:一個task的thread info數據結構定義以下(只保留和本場景相關的內容):
struct thread_info {
……
int preempt_count; /* 0 => preemptable, <0 => bug */
……
};
preempt_count這個成員被用來判斷當前進程是否能夠被搶佔。若是preempt_count不等於0(多是代碼調用preempt_disable顯式的禁止了搶佔,也多是處於中斷上下文等),說明當前不能進行搶佔,若是preempt_count等於0,說明已經具有了搶佔的條件(固然具體是否要搶佔當前進程仍是要看看thread info中的flag成員是否設定了_TIF_NEED_RESCHED這個標記,多是當前的進程的時間片用完了,也多是因爲中斷喚醒了優先級更高的進程)。 具體preempt_count的數據格式能夠參考下圖:
preemption count用來記錄當前被顯式的禁止搶佔的次數,也就是說,每調用一次preempt_disable,preemption count就會加一,調用preempt_enable,該區域的數值會減去一。preempt_disable和preempt_enable必須成對出現,能夠嵌套,最大嵌套的深度是255。
hardirq count描述當前中斷handler嵌套的深度。對於ARM平臺的linux kernel,其中斷部分的代碼以下:
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
struct pt_regs *old_regs = set_irq_regs(regs);irq_enter();
generic_handle_irq(irq);irq_exit();
set_irq_regs(old_regs);
}
通用的IRQ handler被irq_enter和irq_exit這兩個函數包圍。irq_enter說明進入到IRQ context,而irq_exit則說明退出IRQ context。在irq_enter函數中會調用preempt_count_add(HARDIRQ_OFFSET),爲hardirq count的bit field增長1。在irq_exit函數中,會調用preempt_count_sub(HARDIRQ_OFFSET),爲hardirq count的bit field減去1。hardirq count佔用了4個bit,說明硬件中斷handler最大能夠嵌套15層。在舊的內核中,hardirq count佔用了12個bit,支持4096個嵌套。固然,在舊的kernel中還區分fast interrupt handler和slow interrupt handler,中斷handler最大能夠嵌套的次數理論上等於系統IRQ的個數。在實際中,這個數目不可能那麼大(內核棧就受不了),所以,即便系統支持了很是大的中斷個數,也不可能各個中斷依次嵌套,達到理論的上限。基於這樣的考慮,後來內核減小了hardirq count佔用bit數目,改爲了10個bit(在general arch的代碼中修改成10,實際上,各個arch能夠redefine本身的hardirq count的bit數)。可是,當內核大佬們決定廢棄slow interrupt handler的時候,實際上,中斷的嵌套已經不會發生了。所以,理論上,hardirq count要麼是0,要麼是1。不過呢,不能總拿理論說事,實際上,萬一有寫奇葩或者老古董driver在handler中打開中斷,那麼這時候中斷嵌套仍是會發生的,可是,應該不會太多(一個系統中怎麼可能有那麼多奇葩呢?呵呵),所以,目前hardirq count佔用了4個bit,應付15個奇葩driver是妥妥的。
對softirq count進行操做有兩個場景:
(1)也是在進入soft irq handler以前給 softirq count加一,退出soft irq handler以後給 softirq count減去一。因爲soft irq handler在一個CPU上是不會併發的,老是串行執行,所以,這個場景下只須要一個bit就夠了,也就是上圖中的bit 8。經過該bit能夠知道當前task是否在sofirq context。
(2)因爲內核同步的需求,進程上下文須要禁止softirq。這時候,kernel提供了local_bh_enable和local_bh_disable這樣的接口函數。這部分的概念是和preempt disable/enable相似的,佔用了bit9~15,最大能夠支持127次嵌套。
二、一個task的各類上下文
看完了preempt_count以後,咱們來介紹各類context:
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
這裏首先要介紹的是一個叫作IRQ context的術語。這裏的IRQ context其實就是hard irq context,也就是說明當前正在執行中斷handler(top half),只要preempt_count中的hardirq count大於0(=1是沒有中斷嵌套,若是大於1,說明有中斷嵌套),那麼就是IRQ context。
softirq context並無那麼的直接,通常人會認爲當sofirq handler正在執行的時候就是softirq context。這樣說固然沒有錯,sofirq handler正在執行的時候,會增長softirq count,固然是softirq context。不過,在其餘context的狀況下,例如進程上下文中,有有可能由於同步的要求而調用local_bh_disable,這時候,經過local_bh_disable/enable保護起來的代碼也是執行在softirq context中。固然,這時候其實並無正在執行softirq handler。若是你確實想知道當前是否正在執行softirq handler,in_serving_softirq能夠完成這個使命,這是經過操做preempt_count的bit 8來完成的。
所謂中斷上下文,就是IRQ context + softirq context+NMI context。
4、softirq機制
softirq和hardirq(就是硬件中斷啦)是對應的,所以softirq的機制能夠參考hardirq對應理解,固然softirq是純軟件的,不須要硬件參與。
一、softirq number
和IRQ number同樣,對於軟中斷,linux kernel也是用一個softirq number惟一標識一個softirq,具體定義以下:
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */NR_SOFTIRQS
};
HI_SOFTIRQ用於高優先級的tasklet,TASKLET_SOFTIRQ用於普通的tasklet。TIMER_SOFTIRQ是for software timer的(所謂software timer就是說該timer是基於系統tick的)。NET_TX_SOFTIRQ和NET_RX_SOFTIRQ是用於網卡數據收發的。BLOCK_SOFTIRQ和BLOCK_IOPOLL_SOFTIRQ是用於block device的。SCHED_SOFTIRQ用於多CPU之間的負載均衡的。HRTIMER_SOFTIRQ用於高精度timer的。RCU_SOFTIRQ是處理RCU的。這些具體使用情景分析會在各自的子系統中分析,本文只是描述softirq的工做原理。
二、softirq描述符
咱們前面已經說了,softirq是靜態定義的,也就是說系統中有一個定義softirq描述符的數組,而softirq number就是這個數組的index。這個概念和早期的靜態分配的中斷描述符概念是相似的。具體定義以下:
struct softirq_action
{
void (*action)(struct softirq_action *);
};static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
系統支持多少個軟中斷,靜態定義的數組就會有多少個entry。____cacheline_aligned保證了在SMP的狀況下,softirq_vec是對齊到cache line的。softirq描述符很是簡單,只有一個action成員,表示若是觸發了該softirq,那麼應該調用action回調函數來處理這個soft irq。對於硬件中斷而言,其mask、ack等都是和硬件寄存器相關並封裝在irq chip函數中,對於softirq,沒有硬件寄存器,只有「軟件寄存器」,定義以下:
typedef struct {
unsigned int __softirq_pending;
#ifdef CONFIG_SMP
unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
ipi_irqs這個成員用於處理器之間的中斷,咱們留到下一個專題來描述。__softirq_pending就是這個「軟件寄存器」。softirq採用誰觸發,誰負責處理的。例如:當一個驅動的硬件中斷被分發給了指定的CPU,而且在該中斷handler中觸發了一個softirq,那麼該CPU負責調用該softirq number對應的action callback來處理該軟中斷。所以,這個「軟件寄存器」應該是每一個CPU擁有一個(專業術語叫作banked register)。爲了性能,irq_stat中的每個entry被定義對齊到cache line。
三、如何註冊一個softirq
經過調用open_softirq接口函數能夠註冊softirq的action callback函數,具體以下:
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
softirq_vec是一個多CPU之間共享的數據,不過,因爲全部的註冊都是在系統初始化的時候完成的,那時候,系統是串行執行的。此外,softirq是靜態定義的,每一個entry(或者說每一個softirq number)都是固定分配的,所以,不須要保護。
四、如何觸發softirq?
在linux kernel中,能夠調用raise_softirq這個接口函數來觸發本地CPU上的softirq,具體以下:
void raise_softirq(unsigned int nr)
{
unsigned long flags;local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
雖然大部分的使用場景都是在中斷handler中(也就是說關閉本地CPU中斷)來執行softirq的觸發動做,可是,這不是所有,在其餘的上下文中也能夠調用raise_softirq。所以,觸發softirq的接口函數有兩個版本,一個是raise_softirq,有關中斷的保護,另一個是raise_softirq_irqoff,調用者已經關閉了中斷,不須要關中斷來保護「soft irq status register」。
所謂trigger softirq,就是在__softirq_pending(也就是上面說的soft irq status register)的某個bit置一。從上面的定義可知,__softirq_pending是per cpu的,所以不須要考慮多個CPU的併發,只要disable本地中斷,就能夠確保對,__softirq_pending操做的原子性。
具體raise_softirq_irqoff的代碼以下:
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr); ----------------(1)
if (!in_interrupt())
wakeup_softirqd();------------------(2)
}
(1)__raise_softirq_irqoff函數設定本CPU上的__softirq_pending的某個bit等於1,具體的bit是由soft irq number(nr參數)指定的。
(2)若是在中斷上下文,咱們只要set __softirq_pending的某個bit就OK了,在中斷返回的時候天然會進行軟中斷的處理。可是,若是在context上下文調用這個函數的時候,咱們必需要調用wakeup_softirqd函數用來喚醒本CPU上的softirqd這個內核線程。具體softirqd的內容請參考下一個章節。
五、disable/enable softirq
在linux kernel中,可使用local_irq_disable和local_irq_enable來disable和enable本CPU中斷。和硬件中斷同樣,軟中斷也能夠disable,接口函數是local_bh_disable和local_bh_enable。雖然和想像的local_softirq_enable/disable有些出入,不過bh這個名字更準確反應了該接口函數的意涵,由於local_bh_disable/enable函數就是用來disable/enable bottom half的,這裏就包括softirq和tasklet。
先看disable吧,畢竟禁止bottom half比較簡單:
static inline void local_bh_disable(void)
{
__local_bh_disable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}static __always_inline void __local_bh_disable_ip(unsigned long ip, unsigned int cnt)
{
preempt_count_add(cnt);
barrier();
}
看起來disable bottom half比較簡單,就是講current thread info上的preempt_count成員中的softirq count的bit field9~15加上一就OK了。barrier是優化屏障(Optimization barrier),會在內核同步系列文章中描述。
enable函數比較複雜,以下:
static inline void local_bh_enable(void)
{
__local_bh_enable_ip(_THIS_IP_, SOFTIRQ_DISABLE_OFFSET);
}void __local_bh_enable_ip(unsigned long ip, unsigned int cnt)
{
WARN_ON_ONCE(in_irq() || irqs_disabled());-----------(1)
preempt_count_sub(cnt - 1); ------------------(2)if (unlikely(!in_interrupt() && local_softirq_pending())) { -------(3)
do_softirq();
}preempt_count_dec(); ---------------------(4)
preempt_check_resched();
}
(1)disable/enable bottom half是一種內核同步機制。在硬件中斷的handler(top half)中,不該該調用disable/enable bottom half函數來保護共享數據,由於bottom half實際上是不可能搶佔top half的。一樣的,soft irq也不會搶佔另一個soft irq的執行,也就是說,一旦一個softirq handler被調度執行(不管在哪個processor上),那麼,本地的softirq handler都沒法搶佔其運行,要等到當前的softirq handler運行完畢後,才能執行下一個soft irq handler。注意:上面咱們說的是本地,是local,softirq handler是能夠在多個CPU上同時運行的,可是,linux kernel中沒有disable all softirq的接口函數(就好像沒有disable all CPU interrupt的接口同樣,注意體會local_bh_enable/disable中的local的含義)。
說了這麼多,一言以蔽之,local_bh_enable/disable是給進程上下文使用的,用於防止softirq handler搶佔local_bh_enable/disable之間的臨界區的。
irqs_disabled接口函數能夠獲知當前本地CPU中斷是不是disable的,若是返回1,那麼當前是disable 本地CPU的中斷的。若是irqs_disabled返回1,有多是下面這樣的代碼形成的:
local_irq_disable();
……
local_bh_disable();……
local_bh_enable();
……
local_irq_enable();
本質上,關本地中斷是一種比關本地bottom half更強勁的鎖,關本地中斷其實是禁止了top half和bottom half搶佔當前進程上下文的運行。也許你會說:這也沒有什麼,就是有些浪費,至少代碼邏輯沒有問題。但事情沒有這麼簡單,在local_bh_enable--->do_softirq--->__do_softirq中,有一條無條件打開當前中斷的操做,也就是說,本來想經過local_irq_disable/local_irq_enable保護的臨界區被破壞了,其餘的中斷handler能夠插入執行,從而沒法保證local_irq_disable/local_irq_enable保護的臨界區的原子性,從而破壞了代碼邏輯。
in_irq()這個函數若是不等於0的話,說明local_bh_enable被irq_enter和irq_exit包圍,也就是說在中斷handler中調用了local_bh_enable/disable。這道理是和上面相似的,這裏就再也不詳細描述了。
(2)在local_bh_disable中咱們爲preempt_count增長了SOFTIRQ_DISABLE_OFFSET,在local_bh_enable函數中應該減掉一樣的數值。這一步,咱們首先減去了(SOFTIRQ_DISABLE_OFFSET-1),爲什麼不一次性的減去SOFTIRQ_DISABLE_OFFSET呢?考慮下面運行在進程上下文的代碼場景:
……
local_bh_disable
……須要被保護的臨界區……
local_bh_enable
……
在臨界區內,有進程context 和softirq共享的數據,所以,在進程上下文中使用local_bh_enable/disable進行保護。假設在臨界區代碼執行的時候,發生了中斷,因爲代碼並無阻止top half的搶佔,所以中斷handler會搶佔當前正在執行的thread。在中斷handler中,咱們raise了softirq,在返回中斷現場的時候,因爲disable了bottom half,所以雖然觸發了softirq,可是不會調度執行。所以,代碼返回臨界區繼續執行,直到local_bh_enable。一旦enable了bottom half,那麼以前raise的softirq就須要調度執行了,所以,這也是爲何在local_bh_enable會調用do_softirq函數。
調用do_softirq函數來處理pending的softirq的時候,當前的task是不能被搶佔的,由於一旦被搶佔,下一次該task被調度運行的時候極可能在其餘的CPU上去了(還記得嗎?softirq的pending 寄存器是per cpu的)。所以,咱們不能一次性的所有減掉,那樣的話有可能preempt_count等於0,那樣就容許搶佔了。所以,這裏減去了(SOFTIRQ_DISABLE_OFFSET-1),既保證了softirq count的bit field9~15被減去了1,又保持了preempt disable的狀態。
(3)若是當前不是interrupt context的話,而且有pending的softirq,那麼調用do_softirq函數來處理軟中斷。
(4)該來的總會來,在step 2中咱們少減了1,這裏補上,其實也就是preempt count-1。
(5)在softirq handler中極可能wakeup了高優先級的任務,這裏最好要檢查一下,看看是否須要進行調度,確保高優先級的任務得以調度執行。
五、如何處理一個被觸發的soft irq
咱們說softirq是一種defering task的機制,也就是說top half沒有作的事情,須要延遲到bottom half中來執行。那麼具體延遲到何時呢?這是本節須要講述的內容,也就是說soft irq是如何調度執行的。
在上一節已經描述一個softirq被調度執行的場景,本節主要關注在中斷返回現場時候調度softirq的場景。咱們來看中斷退出的代碼,具體以下:
void irq_exit(void)
{
……
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();……
}
代碼中「!in_interrupt()」這個條件能夠確保下面的場景不會觸發sotfirq的調度:
(1)中斷handler是嵌套的。也就是說本次irq_exit是退出到上一個中斷handler。固然,在新的內核中,這種狀況通常不會發生,由於中斷handler都是關中斷執行的。
(2)本次中斷是中斷了softirq handler的執行。也就是說本次irq_exit是否是退出到進程上下文,而是退出到上一個softirq context。這一點也保證了在一個CPU上的softirq是串行執行的(注意:多個CPU上仍是有可能併發的)
咱們繼續看invoke_softirq的代碼:
static inline void invoke_softirq(void)
{
if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
__do_softirq();
#else
do_softirq_own_stack();
#endif
} else {
wakeup_softirqd();
}
}
force_irqthreads是和強制線程化相關的,主要用於interrupt handler的調試(通常而言,在線程環境下比在中斷上下文中更容易收集調試數據)。若是系統選擇了對全部的interrupt handler進行線程化處理,那麼softirq也沒有理由在中斷上下文中處理(中斷handler都在線程中執行了,softirq怎麼可能在中斷上下文中執行)。自己invoke_softirq這個函數是在中斷上下文中被調用的,若是強制線程化,那麼系統中全部的軟中斷都在sofirq的daemon進程中被調度執行。
若是沒有強制線程化,softirq的處理也分紅兩種狀況,主要是和softirq執行的時候使用的stack相關。若是arch支持單獨的IRQ STACK,這時候,因爲要退出中斷,所以irq stack已經接近全空了(不考慮中斷棧嵌套的狀況,所以新內核下,中斷不會嵌套),所以直接調用__do_softirq()處理軟中斷就OK了,不然就調用do_softirq_own_stack函數在softirq本身的stack上執行。固然對ARM而言,softirq的處理就是在當前的內核棧上執行的,所以do_softirq_own_stack的調用就是調用__do_softirq(),代碼以下(刪除了部分無關代碼):
asmlinkage void __do_softirq(void)
{……
pending = local_softirq_pending();---------------獲取softirq pending的狀態
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);---標識下面的代碼是正在處理softirq
cpu = smp_processor_id();
restart:
set_softirq_pending(0); ---------清除pending標誌local_irq_enable(); ------打開中斷,softirq handler是開中斷執行的
h = softirq_vec; -------獲取軟中斷描述符指針
while ((softirq_bit = ffs(pending))) {-------尋找pending中第一個被設定爲1的bit
unsigned int vec_nr;
int prev_count;h += softirq_bit - 1; ------指向pending的那個軟中斷描述符
vec_nr = h - softirq_vec;----獲取soft irq number
h->action(h);---------指向softirq handler
h++;
pending >>= softirq_bit;
}local_irq_disable(); -------關閉本地中斷
pending = local_softirq_pending();----------(注1)
if (pending) {
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;wakeup_softirqd();
}
__local_bh_enable(SOFTIRQ_OFFSET);----------標識softirq處理完畢
}
(注1)再次檢查softirq pending,有可能上面的softirq handler在執行過程當中,發生了中斷,又raise了softirq。若是的確如此,那麼咱們須要跳轉到restart那裏從新處理soft irq。固然,也不能老是在這裏不斷的loop,所以linux kernel設定了下面的條件:
(1)softirq的處理時間沒有超過2個ms
(2)上次的softirq中沒有設定TIF_NEED_RESCHED,也就是說沒有有高優先級任務須要調度
(3)loop的次數小於 10次
所以,只有同時知足上面三個條件,程序纔會跳轉到restart那裏從新處理soft irq。不然wakeup_softirqd就OK了。這樣的設計也是一個平衡的方案。一方面照顧了調度延遲:原本,發生一箇中斷,系統指望在限定的時間內調度某個進程來處理這個中斷,若是softirq handler不斷觸發,其實linux kernel是沒法保證調度延遲時間的。另一方面,也照顧了硬件的thoughput:已經預留了必定的時間來處理softirq。