linux把处理硬件中断的过程分为两部分。上半部简单快速,执行时禁止部分或全部中断。下半部稍后执行,并且执行期间可以响应所有的中断。这样的设计会使系统处于中断屏蔽的状态尽可能的短,从而提高系统的响应能力。
下半部的处理方式主要有soft_irq,tasklet,workqueue三种,他们在使用方式和适用情况上各有不同。soft_irq用在对底半执行时间要求比较紧急或者非常重要的场合,在中断上下文执行。tasklet和work queue在普通的driver里用的相对较多,主要区别是tasklet是在中断上下文执行,而workqueue是在process上下文,因此可以执行可能sleep的操作。
softirq和tasklet都运行在中断上下文,那么他们有什么区别呢?
1.softirq是在编译期间静态分配的,它不像tasklet那样可以动态的分配和删除。
每个软中断在内核中以softirq_action表示。在kernel/softirq.c中定义了一个包含有32个该结构体的数组。每种软中断对应数组的一项,所以软中断最多有32项。
内核目前实现了10中软中断,定义在linux/interrupt.h中。
enum
{
HI_SOFTIRQ=0, /* 高优先级tasklet */ /* 优先级最高 */
TIMER_SOFTIRQ, /* 时钟相关的软中断 */
NET_TX_SOFTIRQ, /* 将数据包传送到网卡 */
NET_RX_SOFTIRQ, /* 从网卡接收数据包 */
BLOCK_SOFTIRQ, /* 块设备的软中断 */
BLOCK_IOPOLL_SOFTIRQ, /* 支持IO轮询的块设备软中断 */
TASKLET_SOFTIRQ, /* 常规tasklet */
SCHED_SOFTIRQ, /* 调度程序软中断 */
HRTIMER_SOFTIRQ, /* 高精度计时器软中断 */
RCU_SOFTIRQ, /* RCU锁软中断,该软中断总是最后一个软中断 */
NR_SOFTIRQS /* 软中断数,为10 */
};
2.同一类型的softirq可以在不同的cpu上并发执行,而tasklet在使用时不需要考虑重入,因此tasklet更佳易用,使用softirq更倾向于性能。
通过调用open_softirq接口函数,将action函数指针指向向该软中断应该执行的函数。
/* 开启软中断 */
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
在start_kernel()进行系统初始化中,就调用了softirq_init()函数对HI_SOFTIRQ和TASKLET_SOFTIRQ两个软中断进行了初始化
void __init softirq_init(void)
{
int cpu;
for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}
/* 开启常规tasklet */
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
/* 开启高优先级tasklet */
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
当使用open_softirq设置好某个软中断的action指针后,该软中断就会开始可以使用了。
调用raise_softirq这个接口函数来触发本地CPU上的softirq。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
先是关闭本地cpu中断,然后调用:raise_softirq_irqoff。
inline void raise_softirq_irqoff(unsigned int nr)
{
__raise_softirq_irqoff(nr);
......
if (!in_interrupt())
wakeup_softirqd();
}
通过in_interrupt判断现在是否在中断上下文中,或者软中断是否被禁止,如果都不成立,否则,我们必须要调用wakeup_softirqd函数用来唤醒本CPU上的softirqd这个内核线程。
1.在中断返回现场时候调度softirq
在中断处理程序中触发软中断是最常见的形式,一个硬件中断处理完成之后。下面的函数在处理完硬件中断之后退出中断处理函数,在irq_exit中会触发软件中断的处理。
中断处理模型:
fastcall unsigned int do_IRQ(struct pt_regs *regs)
{
...
irq_enter();
//handle external interrupt (ISR)
...
irq_exit();
return 1;
}
硬中断执行完毕后
void irq_exit(void)
{
……
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
……
}
invoke_irq :
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 {
/* 如果强制使用软中断线程进行软中断处理,会通知调度器唤醒软中断线程ksoftirqd */
wakeup_softirqd();
}
}
可以看出,如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用
最终调用do_softirq:
asmlinkage __visible void __do_softirq(void)
{
/* 为了防止软中断执行时间太长,设置了一个软中断结束时间 */
unsigned long end = jiffies + MAX_SOFTIRQ_TIME;
/* 保存当前进程的标志 */
unsigned long old_flags = current->flags;
/* 软中断循环执行次数: 10次 */
int max_restart = MAX_SOFTIRQ_RESTART;
/* 软中断的action指针 */
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;
/* 获取此CPU的__softirq_pengding变量值 */
pending = local_softirq_pending();
/* 用于统计进程被软中断使用时间 */
account_irq_enter_time(current);
/* 增加preempt_count软中断计数器,也表明禁止了调度 */
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
/* 循环10次的入口,每次循环都会把所有挂起需要执行的软中断执行一遍 */
restart:
/* 该CPU的__softirq_pending清零,当前的__softirq_pending保存在pending变量中 */
/* 这样做就保证了新的软中断会在下次循环中执行 */
set_softirq_pending(0);
/* 开中断 */
local_irq_enable();
/* h指向软中断数组头 */
h = softirq_vec;
/* 每次获取最高优先级的已挂起软中断 */
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;
/* 获取此软中断描述符地址 */
h += softirq_bit - 1;
/* 减去软中断描述符数组首地址,获得软中断号 */
vec_nr = h - softirq_vec;
/* 获取preempt_count的值 */
prev_count = preempt_count();
/* 增加统计中该软中断发生次数 */
kstat_incr_softirqs_this_cpu(vec_nr);
trace_softirq_entry(vec_nr);
/* 执行该软中断的action操作 */
h->action(h);
trace_softirq_exit(vec_nr);
/* 之前保存的preempt_count并不等于当前的preempt_count的情况处理,也是简单的把之前的复制到当前的preempt_count上,这样做是防止最后软中断计数不为0导致系统不能够执行调度 */
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指向下一个软中断,但下个软中断并不一定需要执行,这里只是配合softirq_bit做到一个处理 */
h++;
pending >>= softirq_bit;
}
rcu_bh_qs();
/* 关中断 */
local_irq_disable();
/* 循环结束后再次获取CPU的__softirq_pending变量,为了检查是否还有软中断未执行 */
pending = local_softirq_pending();
/* 还有软中断需要执行 */
if (pending) {
/* 在还有软中断需要执行的情况下,如果时间片没有执行完,并且循环次数也没到10次,继续执行软中断 */
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
/* 这里是有软中断挂起,但是软中断时间和循环次数已经用完,通知调度器唤醒软中断线程去执行挂起的软中断,软中断线程是ksoftirqd,这里只起到一个通知作用,因为在中断上下文中是禁止调度的 */
wakeup_softirqd();
}
lockdep_softirq_end(in_hardirq);
/* 用于统计进程被软中断使用时间 */
account_irq_exit_time(current);
/* 减少preempt_count中的软中断计数器 */
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
/* 还原进程标志 */
tsk_restore_flags(current, old_flags, PF_MEMALLOC);
}
• 首先取出pending的状态;
• 禁止软中断,主要是为了防止和软中断守护进程发生竞争;
• 清除所有的软中断待决标志;
• 打开本地cpu中断;
• 循环执行待决软中断的回调函数;
• 如果循环完毕,发现新的软中断被触发,则重新启动循环,直到以下条件满足,才退出:
o 没有新的软中断等待执行;
o 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART,目前的设定值时10次;
• 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完,则激活守护进程,处理剩下的软中断;
• 退出前恢复软中断;
2.在守护线程ksoftirq中执行。
虽然大部分的softirq是在中断退出的情况下执行,但是有几种情况会在ksoftirq中执行。
a.从上文看出,raise softirq主动触发,而此时正好不是在中断上下文中,ksoftirq进程将被唤醒。
b.在irq_exit中执行软中断,但是在经过MAX_SOFTIRQ_RESTART次循环后,软中断还未处理完,这种情况,ksoftirq进程也会被唤醒。
所以加入守护线程这一机制,主要是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。
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_sof tirq();
local_irq_enable();
cond_resched_rcu_qs();
return;
}
守护进程最终也会调用__do_softirq执行软中断的回调。