1. 程式人生 > 其它 >軟中斷

軟中斷

軟中斷   

      首先明確一個概念軟中斷(不是軟體中斷int n)。總來來說軟中斷就是核心在啟動時為每一個核心建立了一個特殊的程序,這個程序會不停的poll檢查是否有軟中斷需要執行,如果需要執行則呼叫註冊的介面函式。所以軟中斷是執行在程序上下文的,而且可能併發執行在不同CPU上。所謂的軟中斷就是核心利用核心執行緒配合抽象的資料結構進行管理執行緒合適時間呼叫註冊的介面的一套軟體管理機制。

     先看管理軟中斷的資料結構因為資料結構最能說明邏輯核心對軟體中斷抽象的資料結構主要有如下幾個部分。

中斷服務介面管理

在核心中宣告在\kernel\softirq.c中如下

#ifndef __ARCH_IRQ_STAT
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
EXPORT_SYMBOL(irq_stat);
#endif

static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;


struct softirq_action
{
    void    (*action)(struct softirq_action *);
};

其中的NR_SOFTIRQS由軟中斷型別的列舉物件提供如下定義:

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
};

之所以綜上可以知道核心維護了一個struct softirq_action型別的軟中斷介面陣列,而軟中斷的狀態則是由前面的 irq_cpustat_t 型別的陣列管理,由定義可以知道狀態是和CPU關聯的,表示某一個CPU上的軟中斷狀態。下面看看irq_cpustat_t 的定義,也非常的的簡單主要就是 其中的 __softirq_pending成員,這個成員的每一個bit表示一種型別的中斷型別的狀態資訊,並且低bit的中斷型別的中斷優先順序高。

typedef struct {
    unsigned int __softirq_pending;//標記是否有軟中斷懸起
    long idle_timestamp;
    /* 統計資訊 */
    /* Hard interrupt statistics. */
    unsigned int irq_timer_count;
    unsigned int irq_syscall_count;
    unsigned int irq_resched_count;
    unsigned int irq_hv_flush_count;
    unsigned int irq_call_count;
    unsigned int irq_hv_msg_count;
    unsigned int irq_dev_intr_count;

} ____cacheline_aligned irq_cpustat_t;

在通過Tasklet接介面中斷的建立就可以知道軟體中斷的註冊(open_softirq)過程就是修改前面定義的softirq_vec陣列,就可以完成軟體中斷的註冊,而驅動開發人員也很少直接使用軟體中斷。

//介面中的nr就是上面列舉值,action就是軟中斷服務函式
open_softirq(int nr,void(*action)(struct softirq_action *)); 

再看核心在啟動時為每個CPU建立的執行緒操作:

static struct notifier_block cpu_nfb = {
    .notifier_call = cpu_callback
};

static struct smp_hotplug_thread softirq_threads = {
    .store            = &ksoftirqd,
    .thread_should_run    = ksoftirqd_should_run,
    .thread_fn        = run_ksoftirqd,
    .thread_comm        = "ksoftirqd/%u",
};

static __init int spawn_ksoftirqd(void)
{
    register_cpu_notifier(&cpu_nfb);

    BUG_ON(smpboot_register_percpu_thread(&softirq_threads));

    return 0;
}
early_initcall(spawn_ksoftirqd);

重點是這個介面函式 smpboot_register_percpu_thread如下:

/**
 * smpboot_register_percpu_thread - Register a per_cpu thread related to hotplug
 * @plug_thread:    Hotplug thread descriptor
 *
 * Creates and starts the threads on all online cpus.
 */
int smpboot_register_percpu_thread(struct smp_hotplug_thread *plug_thread)
{
    unsigned int cpu;
    int ret = 0;

    get_online_cpus();
    mutex_lock(&smpboot_threads_lock);
    for_each_online_cpu(cpu) {
        ret = __smpboot_create_thread(plug_thread, cpu);
        if (ret) {
            smpboot_destroy_threads(plug_thread);
            goto out;
        }
        smpboot_unpark_thread(plug_thread, cpu);
    }
    list_add(&plug_thread->list, &hotplug_threads);
out:
    mutex_unlock(&smpboot_threads_lock);
    put_online_cpus();
    return ret;
}

傳進來的引數是 softirq_threads,先獲取線上即啟用的CPU然後遍歷呼叫__smpboot_create_thread 引數同樣是前面定義的softirq_threads繼續向下看:

