一个能思想的人,才真是一个力量无边的人。
一、Cortex-A7系列中断介绍
Cortex-A7内核只有8个异常中断,这8个异常中断的中断向量表如下:
向量地址 | 终端类型 | 中断模式 |
---|---|---|
0x00 | 复位中断(Rest) | 特权模式(SVC) |
0x04 | 未定义指令中断(Undefined Instruction) | 未定义指令中止模式(Undef) |
0x08 | 软中断(Software Interrupt,SWI) | 特权模式(SVC) |
0x0C | 指令预取中止中断(Prefetch Abort) | 中止模式 |
0x10 | 数据访问中止中断(Data Abort) | 中止模式 |
0x14 | 未使用(Not Used) | 未使用 |
0x18 | IRQ 中断(IRQ Interrupt) | 外部中断模式(IRQ) |
0x1C | FIQ 中断(FIQ Interrupt) | 快速中断模式(FIQ) |
Cortex-A7的中断控制器叫做GIC。【类比STM32的NVIC】GIC可以开关中断,设置中断优先级!Cortex-A7用到的版本是GIC V2,该版本最多支持8个核。GIC 将众多的中断源分为分为三类:SPI、PPI、SGI。我们重点关注SPI(Shared Peripheral Interrupt,共享中断)。那些外部中断都属于 SPI 中断(注意!不是 SPI 总线那个中断) 。比如按键中断、串口中断等等,这些中断所有的 Core 都可以处理,不限定特定 Core。
Cortex-A 内核 CPU 的所有外部中断都属于这个 IRQ 中断,当任意一个外部中断发生的时候都会触发 IRQ 中断。在 IRQ 中断服务函数里面就可以读取指定的寄存器来判断发生的具体是什么中断,所以我们重点关注IRQ中断!
为了区分不同的中断,引入了终端号。SPI(共享中断)分到的中断号范围是ID32~ID1019,一共998个终端号。像 GPIO 中断、串口中断等这些外部中断都在这里面 ,至于具体到某个 ID 对应哪个中断,那就由半导体厂商根据实际情况去定义了。
二、Linux中断处理
Linux 系统对中断处理的演进,是使用内核线程来处理中断。关于进程和线程,这里也简单介绍下进程和线程的知识。
- Linux中资源分配的单位是进程,调度的单位是线程。
- 在一个进程里,可能有多个线程,这些线程共用打开的文件句柄、全局变量等等。
- 而这些线程,之间是互相独立的,“同时运行”,也就是说:每一个线程,都有自己的栈。
Linux 系统中不仅含有硬件中断,也有软件中断。我们一一道来!
2.1 硬件中断
Linux中对于硬件产生的中断,称之为“硬件中断”(hard irq)。如按键中断,串口中断,并且每个硬件中断都有对应的处理函数。
2.2 软件中断
- 软件中断何时生产?
- 由软件决定,对于 X 号软件中断,只需要把它的 flag 设置为 1 就表示发生了该中断。
- 软件中断何时处理?
- 软件中断嘛,并不是那么十万火急,有空再处理它好了。什么时候有空?不能让它一直等吧?Linux 系统中,各种硬件中断频繁发生,至少定时器中断每 10ms 发生一次,那取个巧?在处理完硬件中断后,再去处理软件中断?就这么办!
- 有哪些软件中断?
- 查内核源码 include/linux/interrupt.h
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
IRQ_POLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ, /* Unused, but kept as tools rely on the
numbering. Sigh! */
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */
NR_SOFTIRQS
};
- 怎么触发软件中断?
- 最核心的函数是 raise_softirq,简单地理解就是设置 softirq_veq[nr]的标记位。
extern void raise_softirq(unsigned int nr);
- 怎么设置软件中断的处理函数?
extern void open_softirq(int nr, void (*action)(struct softirq_action *));
后面讲到的中断下半部 tasklet 就是使用软件中断实现的。
2.3 中断处理原则
2.3.1 中断处理不能嵌套
中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。所以,为了防止这种情况发生,也是为了简单化中断的处理,在 Linux 系统上中断无法嵌套:即当前中断 A 没处理完之前,不会响应另一个中断 B(即使它的优先级更高)。
2.3.1 中断处理越快越好
在单芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。在 SMP 系统中,假设中断处理很慢,那么正在处理这个中断的 CPU 上的其他线程也无法执行。在中断的处理过程中,该 CPU 是不能进行进程调度的,所以中断的处理要越快越好,尽早让其他中断能被处理──进程调度靠定时器中断来实现。
在 Linux 系统中使用中断,只需为某个中断 irq 注册中断处理函数 handler。
- 使用request_irq 注册Handler,函数原型如下:
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
但是,处理某个中断要做的事情就是很多,没办法加快。 比如对于按键中断,我们需要等待几十毫秒消除机械抖动。难道要在 handler 中等待吗?对于计算机来说,这可是一个段很长的时间。怎么办?
这时,“下半部”的处理思想便由此产生。
2.4 要处理的事情实在太多,拆分为:上半部、下半部
当一个中断要耗费很多时间来处理时,它的坏处是:在这段时间内,其他中断无法被处理。 换句话说, 在这段时间内,系统是关中断的。如果某个中断就是要做那么多事,我们能不能把它拆分成两部分:紧急的、不紧急的?在 handler 函数里只做紧急的事,然后就重新开中断,让系统得以正常运行;那些不紧急的事,以后再处理,处理时是开中断的。
中断下半部的实现有很多种方法,先来了解两种主要的: tasklet(小任务)、 work queue(工作队列)。
2.4.1 下半部要做的事情耗时不是太长: tasklet
假设我们把中断分为上半部、下半部。 发生中断时,上半部下半部的代码何时、如何被
调用?
当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用 tasklet 来处理下半部。 tasklet 是使用软件中断来实现。
贴代码,一目了然:
使用流程图简化一下:
假设硬件中断 A 的上半部函数为 irq_top_half_A,下半部为 irq_bottom_half_A。使用情景化的分析,才能理解上述代码的精华。
-
a. 硬件中断 A 处理过程中,没有其他中断发生:
一开始, preempt_count = 0;
上述流程图①~⑨依次执行,上半部、下半部的代码各执行一次。 -
b. 硬件中断 A 处理过程中,又再次发生了中断 A:
一开始, preempt_count = 0;
执行到第⑥时,一开中断后,中断 A 又再次使得 CPU 跳到中断向量表。
注意:这时 preempt_count 等于 1,并且中断下半部的代码并未执行。
CPU 又从①开始再次执行中断 A 的上半部代码:
在第①步 preempt_count 等于 2;
在第③步 preempt_count 等于 1;
在第④步发现 preempt_count 等于 1,所以直接结束当前第 2 次中断的处理;
注意:重点来了,第 2 次中断发生后,打断了第一次中断的第⑦步处理。当第 2 次中断 A处理完毕, CPU 会继续去执行第⑦步。
可以看到,发生 2 次硬件中断 A 时,它的上半部代码执行了 2 次,但是下半部代码只
执行了一次。所以,同一个中断的上半部、下半部,在执行时是多对一的关系。
- c. 硬件中断 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 的下半部。
所以,多个中断的下半部,是汇集在一起处理的。
总结:
- a. 中断的处理可以分为上半部,下半部
- b. 中断上半部,用来处理紧急的事,它是在关中断的状态下执行的
- c. 中断下半部,用来处理耗时的、不那么紧急的事,它是在开中断的状态下执行的
- d. 中断下半部执行时,有可能会被多次打断,有可能会再次发生同一个中断
- e. 中断上半部执行完后,触发中断下半部的处理
- f. 中断上半部、下半部的执行过程中,不能休眠:中断休眠的话,以后谁来调度进程啊?
2.4.2 下半部要做的事情太多并且很复杂:工作队列(work queue)
在中断下半部的执行过程中,虽然是开中断的,期间可以处理各类中断。但是毕竟整个中断的处理还没走完,这期间 APP 是无法执行的。假设下半部要执行 1、 2 分钟,在这 1、 2 分钟里 APP 都是无法响应的。
这谁受得了?
所以,如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和 APP 都一样竞争执行, APP 有机会执行,系统不会卡顿。
这个内核线程是系统帮我们创建的,一般是 kworker 线程,内核中有很多这样的线程:
kworker 线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。
那我们怎么使用 work、 work queue 呢?
-
a. 创建 work:
- 你得先写出一个函数,然后用这个函数填充一个 work 结构体。比如:
- 你得先写出一个函数,然后用这个函数填充一个 work 结构体。比如:
-
b. 要执行这个函数时,把 work 提交给 work queue 就可以了:
- 下述函数会把 work 提供给系统默认的 work queue: system_wq,它是一个队列。
- 下述函数会把 work 提供给系统默认的 work queue: system_wq,它是一个队列。
-
c. 谁来执行 work 中的函数?
- 不用我们管, schedule_work 函数不仅仅会把 work 放入队列,还会把 kworker 线程唤醒。此线程抢到时间运行时,它就会从队列中取出 work,执行里面的函数。
-
d. 谁把 work 提交给 work queue?
- 在中断场景中,可以在中断上半部调用 schedule_work 函数。
总结:
- a. 很耗时的中断处理,应该放到线程里去
- b. 可以使用 work、 work queue
- c. 在中断上半部调用 schedule_work 函数,触发 work 的处理
- d. 既然是在线程中运行,那对应的函数可以休眠。
2.5 新技术: threaded irq
使用线程来处理中断,并不是什么新鲜事。 使用 work 就可以实现,但是需要定义 work、调用 schedule_work,好麻烦啊。
太懒了太懒了,就这 2 步你们都不愿意做。好,内核是为懒人服务的,再杀出一个函数:
你可以只提供 thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。
说懒是开玩笑,内核开发者也不会那么在乎懒人。
以前用 work 来线程化地处理中断,一个 worker 线程只能由一个 CPU 执行,多个中断的 work 都由同一个 worker 线程来处理,在单 CPU 系统中也只能忍着了。但是在 SMP 系统中,明明有那么多 CPU 空着,你偏偏让多个中断挤在这个 CPU 上?
新技术 threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个 CPU 上执行, 这样便提高了执行效率。
三、Linux中断系统中的重要数据
能弄清楚上面这个图,对 Linux 中断系统的掌握也基本到位了。最核心的结构体是 irq_desc,之前为了易于理解,我们前面说在 Linux 内核中有一个中断数组,对于每一个硬件中断,都有一个数组项, 这个数组就是 irq_desc 数组。
注意: 如果内核配置了 CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree)来代替irq_desc 数组。 SPARSE 的意思是“稀疏”,假设大小为 1000 的数组中只用到 2 个数组项,那不是浪费嘛? 所以在中断比较“稀疏”的情况下可以用基数树来代替数组。
3.1 irq_desc 数组
irq_desc 结构体在 include/linux/irqdesc.h 中定义,主要内容如下图:
每一个 irq_desc 数组项中都有一个函数: handle_irq, 还有一个 action 链表。 要理解它们,需要先看中断结构图:
关注上图中的A号中断和B号中断位置!
外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到 GIC(通用中断控制器)的 A 号中断, GIC 再去中断 CPU。那么软件处理时就是反过来,先读取 GIC 获得中断号 A,再细分出 GPIO 中断 B,最后判断是哪一个外部芯片发生了中断。
所以, 中断的处理函数来源有三:
- ① GIC 的处理函数:
假设 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家), 这个函数需要读取芯片的 GPIO 控制器,细分发生的是哪一个 GPIO 中断(假设是 B),再去调用 irq_desc[B].handle_irq。
注意: irq_desc[A].handle_irq 细分出中断后 B,调用对应的irq_desc[B].handle_irq。显然中断 A 是 CPU 感受到的顶层的中断, GIC 中断 CPU 时, CPU 读取 GIC 状态得到中断 A。
- ② 模块的中断处理函数:
比如对于 GPIO 模块向 GIC 发出的中断 B, 它的处理函数是irq_desc[B].handle_irq。BSP 开发人员会设置对应的处理函数,一般是 handle_level_irq 或 handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。
注意:导致 GPIO 中断 B 发生的原因很多,可能是外部设备 1,可能是外部设备 n,可能只是某一个设备,也可能是多个设备。所以 irq_desc[B].handle_irq 会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生, 若是则处理。
- ③ 外部设备提供的处理函数:
这里说的“外部设备”可能是芯片,也可能总是简单的按键。它们的处理函数由自己驱动程序提供,这是最熟悉这个设备的“人”: 它知道如何判断设备是否发生了中断,如何处理中断。
对于共享中断,比如 GPIO 中断 B, 它的中断来源可能有多个, 每个中断源对应一个中断处理函数。所以 irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,然后一一执行。这个链表就是 action 链表。链表可以在本部分开头的第一张图片看到。
3.2 irqaction 结构体
irqaction 结构体在 include/linux/interrupt.h 中定义,主要内容如下图:
当调用 request_irq、 request_threaded_irq 注册中断处理函数时,内核就会构造一个irqaction 结构体。在里面保存 name、 dev_id 等,最重要的是 handler、 thread_fn、 thread。
- handler 是中断处理的上半部函数, 用来处理紧急的事情。
- thread_fn 对应一个内核线程 thread,当 handler 执行完毕, Linux 内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn 函数。
- 可以提供 handler 而不提供 thread_fn,就退化为一般的 request_irq 函数。(只有上半部)
- 可以不提供 handler 只提供 thread_fn, 完全由内核线程来处理中断。(只有下半部)
- 也可以既提供 handler 也提供 thread_fn,这就是中断上半部、下半部。
里面还有一个名为 sedondary 的 irqaction 结构体,它的作用以后再分析。在 reqeust_irq 时可以传入 dev_id, 为何需要 dev_id? 作用有 2:
- ① 中断处理函数执行时,可以使用 dev_id
- ② 卸载中断时要传入 dev_id,这样才能在 action 链表中根据 dev_id 找到对应项
所以在共享中断中必须提供 dev_id, 非共享中断可以不提供。
3.3 irq_data 结构体
irq_data 结构体在 include/linux/irq.h 中定义,主要内容如下图:
它就是个中转站,里面有 irq_chip 指针、irq_domain 指针,都是指向别的结构体。
比较有意思的是 irq和hwirq,其中irq 是软件中断号, hwirq 是硬件中断号。 比如上面我们举的例子,在 GPIO 中断 B 是软件中断号,可以找到 irq_desc[B]这个数组项; GPIO 里的第 x 号中断, 这就是 hwirq。
谁来建立 irq、 hwirq 之间的联系呢?由 irq_domain 来建立。 irq_domain 会把本地的hwirq 映射为全局的 irq,什么意思?比如 GPIO 控制器里有第 1 号中断, UART 模块里也有第 1 号中断,这两个“第 1 号中断”是不一样的,它们属于不同的“域”──irq_domain。
3.4 irq_domain 结构体
irq_domain 结构体在 include/linux/irqdomain.h 中定义,主要内容如下图:
当我们后面从设备树讲起,如何在设备树中指定中断,设备树的中断如何被转换为 irq时, irq_domain 将会起到极大的作为。这里基于入门的解度简单讲讲,在设备树中你会看到这样的属性:
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;
它表示要使用 gpio1 里的第 5 号中断, hwirq 就是 5。但是我们在驱动中会使用 request_irq(irq, handler)这样的函数来注册中断, irq 是什么?它是软件中断号,它应该从“gpio1 的第 5 号中断”转换得来。
谁把 hwirq 转换为 irq?由 gpio1 的相关数据结构,就是 gpio1 对应的 irq_domain 结构体。
irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数,主要是:
-
① xlate
用来解析设备树的中断属性, 提取出 hwirq、 type 等信息。 -
② map
把 hwirq 转换为 irq。
3.5 irq_chip 结构体
irq_chip 结构体在 include/linux/irq.h 中定义,主要内容如下图:
这个结构体跟“chip”即芯片相关,里面各成员的作用在头文件中也列得很清楚, 摘录部分如下:
* @irq_startup: start up the interrupt (defaults to ->enable if NULL)
* @irq_shutdown: shut down the interrupt (defaults to ->disable if NULL)
* @irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
* @irq_disable: disable the interrupt
* @irq_ack: start of a new interrupt
* @irq_mask: mask an interrupt source
* @irq_mask_ack: ack and mask an interrupt source
* @irq_unmask: unmask an interrupt source
* @irq_eoi: end of interrupt
我们在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断。
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数。
但是对于外部设备相关的清中断操作,还是需要我们自己做的。就像上面图里的“外部设备 1“、“外部设备 n”, 外设备千变万化,内核里可没有对应的清除中断操作。