在 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_ highfrequency threaded job scheduling. For almost all the purposestasklets are more than enough. F.e. all serial device BHs etal. 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_DISABLEDlocal_irq_disable();
#elselockdep_assert_irqs_disabled();
#endifaccount_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() 判断,当前不是处于中断上下文,但是这个时候中断处理程序还没有真正返回,还不是进程上下文,所以此时也是不能睡眠。