天天看點

Linux中斷(interrupt)子系統之五:軟中斷(softIRQ)

軟中斷(softIRQ)是核心提供的一種延遲執行機制,它完全由軟體觸發,雖然說是延遲機制,實際上,在大多數情況下,它與普通程序相比,能得到更快的響應時間。軟中斷也是其他一些核心機制的基礎,比如tasklet,高分辨率timer等。

/*****************************************************************************************************/

聲明:本博内容均由http://blog.csdn.net/droidphone原創,轉載請注明出處,謝謝!

<b>1.  軟中斷的資料結構</b>

<b>1.1  struct softirq_action</b>

核心用softirq_action結構管理軟中斷的注冊和激活等操作,它的定義如下:

點選(此處)折疊或打開

struct softirq_action

{

    void    (*action)(struct softirq_action *);

};

非常簡單,隻有一個用于回調的函數指針。軟中斷的資源是有限的,核心目前隻實作了10種類型的軟中斷,它們是:

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

核心的開發者們不建議我們擅自增加軟中斷的數量,如果需要新的軟中斷,盡可能把它們實作為基于軟中斷的tasklet形式。與上面的枚舉值相對應,核心定義了一個softirq_action的結構數組,每種軟中斷對應數組中的一項:

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

<b>1.2  irq_cpustat_t</b>

多個軟中斷可以同時在多個cpu運作,就算是同一種軟中斷,也有可能同時在多個cpu上運作。核心為每個cpu都管理着一個待決軟中斷變量(pending),它就是irq_cpustat_t:

typedef struct {

    unsigned int __softirq_pending;

} ____cacheline_aligned irq_cpustat_t;

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;

__softirq_pending字段中的每一個bit,對應着某一個軟中斷,某個bit被置位,說明有相應的軟中斷等待處理。

<b>1.3  軟中斷的守護程序ksoftirqd</b>

在cpu的熱插拔階段,核心為每個cpu建立了一個用于執行軟中斷的守護程序ksoftirqd,同時定義了一個per_cpu變量用于儲存每個守護程序的task_struct結構指針:

DEFINE_PER_CPU(struct task_struct *, ksoftirqd);

大多數情況下,軟中斷都會在irq_exit階段被執行,在irq_exit階段沒有處理完的軟中斷才有可能會在守護程序中執行。

<b>2.  觸發軟中斷</b>

要觸發一個軟中斷,隻要調用api:raise_softirq即可,它的實作很簡單,先是關閉本地cpu中斷,然後調用:raise_softirq_irqoff

void raise_softirq(unsigned int nr)

    unsigned long flags;

    local_irq_save(flags);

    raise_softirq_irqoff(nr);

    local_irq_restore(flags);

}

再看看raise_softirq_irqoff:

inline void raise_softirq_irqoff(unsigned int nr)

    __raise_softirq_irqoff(nr);

        ......

    if (!in_interrupt())

        wakeup_softirqd();

是通過__raise_softirq_irqoff設定cpu的軟中斷pending标志位(irq_stat[NR_CPUS]

),然後通過in_interrupt判斷現在是否在中斷上下文中,或者軟中斷是否被禁止,如果都不成立,則喚醒軟中斷的守護程序,在守護程序中執行軟中

斷的回調函數。否則什麼也不做,軟中斷将會在中斷的退出階段被執行。

<b>3.  軟中斷的執行</b>

基于上面所說,軟中斷的執行既可以守護程序中執行,也可以在中斷的退出階段執行。實際上,軟中斷更多的是在中斷的退出階段執行(irq_exit),以便

達到更快的響應,加入守護程序機制,隻是擔心一旦有大量的軟中斷等待執行,會使得核心過長地留在中斷上下文中。

<b>3.1  在irq_exit中執行</b>

看看irq_exit的部分:

void irq_exit(void)

    sub_preempt_count(IRQ_EXIT_OFFSET);

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

        invoke_softirq();

果中斷發生嵌套,in_interrupt()保證了隻有在最外層的中斷的irq_exit階段,invoke_interrupt才會被調用,當

然,local_softirq_pending也會實作判斷目前cpu有無待決的軟中斷。代碼最終會進入__do_softirq中,核心會保證調用

__do_softirq時,本地cpu的中斷處于關閉狀态,進入__do_softirq:

asmlinkage void __do_softirq(void)

    pending = local_softirq_pending();

    __local_bh_disable((unsigned long)__builtin_return_address(0),

                SOFTIRQ_OFFSET);

