软中断和tasklet
- 中断管理
- 2023-03-05
- 220热度
- 0评论
Linux的中断分为上下部机制,上半部在中断上下文中关闭了本地CPU中断响应,下半部是在中断线程中处理。在Linux系统没有引入中断线程化机制之前,就已经出现了一些下半部的机制,如软中断SoftIRQ,Tasklet和workqueue。
SoftIRQ是预留给系统对时间要求比较严格进行使用的,Linux系统已经定义了软中断的类型,通常情况下用户部需要修改软中断的类型,对于用户来说可以使用tasklet机制。
enum
{
HI_SOFTIRQ=0, 最高优先级的软中断
TIMER_SOFTIRQ, Timer定时器软中断
NET_TX_SOFTIRQ, 网络发包软中断
NET_RX_SOFTIRQ, 网络收包软中断
BLOCK_SOFTIRQ, 块设备软中断
IRQ_POLL_SOFTIRQ, IO轮询的块设备软中断
TASKLET_SOFTIRQ, tasklet软中断
SCHED_SOFTIRQ, 进程调度以及负载均衡软中断
HRTIMER_SOFTIRQ, 高精度定时器软中断
RCU_SOFTIRQ, RCU服务软中断
NR_SOFTIRQS
};
抢占计数
在讨论softirq前,我们先来讨论下抢占计数,这与softirq有密切的关系。Linux配置打开了CONFIG_PREEMPT表示允许高优先级的任务抢占低优先级任务,但是在spin lock,中断/软中断上下文中依旧不允许抢占的。在linux系统中使用了一个Per-CPU的32位变量来标识一些特殊场景,如下。
- PREEMPT_BITS:用于记录禁止抢占计数,当调用preempt_enable/preempt_disable会进行减加操作,直到count位0才能允许开启抢占。上述还包括spin lock,read lock等都会禁止抢占。
- SOFTIRQ_BITS:软中断计数区域,进入软中断会调用local_bh_disble计数+1,退出软中断调用local_bh_enable计数-1,当SOFTIRQ_BITS为正数就表明处于softirq上下文中,in_softirq为真。其中第8位表示正在处理软中断。
- HARDIRQ_BITS:硬件中断计数区,进入+1,退出时-1,当HARDIRQ_BITS为正数时表明处于hardirq上下文中,in_hardirq为真。
- MNI_BITS:置位表示进入MNI中断上下文。
#define nmi_count() (preempt_count() & NMI_MASK)
#define hardirq_count() (preempt_count() & HARDIRQ_MASK)
#ifdef CONFIG_PREEMPT_RT
# define softirq_count() (current->softirq_disable_cnt & SOFTIRQ_MASK)
#else
# define softirq_count() (preempt_count() & SOFTIRQ_MASK)
#endif
#define irq_count() (nmi_count() | hardirq_count() | softirq_count())
/*
* Macros to retrieve the current execution context:
*
* in_nmi() - We're in NMI context
* in_hardirq() - We're in hard IRQ context
* in_serving_softirq() - We're in softirq context
* in_task() - We're in task context
*/
#define in_nmi() (nmi_count())
#define in_hardirq() (hardirq_count())
#define in_serving_softirq() (softirq_count() & SOFTIRQ_OFFSET)
#define in_task() (!(in_nmi() | in_hardirq() | in_serving_softirq()))
/*
* The following macros are deprecated and should not be used in new code:
* in_irq() - Obsolete version of in_hardirq()
* in_softirq() - We have BH disabled, or are processing softirqs
* in_interrupt() - We're in NMI,IRQ,SoftIRQ context or have BH disabled
*/
#define in_irq() (hardirq_count())
#define in_softirq() (softirq_count())
#define in_interrupt() (irq_count())
- in_irq: 说明HARDIRQ_BITS位有置起,说明当前有正在处理的中断handler(top half),只要hardirq_count大于0,就说明是IRQ Context。
- in_softirq:说明SOFTIRQ_BITS位有置起,但是也不能完全说是当前正在执行softirq,其稍微有点特殊。当softirq正在执行的时候,softirq count会增加,那是处于softirq context没有错,但是再其他场景下也会将softirq count增加,比如在进程上下文中出于对临界区的包含,会调用local_bh_disable/enable保护临界区,那么这也会置起SOFTIRQ_BITS,但是并没有在执行softirq,因此对于softirq还是使用in_serving_softirq来判断是否softirq正在处理。
当MNI_BITS|HARDIRQ_BITS|SOFTIRQ_BITS置位表示处于中断上下文中,in_interrupt为真,当使能CONFIG_PREEMPT后,中断返回会检测这个preempt_count变量,当该值如果为0时才能允许被抢占。
注册软中断
系统中为每个CPU都创建了一个中断线程用于处理软中断。
DEFINE_PER_CPU(struct task_struct *, ksoftirqd);
EXPORT_PER_CPU_SYMBOL_GPL(ksoftirqd);
static struct smp_hotplug_thread softirq_threads = {
.store = &ksoftirqd,
.thread_should_run = ksoftirqd_should_run,
.thread_fn = run_ksoftirqd,
.thread_comm = "ksoftirqd/
};
static __init int spawn_ksoftirqd(void)
{
cpuhp_setup_state_nocalls(CPUHP_SOFTIRQ_DEAD, "softirq:dead", NULL,
takeover_tasklets);
BUG_ON(smpboot_register_percpu_thread(&softirq_threads));
//为每个CPU创建一个softirq处理线程
return 0;
}
early_initcall(spawn_ksoftirqd);
struct softirq_action
{
void (*action)(struct softirq_action *);
};
static struct softirq_action softirq_vec[NR_SOFTIRQS]
系统中定义了一个全局struct softirq_action变量用于存储各类型中断的回调函数,系统通过open_softirq函数来注册回调函数。
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
在linux系统中为每个CPU都维护了一个softirq的状态信息。
typedef struct {
unsigned int __softirq_pending;
#ifdef ARCH_WANTS_NMI_IRQSTAT
unsigned int __nmi_count;
#endif
} ____cacheline_aligned irq_cpustat_t;
DECLARE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);
触发softirq
从上图可以看出触发softirq主要有两个函数raise_softirq和raise_softirq_irqoff。raise_softirq会关闭本地中断响应,而raise_softirq_irqoff不会。
raise_softirq是用于进程上下文触发软中断的,而在进程上下文关闭本地cpu的中断响应是为了保护临界访问,软中断的触发实际是设置软中断的pending位,而只需要关闭本地中断响应是因为每个cpu都维护了一个软中断的pending位。
raise_softirq_irqoff是用于中断上下文触发中断的,实际上大部分的软中断都是在中断上下文触发的,因为本身就处于中断上下文了,本地中断响应已经关闭了,因此不需要再支持local_irq_save。
软中断处理过程中,调用__raise_softirq_irqoff设置软中断的pengding位,这样再执行软中断是会进行检测该位,如果使能才会执行软中断。
在raise_softirq_irqoff中还会进行判断是否处于中断上下文中且是否有软中断置位,对于处于进程上下文触发的软中断,那么可以直接激活软中断线程进程处理软中断。而处于中断上下文中,则什么都不用做,因为在中断退出时才会检测软中断进行执行软中断,因此对于处于中断上下文触发的软中断,仅仅是设置软中断的pending位。
执行softirq
软中断执行时机可以分为3种情况,分别是在中断退出时检测是否有软中断执行、进程上下文中主动执行、在spin_unlock_bh(实际调用__local_bh_enable_ip)。
先来看一下执行中断退出执行软中断的场景,如上图进程被中断打断后进入到中断上下文中,进入中断后cpu硬件会自动关闭cpu本地中断响应,处理中断完成后在执行irq_exit中断退出时,当检测到有软中断pending时执行软中断;如果软中断是在中断上下文执行时,在软中断处理中会调用local_irq_enable打开CPU本地中断响应再处理软中断程序,如果是触发的软中断线程,硬中断已经完成退出也会使能本地中断。因此在软中断执行过程中打开了中断响应,所以可能会再次进入硬中断上下文。
再来看上面这张图,在一个task中处理一个变量此时被硬件中断打断进行中断处理函数,在中断处理快结束时如果有软中断pending将会先处理软中断,如果软中断中也访问了该变量,那么就出现竞态异常,因此为了处理进程和软中断的竞态,调用spin_lock_bh和spin_unlock_bh进行保护,在硬件中断处理完要进入软中断将会被禁止,硬件中断会被直接退出,继而task可以继续运行,当task再执行spin_unlock_bh时会触发执行软中断。另外如果软中断处理函数中的竞态可能在多核直接发生,为了保护多核的临界处理在软中断中只需要调用spin_lock和spin_unlock即可,不需要调用spin_lock_bh和spin_unlock_bh,因为每个cpu上只有一个软中断可以运行不需要做软中断之间的临界保护。总结就是在进程上下文中要避免软中断和多核的竞态保护就调用spin_lock_bh和spin_unlock_bh,软中断中避免多核的竞态保护就调用spin_lock和spin_unlock即可。
Tasklet
Tasklet是软中断类型的一种,便于用户实现软中断。
struct tasklet_struct
{
struct tasklet_struct *next; 多个tasklet串成一个链表
unsigned long state; 标记tasklet的状态
atomic_t count; 正数表示本地已经有运行的tasklet,只有为0才能运行。
bool use_callback;
union {
void (*func)(unsigned long data);
void (*callback)(struct tasklet_struct *t);
}; tasklet的处理回调函数
unsigned long data; 传参
};
Tasklet是softirq的一种,系统中分配了两个标志可用于tasklet软中断,分别是高优先级的HI_SOFTIRQ和普通优先级的TASKLET_SOFTIRQ,系统中每个CPU对应维护两个tasklet链表,一个HI_SOFTIRQ类型的链表和一个TASKLET_SOFTIRQ类型的链表,用于管理维护用户注册的tasklet。在进行处理软中断是,会优先级处理HI_SOFTIRQ类型的tasklet。
/*
* Tasklets
*/
struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
Softirq在初始化时会为tasklet注册全局的软中断回调函数。
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;
}
open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}
tasklet的创建
用户可以使用下面两个宏来定义一个tasklet,区别一个已经处于enable另外一个处于disable。
#define DECLARE_TASKLET(name, _callback) \\
struct tasklet_struct name = { \\
.count = ATOMIC_INIT(0), \\
.callback = _callback, \\
.use_callback = true, \\
}
#define DECLARE_TASKLET_DISABLED(name, _callback) \\
struct tasklet_struct name = { \\
.count = ATOMIC_INIT(1), \\
.callback = _callback, \\
.use_callback = true, \\
}
如:static DECLARE_TASKLET(fst_tx_task, fst_process_tx_work_q);
除了使用宏来定义外,还可以使用tasklet_init来动态初始化分配。
void tasklet_init(struct tasklet_struct *t,
void (*func)(unsigned long), unsigned long data)
{
t->next = NULL;
t->state = 0;
atomic_set(&t->count, 0);
t->func = func;
t->use_callback = false;
t->data = data;
}
EXPORT_SYMBOL(tasklet_init);
触发执行tasklet
触发tasklet得到执行,也比较简单,上面以普通的tasklet为例。用户可调用tasklet_schedule,该函数将tasklet插入到链表中然后调用raise_softirq_irqoff触发软中断,最终触发了softirq,关于softirq的运行在上一章节中已经描述,最后softirq运行tasklet_action,在该函数中进行while遍历链表执行对应的tasklet回调函数。