在 linux 中,任务执行的载体有很多,包括线程,中断,软中断,tasklet,定时器等。但是从本质上来划分的话,任务执行的载体只有两个:线程和中断。软中断和 tasklet 的执行可能在中断中,也可能在线程中,定时器的执行可能在中断、软中断或者线程中。
在讨论中断和软中断的时候,经常以网卡收包为例子来理解。如下是网卡收包的主要环节,分别说明每个环节:
① 网卡从链路上收包,网卡的作用是成帧,网卡在链路上收到的是字节流,根据前导码,帧开始界定符,帧间隙来界定一个帧,这就是成帧。
② 网卡界定一帧报文之后,将这一帧报文通过 dma 保存到内存中。
③ 网卡触发中断,收包中断有对应的处理程序,对于现在的大多网卡来说,在中断处理程序中做的事情是触发收包软中断。
④ 收包的主要工作是在软中断中处理,而不是在硬中断中处理。
中断是硬件和软件进行通信的一种方式。中断具有异步,响应快的特点。在 linux 中,中断可以打断当前正在运行的程序,并且在处理一个中断的时候,往往需要关闭本地中断。
在网络收包的过程中,从网卡驱动,到链路层代码,再到 ip 层,tcp 层,报文的处理过程比较长,所需要花费的时间往往是比较长的。如果在中断处理程序中把收包的工作都做了,那么很可能会导致本地关中断时间太长,进而影响系统的响应。所以在网卡收包时,往往采用中断和软中断结合的方式,在中断处理程序中做的工作比较简单,只是触发一个软中断,后边的处理过程在软中断中进行。中断处理程序中触发软中断之后立即返回,此时,就可以打开本地中断了。
软中断处理函数是 __do_softirq(),从这个函数中也可以看出来,在处理软中断之前调用函数 local_irq_enable(),也就说在处理软中断的时候,是开中断的。
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
...
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);
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();
...
}
1 软中断种类
如下是软中断的种类,当前定义了 10 种软中断,其中 NET_TX_SOFTIRQ 和 NET_RX_SOFTIRQ 用于网卡发包和收包。从定义可以看出来,软中断的种类是静态的,确定的,不能动态改变。从注释可以看出来,不到万不得已,不要新增软中断,大部分情况下使用 tasklet 已经足够了。
/* PLEASE, avoid to allocate new softirqs, if you need not _really_ high
frequency threaded job scheduling. For almost all the purposes
tasklets are more than enough. F.e. all serial device BHs et
al. should be converted to tasklets, not to softirqs.
*/
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
2 开启软中断和触发软中断
以网卡软中断为例来说明。
通过函数 open_softirq() 来打开软中断,也叫注册这个软中断,在内核初始化的时候会调用函数 net_dev_init() 完成网络相关的初始化,在该函数中打开了网络收发包软中断。
static int __init net_dev_init(void)
{
...
open_softirq(NET_TX_SOFTIRQ, net_tx_action);
open_softirq(NET_RX_SOFTIRQ, net_rx_action);
...
}
软中断使用一个数组来维护,数组是 softirq_vec,数组的下标是软中断的中断号,数组的元素struct softirq_action 类型,这个结构体中只有一个成员,就是软中断的处理函数。网卡发包和收包软中断的处理函数分别是 net_tx_action 和 net_rx_action。
struct softirq_action
{
void (*action)(struct softirq_action *);
};
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
在网卡中断处理程序中,一般会调用函数 __raise_softirq_irqoff() 触发软中断。触发软中断就是在一个全局的变量中设置一个标志,标志有软中断了。在 linux 中,这个全局变量是 per cpu 的。
void __raise_softirq_irqoff(unsigned int nr)
{
lockdep_assert_irqs_disabled();
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}
3 处理软中断
3.1 中断返回时
中断返回时,最终会调用到函数 __irq_exit_rcu(),在这个函数中会进行判断,如果当前不是处于中断上下文并且有软中断需要处理,那么就会调用 invoke_softirq() 来处理软中断。
static inline void __irq_exit_rcu(void)
{
#ifndef __ARCH_IRQ_EXIT_IRQS_DISABLED
local_irq_disable();
#else
lockdep_assert_irqs_disabled();
#endif
account_irq_exit_time(current);
preempt_count_sub(HARDIRQ_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
tick_irq_exit();
}
在函数 invoke_softirq() 中进行判断,如果软中断处理线程当前正在运行,那么直接返回。如果 force_irqthreads 是 false 的话,那么就调用 __do_softirq() 或者 do_softirq_onwn_stack() 处理软中断;如果强制让软中断处理线程来处理软中断,那么意思是在中断返回的时候不处理软中断,会将线程唤醒。
static inline void invoke_softirq(void)
{
if (ksoftirqd_running(local_softirq_pending()))
return;
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();
}
}
3.2 ksoftirqd
除了在中断返回的时候处理软中断,还有专门的线程来处理。在内核中,每个 cpu 核上都会创建一个 ksoftirqd 线程用来处理这个核上的软中断。
ksoftirqd 的信息维护在 softirq_threads 中。
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/%u",
};
在 ksoftirqd 中,最终也是调用函数 __do_softirq() 来处理软中断。
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_softirq();
local_irq_enable();
cond_resched();
return;
}
local_irq_enable();
}
什么时候在中断返回时处理软中断 ?
中断返回时会进行判断,如果当前不是处于中断上下文,并且有软中断需要处理,并且这个时候 ksofrirqd 没有在运行,那么这个时候就会处理软中断。
什么时候在 ksoftirqd 中处理软中断 ?
① 中断返回的时候, 判断 force_irqthreads 为 true,那么只能在 ksoftirqd 中处理软中断,这个时候会通过 wakeup_softirqd() 来唤醒 ksoftirqd。
② 使用 raise_softirq_irqoff() 触发软中断时,如果当前不是处于中断上下文,也会唤醒 ksoftirqd
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();
}
4 软中断处理时为什么不能睡眠
软中断的处理,可能在中断返回的时候,也可能在 ksoftirq 线程中。在中断返回的时候,虽然通过 in_interrupt() 判断,当前不是处于中断上下文,但是这个时候中断处理程序还没有真正返回,还不是进程上下文,所以此时也是不能睡眠。