softirq顾名思义是通过软件触发的中断,与之前介绍的通过硬件的触发的中断相对应;两者在逻辑上又有一定的相关性。tasklet属于softirq的特殊类型,其他实现和触发方式有其他自身的特点。
本文将就softirq的实现、触发、其与硬件中断的相关性,tasklet相对于softirq的特性进行介绍。
内核版本:kernel-4.19
目录
1.内核支持的softriq类型
2.softirq的注册
3.softirq的触发
4.softirq的执行
4.1 irq_exit调用softirq action的过程
4.2 softirq action执行
4.3 local_bh_enable、local_bh_disable bottom half保护函数
5.tasklet
5.1 TASKLET_SOFTIRQ类型的tasklet触发
5.2Tasklet的执行
enum {
HI_SOFTIRQ=0, //如其名,最该优先级的softirq,用于高优先级的tasklet
TIMER_SOFTIRQ, //用于timer的sofirq
NET_TX_SOFTIRQ, //用于网络数据的发送的sotfirq
NET_RX_SOFTIRQ, //用于网络数据的接受的sotfirq
BLOCK_SOFTIRQ, //用于块设备数据操作的softirq
IRQ_POLL_SOFTIRQ, //轮询类软件中断
TASKLET_SOFTIRQ, //普通tasklet专用的软件中断
SCHED_SOFTIRQ, //进程调度负载均衡类软件中断
HRTIMER_SOFTIRQ, //高精度定时器软件中断
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
irq_stat.__softirq_pending 是以系统定义的softirq类型相对应的位域、各CPU独立数据。其每一位被置位代表一中类型的softirq被触发,系统中每个CPU都有一个独立的__softirq_pending数据表示该CPU上有哪些类型的softirq被触发。系统通过__softirq_pending来做到谁触发谁执行,具体后文会详细说明。
与硬件中断不同软件中断的注册比较简单,就是将对应的软件中断的action赋给softirq_vec[ ]数组。softirq_vec[]就是系统软件中断处理函数的向量表,系统支持的所有软件中断的action都会被按照类型填充到该数组中。
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
//nr特定软件中断类型,与系统软件中断类型的枚举结构相对应
//action为该特定软件中断对应的中断处理函数
softirq_vec[nr].action = action;
}
通过__raise_softirq_irqoff函数设置__softirq_pending相应的bit位来表明对应的软中断的触发,至于该软中断action什么时机执行有特定的条件限制,后文会进一步说明。因为__softirq_pending是CPU独立变量,该置位只影响当前执__raise_softirq_irqoff函数的CPU,不会改变其他CPU的__softirq_pending变量。因为__softirq_pending属于CPU独立变量,所以对其访问需要进行临界保护,kernel通过在访问前禁用该CPU的中断、在访问完时再使能该CPU的中断来达到保护该变量的目的,故raise_softirq_irqoff函数调用前后需要通过local_irq_save/local_irq_restore形式禁用/恢复该CPU中断。
inline void raise_softirq_irqoff(unsigned int nr)
{
//获取当前CPU的__softirq_pending变量,并将该变量中与nr对应的softirq bit位进行置位;对于__softirq_pending CPU变量访问需要添加保护。
__raise_softirq_irqoff(nr);
//in_interrupt 用于判定是否在中断上下文,中断上下文包含:hardirq_context、softirq_context、nmi_context等
if (!in_interrupt()) {
//如果不在中断上下文,则唤醒该CPU对应的ksoftirqd线程执行。此判断表明:a.softirq 不可嵌套执行,当存在softirq的上下文时则此次不能唤醒
//该CPU的ksoftirqd线程;b.softirq执行优先级高于普通线程(即非中断线程),此处判断不在中断上下文时即当前在普通线程执行上下文,此时可以
//唤醒该CPU对应的ksoftirqd线程
wakeup_softirqd();
}
}
raise_softirq函数时raise_softirq_irqoff的变种,raise_softirq增加了local_irq_save/local_irq_restore调用、做到了对__softirq_pending CPU变量的保护。
raise_softirq、raise_softirq_irqoff、__raise_softirq_irqoff三个函数均可触发softirq,使用时需要注意各个函数的特性。
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags);
raise_softirq_irqoff(nr);
local_irq_restore(flags);
}
softirq中断action执行有两种方式:一种直接调用,通过__do_softirq、do_softirq函数遍历该CPU变量__softirq_pending,找到pending状态的的softirq并执行其对应的action;另一中是通过在退出hardirq的top half执行时,通过调用irq_exit函数进而调用到__do_softirq,做为中断的bottom half执行。
如图irq_exit函数要执行到invoke_softirq->_do_softirq需要经历4个重要环节:
A.通过给当前进程的preempt count减去HARDIRQ_OFFSET,表明当前退出hardirq执行的上下文环境;
B.通过in_interrupt函数判断当前进程是否还在中断执行的上下文。如果在说明发生了中断嵌套或者之前的softirq action的执行被打断,则此时不能调用invoke_softirq实现softirq的执行,需要返回到被中断打断的代码环境执行;如果不在中断执行的上下文则说明hardirq打断的是普通的进程。
C.该CPU对应的__softirq_pending变量不为零,有pending状态的softirq待执行。
D.判断ksoftirqd thread是否正在运行,如果在运行则不会立即处理pending状态的softirq,而是让ksoftirqd以自己的频率来处理这些pending的softirq。除非我们正在做HI_SOFTIRQ 和TASKLET_SOFTIRQ类型的同步软中断。
__do_softirq是Softirq action执行的核心函数,该函数有3点需要注意:
A.__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET)、__local_bh_enable(SOFTIRQ_OFFSET) 对softirq action执行区间的标识,用以保护softirq action的执行。
B.在sofirq action真正执行前会开启本地CPU的irq,导致softirq执行过程可能被hardirq中断,故在softirq action的执行完后要再次判断是否存在pending状态softirq。如果存在且满足一定的条件时,可以再次唤醒ksoftirqd便于softirq action的及时执行。
C.ffs对于pending状态的解析证明了softirq优先级由底到高的顺序
#define MAX_SOFTIRQ_TIME msecs_to_jiffies(2)
#define MAX_SOFTIRQ_RESTART 10
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 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_pending变量,即当前pending状态的softirq
pending = local_softirq_pending();
account_irq_enter_time(current);
//给当前thread->preempt count的softirq域加1,表明当前在软件中断上下文
__local_bh_disable_ip(_RET_IP_, SOFTIRQ_OFFSET);
in_hardirq = lockdep_softirq_start();
restart:
/* Reset the pending bitmask before enabling irqs */
//清除当前CPU的__softirq_pending变量,即清除了pending状态的softirq;有点硬件中断的进入中断处理后mask中断的感觉。
set_softirq_pending(0);
//使能本地IRQ,为什么使能本地CPU中断?
//猜想原因有:1.softirq属于中断的下半文,如在关闭本地中断的情况下、下半文执行时间过长,可能影响系统对硬件中断响应。2.硬件中断的优先级高于软件中断,如果在软件中断执行是关闭了硬件中断,有背此涉及逻辑;3.软件中断执行时,没有必要的关闭本地中断的理由。
local_irq_enable();
//设置h为软件中断的向量表
h = softirq_vec;
//ffs获取变量中由低到高的第一位为1的位,从bit1开始计算
//因为是ffs是从右向左查找,也就是softirq_vec[]数组index数值小的元素表示的软中断先被处理,故逻辑是上证明软中断的由高到底优先级 HI_SOFTIRQ ->RCU_SOFTIRQ
while ((softirq_bit = ffs(pending))) {
unsigned int vec_nr;
int prev_count;
//h指向了softirq bit的特定的软件中断函数
h += softirq_bit - 1;
//vec_nr = softirq_bit - 1
vec_nr = h - softirq_vec;
//获取当前thread的preempt count
prev_count = preempt_count();
//统计该软中断数
kstat_incr_softirqs_this_cpu(vec_nr);
//执行该软中断对应的action
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_SOFTIRQ中断则触发
rcu_bh_qs();
//disable 本地 CPU 的IRQ
local_irq_disable();
//有新的softirq产生吗 ?可能是RCU_SOFTIRQ或者是开启本地CPU的irq后,在softirq action执行的过程中有收到hardirq导致softirq action被打断,在hardirq执行过程中有arise_softirq,当系统再恢复到softirq action执行时此处可能判定到与新softirq为pending状态。当然此处也不是简单的判定到有pending状态的softirq就执行,而是满足一定的条件才可继续执行新的pending状态的softirq。
pending = local_softirq_pending();
if (pending) {
//1.前半部分的执行小于2ms
//2.该thead无TIF_NEED_RESCHED标志
//3.小于最大restart次数
//满足以上三点时,再次执行对应的softirq_vec
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
//唤醒该cpu的ksoftirq thead,但是并不能保证ksoftirq thread立即执行,而是由系统sched进行负载均衡决策。
//防止softirq_vec执行占用太多资源,导致其他任务饥饿。
wakeup_softirqd();
}
//给当前的thread的softirq_context-1,表示退出softirq执行的上下文
lockdep_softirq_end(in_hardirq);
account_irq_exit_time(current);
//给当前thread的preempt_count减去SOFTIRQ_OFFSET,表示退出softirq的执行
__local_bh_enable(SOFTIRQ_OFFSET);
WARN_ON_ONCE(in_interrupt());
//恢复当前thread的PF_MEMALLOC 属性设置
current_restore_flags(old_flags, PF_MEMALLOC);
}
在多核系统中,Softirq action可能存在并发执行情况。__softirq_pending为CPU变量,多个CPU可能同时存在pending状态的softirq,当触发各自CPU的ksoftirqd线程执行时,各ksoftirqd线程可能同时执行到软中断向量表softirq_vec[ ]中同类型的软中断action,导致该函数出现并发情况;故Softirq action需要考虑并发情况的处理。
该函数用于表示进入(local_bh_disable )和退出( local_bh_enable)bottom half上下文的处理,有点等同与hardirq中的disable irq 和enble irq;区别在于:a.该函数是软件层面防止了sofitirq的抢占; b.该函数只是表示进入了bottom half处理的临界区域,不同与hardirq的从硬件物理上不在影响irq。
另外需要主语该函数与其变种__local_bh_disable_ip、__local_bh_enable_ip函数测差异:当进入或者退出 softirq 处理时,preempt_count的加或者减的偏移是SOFTIRQ_OFFSET;而local_bh_disable或者local_bh_enable处理时,preempt_count的加或者减偏移是2*SOFTIRQ_OFFSET。如上设置可用于区分当前实在处理softirq的上下文还是在bh disable的上下文中。
虽然在softirq action执行过程中 local_bh_enable、local_bh_disable做了软中断处理的临界区保护,但是在该处理过程中可能会使能本地CPU中断导致softirq action被中断,故在执行__local_bh_enable_ip函数退出软中断临界保护区时,需要再次检查 是否存在pengding状态的softirq。如果存在且满足一定的条件则会调用do_softirq执行softirq action。这点与前文介绍的__do_softirq函数的第2点相同。
void __local_bh_enable_ip(unsigned long ip, unsigned int cnt) {
WARN_ON_ONCE(in_irq());
lockdep_assert_irqs_enabled();
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_disable();
#endif
/*
* Are softirqs going to be turned on now:
*/
if (softirq_count() == SOFTIRQ_DISABLE_OFFSET)
trace_softirqs_on(ip);
//preempt count仅仅进行cnt-1操作,执行退出softirq action临界区操作但并没有完全退出;
preempt_count_sub(cnt - 1);
//检测是否在非interrupt上下文且存在pending状态的softirq,如果满足则调用do_softirq执行softirq action
if (unlikely(!in_interrupt() && local_softirq_pending())) {
do_softirq();
}
//完全退出softirq action临界保护区
preempt_count_dec();
#ifdef CONFIG_TRACE_IRQFLAGS
local_irq_enable();
#endif
preempt_check_resched();
}
Tasklet是HI_SOFTIRQ、TASKLET_SOFTIRQ的类型软中断的特殊实现形式。既然属于软中断那么tasklet具有softirq实现的特点:做为中断的bottom hal执行、bh临界保护、action执行过程使能本地中断等,做为软中断的特殊实现其实现也解决了softirq的一些问题:1.避开了软中断action需要考虑并发执行的问题; 2.解决了软中断类型有限带来的软中断资源不足不能支持更多软中断处理的问题。
通过对tasklet的机制来说明tasklet是怎样解决上述问题。
__tasklet_schedule负责了 TASKLET_SOFTIRQ类型的软中断的触发,其触发核心动作也是通过raise_softirq_irqoff实现,此函数前文有说明此处不再赘述。在执行raise_softirq_irqoff函数前,其将tasket_struct结构的tastlet赋给了当前CPU变量tasklet_vec,tasklet_vec是当前CPU的tasklet_struct结构的链表。因为tasklet_vec的链接原则上的无线连接特性,所以可以有很多的tasklet链接到tasklet_vec,从而解决了softirq类型有限不能注册更多软中断的问题。
//触发tasklet的softirq执行
void __tasklet_schedule(struct tasklet_struct *t)
{
__tasklet_schedule_common(t, &tasklet_vec,
TASKLET_SOFTIRQ);
}
同样以TASKLET_SOFTIRQ类型软中断为例,其tasklet执行函数流程为:__do_softirq -> softirq_vec[TASKLET_SOFTIRQ] -> tasklet_action -> tasklet_action_common。
在tasklet_action_common函数中遍历、执行当前CPU的tasklet_vec链表的tasklet。在执行tasklet时会判断taklet->count原子变量,如果该变量不为零则表明该tasklet在执行中,当前CPU当前不能执行该tasklet。此机制防止了tasklet执行的并发性问题。
static void tasklet_action_common(struct softirq_action *a, struct tasklet_head *tl_head,unsigned int softirq_nr)
{
struct tasklet_struct *list;
//关闭本地CPU的irq,用于保护tl_head CPU类型变量
local_irq_disable();
list = tl_head->head;
tl_head->head = NULL;
tl_head->tail = &tl_head->head;
//开启本地中断
local_irq_enable();
//遍历当前CPU的tasklet链表
while (list) {
struct tasklet_struct *t = list;
list = list->next;
//检查tasklet是否在运行状态,如果不是则标记为运行状态;否则将已经在运行状态的tasklet继续添加到该CPU的tasklet链表
if (tasklet_trylock(t)) {
//获取该tasklet->count原子变量值判断该tasklet是否可执行, count: 0 可以执行,tasklet_enable,1不可执行,tasklet_disable
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))
BUG();
//执行该tasklet对应的function函数
t->func(t->data);
//设置tasklet的为非运行状态
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
//将已经在运行状态的tasklet添加到当前cpu的tasklet链表,并设置对应的softirq为pending状态,等待系统再次执行该tasklet
t->next = NULL;
*tl_head->tail = t;
tl_head->tail = &t->next;
__raise_softirq_irqoff(softirq_nr);
local_irq_enable();
}
}
static __latent_entropy void tasklet_action(struct softirq_action *a)
{
tasklet_action_common(a, this_cpu_ptr(&tasklet_vec), TASKLET_SOFTIRQ);
}
到此softirq&tasklet介绍完毕,通过其触发和执行充分说明了其他各自特性,也通过对比说明了其bottom half的特性。