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

二、软中断相关操作
1. 中断处理函数?
1.1 注册一个中断处理函数
要使用软中断,必须先使用 open_softirq() 函数注册对应的软中断处理函数,open_softirq()函数原型如下:
1 | void open_softirq(int nr, void (*action)(struct softirq_action *)) |
向内核注册一个软中断,其实质是设置软中断向量表相应槽位。
参数说明:
nr:要开启的软中断。
action:软中断对应的处理函数。
返回值:无
1.2 注册的函数怎么执行?
软中断的核心处理函数是do_softirq(),它处理当前CPU上的所有软中断。中间好复杂,暂时没有详细去研究了,在arm平台,最终好像调用到 __do_softirq()
1 | asmlinkage __visible void __softirq_entry __do_softirq(void) |
2. 软中断触发机制
2.1 软中断的位图
要实现“谁触发,谁执行”的思想,就必须为每个CPU都定义它自己的触发和控制变量。为此,Linux定义了一个irq_cpustat_t 数据结构来描述一个CPU的中断信息,这个在多个头文件中都有定义:

这里以 hardirq.h - arch/arm/include/asm/hardirq.h 为例:
1 | /* number of IPIS _not_ including IPI_CPU_BACKTRACE */ |
内核中使用这个数据结构定义了一个全局变量 irq_stat:
1 |
|
irq_stat的定义我们可以展开看一下:
1 |
这里牵扯的宏有点多,我就没深挖了,大概是这样的一个变量:
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.1.2 软中断位图
内核使用一个名为 irq_cpustat_t.__softirq_pending 的位图来描述软中断,每一个位对应一个软中断。内核提供了一些宏来对这个位图进行操作。
- or_softirq_pending()用于设置相应的位(位或操作),local_softirq_pending()用于取得整个位图
1 |
- __raise_softirq_irqoff()用于实现激活软中断。
1 | void __raise_softirq_irqoff(unsigned int nr) |
2.2 软中断触发函数
触发软中断的函数为 raise_softirq()
1 | void raise_softirq(unsigned int nr) |
raise_softirq()函数激活软中断,参数nr为要触发的软中断。这里使用术语“激活”而非“调用”, 是因为在很多情况下不能直接调用软中断。所以只能快速地将其标志为“可执行”,等待未来某一时刻调用。
Tips:为什么“在很多情况下不能直接调用软中断”?试想一下下半部引入的理念,就是为了让上半部更快地执行。 如果在中断程序代码中直接调用软中断函数,那么就失去了上半部与下半部的区别,也就是失去了其存在的意义。
我们来看一下这个 raise_softirq_irqoff() 函数:
1 | inline void raise_softirq_irqoff(unsigned int nr) |
(1)最重要的,就是置相应的位图,等待将来被处理;
(2)如果此时已经没有在中断上下文中,则立即调用(其实是内核线程的唤醒操作),现在就是将来;
3. 软中断的初始化
软中断必须在编译的时候静态注册! Linux 内核使用 softirq_init() 函数初始化软中断:
1 | void __init softirq_init(void) |
softirq_init()函数默认会打开 TASKLET_SOFTIRQ 和HI_SOFTIRQ。这个部分在linux初始化的时候就做好了,我们不用再去初始化一遍了。
三、中断的下半部
前面我们知道,一开始 Linux 内核提供了“bottom half”机制来实现下半部,简称“BH”。后面引入了软中断和 tasklet 来替代“BH”机制,完全可以使用软中断和 tasklet 来替代 BH,从 2.5 版本的 Linux内核开始 BH 已经被抛弃了。其实tasklet 也是一种软中断,下面简单了解下软中断完成下半部的流程吧。
1. 软中断怎么完成中断下半部?
中断的上下半部处理流程如下:

画成流程图就是:

假设硬件中断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,找到软中断请求描述符的枚举,并添加我们自己的软中断号:

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

我们需要将这两个函数导出到符号表:
1 | void open_softirq(int nr, void (*action)(struct softirq_action *)) |
1 | void raise_softirq(unsigned int nr) |
然后重新编译镜像:
1 | 编译自己移植的开发板镜像 |
1.2 驱动源码
然后驱动的源码我们可以看这里:13_interrupt/05_nodts_soft_irq · 苏木/imx6ull-driver-demo - 码云 - 开源中国。我们在按键中断中触发一个软中断。
2. 开发板测试
我们更新内核,然后加载驱动:
1 | insmod sdriver_demo.ko |

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