restart:

    /* Reset the pending bitmask before enabling irqs */

    set_softirq_pending(0);

    local_irq_enable();

    h = softirq_vec;

    do {

        if (pending &amp; 1) {

     ......

            trace_softirq_entry(vec_nr);

            h-&gt;action(h);

            trace_softirq_exit(vec_nr);

                        ......

        }

        h++;

        pending &gt;&gt;= 1;

    } while (pending);

    local_irq_disable();

    if (pending &amp;&amp; --max_restart)

        goto restart;

    if (pending)

    lockdep_softirq_exit();

    __local_bh_enable(SOFTIRQ_OFFSET);

首先取出pending的狀态;

禁止軟中斷,主要是為了防止和軟中斷守護程序發生競争;

清除所有的軟中斷待決标志;

打開本地cpu中斷;

循環執行待決軟中斷的回調函數;

如果循環完畢,發現新的軟中斷被觸發,則重新啟動循環,直到以下條件滿足,才退出:

沒有新的軟中斷等待執行;

循環已經達到最大的循環次數MAX_SOFTIRQ_RESTART,目前的設定值時10次;

如果經過MAX_SOFTIRQ_RESTART次循環後還未處理完,則激活守護程序,處理剩下的軟中斷;

推出前恢複軟中斷;

<b>3.2  在ksoftirqd程序中執行</b>

從前面幾節的讨論我們可以看出,軟中斷也可能由ksoftirqd守護程序執行,這要發生在以下兩種情況下:

在irq_exit中執行軟中斷,但是在經過MAX_SOFTIRQ_RESTART次循環後,軟中斷還未處理完,這種情況雖然極少發生,但畢竟有可能;

核心的其它代碼主動調用raise_softirq,而這時正好不是在中斷上下文中,守護程序将被喚醒;

守護程序最終也會調用__do_softirq執行軟中斷的回調,具體的代碼位于run_ksoftirqd函數中,核心會關閉搶占的情況下執行__do_softirq,具體的過程這裡不做讨論。

4.  tasklet

因為核心已經定義好了10種軟中斷類型,并且不建議我們自行添加額外的軟中斷,是以對軟中斷的實作方式,我們主要是做一個簡單的了解,對于驅動程式的開

發者來說,無需實作自己的軟中斷。但是,對于某些情況下,我們不希望一些操作直接在中斷的handler中執行,但是又希望在稍後的時間裡得到快速地處

理,這就需要使用tasklet機制。

tasklet是建立在軟中斷上的一種延遲執行機制,它的實作基于TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷類型。

<b>4.1  tasklet_struct  </b>      

在軟中斷的初始化函數softirq_init的最後,核心注冊了TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷:

void __init softirq_init(void)

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);

    open_softirq(HI_SOFTIRQ, tasklet_hi_action);

<b></b>

核心用一個tasklet_struct來表示一個tasklet,它的定義如下:

struct tasklet_struct

    struct tasklet_struct *next;

    unsigned long state;

    atomic_t count;

    void (*func)(unsigned long);

    unsigned long data;

next用于把同一個cpu的tasklet連結成一個連結清單,state用于表示該tasklet的目前狀态,目前隻是用了最低的兩個bit,分别用于表示已經準備被排程執行和已經在另一個cpu上執行:

    TASKLET_STATE_SCHED,    /* Tasklet is scheduled for execution */

    TASKLET_STATE_RUN    /* Tasklet is running (SMP only) */

子變量count用于tasklet對tasklet_disable和tasklet_enable的計數,count為0時表示允許tasklet執

行,否則不允許執行,每次tasklet_disable時,該值加1,tasklet_enable時該值減1。func是tasklet被執行時的回

調函數指針,data則用作回調函數func的參數。

<b>4.2  初始化一個tasklet</b>

有兩種辦法初始化一個tasklet,第一種是靜态初始化,使用以下兩個宏,這兩個宏定義一個tasklet_struct結構,并用相應的參數對結構中的字段進行初始化:

DECLARE_TASKLET(name, func, data);定義名字為name的tasklet,預設為enable狀态,也就是count字段等于0。

DECLARE_TASKLET_DISABLED(name, func, data);定義名字為name的tasklet,預設為enable狀态,也就是count字段等于1。

第二個是動态初始化方法:先定義一個tasklet_struct,然後用tasklet_init函數進行初始化,該方法預設tasklet處于enable狀态:

struct tasklet_struct tasklet_xxx;

......

tasklet_init(&amp;tasklet_xxx, func, data);

<b>4.3  tasklet的使用方法</b>

使能和禁止tasklet,使用以下函數:

