softirq
linux内核将中断分为上下两部分,在中断发生后马上就需要执行的那部分工作属于中断上半部,比如驱动程序通过request_irq
注册的中断处理函数,而稍微延后执行的那部分工作就属于中断下半部,比如softirq
、tasklet
、workqueue
等。在中断上半部执行期间,中断通常是被禁用的,为了避免中断被长时间禁用,上半部的代码通常只完成一些必要的处理,之后便会快速的退出。而在中断下半部执行期间,中断通常是开启的,CPU可以响应中断,所以,下半部通常用于处理那些量大又耗时的工作。接下来的篇幅中将简单分析softirq的工作过程。
中断发生后,内核会先保存中断现场,然后再执行设备驱动注册的中断处理函数,如果有必要,中断处理函数可以唤醒softirq
。在中断退出的过程中,内核会检查是否有softirq
需要执行,如果有,内核就会去执行softirq,直到softirq执行完成或者softirq处理时间超时后,内核才会继续恢复中断现场。大体的过程如下:
产生中断,硬件关闭中断保存中断现场执行中断处理函数有耗时任务,唤醒softirq退出中断处理函数,检查softirq开启中断执行softirqsoftirq执行时间过长,唤醒softirqd线程关闭中断恢复中断现场
中断被开启
softirq类型
内核最多支持32种softirq
,目前已有的softirq
如下:
// linux_5.10.97/include/linux/interrupt.h
enum
{HI_SOFTIRQ=0, // 高优先级的taskletTIMER_SOFTIRQ, // 定时器NET_TX_SOFTIRQ, // 网卡发包NET_RX_SOFTIRQ, // 网卡收包BLOCK_SOFTIRQ,IRQ_POLL_SOFTIRQ, // irq_pollTASKLET_SOFTIRQ, // 普通优先级的taskletSCHED_SOFTIRQ, // 处理调度相关的事务HRTIMER_SOFTIRQ, // 高精度定时器RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */NR_SOFTIRQS
};
上面那些softirq
已经被内核的各个模块使用了,通常我们自己编写驱动都是借助那些模块提供的接口来间接的使用softirq
。比如我们的网卡驱动想要通过softirq
的机制来收包,就需要向napi模块注册一个回调用于收包,并在网卡接收中断处理函数中调用napi提供的接口来唤醒NET_RX_SOFTIRQ
。当NET_RX_SOFTIRQ
这个类型的softirq
被执行时,napi模块便会调用网卡驱动注册的回调。
如果我们自己的模块需要使用自定义的softirq
,那么就需要新增一个类型的softirq
(新增一个枚举值),此外,还需编写一个softirq
处理函数并在模块初始化时将其注册到内核。
需要注意的是,枚举值越小的softirq
优先级越高,多种类型的softirq
被唤醒时,优先级高的先被处理。
注册softirq
每种softirq
都可以注册一个处理函数,内核在初始化阶段,各个使用softirq
的模块便会向内核注册softirq
处理函数,比如网络模块:
net_dev_initopen_softirq(NET_TX_SOFTIRQ, net_tx_action);open_softirq(NET_RX_SOFTIRQ, net_rx_action);
通过open_softirq
函数就可以向内核注册softirq
处理函数,open_softirq
函数如下:
// linux_5.10.97/kernel/softirq.c
// open_softirq第一个入参是softirq number,第二个入参是softirq处理函数
void open_softirq(int nr, void (*action)(struct softirq_action *))
{softirq_vec[nr].action = action; // 内核使用了一个数组softirq_vec[]来记录softirq的处理函数
}
唤醒softirq
内核使用了一个irq_cpustat_t
类型的结构体变量来记录被唤醒的softirq
,irq_cpustat_t
结果体成员如下,irq_cpustat_t::__softirq_pending
的一个bit就代表一种softirq
,唤醒某个softirq
其实就是把对应的bit置1。
// linux_5.10.97/include/linux/interrupt.h
typedef struct {unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;
内核定义irq_stat
时将其定义为PERCPU的变量,那么就可能出现同一类型的softirq
在多个CPU被唤醒的情形,自然,同一类型的softirq
也就可能同时在多个CPU上执行。
// linux_5.10.97/kernel/softirq.c
DEFINE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);
EXPORT_PER_CPU_SYMBOL(irq_stat);
内核提供了两个函数来唤醒softirq
,分别是raise_softirq
和raise_softirq_irqoff
,raise_softirq
是raise_softirq_irqoff
的包裹函数,raise_softirq
在调用raise_softirq_irqoff
之前会关闭当前CPU的中断,如果我们本就处在中断被禁用的上下文中,可以直接调用raise_softirq_irqoff
。
void raise_softirq(unsigned int nr)
{unsigned long flags;local_irq_save(flags); // 记录本地中断的状态,关闭中断raise_softirq_irqoff(nr);local_irq_restore(flags); // 恢复本地中断
}
我们可以在中断处理函数中唤醒softirq
,也可以在进程上下文中唤醒softirq
,如果在进程上下文中唤醒softirq
,那么内核会使用内核线程来执行softirq
,raise_softirq_irqoff
函数如下:
inline void raise_softirq_irqoff(unsigned int nr)
{__raise_softirq_irqoff(nr); // 将当前CPU的irq_cpustat_t::__softirq_pending的第nr位置1if (!in_interrupt()) // 如果不处于中断上下文,那就唤醒softirqd来执行softirqwakeup_softirqd();
}
softirq执行时机
softirq
既可能在中断退出的过程中被执行,也可能在中断线程中执行。
中断退出过程中检查softirq
的代码入下:
// linux_5.10.97/kernel/softirq.c
static inline void __irq_exit_rcu(void)
{......preempt_count_sub(HARDIRQ_OFFSET); // 减少hardirq计数,标志着离开中断上半部if (!in_interrupt() && local_softirq_pending()) // in_interrupt用于判断当前是否处于中断上下文,local_softirq_pending用于判断当前cpu是否有softirq被唤醒(判断irq_cpustat_t::__softirq_pending)invoke_softirq(); // 执行softirq......
}
invoke_softirq
函数如下:
static inline void invoke_softirq(void)
{if (ksoftirqd_running(local_softirq_pending())) // 如果内核用于处理softirq的线程正在运行,那就直接退出return;if (!force_irqthreads) { // 如果内核没有启动强制线程化softirq,那就直接调用`__do_softirq`来执行softirq......__do_softirq();......} else { // 如果强制要求在线程中执行softirq,那就唤醒用于处理softirq的线程wakeup_softirqd();}
}
内核初始化过程中,softirq
模块会为每个CPU都创建一个用于执行softirq
的线程,如下:
// linux_5.10.97/kernel/softirq.c
tatic struct smp_hotplug_thread softirq_threads = {.store = &ksoftirqd,.thread_should_run = ksoftirqd_should_run, // 当线程被唤醒后,就会调用该函数来判断当前cpu是否有softirq待执行,进而决定是应该睡眠还是应该运行'thread_fn'.thread_fn = run_ksoftirqd, // ksoftirqd线程调用run_ksoftirqd来执行softirq.thread_comm = "ksoftirqd/%u", // 线程的名字叫 ksoftirqd/*
};static __init int spawn_ksoftirqd(void) // spawn_ksoftirqd会在内核初始化过程中被调用
{cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,takeover_tasklets);BUG_ON(smpboot_register_percpu_thread(&softirq_threads)); // 创建softirq内核线程return 0;
}
early_initcall(spawn_ksoftirqd);
ksoftirqd
被唤醒后调用的run_ksoftirqd
如下:
static void run_ksoftirqd(unsigned int cpu)
{local_irq_disable(); // 关闭本地cpu的中断if (local_softirq_pending()) { // 判断是否有有softirq待执行/** We can safely run softirq on inline stack, as we are not deep* in the task stack here.*/__do_softirq(); // 执行softirqlocal_irq_enable(); // 开启本地cpu的中断,这里看似是在关闭中断的环境中执行的softirq,其实不然,__do_softirq函数会打开中断cond_resched(); return;}local_irq_enable();
}
__do_softirq
用于执行softirq
的__do_softirq
函数如下:
asmlinkage __visible void __softirq_entry __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 as the 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 swapping.*/current->flags &= ~PF_MEMALLOC;// 读取当前cpu有哪些softirq被唤醒了pending = local_softirq_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 */// 清除位掩码(irq_cpustat_t::__softirq_pending)set_softirq_pending(0);// 打开中断local_irq_enable();// softirq_vec记录着softirq的处理函数,这里将h指向softirq_vech = softirq_vec;while ((softirq_bit = ffs(pending))) { // 获取pending中第一个不为0的bitunsigned int vec_nr;int prev_count;// 获取softirq处理函数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);// 调用softirq的处理函数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();// 关闭irqlocal_irq_disable();// 重新查看是否又有新的softirq被唤醒了pending = local_softirq_pending();if (pending) {// 如果此次运行__do_softirq的时间没超过MAX_SOFTIRQ_TIME (2ms)且restart的次数低于MAX_SOFTIRQ_RESTART (10),那就跳转到restart处继续执行softirqif (time_before(jiffies, end) && !need_resched() &&--max_restart)goto restart;wakeup_softirqd(); // 如果__do_softirq已经耗费了太多的时间或者restart的次数过多,那就唤醒ksoftirqd}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);
}
从__do_softirq
函数可以看到,在softirq
执行期间,中断是开启的,这期间可能会发生中断,也可能会有新的softirq
被唤醒,所以__do_softirq
会循环的读取softirq
的位掩码并执行softirq
,直到没有softirq
需要执行或者执行softirq
的时间或者次数过多才会退出。如果__do_softirq
是因为执行softirq
的时间或者次数过多才会退出的,那么它还会唤醒ksoftirqd
来继续执行softirq
。
感谢阅读!