1.异常和中断
1.1 异常和中断的概念
异常不可屏蔽,中断可以屏蔽,中断一种特殊的异常。

- 初始化,设置中断源(设置中断源、设置中断控制器、使能CPU总开关)
- 执行程序
- 产生中断:中断源 -> 中断控制器 -> CPU
- CPU每执行完一条指令,都会检查有无中断/异常产生
- CPU发现有中断/异常产生,开始处理
- 对于不同的异常,跳去不同的地址执行指令(中断向量表),这些指令只是一条跳转指令,跳去执行某个函数
- 处理函数:保存现场,分辨中断源并调用相应的处理函数,恢复现场
1.2 异常向量表
u-boot 或是 Linux 内核,都有类似如下的代码:
1 | _start: b reset |
这就是异常向量表,每一条指令对应一种异常。当发生不同的异常时,CPU会由硬件执行向量表中对应位置的指令。
这些指令存放的位置是固定的,比如对于 ARM9 芯片中断向量的地址是 0x18。当发生中断时,CPU 就强制跳去执行 0x18 处的代码。
在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU 就会执行向量表中的跳转指令,去调用更复杂的函数。
当然,向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个 vector base 寄存器,指定向量表在其他位置,比如设置 vector base 为 0x80000000, 指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址是固定的:复位向量偏移地址是 0,中断是 0x18。
对于 ARM 的中断控制器,称为 GIC (Generic Interrupt Controller),到目前已经更新到 v4 版本了。
各个版本的差别可以看这里:https://developer.arm.com/ip-products/system-ip/systemcontrollers/interrupt-controllers
简单地说,GIC v3/v4 用于 ARMv8 架构,即 64 位 ARM 芯片。 而 GIC v2 用于 ARMv7 和其他更低的架构。
2. Linux 对于中断的处理
(1) 进程/线程/中断的核心:栈
当 Linux 在执行进程、线程的切换或者中断函数的处理时,需要保护现场,也就是将CPU内寄存器的值先保存在内存中,将寄存器的空间“腾”出来给待执行程序用。而保存现场这部分内存就是栈结构。
(2) 进程/线程的概念
进程的引入:为例实现程序的并发执行
线程的引入:进程之间传递资源消耗过大,而一个进程内的多个线程可以共享资源(如全局变量)。
进程是资源分配的单位,线程是调度的单位,所以每一个线程都有自己的“栈”,以便在调度时保存/恢复现场。
2.1 Linux 中断的演进
2.1.1 硬件中断和软件中断
硬件中断我们比较熟悉,由硬件本身产生中断信号触发,执行对应的处理函数。而软件中断是一种灵活的,人为的制造中断。
(1)硬件中断
对于硬件中断,触发信号由硬件产生,产生之后会尽快被处理。Linux系统对于硬件中断的处理原则:硬件中断不能嵌套,并且硬件中断的处理越快越好。
不能嵌套是为了防止突发的大量中断将栈空间耗尽。
在中断的处理过程中,CPU 是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理,例如进程调度靠定时器中断来实现。
在 Linux 为某一虚拟中断号 irq 配置处理函数:
1 | static inline int __must_check |
虚拟中断号 irq 的处理函数 handler 的代码尽可能高效,耗时越短越好。
为什么是虚拟中断号,后面会讲(硬件中断号 hwirq ←→ irq 虚拟中断号)
(2)软件中断
而对于软件中断,需要知道软件中断的触发如何产生,以及何时进行软件中断的处理:
- 由软件来触发,对于 X 号软件中断,只需要将它的 flag 设置为 1 就表示发生了该中断。
- 软件中断不像硬件中断那样急迫,不必立刻处理;在 Linux 系统中因为各种硬件中断频发,例如定时器中断(心跳)就每 10ms 发生一次;所以在处理完硬件中断后,检查是否有软件中断被触发,如果有触发就去处理相应的软件中断。
Linux 的软件中断,参见内核源码include/linux/interrupt.h:
1 | enum |
配置软件中断的处理函数:
1 | extern void open_softirq(int nr, void (*action)(struct softirq_action *)); |
触发软件中断:
1 | extern void raise_softirq(unsigned int nr); |
2.1.2 耗时中断
有些硬件中断不可能在短时间内处理完成,例如按键中断,需要等待几十毫秒的时间来消除抖动,不可能在硬件中断的处理函数中等待这么长的时间,使得其他硬件中断不能够及时响应,因此需要一些方法来解决此类问题。
可以将中断拆分成上半部和(硬件中断)下半部(软件中断是一种实现方式),在上半部中快速处理一些紧急的操作,在下半部中进行耗时的,可以被打断的操作:

主要讲两种中断下半部的处理方式:tasklet(小任务)、work queue (工作队列)
2.1.3 下半部耗时不长-tasklet
当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用软件中断 tasklet 来实现:
1 | TASKLET_SOFTIRQ |
通过下图中的函数调用关系,可以比较清楚的看出中断上半部和下半部的处理流程:

在上半部中,处于关中断状态,会根据硬件中断触发源来执行对应的中断处理函数;
如果当前没有下半部的软件中断正在执行并且有软件中断被触发,就根据 softirq_vec 数组来执行对应的软件中断处理函数(注意,多个中断的下半部是汇集在一起进行处理的,即sfotirq_vec数组中可能有多个信号被触发)。
中断上半部、下半部的执行过程中,不能执行休眠函数:在中断中休眠的话,以后谁来调度进程啊
2.1.4 下半部复杂耗时长-工作队列
在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但毕竟整个中断的处理还没走完,这期间 APP 是无法执行的,因此如果下半部耗时过长,并且使用软件中断的方式实现,则 APP 在中断执行过程中将无法相应,这同样是不可接收的,因此这种情况下,下半部可以使用内核线程来实现:在中断上半部唤醒内核线程。内核线程和 APP 都 一样竞争执行,APP 有机会执行,系统不会卡顿。
这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:

kworker 线程要去 work queue 上取出一个一个 work, 来执行它里面的函数。使用方式如下:
创建 work,将 work 和对应的处理函数绑定:
1
2将一个 work_struct 结构体和对应的处理函数 f 绑定
要执行处理函数时,仅需要将 work 提交给 work queue 即可:
1
2
3
4static inline bool schedule_work(struct work_struct *work)
{
return queue_work(system_wq, work);
}上述函数会把 work 提供给系统默认的 work queue:system_wq,它是一 个队列。
在中断场景中,可以在中断上半部调用 schedule_work 函数。
schedule_work 函数不仅仅是把 work 放入队列,还会把 kworker 线程唤醒。此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数。
很耗时的中断处理,应该放到线程里去,从而防止 APP 不能够及时响应
既然是在线程中运行,那对应的函数可以休眠
2.1.5 新技术 - threaded irq
以前用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行, 多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中没有问题,但是在SMP(Symmetric multiprocessing)系统中,这种方式不能够完全发挥多核的性能。因此,新技术 threaded irq 干脆为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU 上执行,这提高了效率。threaded irq 使用方法:
1 | int request_threaded_irq(unsigned int irq, irq_handler_t handler, |
- irq:指示哪个中断
- handler:表示中断处理的上半部函数,可以为空
- thread_fn:表示在线程中运行的函数
2.2 Linux 中断系统中的重要数据结构
Linux 中断系统最核心的结构体是 irq_desc,它是一个结构体数组,该结构体内通过成员 action 构建了一个链表,该数组用以记录所有硬件中断的相关数据。

如果内核配置了 CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree) 来代替 irq_desc 数组。SPARSE 的意思是“稀疏”,假设大小为 1000 的数组中只用到 2 个数组项,那不是浪费嘛?所以在中断比较“稀疏”的情况下可以用基数树来代替数组。
2.2.1 irq_desc 数组
irq_desc 结构体在 include/linux/irqdesc.h 中定义,它的主要成员如下:
1 | struct irq_data irq_data; |
我们首先通过一个例子来理解 irq_desc 数组的整体结构:

