Linux 中断原理之软中断

linux软中断实现原理

原创文章,转载请标明出处。

什么是软中断

软中断,顾名思义软件触发的中断。但这个解释又很容易被误解为"通过软件指令触发的(硬)中断"。其实这里说的软中断只是实现硬件中断处理程序下半部的方法之一。(其他两种实现方法是tasklet 和工作队列, 其中tasklet基于软中断)。
作为中断的下半部处理程序,其本质就在于软中断程序运行的时系统可以继续响应硬件中断。
软中断一般会在硬件中断处理程序(上半部)退出时开始执行, 一个软中断不会抢占另外一个软中断,唯一可以抢占软中断的是硬件中断处理程序。

软中断的实现原理

1. 注册软中断

我们都知道硬件中断有中断向量表,其实软中断也采用了类似的概念。
内核静态定义了一个结构体数组,其实就类似中断向量表,数组的每一个元素都指向一个软中断处理函数。 代表一种类型的软中断,数组元素下标决定了各种软中断的优先级。

static struct softirq_action softirq_vec[NR_SOFTIRQS];

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

内核中已经默认注册了如下的软中断,通常情况下并不需要驱动注册自己的软中断,因为大多数情况下内核提供的其他下半部机制已经足够满足要求。
当然如果要向内核注册自己的软中断,只需要在enum类型中增添一个自定义的索引,也就是向数组中增加一个元素。

enum
{
    HI_SOFTIRQ=0,//最高优先级别软中断
    TIMER_SOFTIRQ,//用于定时器软中断
    NET_TX_SOFTIRQ,//网络收发包
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,//块设备
    BLOCK_IOPOLL_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
};

2. 绑定处理程序

通过函数 open_softirq(TIMER_SOFTIRQ, run_timer_softirq);注册软中断的服务函数,可以看到其实就是为静态数组中的函数指针赋值>。

void open_softirq(int nr, void (*action)(struct softirq_action *))
{
    softirq_vec[nr].action = action;
}

3. 触发软中断

注册了处理程序后,就可以触发软中断了。
如下代码所示:内核针对每个CPU都定义了一个变量__softirq_pending, 变量的每一bit用于保存一类软中断的挂起状态。
变量为unsigned int 型,所以每个核支持注册的最大软中断数量为32个。

typedef struct {
    unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;

irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned;
EXPORT_SYMBOL(irq_stat);

所谓触发就是在变量__softirq_pending中设置软中断的挂起状态(把对应的bit为置1),待执行时机到来时会查询挂起的中断进行执行。
通过调用void raise_softirq(unsigned int nr)函数,触发中断,参数指定了索引号.

其实这也是“软中断”名称的由来,软件触发的中断么。

如下代码所示:
raise_softirq 函数除了设置相应软中断挂起状态外,还会尝试触发软中断函数执行:
首先判断函数是否是在硬件中断服务程序中被调用的。如果是,那么直接退出,等待中断服务程序退出时,会执行软中断。
如果不是,那么会唤醒守护线程去执行软中断服务程序。

void raise_softirq(unsigned int nr)
{
    unsigned long flags;
    local_irq_save(flags);
    raise_softirq_irqoff(nr);
    local_irq_restore(flags);
}
inline void raise_softirq_irqoff(unsigned int nr)
{
    __raise_softirq_irqoff(nr); //设置软中断标志位,表示中断挂起待处理 。

    /*
     * If we're in an interrupt or softirq, we're done
     * (this also catches softirq-disabled code). We will
     * actually run the softirq once we return from
     * the irq or softirq.
     *
     * Otherwise we wake up ksoftirqd to make sure we
     * schedule the softirq soon.
     */
    if (!in_interrupt()) //判断是否在中断函数中(包括硬中断以及软中断),如果在直接退出等到中断退出时会执行。如果不在,唤醒>守护线程去执行。
        wakeup_softirqd();
}

软中断的执行

当调用 void raise_softirq(unsigned int nr) 函数设置软中断挂起后,软中断将要在某个时机被检查执行:

  1. 在硬件中断服务程序退出时调用执行。
  2. 通过唤醒内核软中断守护线程kfofirqd去检查执行。

前面已经说过,raise_softirq函数如果判断其不在硬件中断服务函数中被调用,那么其会唤醒守护线程执行服务程序,否则在中断退出时执行。 两种方式最终都会调用到 __do_softirq()函数。

先分析中断退出时的执行流程:
当硬件中断触发时,会调用到do_IRQ函数进行硬件中断服务程序的回调。在此函数中会执行如下操作:

  1. 设置进入硬件服务程序准备工作,主要包括设置了标记表示在硬件服务程序中。
  2. 执行硬件中断服务程序。
  3. 退出硬件服务程序,清除标记,执行软中断下半部程序。
void __irq_entry do_IRQ(unsigned int irq)
{
    irq_enter();//1.
    check_stack_overflow();
    generic_handle_irq(irq);//2
    irq_exit();//3
}

在irq_exit()函数中,主要执行了如下操作:

  1. 关闭CPU本地中断,可知此时CPU还是不能响应硬件中断的。
    local_irq_disable();
  1. 设置退出硬件中断标志,后续软中断执行时会判断是否在硬件中断服务程序中。
    preempt_count_sub(HARDIRQ_OFFSET);//减少中断标致变量表示退出了硬件中断服务程序。
  1. 判断当前是否在硬件中断服务程序中,并且是否有软中断挂起。
    如果在硬件中断服务程序中,就不会执行软中断。 因为软中断作为“底半部”,是不允许在“上半部”中断程序中调用的。
    if (!in_interrupt() && local_softirq_pending())
        invoke_softirq(); //如果软中断挂起,并且没有在中断服务程序中,调用执行中断服务程序。
  1. 如果软中断挂起,并且没有在硬件中断服务程序中,调用invoke_softirq函数执行中断服务程序。
    如果系统配置了“中断线程化”,那么软中断也会线程化执行,唤醒守护线程去执行__do_softirq。
    否则直接调用执行__do_softirq。

  2. 只有执行到 __do_softirq 函数,才算真正开始执行软中断,最主要一点是,在此函数中打开了之前被关闭的CPU本地中断,使CPU可以继续响应系统硬件中断。 此程序中开始查询CPU软中断挂起状态,判断执行哪些软中断,调用软中断回调函数。
    在遍历执行软中断函数时可能会有新的软中断触发,所以遍历完一遍后会判断是否有新的软中断挂起,如果有会重新遍历。
    但是__do_softirq函数中设置了执行时间和重新遍历次数限制,如果超出了执行时间和遍历次数后依然有挂起的软中断,那么其不会继续遍历。而是唤醒软中断守护线程softirqd去执行__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(); //先把当前cpu的所有软中断挂起状态保持到变量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;

    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);//统计本CPU软中断相应的次数。

        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)//如果检测发现有新的软中断挂起,并且没有超过最大执行时间以及最大重复次数,那么就重新检测执行软中
断。
            goto restart;

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

最后还有一个概念需要说明以下:即如何判断当前是否在中断函数服务程序中。
其实就是通过定义一个preempt_count计数来表示的。

1. 当进入中断函数中时,调用irq_enter, 会增加计数。
```c
#define __irq_enter()                   \
   do {                        \
       account_irq_enter_time(current);    \
       preempt_count_add(HARDIRQ_OFFSET);  \
       trace_hardirq_enter();          \
   } while (0)
  1. 当调用irq_exit退出硬件中断服务程序时,会减少计数。
    preempt_count_sub(HARDIRQ_OFFSET);
  1. 通过计数就可以检测是否在某种中断状态下了。
/*
 * Are we doing bottom half or hardware interrupt processing?
 * Are we in a softirq context? Interrupt context?
 * in_softirq - Are we currently processing softirq or have bh disabled?
 * in_serving_softirq - Are we currently processing softirq?
 */
#define in_irq()        (hardirq_count())
#define in_softirq()        (softirq_count())
#define in_interrupt()      (irq_count())
#define in_serving_softirq()    (softirq_count() & SOFTIRQ_OFFSET)

 static __always_inline int preempt_count(void)
{
        return current_thread_info()->preempt_count;
}

#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
                 | NMI_MASK))
#define softirq_count() (preempt_count() & SOFTIRQ_MASK)
#define irq_count() (preempt_count() & (HARDIRQ_MASK | SOFTIRQ_MASK \
                 | NMI_MASK))

你可能感兴趣的:(linux驱动,中断,驱动开发,网络,linux,驱动程序)