前言
中断服务程序往往实在CPU关中断的条件下执行的,以避免中断嵌套而使控制复杂化。但是CPU关中断的时间不能太长,否则会丢失中断信号。因此Linux将中断服务程序分为“上半部”和“下半部”。前者对时间要求比较严格,必须在中断请求发生后立即(或者一定的时间限制内)完成,为了保证这种处理能原子的完成,上半部通常是在CPU关中断的条件下执行的:从IDT(中断描述符表)中登记的中断入口函数一直到驱动程序注册在中断服务队列中的ISR(中断服务程序)。下半部是上半部根据需要调度执行的,这些处理允许延迟,通常是在CPU开中断的的条件下执行的。
但是,中断下半部有两个缺点:
- 同一时刻,只能有一个CPU执行下半部代码,以防止多CPU同时执行而相互干扰,因此下半部代码的执行是“串行化”的。
- 下半部函数不允许嵌套。
这两个缺点在单CPU系统中无关紧要,但是在SMP(对称多处理器)系统中,中断下半部的“串行化”执行没有充分利用SMP的多CPU特性。因此,Linux Kernel 2.4扩展了softirq(软中断请求)的机制。
请求机制
Linux softirq的核心思想是“谁触发,谁执行”:触发软中断的CPU负责执行它所触发的软中断,而每个CPU都有自己的软中断触发与控制机制(但是各CPU执行的软中断服务程序是相同的)。
软中断描述符
include/linux/interrupt.h头文件中定义了数据结构softirq_action,描述软中断请求:
struct softirq_action {
void (*action)(struct softirq_action *); // 指向软中断请求的服务程序
void *data; // 指向服务程序自行解释的数据
};
基于上述软中断描述符,kernel/softirq.c中定义了一个全局数组softirq_vec[32]:
static struct softirq_action softirq_vec[32] __cacheline_aligned_in_smp;
由此可见,系统定义了32个所有CPU共享软中断描述符。
软中断触发机制
要实现“谁触发,谁执行”,必须为每个CPU定义自己的触发和控制变量。
include/asm- i386/hardirq.h头文件中定义了数据结构irq_cpustat_t,描述一个CPU的中断统计信息:
typedef struct {
unsigned int __softirq_active;
unsigned int __softirq_mask;
unsigned int __local_irq_count;
unsigned int __local_bh_count;
unsigned int __syscall_count;
unsigned int __nmi_count; /* arch dependent */
} ____cacheline_aligned irq_cpustat_t;
其中__softirq_active和__softirq_mask就是用于触发和控制软中断的成员变量:
- __softirq_active:32位的无符号整数,表示软中断向量0~31的状态。如果bit[i](0≤i≤31)为1,则表示软中断向量i在某个CPU上已经被触发而处于active状态;为0表示处于非活跃状态。
- __softirq_mask:32位的无符号整数,软中断向量的屏蔽掩码。如果bit[i](0≤i≤31)为1,则表示使能(enable)软中断向量i,为0表示该软中断向量被禁止(disabled)。
根据系统中当前的CPU个数(NR_CPUS),kernel/softirq.c文件中为每个CPU都定义了它自己的中断统计信息结构:
#if !defined(CONFIG_ARCH_S390)
irq_cpustat_t irq_stat[NR_CPUS];
#endif /* CONFIG_ARCH_S390 */
这样,每个CPU都只操作它自己的中断统计信息结构irq_stat[id],从而使各CPU之间互不影响。这个数组在include/linux/irq_cpustat.h头文件中也作了原型声明。
触发软中断请求的操作函数是__cpu_raise_softirq():
//include/linux/interrupt.h
static inline void __cpu_raise_softirq(int cpu, int nr)
{
softirq_active(cpu) |= (1<<nr);
}
其通过将相应的__softirq_active成员变量中的相应位设置为1来实现软中断触发。
此外,为保证“原子”性地完成软中断的触发过程,Linux对上述内联函数又作了高层封装raise_softirq(),但在调用之前,先通过 local_irq_save()函数来关闭当前CPU的中断并保存标志寄存器的内容,如下所示:
//include/linux/interrupt.h
static inline void raise_softirq(int nr)
{
unsigned long flags;
local_irq_save(flags);
__cpu_raise_softirq(smp_processor_id(), nr);
local_irq_restore(flags);
}
软中断分类
在软中断向量0-31中,Linux内核仅仅使用了软中断向量0-3,其余被留待系统以后扩展。
include/linux/interrupt.h头文件中对软中断向量0-3进行了预定义:
enum
{
HI_SOFTIRQ=0,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
TASKLET_SOFTIRQ
}
- HI_SOFTIRQ用于实现高优先级的软中断,如:高优先级的tasklet。
- NET_TX_SOFTIRQ和NET_RX_SOFTIRQ用于网络数据的发送与接收。
- TASKLET_SOFTIRQ则用于实现诸如tasklet这样的一般性软中断。
软中断初始化
softirq机制的初始化由函数softirq_init()完成:
void __init softirq_init()
{
int i;
for (i = 0; i < 32; i ++)
tasklet_init(bh_task_vec+i, bh_action, i);
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
该函数由内核启动例程start_kernel()所调用,初始化的过程可分为:
- 先用一个for循环来初始化用于实现BH(下半部)机制的bh_task_vec[32]数组(注意不是中断描述符)。
- 调用open_softirq()函数开启使用软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ,并将它们的软中断服务函数指针分别指向 tasklet_action()和tasklet_hi_action()。函数open_softirq()的主要作用是初始化设置软中断请求描述符softirq_vec[nr](这里才是软中断描述符)。
开启软中断向量
open_softirq()用于开启一个指定的软中断向量nr,主要做两件事情:
- 初始化设置软中断向量nr所对应的软中断描述符softirq_vec[nr]。
- 将所有CPU的软中断屏蔽掩码变量__softirq_mask中的对应位设置为1,以使能该软中断向量。
// kernel/softirq.c
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
unsigned long flags;
int i;
spin_lock_irqsave(&softirq_mask_lock, flags);
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
for (i = 0; i < NR_CPUS; i++)
softirq_mask(i) |= (1 << nr);
spin_unlock_irqrestore(&softirq_mask_lock, flags);
}
执行软中断服务
负责执行数组softirq_vec[32]中设置的软中断服务的函数是do_softirq():
asmlinkage void do_softirq()
{
int cpu = smp_processor_id();
__u32 active, mask;
// 检测当前CPU此次是否已经处于中断服务中
if (in_interrupt())
return;
// 当前CPU中断统计信息的__local_bh_count加1,表示当前CPU处于软中断服务状态
local_bh_disable();
// 关闭当前CPU的中断
local_irq_disable();
// __softirq_active和__softirq_mask中的相应位都为1时,软中断服务函数才能执行
mask = softirq_mask(cpu);
active = softirq_active(cpu) & mask;
if (active) {
// 软中断向量被触发
struct softirq_action *h;
restart:
/* Reset active bitmask before enabling irqs */
// 将当前CPU的__softirq_active位清零
softirq_active(cpu) &= ~active;
// 打开当前CPU的中断
local_irq_enable();
h = softirq_vec;
// 将局部变量mask中的相应位清零
// 目的是让do_softirq()函数内不对同一个软中断向量上的再次软中断请求进行服务,而是将等待下一次do_softirq()调用时服务,从而避免do_sottirq()函数陷入无休止的软中断服务中
mask &= ~active;
// 执行相应的软中断服务函数
do {
if (active & 1)
h->action(h);
h++;
active >>= 1;
} while (active);
// 关闭当前CPU的中断
local_irq_disable();
// 检查是否有其他软中断服务被触发
active = softirq_active(cpu);
if ((active &= mask) != 0)
goto retry;
}
// 当前CPU退出软中断服务状态
local_bh_enable();
/* Leave with locally disabled hard irqs. It is critical to close
* window for infinite recursion, while we help local bh count,
* it protected us. Now we are defenceless.
*/
return;
retry:
goto restart;
}
do_softirq()函数一开始就检查当前CPU是否已经处于中断服务中,如果是则立即返回。因此,do_softirq()函数在同一个CPU上的执行是串行的,即使有更高级的软中断触发。
总结
- Linux内核的bh是串行化的,为了解决这个问题,增加了softirq的机制。
- 几个概念的区别:
- 硬中断:由外设发起的对CPU的中断。
- 软中断:硬中断的服务程序发起的对内核的中断。
- 信号:是由内核或进程发起的对其他进程的中断。
- bh,softirq,tasklet之间的关系:
- tasklet和softirq的区别:tasklet只能在一个CPU上执行。
- tasklet和bh的区别:不同的tasklet可以同时在不同的CPU上执行。
- tasklet是在softirq机制中限制并发的一种特例
(bh和tasklet有空再整理)
参考:
- 阿里云开发者社区.内核代码阅读 - softirq和bottom half.https://developer.aliyun.com/article/801039
- Bin Watson
.Linux内核中断系统结构——软中断.https://blog.csdn.net/Bin_Watson/article/details/125860446