linux 內核筆記--中斷子系統之softirq

linux把處理硬件中斷的過程分爲兩部分。上半部簡單快速,執行時禁止部分或全部中斷。下半部稍後執行,並且執行期間可以響應所有的中斷。這樣的設計會使系統處於中斷屏蔽的狀態儘可能的短,從而提高系統的響應能力。
下半部的處理方式主要有soft_irq,tasklet,workqueue三種,他們在使用方式和適用情況上各有不同。soft_irq用在對底半執行時間要求比較緊急或者非常重要的場合,在中斷上下文執行。tasklet和work queue在普通的driver裏用的相對較多,主要區別是tasklet是在中斷上下文執行,而workqueue是在process上下文,因此可以執行可能sleep的操作。

softirq和tasklet都運行在中斷上下文,那麼他們有什麼區別呢?
1.softirq是在編譯期間靜態分配的,它不像tasklet那樣可以動態的分配和刪除。
每個軟中斷在內核中以softirq_action表示。在kernel/softirq.c中定義了一個包含有32個該結構體的數組。每種軟中斷對應數組的一項,所以軟中斷最多有32項。

內核目前實現了10中軟中斷,定義在linux/interrupt.h中。

enum
{
    HI_SOFTIRQ=0,                     /* 高優先級tasklet */ /* 優先級最高 */
    TIMER_SOFTIRQ,                    /* 時鐘相關的軟中斷 */
    NET_TX_SOFTIRQ,                   /* 將數據包傳送到網卡 */
    NET_RX_SOFTIRQ,                   /* 從網卡接收數據包 */
    BLOCK_SOFTIRQ,                    /* 塊設備的軟中斷 */
    BLOCK_IOPOLL_SOFTIRQ,             /* 支持IO輪詢的塊設備軟中斷 */
    TASKLET_SOFTIRQ,                  /* 常規tasklet */
    SCHED_SOFTIRQ,                    /* 調度程序軟中斷 */
    HRTIMER_SOFTIRQ,                  /* 高精度計時器軟中斷 */
    RCU_SOFTIRQ,                      /* RCU鎖軟中斷,該軟中斷總是最後一個軟中斷 */ 
    NR_SOFTIRQS                       /* 軟中斷數,爲10 */
};

2.同一類型的softirq可以在不同的cpu上併發執行,而tasklet在使用時不需要考慮重入,因此tasklet更佳易用,使用softirq更傾向於性能。

softirq的使用

註冊軟中斷

通過調用open_softirq接口函數,將action函數指針指向向該軟中斷應該執行的函數。

/* 開啓軟中斷 */
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

在start_kernel()進行系統初始化中,就調用了softirq_init()函數對HI_SOFTIRQ和TASKLET_SOFTIRQ兩個軟中斷進行了初始化

void __init softirq_init(void)
{
    int cpu;

    for_each_possible_cpu(cpu) {
        per_cpu(tasklet_vec, cpu).tail =
            &per_cpu(tasklet_vec, cpu).head;
        per_cpu(tasklet_hi_vec, cpu).tail =
            &per_cpu(tasklet_hi_vec, cpu).head;
    }

    /* 開啓常規tasklet */
    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    /* 開啓高優先級tasklet */
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

當使用open_softirq設置好某個軟中斷的action指針後,該軟中斷就會開始可以使用了。

觸發軟中斷

調用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);
}

先是關閉本地cpu中斷,然後調用:raise_softirq_irqoff。

inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr);

        ......
    if (!in_interrupt())
        wakeup_softirqd();
}

通過in_interrupt判斷現在是否在中斷上下文中,或者軟中斷是否被禁止,如果都不成立,否則,我們必須要調用wakeup_softirqd函數用來喚醒本CPU上的softirqd這個內核線程。

處理一個被觸發的軟中斷

1.在中斷返回現場時候調度softirq
在中斷處理程序中觸發軟中斷是最常見的形式,一個硬件中斷處理完成之後。下面的函數在處理完硬件中斷之後退出中斷處理函數,在irq_exit中會觸發軟件中斷的處理。
中斷處理模型:
這裏寫圖片描述

fastcall unsigned int do_IRQ(struct pt_regs *regs)
{
        ...
        irq_enter();

        //handle external interrupt (ISR)
        ...
          irq_exit();

        return 1;
}

硬中斷執行完畢後

void irq_exit(void) 
{ 
…… 
    if (!in_interrupt() && local_softirq_pending()) 
        invoke_softirq(); 
…… 
}  

invoke_irq :

static inline void invoke_softirq(void)
{

    if (!force_irqthreads) {
#ifdef CONFIG_HAVE_IRQ_EXIT_ON_IRQ_STACK
        /*
         * We can safely execute softirq on the current stack if
         * it is the irq stack, because it should be near empty
         * at this stage.
         */
        /* 軟中斷處理函數 */
        __do_softirq();
#else
        /*
         * Otherwise, irq_exit() is called on the task stack that can
         * be potentially deep already. So call softirq in its own stack
         * to prevent from any overrun.
         */
        do_softirq_own_stack();
#endif
    } else {
        /* 如果強制使用軟中斷線程進行軟中斷處理,會通知調度器喚醒軟中斷線程ksoftirqd */
        wakeup_softirqd();
    }
}

可以看出,如果中斷髮生嵌套,in_interrupt()保證了只有在最外層的中斷的irq_exit階段,invoke_interrupt纔會被調用

最終調用do_softirq:

