软中断 简介

前言

中断服务程序往往实在CPU关中断的条件下执行的,以避免中断嵌套而使控制复杂化。但是CPU关中断的时间不能太长,否则会丢失中断信号。因此Linux将中断服务程序分为“上半部”和“下半部”。前者对时间要求比较严格,必须在中断请求发生后立即(或者一定的时间限制内)完成,为了保证这种处理能原子的完成,上半部通常是在CPU关中断的条件下执行的:从IDT(中断描述符表)中登记的中断入口函数一直到驱动程序注册在中断服务队列中的ISR(中断服务程序)。下半部是上半部根据需要调度执行的,这些处理允许延迟,通常是在CPU开中断的的条件下执行的。
但是,中断下半部有两个缺点:

  1. 同一时刻,只能有一个CPU执行下半部代码,以防止多CPU同时执行而相互干扰,因此下半部代码的执行是“串行化”的。
  2. 下半部函数不允许嵌套。

这两个缺点在单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就是用于触发和控制软中断的成员变量:

  1. __softirq_active:32位的无符号整数,表示软中断向量0~31的状态。如果bit[i](0≤i≤31)为1,则表示软中断向量i在某个CPU上已经被触发而处于active状态;为0表示处于非活跃状态。
  2. __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()所调用,初始化的过程可分为:

  1. 先用一个for循环来初始化用于实现BH(下半部)机制的bh_task_vec[32]数组(注意不是中断描述符)。
  2. 调用open_softirq()函数开启使用软中断向量TASKLET_SOFTIRQ和HI_SOFTIRQ,并将它们的软中断服务函数指针分别指向 tasklet_action()和tasklet_hi_action()。函数open_softirq()的主要作用是初始化设置软中断请求描述符softirq_vec[nr](这里才是软中断描述符)。

开启软中断向量

open_softirq()用于开启一个指定的软中断向量nr,主要做两件事情:

  1. 初始化设置软中断向量nr所对应的软中断描述符softirq_vec[nr]。
  2. 将所有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有空再整理)


参考:

  1. 阿里云开发者社区.内核代码阅读 - softirq和bottom half.https://developer.aliyun.com/article/801039
  2. Bin Watson
    .Linux内核中断系统结构——软中断.https://blog.csdn.net/Bin_Watson/article/details/125860446
  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值