异常与中断的概念以及处理流程
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):