【Linux中断】中断下半部-软中断softirq的原理与使用

    软中断

    软中断是中断下半部的典型处理机制,是随着SMP的出现应运而生的,也是tasklet实现的基础,软中断的出现是为了满足中断上半部和下半部的区别,使得对时间不敏感的任务延后执行,而且可以在多个CPU上并行执行,使得总的系统效率可以更高。

    软中断有以下特性:

    产生后并不是马上可以执行,必须要等待内核的调度才能执行。软中断不能被自己打断(单个cpu上软中断不能嵌套执行),只能被硬件中断打断(上半部)

    可以并发运行在多个cpu上。所以软中断必须要设计为可重入的函数(允许多个CPU同时操作)。

    软中断数据结构

    软中断描述符

    sturct softirq_action

    {

    void *(action)(struct softirq_action *);

    };

    描述每一种类型的软中断,void (*action)是软中断触发时的执行函数。

    软中断全局数据和支持的枚举类型

    static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

    enum

    {

    HI_SOFTIRQ=0, // 高优先级的tasklet

    TIMER_SOFTIRQ, // 用于定时器的下半部

    NET_TX_SOFTIRQ, // 用于网络层发数据包

    NET_RX_SOFTIRQ, // 用于网络层收数据包

    BLOCK_SOFTIRQ,

    IRQ_POLL_SOFTIRQ,

    TASKLET_SOFTIRQ, // 低优先集的tasKlet

    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

    };

    软中断的API接口

    注册软中断

    void open_softirq(int nr, void (*action)(struct softirq_action *));

    注册对应类型的处理函数到全局数组softirq_vec中。 网络法宝的对应类型是NET_TX_SOFTIRQ的处理函数net_tx_action。

    触发软中断

    void raise_softirq(unsigned int nr);

    软中断类型nr作为偏移量置位每个CPU变量irq_stat[cpu_id]的成员变量_softirq_pending,这是同一类型的软中断可以在多个cpu核心上并行运行的根本原因。

    软中断执行函数

    do_softirq() -> __do_softirq()

    在执行软中断处理函数__do_softirq()前需要满足两个条件:

    (1)不在中断中(包括硬中断,软中断和NMI)

    (2)有软中断处于pending状态

    这样设计的原因是为了避免软件中断在中断嵌套中调用,达到在单个CPU上软件中断不能被重入的目的。

    ARM架构的CPU不存在中断嵌套中调用软件中断的问题,因为ARM架构的CPU在处理硬件中断的过程中关闭掉中断,在进入软中断处理过程之后才会重新开启硬件中断,如果在软件中断处理过程中有硬件中断嵌套,也不会再次调用软中断,因为硬中断是软中断处理过程中再次进入的。

    软中断实现原理

    中断处理过程图

    软中断的调度时机

    1.do_irq完成I/O中断时调用irq_exit

    2.系统使用I/O APIC,在处理玩本地时钟中断时

    3.local_bh_enable,开启本地软中断时

    4.SMP系统中,cpu处理完备CALL_FUNCTION_VECTOR处理器间中断所触发的函数时。

    5.ksoftirqd/n线程被唤醒时

    软中断处理流程

    asmlinkage __visible void __softirq_entry __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 as the 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 swapping.

    */

    current->flags &= ~PF_MEMALLOC;

    // 获取当前有哪些等待的软中断

    pending = local_softirq_pending();

    account_irq_enter_time(current);

    // 关闭软中断,实质是设置正在处理软件的中断标记,在同一个CPU上使得不能重入_do_softirq函数

    __local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);

    in_hardirq = lockdep_softirq_start();

    restart:

    /* Reset the pending bitmask before enabling irqs */

    // 重新设置软中断标志为0,这样在重新烤漆中断后硬件中断中又可以设置软件中断位

    set_softirq_pending(0);

    // 开启硬件中断

    local_irq_enable();

    h = softirq_vec;

    // 遍历pending标志的每一位,如果这一位设置就会调用软件中断的处理函数

    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;

    }

    if (__this_cpu_read(ksoftirqd) == current)

    rcu_softirq_qs();

    // 关闭硬件中断

    local_irq_disable();

    // 查看是否有软件中断处于pending状态

    pending = local_softirq_pending();

    if (pending) {

    if (time_before(jiffies, end) && !need_resched() &&

    --max_restart)

    goto restart;

    // 如累积重复进入软件中断处理的次数超过max_restart=10次,就唤醒内核进程来处理软件中断

    wakeup_softirqd();

    }

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

    }

    软中断内核线程

    触发软件中断的位置是在中断上下文,而软中断的内核线程就已经进入到进程的上下文。软中断的上下文是系统为每个CPU建立的ksoftirqd进程。软中断的内核进程中主要有两个大循环,外层的循环处理有软件中断就处理,没有软件中断就休眠。内层的循环处理软件中断,每循环一次都试探一次是否过长时间占据了CPU,需要调度就是放CPU给其他进程。

    set_current_state(TASK_INTERRUPTIBLE);

    //外层大循环。

    while (!kthread_should_stop()) {

    preempt_disable();//禁止内核抢占,自己掌握cpu

    if (!local_softirq_pending()) {

    preempt_enable_no_resched();

    //如果没有软中断在pending中就让出cpu

    schedule();

    //调度之后重新掌握cpu

    preempt_disable();

    }

    __set_current_state(TASK_RUNNING);

    while (local_softirq_pending()) {

    /* Preempt disable stops cpu going offline.

    If already offline, we'll be on wrong CPU:

    don't process */

    if (cpu_is_offline((long)__bind_cpu))

    goto wait_to_die;

    //有软中断则开始软中断调度

    do_softirq();

    //查看是否需要调度,避免一直占用cpu

    preempt_enable_no_resched();

    cond_resched();

    preempt_disable();

    rcu_sched_qs((long)__bind_cpu);

    }

    preempt_enable();

    set_current_state(TASK_INTERRUPTIBLE);

    }

    __set_current_state(TASK_RUNNING);

    return 0;

    wait_to_die:

    preempt_enable();

    /* Wait for kthread_stop */

    set_current_state(TASK_INTERRUPTIBLE);

    while (!kthread_should_stop()) {

    schedule();

    set_current_state(TASK_INTERRUPTIBLE);

    }

    __set_current_state(TASK_RUNNING);

    return 0;

    参考文章:

    https://zhuanlan.zhihu.com/p/265705850