异常与中断的概念以及处理流程

异常与中断的概念以及处理流程

1.CPU理解的中断

CPU 在运行的过程中,也会被各种“异常”打断。这些“异常”有:

指令未定义

指令、数据访问异常

SWI(软中断)

快中断

中断

中断也是 “异常” 的一种,导致中断发生的情况有

按键

定时器

ADC转换完成

uart 发送完数据,收到收据

等等

这些众多的“中断源”,汇集到“中断控制器”,由“中断控制器”选择优先级最高的中断并通知 CPU。

中断的处理流程

arm 对异常(中断)处理过程:

1. 初始化

a.设置中断源,让他可以产生中断

b.设置中断控制器(可以屏蔽某个中断,优先级)

c.设置CPU总开关

2.执行其他程序:正常程序

3.产生中断:比如按下按键------->中断控制器------>cpu

4.cpu 每执行一条指令都会去检查有无中断/异常发生

5.cpu 发现有中断/异常产生,开始执行

对于不同的异常,跳去不同的地址的执行程序。这地址上只是一条跳转指令,跳去执行某个函数(地址),这个就是异常向量。针对以上的 3、4、5 都是硬件做的

6.这些函数做什么事情?

软件做的:

a:保留现场(各种寄存器)

b: 处理异常(中断):分辨中断源,再调用不同的处理函数

c:恢复现场

异常向量表

u-boot 或是 Linux 内核,都有类似如下的代码

这就是异常向量表,每一条指令对应一种异常。

发生复位时,CPU 就去 执行第 1 条指令:b reset。

发生中断时,CPU 就去执行“ldr pc, _irq”这条指令。

这些指令存放的位置是固定的,比如对于 ARM9 芯片中断向量的地址是0x18。

当发生中断时,CPU 就强制跳去执行 0x18 处的代码。

在向量表里,一般都是放置一条跳转指令,发生该异常时,CPU 就会执行

向量表中的跳转指令,去调用更复杂的函数。

当然,向量表的位置并不总是从 0 地址开始,很多芯片可以设置某个

vector base 寄存器,指定向量表在其他位置,比如设置 vector base 为

0x80000000,指定为 DDR 的某个地址。但是表中的各个异常向量的偏移地址,

是固定的:复位向量偏移地址是 0,中断是 0x18。

Linux 系统对中断的处理

进程、线程、中断的核心:栈

中断中断,中断谁?

中断当前正在运行的进程、线程。

进程、线程是什么?内核如何切换进程、线程、中断?

要理解这些概念,必须理解栈的作用。

ARM 处理器程序运行的过程

① 对内存只有读、写指令

② 对于数据的运算是在 CPU 内部实现

③ 使用 RISC 指令的 CPU 复杂度小一点,易于设计

比如对于 a=a+b 这样的算式,需要经过下面 4 个步骤才可以实现:

细看这几个步骤,有些疑问:

① 读 a,那么 a 的值读出来后保存在 CPU 里面哪里?

② 读 b,那么 b 的值读出来后保存在 CPU 里面哪里?

③ a+b 的结果又保存在哪里?

程序被中断时,怎么保存现场

CPU 内部的寄存器很重要,如果要暂停一个程序,中断一个

程序,就需要把这些寄存器的值保存下来:这就称为保存现场。

保存在哪里?内存,这块内存就称之为栈。

程序要继续执行,就先从栈中恢复那些 CPU 内部寄存器的值。

这个场景并不局限于中断,下图可以概括程序 A、B 的切换过程,其他情况

是类似的:

① 函数调用:

a) 在函数 A 里调用函数 B,实际就是中断函数 A 的执行。

b) 那么需要把函数 A 调用 B 之前瞬间的 CPU 寄存器的值,保存到栈里;

c) 再去执行函数 B;

d) 函数 B 返回之后,就从栈中恢复函数 A 对应的 CPU 寄存器值,继续执行。

② 中断处理

a) 进程 A 正在执行,这时候发生了中断。

b) CPU 强制跳到中断异常向量地址去执行,

c) 这时就需要保存进程 A 被中断瞬间的 CPU 寄存器值,

d) 可以保存在进程 A 的内核态栈,也可以保存在进程 A 的内核结构体中。

e) 中断处理完毕,要继续运行进程 A 之前,恢复这些值。

③ 进程切换

a) 在所谓的多任务操作系统中,我们以为多个程序是同时运行的。

b) 如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序

依次执行一小段时间,进程 A 的时间用完了,就切换到进程 B。

c) 怎么切换?

d) 切换过程是发生在内核态里的,跟中断的处理类似。

e) 进程 A 的被切换瞬间的 CPU 寄存器值保存在某个地方;

f) 恢复进程 B 之前保存的 CPU 寄存器值,这样就可以运行进程 B 了。

