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中会触发软件中断的处理。
中断处理模型:
linux 内核笔记--中断子系统之softirq_第1张图片

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执行软中断的回调。

你可能感兴趣的:(中断子系统)