LV06-13-中断-03-软中断

什么是软中断?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

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

1. 什么是软中断

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

2. 软中断请求描述符

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

1
2
3
4
struct softirq_action
{
void (*action)(struct softirq_action *);
};

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

1
static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp;

NR_SOFTIRQS是枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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中的表现

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

1
cat /proc/softirqs
image-20250323112748234

二、软中断相关操作

1. 中断处理函数?

1.1 注册一个中断处理函数

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

1
2
3
4
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()

1
2
3
4
5
6
7
8
9
10
11
12
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-20250215092439483

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

1
2
3
4
5
6
7
8
9
/* 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

1
2
3
4
#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的定义我们可以展开看一下:

1
2
3
#define DECLARE_PER_CPU_ALIGNED(type, name)				\
DECLARE_PER_CPU_SECTION(type, name, PER_CPU_ALIGNED_SECTION) \
____cacheline_aligned

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

1
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):

1
2
3
4
5
6
7
8
#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 的位图来描述软中断,每一个位对应一个软中断。内核提供了一些宏来对这个位图进行操作。

1
2
3
4
5
6
7
#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)))
1
2
3
4
5
void __raise_softirq_irqoff(unsigned int nr)
{
trace_softirq_raise(nr);
or_softirq_pending(1UL << nr);
}

2.2 软中断触发函数

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

1
2
3
4
5
6
7
8
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() 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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() 函数初始化软中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

画成流程图就是:

假设硬件中断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

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

1
2
3
4
5
void open_softirq(int nr, void (*action)(struct softirq_action *))
{
softirq_vec[nr].action = action;
}
EXPORT_SYMBOL(open_softirq);
1
2
3
4
5
6
7
8
9
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);

然后重新编译镜像:

1
2
3
4
5
6
7
8
9
# 编译自己移植的开发板镜像
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 · 苏木/imx6ull-driver-demo - 码云 - 开源中国。我们在按键中断中触发一个软中断。

2. 开发板测试

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

1
insmod sdriver_demo.ko
image-20250323114255189

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

image-20250323114328644