LV06-13-中断-01-中断基础
什么是中断?若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
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源码 |
一、异常与中断
1. 什么是中断?
中断是指在 CPU 正常运行期间, 由外部或内部事件引起的一种机制。 当中断发生时, CPU会停止当前正在执行的程序, 并转而执行触发该中断的中断处理程序。 处理完中断处理程序后, CPU 会返回到中断发生的地方, 继续执行被中断的程序。 中断机制允许 CPU 在实时响应外部或内部事件的同时, 保持对其他任务的处理能力。
举个例子,有这样的一个场景,我们正在看书,这就是我们主要的任务,现在发生了下边的事:
(1)厨房灶台上的水烧开了
(2)有电话到来
这些事情发生的时候,我们就不得不去处理,我们该怎么办?
(1)书中放上书签,合上书(保存现场,以免一会回来找不到自己看到哪里了)
(2)处理刚才的两件事,水烧开了就将煤气关掉,把开水装起来,电话到来,就接听电话,先做哪个?那就看哪个更紧急啦。处理这两件事就要“中断”前面看书的过程。
(3)回来继续看书(恢复现场)。
如下图:

2. 中断有什么用?
在上面的场景中, 作为唯一具有处理能力的主体, 我们一次只能专注于一个任务, 可以等待水烧开、 看书等等。 然而, 当我们专心致志地完成一项任务时, 常常会有紧迫或不紧迫的其他事情突然出现, 需要我们关注和处理。 有些情况甚至要求我们立即停下手头的工作来应对。只有在处理完这些中断事件之后, 我们才能回到先前的任务。
中断机制赋予了我们处理意外情况的能力, 而且如果我们能充分利用这个机制, 就能够同时完成多个任务。 回到烧水的例子, 无论我们是否在厨房, 煤气灶都会将水烧开。 我们只需要在水烧开后及时关掉煤气。 为了避免在厨房等待的时间, 而水烧开时产生的声音就是中断信号,提醒我们炉子上的水已经烧开。 这样, 我们就可以在等待的时间里做其他事情, 比如看书。当水壶烧开发出声音之后, 它会打断当前的任务, 提醒水已经烧开, 这时只需要前往厨房关掉煤气即可。
中断机制使我们能够有条不紊地同时处理多个任务, 从而提高了并发处理能力。 类似地,计算机系统中也使用中断机制来应对各种外部事件。 例如, 在键盘输入时, 会发送一个中断信号给 CPU, 以便及时响应用户的操作。 这样, CPU 就不必一直轮询键盘的状态, 而可以专注于其他任务。 中断机制还可以用于处理硬盘读写完成、 网络数据包接收等事件, 提高了系统的资源利用率和并发处理能力。
3. 什么是异常?
由CPU内部产生的意外事件被称为异常,也叫内中断。异常是CPU执行一条指令时,由CPU在其内部检测到的、与正在执行的指令相关的同步事件;中断是一种典型的由外部设备触发的、与当前正在执行的指令无关的异步事件。
这里我们不深入了解异常,因为这里主要是学习中断。
4. 中断的处理流程
arm 对异常(中断)处理过程:
(1)中断相关的初始化:(a) 设置中断源,让它可以产生中断。(b) 设置中断控制器(可以屏蔽某个中断,优先级)。(c) 设置 CPU 总开关(使能中断)。
(2)执行其他程序:正常程序
(3)产生中断:比如按下按键→中断控制器→CPU
(4)CPU 每执行完一条指令都会检查有无中断/异常产生
(5)CPU 发现有中断/异常产生,开始处理。对于不同的异常,跳去不同的地址执行程序。这些地址上,只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。(3)(4)(5)都是硬件做的。这些函数做什么事情?软件做的事情:(a) 保存现场(各种寄存器)(b) 处理异常(中断):分辨中断源,再调用不同的处理函数(c) 恢复现场
二、中断子系统框架
一个完整的中断子系统框架可以分为四个层次, 由上到下分别为用户层、 通用层、 硬件相关层和硬件层, 每个层相关的介绍如下:

用户层: 用户层是中断的使用者, 主要包括各类设备驱动。 这些驱动程序通过中断相关的接口进行中断的申请和注册。 当外设触发中断时, 用户层驱动程序会进行相应的回调处理, 执行特定的操作。
通用层: 通用层也可称为框架层, 它是硬件无关的层次。 通用层的代码在所有硬件平台上都是通用的, 不依赖于具体的硬件架构或中断控制器。 通用层提供了统一的接口和功能, 用于管理和处理中断, 使得驱动程序能够在不同的硬件平台上复用。
硬件相关层: 硬件相关层包含两部分代码。 一部分是与特定处理器架构相关的代码, 比如ARM64 处理器的中断处理相关代码。 这些代码负责处理特定架构的中断机制, 包括中断向量表、 中断处理程序等。 另一部分是中断控制器的驱动代码, 用于与中断控制器进行通信和配置。这些代码与具体的中断控制器硬件相关。
硬件层: 硬件层位于最底层, 与具体的硬件连接相关。 它包括外设与 SoC(系统片上芯片)的物理连接部分。 中断信号从外设传递到中断控制器, 由中断控制器统一管理和路由到处理器。硬件层的设计和实现决定了中断信号的传递方式和硬件的中断处理能力。
1. 中断控制器 GIC
从硬件角度来看,中断由CPU、中断控制器(Interrupt Controller),其他外设 组成。各个外设在硬件上是通过中断线(irq request line)与CPU相连的,在复杂的系统中,外设比较多的情况下,就需要一个中断控制器来协助CPU进行中断的处理,比如ARM架构下的GIC,或者X86架构中的APIC。根据外设的多少,Interrupt Controller可以级联。
1.1 GIC简介
中断控制器 GIC(Generic Interrupt Controller) 是中断子系统框架硬件层中的一个关键组件,用于管理和控制中断。 它接收来自各种中断源的中断请求, 并根据预先配置的中断优先级、 屏蔽和路由规则, 将中断请求分发给适当的处理器核心或中断服务例程。
GIC 是由 ARM 公司提出设计规范, 当前有四个版本, GIC V1-V4(V2最多支持8个ARM core,V3/V4支持更多的ARM core,主要用于ARM64服务器系统结构)。 设计规范中最常用的, 有3 个版本 V2.0、 V3.1、 V4.1, GICv3 版本设计主要运行在 Armv8-A, Armv9-A 等架构上。 ARM 公司并给出一个实际的控制器设计参考, 比如 GIC-400(支持 GIC v2 架构)、 gic500(支持 GIC v3 架构)、 GIC-600(支持 GIC v3 和 GIC v4 架构)。 最终芯片厂商可以自己实现 GIC 或者直接购买 ARM提供的设计。
具体GIC硬件的实现形态有两种,一种是在ARM vensor研发自己的SOC的时候,会向ARM公司购买GIC的IP,这些IP包括的型号有:PL390,GIC-400,GIC-500。其中GIC-500最多支持128个 cpu core,它要求ARM core必须是ARMV8指令集的(例如Cortex-A57),符合GIC architecture specification version 3。另外一种形态是ARM vensor直接购买ARM公司的Cortex A9或者A15的IP,Cortex A9或者A15中会包括了GIC的实现,当然,这些实现也是符合GIC V2的规格。
每个 GIC 版本及相应特性如下表
版本 | 关键特性 | 常用核心 |
GICv1 | -支持最多八个处理器核心(PE) - 支持最多 1020 个中断 ID | ARM Cortex-A5 MPCore ARM Cortex-A9 MPCore ARM Cortex-R7 MPCore |
GICv2 | - GICv1 的所有关键特性 -支持虚拟化 | ARM Cortex-A7 MPCore ARM Cortex-A15 MPCore ARM Cortex-A53 MPCore ARM Cortex-A57 MPCore |
GICv3 | - GICv2 的所有关键特性 -支持超过 8 个处理器核心 -支持基于消息的中断 -支持超过 1020 个中断 ID - CPU 接口寄存器的系统寄存器访问 -增强的安全模型, 分离安全和非安全的 Group 1 中断 | ARM Cortex-A53MPCore ARM Cortex-A57MPCore ARM Cortex-A72 MPCore |
GICv4 | - GICv3 的所有关键特性 -虚拟中断的直接注入 | ARM Cortex-A53 MPCore ARMCortex-A57MPCore ARM Cortex-A72 MPCore |
这里拿RK3568(因为这里刚好有张图,比较容易理解)的举个例子,在 RK3568 上使用的 GIC 版本为 GICv3, 相应的中断控制器模型:

GIC相关文档可以看这里:
Learn the architecture - Generic Interrupt Controller v3 and v4, Overview
ARM Generic Interrupt Controller Architecture Specification - Version 2.0 (B.b)
GIC 中断控制器可以分为 Distributor 接口、 Redistributor 接口和 CPU 接口。
1.1.1 Distributor 中断仲裁器
含影响所有处理器核心中断的全局设置。 包含以下编程接口 :
启用和禁用 SPI。
设置每个 SPI 的优先级级别。
每个 SPI 的路由信息。
将每个 SPI 设置为电平触发或边沿触发。
生成基于消息的 SPI。
控制 SPI 的活动和挂起状态。
用于确定在每个安全状态中使用的程序员模型的控制(亲和性路由或遗留模型) 。
1.1.2 Redistributor 重新分配器
对于每个连接的处理器核心(PE) , 都有一个重新分配器(Redistributor) 。 重新分配器提供以下编程接口:
启用和禁用 SGI(软件生成的中断) 和 PPI(处理器专用中断) 。
设置 SGI 和 PPI 的优先级级别。
将每个 PPI 设置为电平触发或边沿触发。
将每个 SGI 和 PPI 分配给一个中断组。
控制 SGI 和 PPI 的状态。
对支持关联 LP(I 低功耗中断)的中断属性和挂起状态的内存中的数据结构进行基址控制。
支持与连接的处理器核心的电源管理。
1.1.3 CPU 接口
每个重新分配器都连接到一个 CPU 接口。 CPU 接口提供以下编程接口:
- 通用控制和配置, 用于启用中断处理。
- 确认中断。
- 执行中断的优先级降低和停用。
- 为处理器核心设置中断优先级屏蔽。
- 定义处理器核心的抢占策略。
- 确定处理器核心最高优先级的挂起中断。
1.2 中断类型
GIC-V3 支持四种类型的中断, 分别是 SGI、 PPI、 SPI 和 LPI, 每个中断类型的介绍如下:
SGI(Software Generated Interrupt, 软件生成中断) : SGI 是通过向 GIC 中的 SGI 寄存器写入来生成的中断。 它通常用于处理器之间的通信, 允许一个 PE 发送中断给一个或多个指定的 PE, 中断号 ID0 - ID15 用于 SGI。
PPI(I Private Peripheral Interrupt, 私有外设中断) : 针对特定 PE 的外设中断。 不与其他 PE共享, 中断号 ID16 - ID31 用于 PPI。
SPI(Shared Peripheral Interrupt, 共享外设中断) : 全局外设中断, 可以路由到指定的处理器核心(PE) 或一组 PE, 它允许多个 PE 接收同一个中断。 中断号 ID32 - ID1019 用于 SPI。
LPI(Locality-specific Peripheral Interrupt, 特定局部外设中断) : LPI 是 GICv3 中引入的一种中断类型, 与其他类型的中断有几个不同之处。 LPI 总是基于消息的中断, 其配置存储在内存表中, 而不是寄存器中。
INTID 范围 | 中断类型 | 备注 |
---|---|---|
0 - 15 | SGI(软件生成中断) | 每个核心分别存储 |
16 - 31 | PPI(私有外设中断) | 每个核心分别存储 |
32 - 1019 | SPI(共享外设中断) | |
1020 - 1023 | 特殊中断号 | 用于表示特殊情况 |
1024 - 8191 | 保留 | |
8192 及更大 | LPI(特定局部外设中断) | 上限由实现定义 |
2. 中断处理的状态
中断处理的状态机如下图:

- Inactive(非活动状态) : 中断源当前未被触发。
- Pending(等待状态) : 中断源已被触发, 但尚未被处理器核心确认。
- Active(活动状态) : 中断源已被触发, 并且已被处理器核心确认。
- Active and Pending(活动且等待状态) : 已确认一个中断实例, 同时另一个中断实例正在等待处理。
每个外设中断可以是以下两种类型之一 :
边沿触发(Edge-triggered) :这是一种在检测到中断信号上升沿时触发的中断, 然后无论信号状态如何, 都保持触发状态, 直到满足本规范定义的条件来清除中断。
电平触发(Level-sensitive) :这是一种在中断信号电平处于活动状态时触发的中断, 并且在电平不处于活动状态时取消触发。
3. 两个中断号?
在 linux 内核中, 我们使用 IRQ number 和 HW interrupt ID 两个 ID 来标识一个来自外设的中断:
IRQ number: CPU 需要为每一个外设中断编号, 我们称之 IRQ Number。 这个 IRQ number是一个虚拟的 interrupt ID, 和硬件无关, 仅仅是被 CPU 用来标识一个外设中断。 我们后面使用imx6ull的时候,申请的GPIO1_IO18对应的中断号就是79,但是根据参考手册,上面就事99,这就是不一样的,这里的99应该是下面要提到的HW interrupt ID。
HW interrupt ID: 对于 GIC 中断控制器而言, 它收集了多个外设的 interrupt request line 并向上传递, 因此, GIC 中断控制器需要对外设中断进行编码。 GIC 中断控制器用 HW interrupt ID来标识外设的中断。 如果只有一个 GIC 中断控制器, 那 IRQ number 和 HW interrupt ID 是可以一一对应的, 如下图

但如果是在 GIC 中断控制器级联的情况下, 仅仅用 HW interrupt ID 就不能唯一标识一个外设中断, 还需要知道该 HW interrupt ID 所属的 GIC 中断控制器( HW interrupt ID 在不同的Interrupt controller 上是会重复编码的) :

这样, CPU 和中断控制器在标识中断上就有了一些不同的概念, 但是, 对于驱动开发者而言, 我们和 CPU 视角是一样的, 我们只希望得到一个 IRQ number, 而不关系具体是那个 GIC中断控制器上的那个 HW interrupt ID。 这样一个好处是在中断相关的硬件发生变化的时候, 驱动软件不需要修改。 因此, linux kernel 中的中断子系统需要提供一个将 HW interrupt ID 映射到IRQ number 上来的机制, 也就是 irq domain。
Linux kernel中使用IRQ domain来描述一个中断控制器所管理的中断源。也就是说,每个中断控制器都有自己的domain,可以将IRQ Domain看作是Interrupt controller的软件抽象。
这里的Interrupt controller并不仅仅是指传统意义上的中断控制器,如GIC,也可以代表一种“虚拟”的中断控制器,如GPIO 控制器。GPIO控制器也可以注册一个IRQ domain来管理GPIO中断,所以它也可以实现成为一个虚拟的中断控制器。
4. 从源码上看看这个框架
4.1 irqchip_init()
按照这个调用关系可以找到irqchip_init()函数:start_kernel()→init_IRQ()→irqchip_init()(有设备树的情况)
1 | void __init irqchip_init(void) |
4.2 __irqchip_of_table
__irqchip_of_table是什么?这里有它的声明:
1 | extern struct of_device_id __irqchip_of_table[]; |
以通用的 irq-gic.c - drivers/irqchip/irq-gic.c 为例,通过IRQCHIP_DECLARE这个宏定义若干个静态的struct of_device_id常量:

编译系统会把所有的IRQCHIP_DECLARE宏定义的数据放入到一个特殊的section中(__irqchip_of_table),我们称这个特殊的section叫做irq chip table,这个table也就保存了kernel支持的所有的中断控制器的ID信息:
1 |
我们以下面这个 cortex_a15_gic 为例看一下:
1 | IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init); |
对应的compatible为”arm,gic-400”,data字段这是 gic_of_init()函数。我们打开我们的imx6ul.dtsi,找到对应的设备树节点为:
1 | intc: interrupt-controller@a01000 { |
4.3 of_irq_init()
of_irq_init()函数定义如下:
1 | void __init of_irq_init(const struct of_device_id *matches) |
在machine driver初始化的时候会调用of_irq_init()函数,在该函数中会扫描所有interrupt controller的节点,并调用适合的interrupt controller driver进行初始化,根据里面配置的级联中断信息按顺序做好映射的工作。毫无疑问,初始化需要注意顺序,首先初始化root,然后first level,second level,最后是leaf node。
4.4 gic_of_init()
继续跟踪desc->irq_init_cb的回调函数gic_of_init():
1 | int __init gic_of_init(struct device_node *node, struct device_node *parent) |
值得说明的是,root GIC不会执行执行irq_of_parse_and_map函数,关于irq_of_parse_and_map函数,我们放到后面叙述。
4.5 说明
后面的部分有点多,感觉不是现在学习的重点,可以参考下面的文章:
嵌入式Linux驱动笔记(二十七)——中断子系统框架分析_irq: type mismatch-CSDN博客
Linux kernel的中断子系统之(二):IRQ Domain介绍
linux kernel的中断子系统之(七):GIC代码分析
Linux中断子系统框架流程详解(基于Kernel 3.16,arm,设备树)_linux 硬件中断详细流程处理视频-CSDN博客
三、Linux 系统对中断的处理
1. 进程、线程、中断的核心:栈
中断谁?中断当前正在运行的进程、线程。进程、线程是什么?内核如何切换进程、线程、中断?要理解这些概念,必须理解栈的作用。
1.1 ARM 处理器程序运行的过程
ARM 芯片属于精简指令集计算机(RISC: Reduced Instruction Set Computing),它所用的指令比较简单,有如下特点:① 对内存只有读、写指令;② 对于数据的运算是在 CPU 内部实现;③ 使用 RISC 指令的 CPU 复杂度小一点,易于设计。
比如对于 a=a+b 这样的算式,需要经过下面 4 个步骤才可以实现:

细看这几个步骤,有些疑问:
① 读 a,那么 a 的值读出来后保存在 CPU 里面哪里?
② 读 b,那么 b 的值读出来后保存在 CPU 里面哪里?
③ a+b 的结果又保存在哪里?

我们需要深入 ARM 处理器的内部。简单概括如下,我们先忽略各种 CPU 模式(系统模式、用户模式等等)。 CPU 运行时,先去取得指令,再执行指令:
① 把内存 a 的值读入 CPU 寄存器 R0
② 把内存 b 的值读入 CPU 寄存器 R1
③ 把 R0、 R1 累加,存入 R0
④ 把 R0 的值写入内存 a
1.2 程序被中断时,怎么保存现场
从上图可知, CPU 内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。保存在哪里?内存,这块内存就称之为栈。程序要继续执行,就先从栈中恢复那些 CPU 内部寄存器的值。这个场景并不局限于中断,下图可以概括程序 A、 B 的切换过程,其他情况是类似的:

- (1)函数调用:
在函数 A 里调用函数 B,实际就是中断函数 A 的执行。那么需要把函数 A 调用 B 之前瞬间的 CPU 寄存器的值,保存到栈里;再去执行函数 B;函数 B 返回之后,就从栈中恢复函数 A 对应的 CPU 寄存器值,继续执行。
- (2)中断处理
进程 A 正在执行,这时候发生了中断。CPU 强制跳到中断异常向量地址去执行,这时就需要保存进程 A 被中断瞬间的 CPU 寄存器值,可以保存在进程 A 的内核态栈,也可以保存在进程 A 的内核结构体中。中断处理完毕,要继续运行进程 A 之前,恢复这些值。
- (3)进程切换
在所谓的多任务操作系统中,我们以为多个程序是同时运行的。如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序依次执行一小段时间,进程 A 的时间用完了,就切换到进程 B。怎么切换?切换过程是发生在内核态里的,跟中断的处理类似。进程 A 的被切换瞬间的 CPU 寄存器值保存在某个地方;恢复进程 B 之前保存的 CPU 寄存器值,这样就可以运行进程 B 了。所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。 进程的调度也是使用栈来保存、恢复现场:

1.3 进程、线程的概念
假设我们写一个音乐播放器,在播放音乐的同时会根据按键选择下一首歌。把事情简化为 2 件事:发送音频数据、读取按键。那可以这样写程序:
1 | int main(int argc, char **argv) |
这个程序只有一条主线,读按键、播放音乐都是顺序执行。无论按键是否被按下, read_key 函数必须马上返回,否则会使得后续的send_music 受到阻滞导致音乐播放不流畅。
读取按键、播放音乐能否分为两个程序进行?可以,但是开销太大:读按键的程序,要把按键通知播放音乐的程序,进程间通信的效率没那么高。
这时可以用多线程之编程,读取按键是一个线程,播放音乐是另一个线程,它们之间可以通过全局变量传递数据,示意代码如下:
1 | int g_key; |
这样,按键的读取及 GUI 显示、音乐的播放,可以分开来,不必混杂在一起。按键线程可以使用阻塞方式读取按键,无按键时是休眠的,这可以节省 CPU资源。
音乐线程专注于音乐的播放和控制,不用理会按键的具体读取工作。并且这 2 个线程通过全局变量 g_key 传递数据,高效而简单。
在 Linux 中:资源分配的单位是进程,调度的单位是线程。也就是说,在一个进程里,可能有多个线程,这些线程共用打开的文件句柄、全局变量等等。而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈。如下图示:

2. Linux 系统对中断处理的演进
Linux 中断系统的变化并不大。比较重要的就是引入了 threaded irq:使用内核线程来处理中断。Linux 系统中有硬件中断,也有软件中断。中断的执行需要快速响应, 但并不是所有中断都能迅速完成。
此外, Linux 中的中断不支持嵌套, 意味着在正式处理中断之前会屏蔽其他中断, 直到中断处理完成后再重新允许接收中断, 如果中断处理时间过长, 将会引发问题。所以对中断的处理有 2 个原则:不能嵌套,越快越好。
2.1 Linux 对中断的扩展:硬件中断、软件中断
2.1.1 硬件中断
Linux 系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为“硬件中断” (hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。
为方便理解,我们可以先认为对硬件中断的处理是用数组来实现的,数组里存放的是函数指针:

注意:上图是简化的, Linux 中这个数组复杂多了。当发生 A 中断时,对应的 irq_function_A 函数被调用。硬件导致该函数被调用。
2.1.2 软件中断
相对的,还可以人为地制造中断:软件中断(soft irq),如下图所示:

注意:上图是简化的, Linux 中这个数组复杂多了。
问题来了:
(1)软件中断何时生产? 由软件决定,对于 X 号软件中断,只需要把它的 flag 设置为 1 就表示发生了该中断。
(2)软件中断何时处理? 软件中断嘛,并不是那么十万火急,有空再处理它好了。什么时候有空?不能让它一直等吧?Linux 系统中,各种硬件中断频繁发生,至少定时器中断每 10ms 发生一次,那取个巧?在处理完硬件中断后,再去处理软件中断?就这么办!
(3)有哪些软件中断?可以看这个文件 interrupt.h - include/linux/interrupt.h
1 | enum |
这些中断定义在一个数组中:softirq_vec
1 | static struct softirq_action softirq_vec[NR_SOFTIRQS] __cacheline_aligned_in_smp; |
可以看出,一共有 10 个软中断,因此 NR_SOFTIRQS 为 10,因此数组 softirq_vec 有 10 个元素。 softirq_action 结构体中的 action 成员变量就是软中断的服务函数,数组 softirq_vec 是个全局数组,因此所有的 CPU(对于 SMP 系统而言)都可以访问到,每个 CPU 都有自己的触发和控制机制,并且只执行自己所触发的软中断。但是各个 CPU 所执行的软中断服务函数确是相同的,都是数组 softirq_vec 中定义的 action 函数。
(4)怎么触发软件中断?最核心的函数是 raise_softirq(),简单地理解就是设置 softirq_veq[nr]的标记位:
1 | void raise_softirq(unsigned int nr); |
其中nr表示要触发的软中断。
(5)怎么设置软件中断的处理函数?可以用 open_softirq()来设置。
1 | void open_softirq(int nr, void (*action)(struct softirq_action *)); |
nr表示要开启的软中断,action表示软中断对应的处理函数。该函数没有返回值。
后面会学习到中断下半部 tasklet ,它就是使用软件中断实现的。
2.2 中断处理原则 1:不能嵌套
kernel内核官网资料:genirq: Run irq handlers with interrupts disabled - kernel/git/torvalds/linux.git - Linux kernel source tree
中断处理函数需要调用 C 函数,这就需要用到栈。中断 A 正在处理的过程中,假设又发生了中断 B,那么在栈里要保存 A 的现场,然后处理 B。在处理 B 的过程中又发生了中断 C,那么在栈里要保存 B 的现场,然后处理C。
如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。所以,为了防止这种情况发生,也是为了简单化中断的处理,在 Linux 系统上中断无法嵌套:即当前中断 A 没处理完之前,不会响应另一个中断 B(即使它的优先级更高)。
2.3 中断处理原则 2:越快越好
还是之前的例子,我们看书的时候,手机响起, 发出紧急电话的铃声, 打破了我们看书的过程,接电话的时间很短并不会对看书产生很大的影响, 而接电话的时候水烧开了的话可能就有问题了, 接电话时间过长的话,水可能会烧干。
同理,在 Linux 系统中,中断的处理也是越快越好。在单芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。在 SMP 系统中,假设中断处理很慢,那么正在处理这个中断的 CPU 上的其他线程也无法执行。
在中断的处理过程中,该 CPU 是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理──进程调度靠定时器中断来实现。 在 Linux 系统中使用中断很简单,为某个中断 irq 注册中断处理函数handler,可以使用 request_irq()函数:
1 | static inline int __must_check |
在 handler 函数中,代码尽可能高效。但是,处理某个中断要做的事情就是很多,没办法加快。比如对于按键中断,我们需要等待几十毫秒消除机械抖动。难道要在 handler 中等待吗?对于计算机来说,这可是一个段很长的时间。怎么办?
2.3.1 拆分为:上半部、下半部
当一个中断要耗费很多时间来处理时,它的坏处是:在这段时间内,其他中断无法被处理。换句话说,在这段时间内,系统是关中断的。如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、不紧急的?
在 handler 函数里只做紧急的事,然后就重新开中断,让系统得以正常运行;那些不紧急的事,以后再处理,处理时是开中断的。

所以为了让系统可以更好地处理中断事件, 提高实时性和响应能力, 可以将中断服务程序划分为上半部和下半部两部分,上半部 (就是中断服务程序)内核立即执行,而下半部(就是一些内核函数)留着稍后处理:
中断上半部是中断服务程序的第一部分, 它主要处理一些紧急且需要快速响应的任务。 中断上半部的特点是执行时间较短, 旨在尽快完成对中断的处理。 这些任务可能包括保存寄存器状态、更新计数器等, 以便在中断处理完成后能够正确地返回到中断前的执行位置。
中断下半部是中断服务程序的第二部分, 它主要处理一些相对耗时的任务。 由于中断上半部需要尽快完成, 因此中断下半部负责处理那些不能立即完成的、 需要更多时间的任务。 这些任务可能包括复杂的计算、 访问外部设备或进行长时间的数据处理等。
至于哪些代码属于上半部,哪些代码属于下半部并没有明确的规定,一切根据实际使用情况去判断。这里有一些可以借鉴的参考点:
①、如果要处理的内容不希望被其他中断打断,那么可以放到上半部。
②、如果要处理的任务对时间敏感,可以放到上半部。
③、如果要处理的任务与硬件有关,可以放到上半部
④、除了上述三点以外的其他任务,优先考虑放到下半部。
2.3.2 下半部的实现机制有哪些?
一个快速的“上半部”来处理硬件发出的请求,它必须在一个新的中断产生之前终止。通常,除了在设备和一些内存缓冲区(如果设备用到了DMA,就不止这些)之间移动或传送数据,确定硬件是否处于健全的状态之外,这一部分做的工作很少。“下半部”运行时是允许中断请求的,而上半部运行时是关中断的,这是二者之间的主要区别。
内核到底什么时候执行下半部,以何种方式组织下半部?
以前的内核中,下半部的机制叫做bottom-half(以下简称BH)。但是,Linux的这种bottom-half机制有两个缺点:
(1)在任意一时刻,系统只能有一个CPU可以执行BH代码,以防止两个或多个CPU同时来执行BH函数而相互干扰。因此BH代码的执行是严格“串行化”的。
(2)BH函数不允许嵌套。
这两个缺点在单CPU系统中是无关紧要的,但在SMP系统中却是非常致命的。因为BH机制的严格串行化执行显然没有充分利用SMP系统的多CPU特点。为此,在2.4以后的版本中有了新的发展和改进,改进的目标使下半部可以在多处理机上并行执行,并有助于驱动程序的开发者进行驱动程序的开发。目前的内核中,下半部处理机制主要是以下三种:
Item | 软中断请求(softirq)机制 | 小任务(tasklet)机制 | Workqueue工作队列 |
运行Context | 软中断 | 软中断(HI_SOFTIRQ和TASKLET_SOFTIRQ) | 进程(kernel态) |
可以Sleep? | 否 | 否 | 否 |
关中断? | 否 | 否 | 否 |
可重新调度? | 否 | 否 | 是 |
可带参数? | 否 | 是 | 否 |
谁触发谁执行 | 是 | 是 | 默认是 |
可同时被多个CPU执行? | 同一个softirq_action可同时被多CPU执行 | 同一个tasklet在任意时刻只能被一个CPU执行 | 由进程调度决定 |
可延时执行? | 否 | 否 | 是 |
数据结构 | softirq_action(中断服务) irq_cpustat_t(触发状态) | tasklet_struct tasklet_head | work_struct workqueue_struct |
初始化 | open_softirq | DECLARE_TASKLET DECLARE_TASKLET_DISABLED tasklet_init | DECLARE_WORK INIT_WORK DECLARE_DELAYED_WORK INIT_DELAYED_WORK |
改变运行状态 | tasklet_trylock tasklet_unlock tasklet_unlock_wait | ||
使能/静止 | tasklet_disable tasklet_enable | ||
触发 | raise_softirq raise_softirq_irqoff | tasklet_schedule tasklet_hi_schedule | schedule_work queue_work schedule_delayed_work queue_delayed_work |
执行 | do_softirq | tasklet_action tasklet_hi_action | rescuer_thread被cpu调度执行 |
创建线程 | alloc_work_queue create_singlethread_workqueue | ||
结束 | tasklet_kill | destroy_worker destroy_workqueue |
具体这三种机制,后面再详细学习。
3. 总结
中断的处理有两个原则:
- 不能嵌套
- 越快越好
在处理当前中断时,即使发生了其他中断,其他中断也不会得到处理,所以中断的处理要越快越好。但是某些中断要做的事情稍微耗时,这时可以把中断拆分为上半部、下半部。
在上半部处理紧急的事情,在上半部的处理过程中,中断是被禁止的;在下半部处理耗时的事情,在下半部的处理过程中,中断是使能的。
参考资料: