硬件的中断处理函数处于中断上半部分,在CPU关中断的状态下执行,中断线程、软中断(softirq)及小任务(tasklet)属于中断的下半部分(bottom half),在CPU开中断的状态下执行。小任务基于软中断实现,实质是对软中断的进一步封装, 在实际使用中应尽量使用小任务 。软中断及小任务的执行时机通常是中断上半部分返回(中断服务函数还未完全退出)的时候,其执行的上下文环境也处于(软)中断当中,因此其调用的处理函数不允许睡眠。这里说的软中断是中断下半部分的一种处理机制,和执行指令(ARM架构的SWI、SVC指令)触发中断的软中断不是一个概念。
软中断和小任务如果在某段时间内大量出现的话,内核会把多余的软中断和小任务放入ksoftirqd内核线程中执行。中断优先级高于软中断,软中断优先级高于线程。软中断适度线程化,可以缓解高负载情况下系统的响应。
Linux内核的软中断类型有10种。按优先级划分,优先级从0-9,数字越小优先级越高,优先级越高的软中断能得到优先执行。tasklet使用优先级为6的软中断。
[include/linux/interrupt.h]
enum
{
HI_SOFTIRQ=0, // 优先级为0,最高优先级的软中断
TIMER_SOFTIRQ, // 优先级为1,Timer定时器软中断
NET_TX_SOFTIRQ, // 优先级为2,发送网络数据包的软中断
NET_RX_SOFTIRQ, // 优先级为3,接收网络数据包的软中断
BLOCK_SOFTIRQ, // 优先级为4,用于块设备的软中断
IRQ_POLL_SOFTIRQ, // 优先级为5,用于轮训中断的软中断
TASKLET_SOFTIRQ, // 优先级为6,tasklet类型的软中断
SCHED_SOFTIRQ, // 优先级为7,用于进程调度以及负载均衡的软中断
HRTIMER_SOFTIRQ, // 优先级为8,用于高精度定时器的软中断
RCU_SOFTIRQ, // 优先级为9,用于RCU的软中断
NR_SOFTIRQS // 软中断数量为10
};
软中断执行的回调函数被封装在softirq_action
的结构体中,其内部保存了一个函数指针,注册软中断时需要设置此函数指针。内核为所有软中断定义了一个类型为softirq_action
的softirq_vec
数组,数组长度为NR_SOFTIRQS
,正好每个软中断对应一个softirq_action
,数组的索引为软中断的优先级。softirq_vec
数组为所有CPU共享。内核还定义了一个类型为irq_cpustat_t
的数组irq_stat
,数组长度为系统CPU的个数,索引为CPU的编号,每个CPU对应一个数组元素,此数组用来标记某个CPU上的某个软中断的状态,软中断pending,则对应的bit置1,否则为0。
[include/linux/interrupt.h]
struct softirq_action
{
void (*action)(struct softirq_action *);
};
[kernel/softirq.c]
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;
irq_cpustat_t irq_stat[NR_CPUS] ____cacheline_aligned; // 软中断状态标记
[include/asm-generic/hardirq.h]
typedef struct {
unsigned int __softirq_pending;
} ____cacheline_aligned irq_cpustat_t;
使用open_softirq
注册软中断,nr
为软中断的优先级,action
为软中断执行的回调函数。
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
只有处于pending状态的软中断才会得到执行,可使用raise_softirq
和raise_softirq_irqoff
函数使某个软中断处于pending状态(即触发软中断),raise_softirq
会关闭中断,raise_softirq_irqoff
不会关闭中断。由于__softirq_pending
变量为Per-CPU类型,因此在修改此变量时只需要关闭本地中断即可,不需要额外的同步措施。raise_softirq
首先关闭本地中断,然后调用raise_softirq_irqoff
设置__softirq_pending
变量,如果不处于硬中断、软中断及不可屏蔽中断上下文,则会唤醒内核线程ksoftirqd执行软中断,最后打开中断。可以看出一个CPU上的软中断执行是串行的,不会发生软中断嵌套的现象
[kernel/softirq.c]
void raise_softirq(unsigned int nr)
{
unsigned long flags;
local_irq_save(flags); // 保存cpsr,然后关闭中断
raise_softirq_irqoff(nr);
local_irq_restore(flags); // 恢复cpsr
}
// 设置软中断pending标记的宏
#define or_softirq_pending(x) (local_softirq_pending() |= (x))
#define local_softirq_pending() __IRQ_STAT(smp_processor_id(), __softirq_pending)
#define __IRQ_STAT(cpu, member) (irq_stat[cpu].member)
raise_softirq_irqoff
->__raise_softirq_irqoff
->or_softirq_pending(1UL << nr) // 将本地cpu的__softirq_pending变量的nr bit设置为1
// 如果不处于硬中断、软中断及不可屏蔽中断上下文中,则唤醒内核线程ksoftirqd执行软中断,
if (!in_interrupt())
wakeup_softirqd
->__this_cpu_read(ksoftirqd) // 获取ksoftirqd内核线程的task_struct指针
// 如果线程没有运行,则唤醒线程
if (tsk && tsk->state != TASK_RUNNING)
wake_up_process(tsk); // 唤醒内核线程,由内核线程执行软中断
软中断有两个执行时机,一个是在硬件中断处理结束的时候,二是触发软中断时唤醒内核线程之后。首先来看第一个,硬件中断处理结束的时候会调用irq_exit
,in_interrupt
判断当前执行环境是否处于硬中断、软中断及不可屏蔽中断上下文中,local_softirq_pending
判断本地CPU是否有软中断pending,如果当前执行环境不处于硬中断、软中断及不可屏蔽中断上下文且本地CPU有软中断pending,则调用invoke_softirq
执行软中断。__local_bh_disable_ip
(进入软中断使用)和__local_bh_enable
(退出软中断使用)需要配对使用,前者增加preempt_count
变量SOFTIRQ
域的值,后者减小preempt_count
变量SOFTIRQ
域的值,若SOFTIRQ
域大于0,说明软中断正在被执行,反之则说明软中断没有被执行,SOFTIRQ
域保证了软中断在本地CPU上的串行执行。执行软中断之前需要调用local_irq_enable
开启本地中断,执行完毕调用local_irq_disable
关闭本地中断,说明软中断是在开中断的环境下执行。根据本地CPU软中断的pending状态,调用softirq_vec
数组中对应的回调函数action
执行软中断,直到处理完所有pending状态的软中断才跳出while循环。如果在开中断期间又有软中断触发,则会跳转到restart
处继续处理软中断,但此时有条件限制,软中断执行时间小于2ms、不需要调度且restart执行次数不超过10次才会跳转,否则唤醒ksoftirqd
内核线程处理软中断。
[kernel/softirq.c]
void irq_exit(void)
{
......
preempt_count_sub(HARDIRQ_OFFSET); // 减少硬件中断计数
if (!in_interrupt() && local_softirq_pending()) // 检查是否有挂起的软中断,如有则执行软中断
invoke_softirq();
......
}
#define MAX_SOFTIRQ_TIME msecs_to_jiffies(2) // 最多执行2毫秒,超过唤醒内核线程执行
#define MAX_SOFTIRQ_RESTART 10 // restart最多执行10次
invoke_softirq
->__do_softirq
end = jiffies + MAX_SOFTIRQ_TIME // 软中断在这里最多执行2毫秒
max_restart = MAX_SOFTIRQ_RESTART // restart最对执行次数为10次
pending = local_softirq_pending() // 记录本地CPU软中断pending状态
// 开始执行软中断之前增加软中断计数,表明要进入软中断上下文
->__local_bh_disable_ip // 增加preempt_count变量SOFTIRQ域的值
restart: // restart标记
// 在使能本地中断之前清空本地cpu软中断pending状态
->set_softirq_pending(0)
->local_irq_enable() // 开启本地cpu中断,软中断是在开中断的状态下执行
h = softirq_vec // 获取softirq_vec数组的首地址
// 循环执行所有pending状态的软中断
while ((softirq_bit = ffs(pending))) { // ff获取pending第一个设置为1的bit序号
unsigned int vec_nr;
int prev_count;
h += softirq_bit - 1; // bit序号从0开始,h指向softirq_vec数组中需要执行的软中断
vec_nr = h - softirq_vec; // 获取软中断优先级,即软中断类型
prev_count = preempt_count(); // 保存当前进程preempt_count字段值
h->action(h); // 调用回调函数执行软中断
h++;
pending >>= softirq_bit; // 更新pending标志,丢弃已经执行的软中断pending位
}
->local_irq_disable // 关闭中断
// 获取本地CPU软中断pending状态,cpu开中断期间可能再次触发软中断
->pending = local_softirq_pending()
if (pending) {
// 如果时间小于2ms,不需要调度,restart执行次数不超过10次
// 则跳转到restart处继续执行软中断,否则唤醒内核线程处理软中断
if (time_before(jiffies, end) && !need_resched() &&
--max_restart)
goto restart;
wakeup_softirqd(); // 唤醒内核线程ksoftirqd执行软中断
}
// 退出软中断时减小软中断计数,表明要退出软中断上下文
__local_bh_enable(SOFTIRQ_OFFSET)
接着分析一下执行软中断的内核线程ksoftirqd
。DECLARE_PER_CPU
宏用于定义一个Per-CPU类型的变量,这里定义了内核线程ksoftirqd
的task_struct
结构体指针。可以看出每个CPU都会对应一个ksoftirqd
线程,用于执行本地CPU的软中断。ksoftirqd
线程调用run_ksoftirqd
函数执行软中断,首先关闭中断,使用local_softirq_pending
宏判断本地CPU是否有软中断pending,有则调用__do_softirq
执行软中断,执行完毕开启中断,反之开启中断后退出。
// Per-CPU类型,每个CPU都有一个ksoftirqd内核线程
[kernel/softirq.c]
DECLARE_PER_CPU(struct task_struct *, ksoftirqd);
// ksoftirqd线程执行的函数
static void run_ksoftirqd(unsigned int cpu)
{
local_irq_disable(); // 关闭中断
if (local_softirq_pending()) { // 本地CPU是否有软中断pending
__do_softirq(); // 执行软中断
local_irq_enable(); // 开启中断
cond_resched_rcu_qs();
return;
}
local_irq_enable(); // 开启中断
}
tasklet是利用软中断实现的中断下半部分机制,运行在软中断上下文中。tasklet使用tasklet_struct
描述。
[include/linux/interrupt.h]
struct tasklet_struct
{
struct tasklet_struct *next; // tasklet_struct链表指针
unsigned long state; // state为一个位图,表示tasklet_struct运行状态
atomic_t count; // 0:tasklet可以执行,非0:tasklet不允许执行
void (*func)(unsigned long); // 执行tasklet的回调函数
unsigned long data; // 传递给执行tasklet的回调函数的参数
};
enum // 表示tasklet的状态,即state成员的值
{
TASKLET_STATE_SCHED, /* bit0为1表示tasklet被调度,正准备运行 */
TASKLET_STATE_RUN /* bit1位1表示tasklet正在运行 (SMP only) */
};
每个CPU定义了两种tasklet,分别为tasklet_vec
和tasklet_hi_vec
,各自组成一个单项循环链表。在start_kernel
中调用softirq_init
初始化tasklet,tasklet_vec
的软中断优先级为6
,回调函数为tasklet_action
;tasklet_hi_vec
的软中断优先级为0
,回调函数为tasklet_hi_action
。tasklet_hi_vec
的优先级高于tasklet_vec
。
[kernel/softirq.c]
// 每个CPU静态定义了两种tasklet
static DEFINE_PER_CPU(struct tasklet_head, tasklet_vec);
static DEFINE_PER_CPU(struct tasklet_head, tasklet_hi_vec);
struct tasklet_head {
struct tasklet_struct *head;
struct tasklet_struct **tail;
};
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); // 注册tasklet_vec的软中断
open_softirq(HI_SOFTIRQ, tasklet_hi_action); // 注册tasklet_hi_vec的软中断
}
tasklet_action
是优先级为6的tasklet tasklet_vec
的执行函数;首先保存链表的头节点指针,然后将头结点设置为NULL
,链表的操作在关中断的情况进行,保证原子操作;接着遍历tasklet_vec
的链表,检测每一个节点是否设置了TASKLET_STATE_RUN
标志,若没有设置,则设置,反之则此节点的tasklet正在被执行,则跳过此节点,同时还要根据count
变量的值判断节点是否可执行,为0时执行,反之则不执行;每遍历完一个节点,则从链表中删除此节点。tasklet_hi_action
和tasklet_action
的处理逻辑一致,这里不再赘述。
[kernel/softirq.c]
static void tasklet_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable(); // 关闭中断
list = __this_cpu_read(tasklet_vec.head); // 保存tasklet_vec链表的头指针
__this_cpu_write(tasklet_vec.head, NULL); // tasklet_vec链表的头指针指向NULL
__this_cpu_write(tasklet_vec.tail, this_cpu_ptr(&tasklet_vec.head));
local_irq_enable(); // 开启中断
while (list) { // 遍历tasklet_vec链表,调用所有回调函数
struct tasklet_struct *t = list;
list = list->next;
// 单核tasklet_trylock返回1,SMP系统tasklet_trylock原子的设置TASKLET_STATE_RUN标志,
// 并返回原来bit位的值,若原来设置了TASKLET_STATE_RUN标志,说明此节点的tasklet正在执行,
// 则跳过此节点,否则执行此节点的回调函数
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) { // 原子的读取count字段,为0则执行tasklet,否则不执行
// 清除TASKLET_STATE_SCHED(bit0),并返回原来的值,如果已被清除,说明存在bug
if (!test_and_clear_bit(TASKLET_STATE_SCHED, &t->state))
BUG(); // 报告bug信息
t->func(t->data); // 调用回调函数
tasklet_unlock(t); // 原子的清除TASKLET_STATE_RUN标志
continue;
}
tasklet_unlock(t); // 原子的清除TASKLET_STATE_RUN标志
}
local_irq_disable(); // 关闭中断
// 执行完tasklet_vec链表中的一个节点,则将这个节点从链表中删除
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
__raise_softirq_irqoff(TASKLET_SOFTIRQ);
local_irq_enable(); // 开启中断
}
}
static void tasklet_hi_action(struct softirq_action *a)
{
struct tasklet_struct *list;
local_irq_disable();
list = __this_cpu_read(tasklet_hi_vec.head);
__this_cpu_write(tasklet_hi_vec.head, NULL);
__this_cpu_write(tasklet_hi_vec.tail, this_cpu_ptr(&tasklet_hi_vec.head));
local_irq_enable();
while (list) {
struct tasklet_struct *t = list;
list = list->next;
if (tasklet_trylock(t)) {
if (!atomic_read(&t->count)) {
if (!test_and_clear_bit(TASKLET_STATE_SCHED,
&t->state))
BUG();
t->func(t->data);
tasklet_unlock(t);
continue;
}
tasklet_unlock(t);
}
local_irq_disable();
t->next = NULL;
*__this_cpu_read(tasklet_hi_vec.tail) = t;
__this_cpu_write(tasklet_hi_vec.tail, &(t->next));
__raise_softirq_irqoff(HI_SOFTIRQ);
local_irq_enable();
}
}
DECLARE_TASKLET
和DECLARE_TASKLET_DISABLED
可以静态的定义一个tasklet_struct
,前者定义的tasklet可以被执行,后者定义的tasklet不能被执行。tasklet_init
可以动态的初始化tasklet_struct
。
[include/linux/interrupt.h]
#define DECLARE_TASKLET(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \
struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
[kernel/softirq.c]
// 初始化tasklet链表节点
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); // count被初始化为0,说明处于可执行状态
t->func = func;
t->data = data;
}
tasklet_enable
和tasklet_disable
可开启和禁止tasklet,前置减少count
字段的值,为0表示可执行,后者增大count
字段的值,为1表示不执行。tasklet_schedule
设置TASKLET_STATE_SCHED
标志并将节点插入到tasklet链表中,最后触发TASKLET_SOFTIRQ
类型的软中断,调用tasklet_schedule
后,该节点的tasklet处于就绪状态,执行软中断时会得到处理,只有没有设置TASKLET_STATE_SCHED
的节点才会加入链表,若设置了,则不做任何操作。tasklet_schedule
将要执行的节点挂到tasklet_vec
链表中,tasklet_hi_schedule
将要执行的节点挂到tasklet_hi_vec
链表中。
[include/linux/interrupt.h] // 使能tasklet
static inline void tasklet_enable(struct tasklet_struct *t)
{
smp_mb__before_atomic();
atomic_dec(&t->count);
}
static inline void tasklet_disable(struct tasklet_struct *t) // 禁止tasklet
{
tasklet_disable_nosync(t);
tasklet_unlock_wait(t);
smp_mb();
}
// 设置tasklet状态为TASKLET_STATE_SCHED,同时将t节点插入到tasklet_vec链表中
static inline void tasklet_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_schedule(t);
}
void __tasklet_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_vec.tail) = t;
__this_cpu_write(tasklet_vec.tail, &(t->next));
raise_softirq_irqoff(TASKLET_SOFTIRQ); // 触发TASKLET_SOFTIRQ类型的软中断
local_irq_restore(flags);
}
static inline void tasklet_hi_schedule(struct tasklet_struct *t)
{
if (!test_and_set_bit(TASKLET_STATE_SCHED, &t->state))
__tasklet_hi_schedule(t);
}
void __tasklet_hi_schedule(struct tasklet_struct *t)
{
unsigned long flags;
local_irq_save(flags);
t->next = NULL;
*__this_cpu_read(tasklet_hi_vec.tail) = t;
__this_cpu_write(tasklet_hi_vec.tail, &(t->next));
raise_softirq_irqoff(HI_SOFTIRQ); // 触发HI_SOFTIRQ类型的软中断
local_irq_restore(flags);
}
tasklet不是在某个固定的CPU上执行。tasklet挂接到那个CPU的链表上,则由那个CPU执行。一旦挂入某个CPU的链表,就必须等待该CPU清除TASKLET_STATE_SCHED
标志后,才有机会到其他CPU上运行。执行tasklet_schedule
和tasklet_hi_schedule
的CPU就是执行tasklet的CPU。