Skip to content

LV030-软中断

一、软中断请求(softirq)简介

1. 什么是软中断

Linux 的 softirq 机制是与 SMP 紧密不可分的。为此,整个 softirq 机制的设计与实现中自始自终都贯彻了一个思想:“谁触发,谁执行”(Who marks,Who runs),也即触发软中断的那个 CPU 负责执行它所触发的软中断,而且每个 CPU 都有它自己的软中断触发与控制机制。这个设计思想也使得 softirq 机制充分利用了 SMP 系统的性能和特点。

2. 软中断请求描述符

Linux 在 interrupt.h - include/linux/interrupt.h 头文件中定义了数据结构 softirq_action ,来描述一个软中断请求,如下所示:

c
struct softirq_action
{
	void	(*action)(struct softirq_action *);
};

Linux 在 softirq.c - kernel/softirq.c 文件中定义了一个全局的 softirq_vec 数组:

c
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

NR_SOFTIRQS 是枚举类型:

c
enum
{
	HI_SOFTIRQ=0,  // 高优先级软中断
	TIMER_SOFTIRQ, // 定时器软中断
	NET_TX_SOFTIRQ,// 网络传输发送软中断
	NET_RX_SOFTIRQ,// 网络传输接收软中断
	BLOCK_SOFTIRQ, // 块设备软中断
	IRQ_POLL_SOFTIRQ,// 中断轮询软中断
	TASKLET_SOFTIRQ, // 任务软中断
	SCHED_SOFTIRQ,   // 调度软中断
	HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the numbering. Sigh! */
	RCU_SOFTIRQ,    /* Preferable RCU should always be the last softirq */

	NR_SOFTIRQS      // 表示软中断的总数, 用于指示软中断类型的数据
};

在这里系统一共定义了 10 个软中断请求描述符,中断号的优先级越小, 代表优先级越高。 在驱动代码中, 我们可以使用 Linux 驱动代码中上述的软中断, 当然我们也可以自己添加软中断。软中断向量 i(0≤i≤9)所对应的软中断请求描述符就是 softirq_vec [i]。这个数组是个系统全局数组,即它被所有的 CPU(对于 SMP 系统而言)所共享。这里需要注意的一点是:每个 CPU 虽然都有它自己的触发和控制机制,并且只执行自己所触发的软中断请求,但是各个 CPU 所执行的软中断服务函数却是相同的,也即都是执行 softirq_vec [ ] 数组中定义的 action 软中断服务函数。

3. 在 linux 中的表现

我们可以用以下命令查看一下现在系统中支持哪些软中断:

shell
cat /proc/softirqs
image-20250323112748234

二、软中断相关操作

1. 中断处理函数?

1.1 注册一个中断处理函数

要使用软中断,必须先使用 open_softirq() 函数注册对应的软中断处理函数,open_softirq() 函数原型如下:

c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}

向内核注册一个软中断,其实质是设置软中断向量表相应槽位。

参数说明

  • nr:要开启的软中断。

  • action:软中断对应的处理函数。

返回值】 无

1.2 注册的函数怎么执行?

软中断的核心处理函数是 do_softirq(),它处理当前 CPU 上的所有软中断。中间好复杂,暂时没有详细去研究了,在 arm 平台,最终好像调用到 __do_softirq()

c
asmlinkage __visible void __softirq_entry __do_softirq(void)
{
	//......
	h = softirq_vec; // 取得软中断向量 

	while ((softirq_bit = ffs(pending))) {
		//......
		h->action(h); // 调用软中断
		//......
	}
	//......
}

2. 软中断触发机制

2.1 软中断的位图

要实现“谁触发,谁执行”的思想,就必须为每个 CPU 都定义它自己的触发和控制变量。为此,Linux 定义了一个 irq_cpustat_t 数据结构来描述一个 CPU 的中断信息,这个在多个头文件中都有定义:

image-20260120092727951

这里以 hardirq.h - arch/arm/include/asm/hardirq.h 为例:

c
/* number of IPIS _not_ including IPI_CPU_BACKTRACE */
#define NR_IPI	7