tatic int
__smpboot_create_thread(struct smp_hotplug_thread *ht, unsigned int cpu)
{
    struct task_struct *tsk = *per_cpu_ptr(ht->store, cpu);
    struct smpboot_thread_data *td;

    if (tsk)
        return 0;

    td = kzalloc_node(sizeof(*td), GFP_KERNEL, cpu_to_node(cpu));
    if (!td)
        return -ENOMEM;
    td->cpu = cpu;
    td->ht = ht;

    tsk = kthread_create_on_cpu(smpboot_thread_fn, td, cpu,
                    ht->thread_comm);
    if (IS_ERR(tsk)) {
        kfree(td);
        return PTR_ERR(tsk);
    }
    get_task_struct(tsk);
    *per_cpu_ptr(ht->store, cpu) = tsk;
    if (ht->create) {
        /*
         * Make sure that the task has actually scheduled out
         * into park position, before calling the create
         * callback. At least the migration thread callback
         * requires that the task is off the runqueue.
         */
        if (!wait_task_inactive(tsk, TASK_PARKED))
            WARN_ON(1);
        else
            ht->create(cpu);
    }
    return 0;
}

 

看建立了一個核心執行緒在特定CPU上通過kthread_create_on_cpu(smpboot_thread_fn, td, cpu,ht->thread_comm)介面,不在往深入繼續看,這裡只需要建立了一個繫結CPU的執行緒,執行緒函式是smpboot_thread_fn這個比較重要需要詳細看一下。傳入的data就是一個struct smpboot_thread_data型別的資料這個資料中儲存了softirq_threads在ht中如下,程序開始執行時先關閉搶佔,檢查是否需要停止當前執行緒如果需要則立馬停止當前執行緒,這裡肯定不需要停止除非是關機(我的理解)。然就是檢查是否要暫停,因為使用者的軟中斷介面可能呼叫阻塞介面會阻塞當前內爾後程序所以需要暫停當前執行緒最後的恢復也是有使用者軟體中斷服務函式完成(我的理解)最後部分原始碼註釋如下:

static int smpboot_thread_fn(void *data)
{
    struct smpboot_thread_data *td = data;
    struct smp_hotplug_thread *ht = td->ht;

    while (1) {
        set_current_state(TASK_INTERRUPTIBLE);
//關閉核心搶佔機制 preempt_disable();
     //是否需要停止當前執行緒關機時才執行?? if (kthread_should_stop()) { __set_current_state(TASK_RUNNING); preempt_enable(); if (ht->cleanup) ht->cleanup(td->cpu, cpu_online(td->cpu)); kfree(td); return 0; } if (kthread_should_park()) { __set_current_state(TASK_RUNNING); preempt_enable(); if (ht->park && td->status == HP_THREAD_ACTIVE) { BUG_ON(td->cpu != smp_processor_id()); ht->park(td->cpu); td->status = HP_THREAD_PARKED; } kthread_parkme(); /* We might have been woken for stop */ continue; } BUG_ON(td->cpu != smp_processor_id()); /* Check for state change setup */ switch (td->status) { case HP_THREAD_NONE: __set_current_state(TASK_RUNNING); preempt_enable(); if (ht->setup) ht->setup(td->cpu); td->status = HP_THREAD_ACTIVE; continue; case HP_THREAD_PARKED: __set_current_state(TASK_RUNNING); preempt_enable(); if (ht->unpark) ht->unpark(td->cpu); td->status = HP_THREAD_ACTIVE; continue; } /* * 就是通過呼叫ksoftirqd_should_run 這是在一開始定義的softirq_threads中指定的,檢查當前CPU上維護的軟體中斷陣列中是否有中斷 * 的置起了從而決定當前的軟體中斷執行緒是否需要執行,不需要執行則放棄時間片 */ if (!ht->thread_should_run(td->cpu)) {
       /*
       *沒有需要的軟體中斷需要執行,則放棄時間片
       */ preempt_enable_no_resched(); schedule(); } else { /* * 有中斷需要執行則直接呼叫 run_ksoftirqd 執行軟體中斷註冊的介面的呼叫 */ __set_current_state(TASK_RUNNING); preempt_enable(); //這個介面在上面初始化時繫結為run_ksoftirqd ht->thread_fn(td->cpu); } } }
可以看到run_ksoftirqd如下:
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_softirq();
        local_irq_enable();
        cond_resched_rcu_qs();
        return;
    }
    local_irq_enable();
}

 關閉本CPU上的硬中斷然後執行__do_softirq();這個是軟體中斷的重點介面如下,註釋了一部分:

asmlinkage __visible void __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;
    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;
    //儲存懸起的軟體中斷的點陣圖
    pending = local_softirq_pending();
    account_irq_enter_time(current);
    //標記進入軟體中斷上下文
    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
    in_hardirq = lockdep_softirq_start();

restart:
    /* Reset the pending bitmask before enabling irqs */
    //清除懸起的軟體中斷的點陣圖
    set_softirq_pending(0);
    //開啟硬體中斷
    local_irq_enable();
    //取軟體中斷的全域性中斷介面連結串列
    h = softirq_vec;
    //判斷是否有懸起的軟體中斷bit,返回地最低置起的bit位置 1開始而不是0,軟中斷也是由優先順序的低bit優先
    while ((softirq_bit = ffs(pending))) {
        unsigned int vec_nr;
        int prev_count;
        //取出對應的中斷物件
        h += softirq_bit - 1;
        //取出對應的中斷index
        vec_nr = h - softirq_vec;
        prev_count = preempt_count();

        kstat_incr_softirqs_this_cpu(vec_nr);

        trace_softirq_entry(vec_nr);
        //執行軟體中斷註冊的介面函式
        h->action(h);
        trace_softirq_exit(vec_nr);
        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);
        }
        //清除剛才處理過的中斷bit並右移動整個點陣圖,然後移動軟體中斷控制代碼
        h++;
        pending >>= softirq_bit;
        //移動後繼續回去處理剩下置起的bit
    }
    //到這裡說明本次進來時置起的bit全部處理完了
    rcu_bh_qs();
    local_irq_disable();
    //再檢查在處理期間有無新置起的軟體中斷,如果有則需要繼續處理軟體中斷
    pending = local_softirq_pending();
    if (pending) {
        /*
        *又有新的軟體標誌置起需要處理,則開始處理,這裡有一個保護機制,因為軟體中斷的優先順序是很高的相對於使用者程序如果軟體中斷
        *源源不斷則需要進行保護避免其他程序無法執行而導致系統實時性差,這裡有三個條件一個步滿足就會會停止本次的軟體中斷的執行
        *而先去執行其他程序排程
        *1、軟中斷處理時間不超過2jiffies,200Hz的系統對應10ms;
        *2、當前沒有有程序需要排程,即!need_resched();
        *3、這種迴圈不超過MAX_SOFTIRQ_RESTART次 一般是10
        */
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)
            goto restart;
        //不滿足其中一個條件則重新喚醒ksoftirq核心執行緒來處理軟中斷,因為這個函式可能在中斷上下文執行所以需要進行限制
        wakeup_softirqd();
    }

    lockdep_softirq_end(in_hardirq);
    account_irq_exit_time(current);
    //使能中斷底半部
    __local_bh_enable(SOFTIRQ_OFFSET);
    WARN_ON_ONCE(in_interrupt());
    tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}

注意軟體中斷的處理過程對軟中斷連續執行的時間進行了限制其實是有原因的,因為上述軟中斷處理部分的程式碼執行機會有可能在中斷上下文irq_exit()具體的呼叫鏈就是irq_exit()->invoke_softirq()->wakeup_softirq()如下(可參考硬中斷的分析過程):

void irq_exit(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
    local_irq_disable();
#else
    WARN_ON_ONCE(!irqs_disabled());
#endif

    account_irq_exit_time(current);
    preempt_count_sub(HARDIRQ_OFFSET);
    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq();

    tick_irq_exit();
    rcu_irq_exit();
    trace_hardirq_exit(); /* must be last! */
}

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 {
        wakeup_softirqd();
    }
}
               

軟中斷優先在 irq_exit() 中執行,如果超過時間等條件轉為 softirqd 執行緒中執行。滿足以下任一條件軟中斷在 softirqd 執行緒中執行:

在 irq_exit()->__do_softirq() 中執行,時間超過 2ms。

在 irq_exit()->__do_softirq() 中執行,輪詢軟中斷超過 10 次。

在 irq_exit()->__do_softirq() 中執行,本執行緒需要被排程。

注:呼叫 raise_softirq() 喚醒軟中斷時,不在中斷環境中。