tasklet_disable()  通過給count字段加1來禁止一個tasklet,如果tasklet正在運作中,則等待運作完畢才傳回(通過TASKLET_STATE_RUN标志)。

tasklet_disable_nosync()  tasklet_disable的異步版本,它不會等待tasklet運作完畢。

tasklet_enable()  使能tasklet,隻是簡單地給count字段減1。

排程tasklet的執行,使用以下函數:

tasklet_schedule(struct tasklet_struct *t)

 如果TASKLET_STATE_SCHED标志為0,則置位TASKLET_STATE_SCHED,然後把tasklet挂到該cpu等待執行的

tasklet連結清單上,接着發出TASKLET_SOFTIRQ軟中斷請求。

tasklet_hi_schedule(struct tasklet_struct *t)  效果同上,差別是它發出的是HI_SOFTIRQ軟中斷請求。

銷毀tasklet,使用以下函數:

tasklet_kill(struct tasklet_struct *t)  如果tasklet處于TASKLET_STATE_SCHED狀态,或者tasklet正在執行,則會等待tasklet執行完畢,然後清除TASKLET_STATE_SCHED狀态。

<b>4.4  tasklet的内部執行機制</b>

核心為每個cpu用定義了一個tasklet_head結構,用于管理每個cpu上的tasklet的排程和執行:

struct tasklet_head

    struct tasklet_struct *head;

    struct tasklet_struct **tail;

static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);

static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);

回到4.1節,我們知道,tasklet是利用TASKLET_SOFTIRQ和HI_SOFTIRQ這兩個軟中斷來實作的,兩個軟中斷隻是有優先級的差

别,是以我們隻讨論TASKLET_SOFTIRQ的實作,TASKLET_SOFTIRQ的中斷回調函數是tasklet_action,我們看看它的

代碼:

static void tasklet_action(struct softirq_action *a)

    struct tasklet_struct *list;

    list = __this_cpu_read(tasklet_vec.head);

    __this_cpu_write(tasklet_vec.head, NULL);

    __this_cpu_write(tasklet_vec.tail, &amp;__get_cpu_var(tasklet_vec).head);

    while (list) {

        struct tasklet_struct *t = list;

        list = list-&gt;next;

        if (tasklet_trylock(t)) {

            if (!atomic_read(&amp;t-&gt;count)) {

                if (!test_and_clear_bit(TASKLET_STATE_SCHED, &amp;t-&gt;state))

                    BUG();

                t-&gt;func(t-&gt;data);

                tasklet_unlock(t);

                continue;

            }

            tasklet_unlock(t);

        local_irq_disable();

        t-&gt;next = NULL;

        *__this_cpu_read(tasklet_vec.tail) = t;

        __this_cpu_write(tasklet_vec.tail, &amp;(t-&gt;next));

        __raise_softirq_irqoff(TASKLET_SOFTIRQ);

        local_irq_enable();

    }

解析如下:

關閉本地中斷的前提下,移出目前cpu的待處理tasklet連結清單到一個臨時連結清單後,清除目前cpu的tasklet連結清單,之是以這樣處理,是為了處理目前tasklet連結清單的時候,允許新的tasklet被排程進待處理連結清單中。

周遊臨時連結清單,用tasklet_trylock判斷目前tasklet是否已經在其他cpu上運作,而且tasklet沒有被禁止:

如果沒有運作,也沒有禁止,則清除TASKLET_STATE_SCHED狀态位,執行tasklet的回調函數。

如果已經在運作,或者被禁止,則把該tasklet重新添加會目前cpu的待處理tasklet連結清單上,然後觸發TASKLET_SOFTIRQ軟中斷,等待下一次軟中斷時再次執行。

分析到這了我有個疑問,看了上面的代碼,如果一個tasklet被tasklet_schedule後,在沒有被執行前被tasklet_disable了,豈不是會無窮無盡地引發TASKLET_SOFTIRQ軟中斷?

通過以上的分析,我們需要注意的是,tasklet有以下幾個特征:

同一個tasklet隻能同時在一個cpu上執行,但不同的tasklet可以同時在不同的cpu上執行;

一旦tasklet_schedule被調用,核心會保證tasklet一定會在某個cpu上執行一次;

如果tasklet_schedule被調用時,tasklet不是出于正在執行狀态,則它隻會執行一次;

如果tasklet_schedule被調用時,tasklet已經正在執行,則它會在稍後被排程再次被執行;

兩個tasklet之間如果有資源沖突,應該要用自旋鎖進行同步保護;

繼續閱讀