咱们知道,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
};socket
struct softirq_action
{
void (*action)(struct softirq_action *);
};函数
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;性能
从上面的定义能够看出:内核预约义了一组软中断类型,每种类型的软中断的数据结构都是一个 softirq_action,一个softirq_action就是一个回调函数。那么这个数组是如何被调度执行的?this
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上,这就使得用户无需考虑函数重入问题,减轻了程序员的负担。