typedef struct {
	unsigned int __softirq_pending;
#ifdef CONFIG_SMP
	unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;

内核中使用这个数据结构定义了一个全局变量 irq_stat

c
#ifndef __ARCH_IRQ_STAT
DECLARE_PER_CPU_ALIGNED(irq_cpustat_t, irq_stat);	/* defined in asm/hardirq.h */
#define __IRQ_STAT(cpu, member)	(per_cpu(irq_stat.member, cpu))
#endif

irq_stat 的定义我们可以展开看一下:

c
#define DECLARE_PER_CPU_ALIGNED(type, name)				\
	DECLARE_PER_CPU_SECTION(type, name, PER_CPU_ALIGNED_SECTION)	\
	____cacheline_aligned

这里牵扯的宏有点多,我就没深挖了,大概是这样的一个变量:

c
irq_cpustat_t irq_stat[NR_CPUS] __cacheline_aligned;

NR_CPUS 为系统中 CPU 个数,这样,每个 CPU 都只操作它自己的中断统计信息结构。假设有一个编号为 id 的 CPU,那么它只能操作它自己的中断统计信息结构 irq_stat[id](0≤id≤NR_CPUS-1),从而使各 CPU 之间互不影响。

2.1.1 怎么确定是哪个 CPU 的?

irq_cpustat_t 中的 IPI 表示处理器间的中断(Inter-Processor Interrupts):

c
#define NR_IPI	7

typedef struct {
	unsigned int __softirq_pending;
#ifdef CONFIG_SMP
	unsigned int ipi_irqs[NR_IPI];
#endif
} ____cacheline_aligned irq_cpustat_t;
2.1.2 软中断位图

内核使用一个名为 irq_cpustat_t.__softirq_pending 的位图来描述软中断,每一个位对应一个软中断。内核提供了一些宏来对这个位图进行操作。

c
#ifndef local_softirq_pending_ref
#define local_softirq_pending_ref irq_stat.__softirq_pending
#endif

#define local_softirq_pending()	(__this_cpu_read(local_softirq_pending_ref))
#define set_softirq_pending(x)	(__this_cpu_write(local_softirq_pending_ref, (x)))
#define or_softirq_pending(x)	(__this_cpu_or(local_softirq_pending_ref, (x)))
c
void __raise_softirq_irqoff(unsigned int nr)
{
	trace_softirq_raise(nr);
	or_softirq_pending(1UL << nr);
}

2.2 软中断触发函数

触发软中断的函数为 raise_softirq()

c
void raise_softirq(unsigned int nr)
{
	unsigned long flags;

	local_irq_save(flags);
	raise_softirq_irqoff(nr);
	local_irq_restore(flags);
}

raise_softirq()函数激活软中断,参数 nr 为要触发的软中断。这里使用术语“激活”而非“调用”, 是因为在很多情况下不能直接调用软中断。所以只能快速地将其标志为“可执行”,等待未来某一时刻调用。

Tips:为什么“在很多情况下不能直接调用软中断”?试想一下下半部引入的理念,就是为了让上半部更快地执行。 如果在中断程序代码中直接调用软中断函数,那么就失去了上半部与下半部的区别,也就是失去了其存在的意义。

我们来看一下这个 raise_softirq_irqoff() 函数:

c
inline void raise_softirq_irqoff(unsigned int nr)
{
	__raise_softirq_irqoff(nr); //置位图,即标记为可执行状态

	/*
	 * If we're in an interrupt or softirq, we're done
	 * (this also catches softirq-disabled code). We will
	 * actually run the softirq once we return from
	 * the irq or softirq.
	 *
	 * Otherwise we wake up ksoftirqd to make sure we
	 * schedule the softirq soon.
	 */
    // 设置了位图后,可以判断是否已经没有在中断上下文中了,如果没有,则是一个立即调用软中断的好时机。
	if (!in_interrupt())   // in_interrupt 另一个作用是判断软中断是否被禁用。
		wakeup_softirqd(); // wakeup_softirqd 唤醒软中断的守护进程 ksoftirq。
}

(1)最重要的,就是置相应的位图,等待将来被处理;

(2)如果此时已经没有在中断上下文中,则立即调用(其实是内核线程的唤醒操作),现在就是将来;

3. 软中断的初始化

软中断必须在编译的时候静态注册! Linux 内核使用 softirq_init() 函数初始化软中断:

c
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);
}

softirq_init() 函数默认会打开 TASKLET_SOFTIRQ 和 HI_SOFTIRQ。这个部分在 linux 初始化的时候就做好了,我们不用再去初始化一遍了。

三、中断的下半部

前面我们知道,一开始 Linux 内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和 tasklet 来替代“BH”机制,完全可以使用软中断和 tasklet 来替代 BH,从 2.5 版本的 Linux 内核开始 BH 已经被抛弃了。其实 tasklet 也是一种软中断,下面简单了解下软中断完成下半部的流程吧。