③ 进程切换

a) 在所谓的多任务操作系统中,我们以为多个程序是同时运行的。

b) 如果我们能感知微秒、纳秒级的事件,可以发现操作系统时让这些程序

依次执行一小段时间,进程 A 的时间用完了,就切换到进程 B。

c) 怎么切换?

d) 切换过程是发生在内核态里的,跟中断的处理类似。

e) 进程 A 的被切换瞬间的 CPU 寄存器值保存在某个地方;

f) 恢复进程 B 之前保存的 CPU 寄存器值,这样就可以运行进程 B 了

所以,在中断处理的过程中,伴存着进程的保存现场、恢复现场。进程的调度

也是使用栈来保存、恢复现场:

在 Linux 中:资源分配的单位是进程,调度的单位是线程,也就是说,在

一个进程里,可能有多个线程,这些线程共用打开的文件句柄、全局变量等等。

而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,

都有自己的栈。如下图示:

Linux 系统对中断处理的演进

,Linux 中断系统的变化并不大。

比较重要的就是引入了 threaded irq:使用内核线程来处理中断。Linux 系统中有硬件中断,也有软件中断。对硬件中断的处理有 2 个原则:

不能嵌套,越快越好。

中断处理函数需要调用 C 函数,这就需要用到栈。

中断处理原则 1:不能嵌套

⚫ 中断 A 正在处理的过程中,假设又发生了中断 B,那么在栈里要保存 A 的现

场,然后处理 B。

⚫ 在处理 B 的过程中又发生了中断 C,那么在栈里要保存 B 的现场,然后处理

C。

如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。所以,为了防

止这种情况发生,也是为了简单化中断的处理,在 Linux 系统上中断无法嵌套:

即当前中断 A 没处理完之前,不会响应另一个中断 B(即使它的优先级更高)。

中断处理原则 2:越快越好

在 Linux 系统中使用中断是挺简单的,为某个中断 irq 注册中断处理函数handler,可以使用 request_irq 函数:

在 handler 函数中,代码尽可能高效。

但是,处理某个中断要做的事情就是很多,没办法加快。比如对于按键中断,我们需要等待几十毫秒消除机械抖动。难道要在 handler 中等待吗?对于计算机来说,这可是一个段很长的时间。怎么办?

拆分为:上半部、下半部

当一个中断要耗费很多时间来处理时,它的坏处是:在这段时间内,其他

中断无法被处理。换句话说,在这段时间内,系统是关中断的。

如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、

不紧急的?

在 handler 函数里只做紧急的事,然后就重新开中断,让系统得以正常运

行;那些不紧急的事,以后再处理,处理时是开中断的。

中断下半部的实现有很多种方法,讲 2 种主要的:tasklet(小任务)、

work queue(工作队列)。

tasklet

下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用

tasklet 来处理下半部。tasklet 是使用软件中断来实现。

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

irq_bottom_half_A。

使用情景化的分析,才能理解上述代码的精华。

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

一开始,preempt_count = 0;

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

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

一开始,preempt_count = 0;

执行到第⑥时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表。

注意:这时 preempt_count 等于 1,并且中断下半部的代码并未执行。

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

在第①步 preempt_count 等于 2;

在第③步 preempt_count 等于 1;

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

理;

注意:重点来了,第 2 次中断发生后,打断了第一次中断的第⑦步处理。当第

2 次中断处理完毕,CPU 会继续去执行第⑦步。

可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是下

半部代码只执行了一次。

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

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

一开始,preempt_count = 0;

执行到第⑥时,一开中断后,中断 B 又再次使得 CPU 跳到中断向量表。

注意:这时 preempt_count 等于 1,并且中断 A 下半部的代码并未执行。

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

在第①步 preempt_count 等于 2;

在第③步 preempt_count 等于 1;

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

理;

注意:重点来了,第 2 次中断发生后,打断了第一次中断 A 的第⑦步处理。当

第 2 次中断 B 处理完毕,CPU 会继续去执行第⑦步。

在第⑦步里,它会去执行中断 A 的下半部,也会去执行中断 B 的下半部。

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

总结:

① 中断的处理可以分为上半部,下半部

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

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

行的

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

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

⑥ 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调

度进程啊?

工作队列

在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。

但是毕竟整个中断的处理还没走完,这期间 APP 是无法执行的。

假设下半部要执行 1、2 分钟,在这 1、2 分钟里 APP 都是无法响应的。

这谁受得了?所以,如果中断要做的事情实在太耗时,那就不能用软件中

断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和

APP 都一样竞争执行,APP 有机会执行,系统不会卡顿。

这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多

这样的线程:

kworker 线程要去“工作队列”(work queue)上取出一个一个“工作”

