linux kernel的中斷子系統之(八):softirq

講的蠻好的,對於鎖的使用結合起來易於理解。

一、前言

對於中斷處理而言,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

 

二、爲何有softirq和tasklet

1、爲何有top half和bottom half

中斷處理模塊是任何OS中最重要的一個模塊,對系統的性能會有直接的影響。想像一下:如果在通過U盤進行大量數據拷貝的時候,你按下一個key,需要半秒的時間才顯示出來,這個場景是否讓你崩潰?因此,對於那些複雜的、需要大量數據處理的硬件中斷,我們不能讓handler中處理完一切再恢復現場(handler是全程關閉中斷的),而是僅僅在handler中處理一部分,具體包括:

(1)有實時性要求的

(2)和硬件相關的。例如ack中斷,read HW FIFO to ram等

(3)如果是共享中斷,那麼獲取硬件中斷狀態以便判斷是否是本中斷髮生

除此之外,其他的內容都是放到bottom half中處理。在把中斷處理過程劃分成top half和bottom half之後,關中斷的top half被瘦身,可以非常快速的執行完畢,大大減少了系統關中斷的時間,提高了系統的性能。

我們可以基於下面的系統進一步的進行討論:

rrr

當網卡控制器的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中執行。

 

2、爲何有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才能被執行。這樣的串行化操作雖然對驅動工程師是一個福利,但是對性能而言是極大的損傷。

 

三、理解softirq需要的基礎知識(各種context)

1、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的數據格式可以參考下圖:

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_bf_enable和local_bf_disable這樣的接口函數。這部分的概念是和preempt disable/enable類似的,佔用了bit9~15,最大可以支持127次嵌套。

 

2、一個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。

 

四、softirq機制

softirq和hardirq(就是硬件中斷啦)是對應的,因此softirq的機制可以參考hardirq對應理解,當然softirq是純軟件的,不需要硬件參與。

1、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的工作原理。

2、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。

3、如何註冊一個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)都是固定分配的,因此,不需要保護。

4、如何觸發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的內容請參考下一個章節。

 

5、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了高優先級的任務,這裏最好要檢查一下,看看是否需要進行調度,確保高優先級的任務得以調度執行。

 

5、如何處理一個被觸發的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。



原創文章,轉發請註明出處。蝸窩科技

http://www.wowotech.net/linux_kenrel/soft-irq.html

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