1. 软中断怎么完成中断下半部?

中断的上下半部处理流程如下:

image-20250318155714988

画成流程图就是:

1.iodraw

假设硬件中断 A 的上半部函数为 irq_top_half_A ,下半部为 irq_bottom_half_A。

  • 硬件中断 A 处理过程中,没有其他中断发生:

(1)一开始, preempt_count = 0;

(2)上述流程图 ①~⑨ 依次执行,上半部、下半部的代码各执行一次。

  • 硬件中断 A 处理过程中,又再次发生了中断 A

(1)一开始, preempt_count = 0;

(2)执行到第 ⑥ 时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表。注意:这时 preempt_count 等于 1,并且中断下半部的代码并未执行。

(3)CPU 又从 ① 开始再次执行中断 A 的上半部代码:

(4)在第 ① 步 preempt_count 等于 2;

(5)在第 ③ 步 preempt_count 等于 1;

(6)在第 ④ 步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理

注意:第 2 次中断发生后,打断了第一次中断的第 ⑦ 步处理。当第 2 次中断处理完毕, CPU 会继续去执行第 ⑦ 步。 可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是 下半部代码只执行了一次

所以,同一个中断的上半部、下半部,在执行时是多对一的关系

  • 硬件中断 A 处理过程中,又再次发生了中断 B

(1)一开始, preempt_count = 0;

(2)执行到第 ⑥ 时,一开中断后,中断 B 又再次使得 CPU 跳到中断向量表。注意:这时 preempt_count 等于 1,并且中断 A 下半部的代码并未执行。

(3)CPU 又从 ① 开始再次执行中断 B 的上半部代码:

(4)在第 ① 步 preempt_count 等于 2;

(5)在第 ③ 步 preempt_count 等于 1;

(6)在第 ④ 步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理

注意:第 2 次中断发生后,打断了第一次中断 A 的第 ⑦ 步处理。当第 2 次中断 B 处理完毕, CPU 会继续去执行第 ⑦ 步。在第 ⑦ 步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。

所以,多个中断的下半部,是汇集在一起处理的。

总结

(1)中断的处理可以分为上半部,下半部

(2)中断上半部,用来处理紧急的事,它是在关中断的状态下执行的

(3)中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行

(4)中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断

(5)中断上半部执行完后,触发中断下半部的处理

(6)中断上半部、下半部的执行过程中,不能休眠。(中断休眠的话,以后谁来调度进程?)

四、软中断 demo

1. demo 源码

1.1 linux 内核源码修改

这个我们要添加一个自定义软中断,我们打开 interrupt.h - include/linux/interrupt.h,找到软中断请求描述符的枚举,并添加我们自己的软中断号:

image-20250323110944722

还有一个地方也要改一下,如下图编译驱动的时候会有警告,open_softirq 和 raise_softirq 没有被定义, 但是为什么还会提示这样的错误呢?这是因为 Linux 内核开发者不希望驱动工程师擅自在枚举类型中添加软中断。

image-20250323113247086

我们需要将这两个函数导出到符号表:

c
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
	softirq_vec[nr].action = action;
}
EXPORT_SYMBOL(open_softirq);
c
void raise_softirq(unsigned int nr)
{
	unsigned long flags;

	local_irq_save(flags);
	raise_softirq_irqoff(nr);
	local_irq_restore(flags);
}
EXPORT_SYMBOL(raise_softirq);

然后重新编译镜像:

shell
# 编译自己移植的开发板镜像
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- clean
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- imx_alpha_emmc_defconfig
# make ARCH = arm CROSS_COMPILE = arm-linux-gnueabihf- all -j16 # 全编译
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- zImage -j16 # 只编译内核镜像
# make ARCH = arm CROSS_COMPILE = arm-linux-gnueabihf- dtbs -j16   # 只编译所有的设备树
# make ARCH = arm CROSS_COMPILE = arm-linux-gnueabihf- imx6ull-alpha-emmc.dtb -j16 # 只编译指定的设备树

cp -avf arch/arm/boot/zImage ~/3tftp

1.2 驱动源码

然后驱动的源码我们可以看这里:13_interrupt/05_nodts_soft_irq。我们在按键中断中触发一个软中断。

2. 开发板测试

我们更新内核,然后加载驱动:

c
insmod sdriver_demo.ko
image-20250323114255189

然后我们按下按键,就会发现,按键的中断触发了,软中断也触发了:

image-20250323114328644