外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到 GIC (Generic Interrupt Controller)的 A 号中断,GIC 再去中断 CPU。所以中断触发信号在图中一路从左向右传递到CPU。
当CPU收到中断触发信号之后,软件应该在图中从右到左依次询问,来定位到底是那个硬件触发了中断,具体来说:
GIC 的处理函数:
假设 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家), 这个函数需要读取芯片的 GPIO 控制器,细分发生的是哪一个 GPIO 中断(假设是 B),再去调用 irq_desc[B]. handle_irq。
模块的中断处理函数:
irq_desc[B]. handle_irq,BSP 开发人员会设置对应的处理函数,一般是 handle_level_irq 或 handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。该函数会去判断哪个设备触发了中断,可能有某个设备触发了中断,也可能有多个设备触发了中断,因此irq_desc[B].handle_irq 会遍历 action 成员指向的链表,并调用链表元素中的“具体的中断处理函数”,这些函数自行判断该中断是否由自己产生,若是则处理。
外部设备提供的中断处理函数:
对应 irqaction 结构体中的 handler 或者 thread_fn 成员,这些处理函数由自己驱动程序提供,应该具有判断设备是否发生了中断,如何处理中断的能力。
总的来说,irq_desc 数组对应“中断控制器模块”,链表中 irqaction 结构体对应产生中断源的外部设备。
上述过程中忽略了一点:当执行中断处理函数时,从中获取的是硬件中断号 (hwirq),而irq_desc 数组的寻址使用的是虚拟中断号 (irq) ,二者的映射在 2.2.3 & 2.2.4 节中讨论
2.2.2 irqaction 结构体
irqaction 结构体在 include/linux/interrupt.h 中定义,主要内容如下:
1 | struct irqaction { |
当调用 request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体,该结构体最重要的是 handler、thread_fn、thread 成员:
- handler 是中断处理的上半部函数,用来处理紧急的事情。
- thread_fn 对应一个内核线程 thread,当 handler 执行完毕,Linux 内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn 函数。
- 可以提供 handler 而不提供 thread_fn,就退化为一般的 request_irq 函数。
- 可以不提供 handler 只提供 thread_fn,完全由内核线程来处理中断。
- 也可以既提供 handler 也提供 thread_fn,这就是中断上半部、下半部。
其次是 dev_id 成员,在 reqeust_irq 时可以传入 dev_id,它的作用如下:
- 中断处理函数执行时,可以使用 dev_id
- 卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项
2.2.3 irq_data 结构体
irq_data 结构体在 include/linux/irq.h 中定义,主要内容如下图:

它就是个中转站,里面有 irq_chip 指针 和 irq_domain 指针,都是指向别的结构体。
2.2.4 irq_domain 结构体
irq_domain 结构体在 include/linux/irqdomain.h 中定义,主要内容如下图:

当我们后面从设备树讲起,如何在设备树中指定中断,设备树的中断如何被 转换为 irq 时,irq_domain 将会起到极大的作用,举例来说,假设在设备树中有这样的属性:
1 | interrupt-parent = <&gpio1>; |
它表示要使用 gpio1 里的第 5 号中断,因此,当我们调用 GPIO1 的中断处理函数,会获取到的 hwirq = 5。但是我们在 irq_desc 数组中寻址使用的是全局的虚拟中断号 irq。二者之间的转换就是由 GPIO1 对应的 irq_domain 结构体中的相关成员实现的。具体来说,irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数:
- xlate:用来解析设备树的中断属性,提取出 hwirq、type 等信息。
- map:把 hwirq 转换为 irq。
2.2.5 irq_chip 结构体
irq_chip 结构体在 include/linux/irq.h 中定义,主要内容如下图:

这个结构体封装了主控芯片有关中断的相关硬件操作,例如我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断;我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数。
但是对于外部设备相关的清中断操作,还是需要我们自己做的,外部设备对应我们编写的驱动程序。
2.3 配置中断资源
2.3.1 设备树中断节点的语法
参考文档:内核\Documentation\devicetree\bindings\interrupt-controller\interrupts.txt

上图为 IMX6ULL 的中断体系,比之前多了一个 GPC INTC (General Power Controller,Interrupt Controller),我们参考 imx6ull.dtsi 中有关中断的部分内容来学习设备树中有关中断的语法:
(1) intc
1 | #interrupt-cells = <3>; |
- 凡是包含
#interrupt-controller;表明该节点是一个中断控制器节点 #interrupt-cells = <3>;指明想要指定该中断控制器的某个中断源应该使用多少个 cell 来表示
对于顶层中断控制器 GIC 而言,3 个 cell 分别表示:
- 第一个 cell 表示中断类别,可选
GIC_PPI,GIC_SPI,GIC_SGI:- Private Peripheral Interrupt (PPI):以单个特定CPU核为目标的外围中断
- Shared Peripheral Interrrupt (SPI):可以以任意CPU核为目标的外围中断
- Software-generated interrupt (SGI):用于CPU核之间通信的中断
- 第二个 cell 表示中断控制器内的中断序号
- 第三个 cell 表示触发条件
详情可参考GIC架构规格书:IHI0069E_gic_architecture_specification.pdf
(2) gpc
1 | #interrupt-controller; |
这也是一个中断控制器,并且它的子节点声明中断时要使用 3 个 cell
interrupts-parent =:指明该中断设备的父节点interrupts =:描述本中断节点的中断触发条件
(3) gpio1
1 | interrupts = <GIC_SPI 66 IRQ_TYPE_LEVEL_HIGH>, |
这是一个中断控制器,并且它的中断信号,直接传递到 GIC 的 66 和 67 号中断上。此外,它的子节点声明中断时要使用 2 个 cell。参考内核文档interrupt.txt 可知两个 cell 分别表示:
第一个 cell 表示中断控制器内的中断序号
第二个 cell 表示中断触发条件:
1
2
3
4
5- bits[3:0] trigger type and level flags
1 = low-to-high edge triggered
2 = high-to-low edge triggered
4 = active high level-sensitive
8 = active low level-sensitive
此外,它的属性中没有指明该节点的中断父节点,因此在该节点直接继承其父节点的 interrupts-parent 属性表明其中断父节点为 gpc
此外,通过查询IMX6ULL的数据手册,可知GPIO的共享中断触发源信息:

(4) spidev
1 | interrupt-parent = <&gpio1>; |
该节点就不是中断控制器了,是设备节点的中断信息。它的中断父节点是 gpio1,它在 gpio1 内的中断序号是1,并且中断触发条件是上升沿。这种设备节点的相关信息是要由我们来设置的。
(5) 新写法 - interrupts-extended 属性
一个“interrupts-extended”属性就可以既指定“interrupt-parent”, 也指定“interrupts”,比如:
1 | interrupts-extended = <&gpio1 5 1>, <&gpio1 1 1>; |
2.3.2 在代码中获得中断
在 “使用设备树的LED驱动框架”文章的的 1.4.2 节记录了设备树的哪些 device_node 会被转换为 platform_device。
(1)对于 platform_device
一个节点能被转换为 platform_device,并且它的设备树里指定了中断属性,那么可以从 platform_device 中获得“中断资源”:
1 | /** |
(2)对于 I2C 设备、SPI 设备
对于 I2C 设备节点,I2C 总线驱动在处理设备树里的 I2C 子节点时,也会处理其中的中断信息。一个 I2C 设备会被转换为一个 i2c_client 结构体,中断号会保存在 i2c_client 的 irq 成员里,代码如下 drviers/i2c/i2c-core.c:
1 | static int i2c_device_probe(struct device *dev) |
对于 SPI 设备节点,SPI 总线驱动在处理设备树里的 SPI 子节点时,也会处理其中的中断信息。一个 SPI 设备会被转换为一个 spi_device 结构体,中断号会保存在 spi_device 的 irq 成员里,代码如下 drivers/spi/spi.c:
1 | static int spi_drv_probe(struct device *dev) |
(3)调用 of_irq_get 获得中断号
如果你的设备节点既不能转换为 platform_device,它也不是 I2C 设备, 不是 SPI 设备,那么在驱动程序中可以自行调用 of_irq_get 函数去解析设备树,得到中断号。
1 | /** |
(3)对于 GPIO
在drivers/input/keyboard/gpio_keys.c,可以使用 gpio_to_irq 或 gpiod_to_irq 获得中断号:
首先使用该函数来获取第 i 个引脚的 GPIO 的编号,并将其 flags (例如 GPIO_ACTIVE_HIGH )保存下来:
1 | static inline int of_get_gpio_flags(struct device_node *np, int index, enum of_gpio_flags *flags) |
然后根据 GPIO 编号转换为该 GPIO 的描述符:
1 | /** * Convert a GPIO number to its descriptor */ |
最后使用 gpio_to_irq 或 gpiod_to_irq 获得中断号:
1 | /** |
3. 编写 GPIO 触发中断
(1)在设备树中添加节点

1 | krocz_gpio { |
因为在内核中有 gpiod_to_irq 这个函数,所以不必在设备节点中指定中断。
(2)编写驱动程序
参考内核源码:drivers/input/keyboard/gpio_keys.c
注册内核模块的代码省略,主要是 platform_driver 的注册:
1 | struct gpio_key{ |
probe 函数的核心就是将中断处理函数 gpio_key_isr 进行注册,以便我们在按下按键时能够得到处理,因为硬件设计上有电容用来去抖动,所以不需要其他软件上的处理了。
