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

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

        static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

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

        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上,这就使得用户无需考虑函数重入问题,减轻了程序员的负担。

 

    

        

            

转载于:https://my.oschina.net/yepanl/blog/3049157

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值