asmlinkage __visible void __do_softirq(void)
{
    /* 爲了防止軟中斷執行時間太長,設置了一個軟中斷結束時間 */
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    /* 保存當前進程的標誌 */
    unsigned long old_flags = current->flags;
    /* 軟中斷循環執行次數: 10次 */
    int max_restart = MAX_SOFTIRQ_RESTART;
    /* 軟中斷的action指針 */
    struct softirq_action *h;
    bool in_hardirq;
    __u32 pending;
    int softirq_bit;

    /*
     * Mask out PF_MEMALLOC s current task context is borrowed for the
     * softirq. A softirq handled such as network RX might set PF_MEMALLOC
     * again if the socket is related to swap
     */
    current->flags &= ~PF_MEMALLOC;

    /* 獲取此CPU的__softirq_pengding變量值 */
    pending = local_softirq_pending();
    /* 用於統計進程被軟中斷使用時間 */
    account_irq_enter_time(current);

    /* 增加preempt_count軟中斷計數器,也表明禁止了調度 */
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
    in_hardirq = lockdep_softirq_start();

/* 循環10次的入口,每次循環都會把所有掛起需要執行的軟中斷執行一遍 */
restart:
    /* 該CPU的__softirq_pending清零,當前的__softirq_pending保存在pending變量中 */
    /* 這樣做就保證了新的軟中斷會在下次循環中執行 */
    set_softirq_pending(0);

    /* 開中斷 */
    local_irq_enable();

    /* h指向軟中斷數組頭 */
    h = softirq_vec;

    /* 每次獲取最高優先級的已掛起軟中斷 */
    while ((softirq_bit = ffs(pending))) {
        unsigned int vec_nr;
        int prev_count;
        /* 獲取此軟中斷描述符地址 */
        h += softirq_bit - 1;

        /* 減去軟中斷描述符數組首地址,獲得軟中斷號 */
        vec_nr = h - softirq_vec;
        /* 獲取preempt_count的值 */
        prev_count = preempt_count();

        /* 增加統計中該軟中斷髮生次數 */
        kstat_incr_softirqs_this_cpu(vec_nr);

        trace_softirq_entry(vec_nr);
        /* 執行該軟中斷的action操作 */
        h->action(h);
        trace_softirq_exit(vec_nr);

        /* 之前保存的preempt_count並不等於當前的preempt_count的情況處理,也是簡單的把之前的複製到當前的preempt_count上,這樣做是防止最後軟中斷計數不爲0導致系統不能夠執行調度 */
        if (unlikely(prev_count != preempt_count())) {
            pr_err("huh, entered softirq %u %s %p with preempt_count %08x, exited with %08x?\n",
                   vec_nr, softirq_to_name[vec_nr], h->action,
                   prev_count, preempt_count());
            preempt_count_set(prev_count);
        }
        /* h指向下一個軟中斷,但下個軟中斷並不一定需要執行,這裏只是配合softirq_bit做到一個處理 */
        h++;
        pending >>= softirq_bit;
    }

    rcu_bh_qs();
    /* 關中斷 */
    local_irq_disable();

    /* 循環結束後再次獲取CPU的__softirq_pending變量,爲了檢查是否還有軟中斷未執行 */
    pending = local_softirq_pending();
    /* 還有軟中斷需要執行 */
    if (pending) {
        /* 在還有軟中斷需要執行的情況下,如果時間片沒有執行完,並且循環次數也沒到10次,繼續執行軟中斷 */
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;
        /* 這裏是有軟中斷掛起,但是軟中斷時間和循環次數已經用完,通知調度器喚醒軟中斷線程去執行掛起的軟中斷,軟中斷線程是ksoftirqd,這裏只起到一個通知作用,因爲在中斷上下文中是禁止調度的 */
        wakeup_softirqd();
    }

    lockdep_softirq_end(in_hardirq);
    /* 用於統計進程被軟中斷使用時間 */
    account_irq_exit_time(current);
    /* 減少preempt_count中的軟中斷計數器 */
    __local_bh_enable(SOFTIRQ_OFFSET);
    WARN_ON_ONCE(in_interrupt());
    /* 還原進程標誌 */
    tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

• 首先取出pending的狀態;
• 禁止軟中斷,主要是爲了防止和軟中斷守護進程發生競爭;
• 清除所有的軟中斷待決標誌;
• 打開本地cpu中斷;
• 循環執行待決軟中斷的回調函數;
• 如果循環完畢,發現新的軟中斷被觸發,則重新啓動循環,直到以下條件滿足,才退出:
o 沒有新的軟中斷等待執行;
o 循環已經達到最大的循環次數MAX_SOFTIRQ_RESTART,目前的設定值時10次;
• 如果經過MAX_SOFTIRQ_RESTART次循環後還未處理完,則激活守護進程,處理剩下的軟中斷;
• 退出前恢復軟中斷;

2.在守護線程ksoftirq中執行。
雖然大部分的softirq是在中斷退出的情況下執行,但是有幾種情況會在ksoftirq中執行。
a.從上文看出,raise softirq主動觸發,而此時正好不是在中斷上下文中,ksoftirq進程將被喚醒。
b.在irq_exit中執行軟中斷,但是在經過MAX_SOFTIRQ_RESTART次循環後,軟中斷還未處理完,這種情況,ksoftirq進程也會被喚醒。

所以加入守護線程這一機制,主要是擔心一旦有大量的軟中斷等待執行,會使得內核過長地留在中斷上下文中。

static void run_ksoftirqd(unsigned int cpu)

{

    local_irq_disable();

    if (local_softirq_pending()) {

    /*

         * We can safely run softirq on inline stack, as we are not deep

         * in the task stack here.              

     */

         __do_sof   tirq();

    local_irq_enable();

    cond_resched_rcu_qs();

    return;

     }

守護進程最終也會調用__do_softirq執行軟中斷的回調。

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