(work),来执行它里面的函数。

那我们怎么使用 work、work queue 呢?

① 创建 work:

你得先写出一个函数,然后用这个函数填充一个 work 结构体。比如:

总结:

⚫ 很耗时的中断处理,应该放到线程里去

⚫ 可以使用 work、work queue

⚫ 在中断上半部调用 schedule_work 函数,触发 work 的处理

⚫ 既然是在线程中运行,那对应的函数可以休眠。

在设备树中指定中断_在代码中获得中断

在硬件上,“中断控制器”只有 GIC 这一个,但是我们在软件上也可以把上

图中的“GPIO”称为“中断控制器”。很多芯片有多个 GPIO 模块,比如 GPIO1、

GPIO2 等等。所以软件上的“中断控制器”就有很多个:GIC、GPIO1、GPIO2

等等。

GPIO1 连接到 GIC,GPIO2 连接到 GIC,所以 GPIO1 的父亲是 GIC,GPIO2

的父亲是 GIC。

假设 GPIO1 有 32 个中断源,但是它把其中的 16 个汇聚起来向 GIC 发出一

个中断,把另外 16 个汇聚起来向 GIC 发出另一个中断。这就意味着 GPIO1 会

用到 GIC 的两个中断,会涉及 GIC 里的 2 个 hwirq。

这些层级关系、中断号(hwirq),都会在设备树中有所体现。

在设备树中,中断控制器节点中必须有一个属性: interruptcontroller,表明它是“中断控制器”。

还必须有一个属性:#interrupt-cells,表明引用这个中断控制器的话需

要多少个 cell。

interrupt-cells 的值一般有如下取值:

#interrupt-cells=<1>

别的节点要使用这个中断控制器时,只需要一个cell 来表明使用“哪一个中断”。

#interrupt-cells=<2>

别的节点要使用这个中断控制器时,需要一个 cell 来表明使用“哪一个中断”;

还需要另一个 cell 来描述中断,一般是表明触发类型:

示例如下:

如果中断控制器有级联关系,下级的中断控制器还需要表明它的“ interrupt-parent ”是谁,用了 interrupt-parent ”中的哪一个“interrupts”,请看下一小节。

设备树里使用中断

一个外设,它的中断信号接到哪个“中断控制器” 的 哪个“中断引脚”,这个中断的触发方式是怎样的?

这3个问题,在设备树里使用中断时,都要有所体现。

⚫ interrupt-parent=<&XXXX>

你要用哪一个中断控制器里的中断?

⚫ interrupts

你要用哪一个中断?

Interrupts 里要用几个 cell,由 interrupt-parent 对应的中断控制器

决定。在中断控制器里有“#interrupt-cells”属性,它指明了要用几个

cell 来描述中断。

比如:

新写法:interrupts-extended

一个“interrupts-extended”属性就可以既指定“interrupt-parent”,

也指定“interrupts”,比如:

interrupts-extended = <&intc1 5 1>, <&intc2 1 0>;

设备树里中断节点的示例

以 100ASK_IMX6ULL 开发板为例,在 arch/arm/boot/dts 目录下可以看

到 2 个文件:imx6ull.dtsi、100ask_imx6ull-14x14.dts,把里面有关中

断的部分内容抽取出来。

在代码中获得中断

设备树中的节点有些能被转换为内核里的platform_device,有些不能,回顾如下:

① 根节点下含有 compatile 属性的子节点,会转换为 platform_device

② 含有特定 compatile 属性的节点的子节点,会转换为 platform_device

如果一个节点的 compatile 属性,它的值是这 4 者之一:"simplebus","simple-mfd","isa","arm,amba-bus",

③ 总线 I2C、SPI 节点下的子节点:不转换为 platform_device

某个总线下到子节点,应该交给对应的总线驱动程序来处理, 它们不应该

被转换为 platform_device。

1 对于 platform_device

一个节点能被转换为 platform_device,如果它的设备树里指定了中断属

性,那么可以从 platform_device 中获得“中断资源”,函数如下,可以使用

下列函数获得 IORESOURCE_IRQ 资源,即中断号:

2 对于 I2C 设备、SPI 设备

对于I2C设备节点,I2C总线驱动在处理设备树里的I2C子节点时,也会

处理其中的中断信息。一个 I2C 设备会被转换为一个 i2c_client 结构体,中

断号会保存在 i2c_client 的 irq 成员里,代码如下(drivers/i2c/i2c-core.c):

对于SPI设备节点,SPI总线驱动在处理设备树里的SPI子节点时,也会处理其中的中断信息。一个SPI设备会被转换为一个spi_device结构体,中断号会保存在spi_device的irq成员里,代码如下(drivers/spi/spi.c):

相关推荐