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)
函数设置软中断挂起后,软中断将要在某个时机被检查执行:
- 在硬件中断服务程序退出时调用执行。
- 通过唤醒内核软中断守护线程kfofirqd去检查执行。
前面已经说过,raise_softirq
函数如果判断其不在硬件中断服务函数中被调用,那么其会唤醒守护线程执行服务程序,否则在中断退出时执行。 两种方式最终都会调用到 __do_softirq()
函数。
先分析中断退出时的执行流程:
当硬件中断触发时,会调用到do_IRQ
函数进行硬件中断服务程序的回调。在此函数中会执行如下操作:
- 设置进入硬件服务程序准备工作,主要包括设置了标记表示在硬件服务程序中。
- 执行硬件中断服务程序。
- 退出硬件服务程序,清除标记,执行软中断下半部程序。
void __irq_entry do_IRQ(unsigned int irq)
{
irq_enter();//1.
check_stack_overflow();
generic_handle_irq(irq);//2
irq_exit();//3
}
在irq_exit()函数中,主要执行了如下操作:
- 关闭CPU本地中断,可知此时CPU还是不能响应硬件中断的。
local_irq_disable();
- 设置退出硬件中断标志,后续软中断执行时会判断是否在硬件中断服务程序中。
preempt_count_sub(HARDIRQ_OFFSET);//减少中断标致变量表示退出了硬件中断服务程序。
- 判断当前是否在硬件中断服务程序中,并且是否有软中断挂起。
如果在硬件中断服务程序中,就不会执行软中断。 因为软中断作为“底半部”,是不允许在“上半部”中断程序中调用的。
if (!in_interrupt() && local_softirq_pending())
invoke_softirq(); //如果软中断挂起,并且没有在中断服务程序中,调用执行中断服务程序。
-
如果软中断挂起,并且没有在硬件中断服务程序中,调用
invoke_softirq
函数执行中断服务程序。
如果系统配置了“中断线程化”,那么软中断也会线程化执行,唤醒守护线程去执行__do_softirq。
否则直接调用执行__do_softirq。 -
只有执行到 __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)
- 当调用irq_exit退出硬件中断服务程序时,会减少计数。
preempt_count_sub(HARDIRQ_OFFSET);
- 通过计数就可以检测是否在某种中断状态下了。
/*
* 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))