Kernel 软中断Softirq

    咱们知道,Linux中断的上半部用于处理很是紧急的任务,而延时处理的任务一般须要放到中断的下半部去处理。软中断(Softirq)是Linux内核中断下半部的一部分,是中断下半部tasklet的组成基础。设计软中断,是为了尽快释放中断上半部,使得软中断中处理的耗时任务不去阻塞中断上半部的执行,从而提高系统的响应。程序员

    那么,软中断是如何设计的?又是如何被调度执行的?如何实现新的软中断?软中断的使用须要注意些什么?清楚了这些逻辑,咱们也就清楚了软中断的框架结构。数组

    1,软中断是如何设计的?数据结构

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
                numbering. Sigh! */
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */框架

    NR_SOFTIRQS
};socket

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

        static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;性能

        从上面的定义能够看出:内核预约义了一组软中断类型,每种类型的软中断的数据结构都是一个 softirq_action,一个softirq_action就是一个回调函数。那么这个数组是如何被调度执行的?this

        2,软中断如何被调度执行?线程

            咱们知道,中断上半部执行完以后(好比,从网卡接收了数据包存放在内存某处),后续的耗时任务(如,对数据包的解析,本地递交或转发等)须要调度软中断来处理,那么内核是如何调度软中断来处理的呢?咱们来看其中一个路径,咱们知道中断上半部执行完以后会调用 irq_exit()函数:设计

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! */
}
    从上面的调用路径,咱们可以看出来:当内核路径从中断上下文退出,而且有软中断任务等待处理时,内核主动调用 invoke_softirq(),调度软中断处理。in_interrupt() 判断是否退出全部嵌套的中断上下文。local_softirq_pending()用于判断是否有软中断任务待处理:

        #define local_softirq_pending()    this_cpu_read(irq_stat.__softirq_pending)

    中断上半部在处理完后,若是须要在软中断中做后续处理,经过set_softirq_pending(x)设置便可:

        #define set_softirq_pending(x)    \
        this_cpu_write(irq_stat.__softirq_pending, (x))

    再看 invoke_softirq():

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();    // 软中断线程中执行软中断任务
    }
}

    以上代码能够看出:软中断任务能够在两个环境下执行:一种是软中断上下文,另一种是在软中断内核线程中执行。二者的区别是,软中断上下文中不能睡眠,不能被调度;软中断内核线程能够睡眠,能够被调度。软中断被设计在两种上下文中执行,是Linux内核对系统运行性能策略的折衷。优先在软中断上下文执行必定的时间,若是任务还未完成,再唤醒软中断内核线程调度软中断任务执行。这样在保证任务实时性的同时,也不至于系统被软中断任务挂死。咱们分析软中断上下文的处理过程:

asmlinkage __visible void __softirq_entry __do_softirq(void)
{
    unsigned long end = jiffies + MAX_SOFTIRQ_TIME;    // 最大处理时间:2毫秒
    unsigned long old_flags = current->flags;
    int max_restart = MAX_SOFTIRQ_RESTART;    // 最大循环次数:10次
    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();    // 获取本地CPU上等待处理的软中断掩码
    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);    // 清除本地CPU上等待处理的软中断掩码

    local_irq_enable();    // 开中断状态下处理软中断

    h = softirq_vec;    // h指向软中断处理函数数组首元素

    while ((softirq_bit = ffs(pending))) {    // 依次处理软中断,软中断编号越小,越优先处理,优先级越高
        unsigned int vec_nr;
        int prev_count;

        h += softirq_bit - 1;

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

        // 循环下一个等待处理的软中断
        h++;
        pending >>= softirq_bit;

    }

    rcu_bh_qs();
    local_irq_disable();    // 关中断,判断在处理上次软中断期间,硬中断处理函数是否又调度了软中断

    pending = local_softirq_pending();
    if (pending) {    // 软中断再次被调度
        if (time_before(jiffies, end) && !need_resched() &&
            --max_restart)    // 没有达到超时时间,也不须要被调度,而且调度次数也没有超过10次

            goto restart;    // 从新执行软中断

        wakeup_softirqd();    // 不然唤醒软中断内核线程处理剩下的软中断,当前CPU退出软中断上下文
    }

    lockdep_softirq_end(in_hardirq);
    account_irq_exit_time(current);
    __local_bh_enable(SOFTIRQ_OFFSET);
    WARN_ON_ONCE(in_interrupt());
    current_restore_flags(old_flags, PF_MEMALLOC);
}

    这段代码展现了软中断上下文的处理策略:一次处理全部等待的软中断的,循环处理最多10次,而且最大处理时间为2ms。不管是循环超过10次,仍是总处理时间超过2ms,CPU都要退出软中断上下文,调度软中断内核线程处理软中断,给其余进程/线程运行机会,避免系统响应过慢。

        3,如何加入新的软中断?

        咱们知道,内核预约义了好几种软中断:

enum
{
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    IRQ_POLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
                numbering. Sigh! */
    RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
};

        内核提供了open_softirq()接口供各相关模块注册相应的软中断处理函数:

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;    // 注册软中断处理函数对应相应的软中断类型
}

        4,软中断的使用有哪些注意事项?

    (1)软中断是内核静态编译的,添加新的软中断,须要修改内核软中断编号枚举结构。

    (2)同一类型的软中断能够在不一样CPU上同时运行(由于软中断的掩码是per-cpu变量),同一类型的软中断处理函数只有一个。所以,软中断的处理函数必须是可重入的,须要程序员保证资源的互斥访问,这无疑增长了用户的负担,从而使得使用软中断的复杂度变高。后续咱们将看到,tasklet机制的出现将会有效的解决这个问题:不一样类型的tasklet能够同时运行在不一样CPU上,但同一类型的tasklet同时只能运行在一个CPU上,这就使得用户无需考虑函数重入问题,减轻了程序员的负担。

相关文章
相关标签/搜索