异常与中断的概念及处理流程
异常向量表
_start: b reset
ldr pc, _undefined_instruction
ldr pc, _software_interrupt
ldr pc, _prefetch_abort
ldr pc, _data_abort
ldr pc, _not_used
ldr pc, _irq //发生中断时,CPU跳到这个地址执行该指令 **假设地址为0x18**
ldr pc, _fiq
进程线程中断的核心_栈
CPU内部的寄存器很重要,如果要暂停一个程序,中断一个程序,就需要把这些寄存器的值保存下来:这就称为保存现场。 保存在内存中,这块内存就称之为栈。程序要继续执行,就先从栈中恢复那些CPU内部寄存器的值。
并不局限于中断,包括:
- 函数调用
- 中断处理
- 进程切换
Linux系统对中断处理的演进
Linux对中断的扩展:硬件中断、软件中断
- Linux系统把中断的意义扩展了,对于按键中断等硬件产生的中断,称之为“硬件中断”(hard irq)。每个硬件中断都有对应的处理函数,比如按键中断、网卡中断的处理函数肯定不一样。
-
相对的,还可以人为地制造中断:软件中断(soft irq)
- 有哪些软件中断?查内核源码
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就是使用软件中断实现的
- 有哪些软件中断?查内核源码
中断处理原则1:不能嵌套
如果中断嵌套突然暴发,那么栈将越来越大,栈终将耗尽。
中断处理原则2:越快越好
- 在单芯片系统中,假设中断处理很慢,那应用程序在这段时间内就无法执行:系统显得很迟顿。
- 在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种主要的:tasklet(小任务)、work queue(工作队列)。
下半部要做的事情耗时不是太长:tasklet
当下半部比较耗时但是能忍受,并且它的处理比较简单时,可以用tasklet来处理下半部。tasklet是使用软件中断来实现。
写字太多,不如贴代码,代码一目了然:
使用流程图简化一下:
简单说:
- 下半部,开中断的时候,可以被中断,但是被中断前,
count >= 1
- 再次中断上半部结束的时候,会检查 count 的值,正常情况 count 是0,但是如果是再下半部中被中断的,那么 count 就不是0,就会直接退出结束本次中断
- cpu 会回到上次下半部被中断的地方继续执行下去。
注意:从上面的流程可以看出,上半部和下半部的关系是 N:1. 所以,下半部处理的时候要完整一点。 比如按键处理,那就要处理所有按键。
下半部要做的事情太多并且很复杂:工作队列
如果中断要做的事情实在太耗时,那就不能用软件中断来做,而应该用内核线程来做:在中断上半部唤醒内核线程。内核线程和APP都一样竞争执行,APP有机会执行,系统不会卡顿。
这个内核线程是系统帮我们创建的,一般是kworker线程,内核中有很多这样的线程 ps -A | grep kworker
, kworker线程要去“工作队列”(work queue)上取出一个一个“工作”(work),来执行它里面的函数。
- 创建work:
#define DECLARE_WORK(n, f) struct work_struct n = __WORK_INITIALIZER(n, f)
先写出一个函数,然后用这个宏填充一个work结构体。 - 要执行这个函数时,把work提交给work queue就可以了
static inline bool schedule_work(struct work_struct *work)
上述函数会把work提供给系统默认的work queue:system_wq,它是一个队列。 - 谁来执行work中的函数?schedule_work函数不仅仅是把work放入队列,还会把kworker线程唤醒。此线程抢到时间运行时,它就会从队列中取出work,执行里面的函数。
- 谁把work提交给work queue?在中断场景中,可以在中断上半部调用schedule_work函数。
新技术:threaded irq
新技术threaded irq,为每一个中断都创建一个内核线程;多个中断的内核线程可以分配到多个CPU上执行,这提高了效率。
你可以只提供thread_fn,系统会为这个函数创建一个内核线程。发生中断时,内核线程就会执行这个函数。 int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id)
下半部总结
为什么要有这么多种下半部处理方法?
- tasklet: 比上半部处理的好处是,可以被中断,用于处理稍微需要点时间的事务。 本质上还是在中断处理。
- work_queue: 比 tasklet 的好处是,work_queue 运行在进程上下文,可以进行休眠、阻塞、发生调度等。在内核线程中处理。
- threaded_irq: 比 work_queue 的好处是,在多核处理器上运行,效率高,因为每个 threaded_irq 创建一个内核线程,所以可以被多核分别调度。 而 work_queue 只能挤在一个核上面。
Linux中断系统中的重要数据结构
最核心的结构体是irq_desc,之前为了易于理解,我们说在Linux内核中有一个中断数组,对于每一个硬件中断,都有一个数组项,这个数组就是irq_desc数组。
注意:如果内核配置了CONFIG_SPARSE_IRQ,那么它就会用基数树(radix tree)来代替irq_desc数组。SPARSE的意思是“稀疏”,假设大小为1000的数组中只用到2个数组项,那不是浪费嘛?所以在中断比较“稀疏”的情况下可以用基数树来代替数组。
irq_desc数组
irq_desc结构体在include/linux/irqdesc.h
中定义,主要内容如下图, 每一个irq_desc数组项中都有一个函数:handle_irq,还有一个action链表。
struct irq_desc {
...
struct irq_data irq_data;
irq_flow_handler_t handle_irq;
struct irqaction *action; /* IRQ action list */
const char *name;
...
} ____cacheline_internodealigned_in_smp;
外部设备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链表。
irqaction结构体
irqaction结构体在 include/linux/interrupt.h
中定义,主要内容如下图:
struct irqaction {
irq_handler_t handler;
void *dev_id;
struct irqaction *next;
irq_handler_t thread_fn;
struct task_struct *thread;
struct irqaction *secondary;
unsigned int irq;
unsigned int flags;
const char *name;
...
} ____cacheline_internodealigned_in_smp;
- 当调用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,这就是中断上半部、下半部。
- 在reqeust_irq时可以传入dev_id,为何需要dev_id?作用如下,在共享中断中必须提供dev_id,非共享中断可以不提供。
- 中断处理函数执行时,可以使用dev_id
- 卸载中断时要传入dev_id,这样才能在action链表中根据dev_id找到对应项
irq_data结构体
irq_data结构体在 include/linux/irq.h
中定义,主要内容如下:
struct irq_data {
u32 mask;
unsigned int irq;
unsigned long hwirq;
struct irq_common_data *common;
struct irq_chip *chip;
struct irq_domain *domain;
#ifdef CONFIG_IRQ_DOMAIN_HIERARCHY
struct irq_data *parent_data;
#endif
void *chip_data;
};
- irq_data 就是个中转站,里面有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。
irq_domain结构体
irq_domain结构体在 include/linux/irqdomain.h
中定义,主要内容如下:
struct irq_domain {
const struct irq_domain_ops *ops;
irq_hw_number_t hwirq_max;
unsigned int revmap_size;
unsigned int linear_revmap[];
...
};
如何在设备树中指定中断,设备树的中断如何被转换为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。
irq_chip结构体
irq_chip结构体在 include/linux/irq.h
中定义,主要内容如下
struct irq_chip {
// irq_enable: enable the interrupt (defaults to chip->unmask if NULL)
void (*irq_enable)(struct irq_data *data);
// irq_disable: disable the interrupt
void (*irq_disable)(struct irq_data *data);
// irq_ack: start of a new interrupt
void (*irq_ack)(struct irq_data *data);
// irq_mask: mask an interrupt source
void (*irq_mask)(struct irq_data *data);
// irq_mask_ack: ack and mask an interrupt source
void (*irq_mask_ack)(struct irq_data *data);
...
};
- 我们在request_irq后,并不需要手工去使能中断,原因就是系统调用对应的irq_chip里的函数帮我们使能了中断。
- 我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用irq_chip中的相关函数。
- 但是对于外部设备相关的清中断操作,还是需要我们自己做的。外设备千变万化,内核里可没有对应的清除中断操作
在设备树中指定中断_在代码中获得中断
设备树里中断节点的语法
参考文档:
内核 Documentation\devicetree\bindings\interrupt-controller\interrupts.txt
设备树
设备树里的中断控制器
GPIO1有32个中断源,但是它把其中的16个汇聚起来向GIC发出一个中断,把另外16个汇聚起来向GIC发出另一个中断。这就意味着GPIO1会用到GIC的两个中断,会涉及GIC里的2个hwirq。这些层级关系、中断号(hwirq),都会在设备树中有所体现。
在设备树中,中断控制器节点中必须有一个属性:interrupt-controller
,表明它是“中断控制器”。还必须有一个属性:#interrupt-cells
,表明引用这个中断控制器的话需要多少个cell。
#interrupt-cells=<1>
只需要一个cell来表明使用“哪一个中断”。#interrupt-cells=<2>
需要一个cell来表明使用“哪一个中断”;还需要另一个cell来描述中断,一般是表明触发类型:第2个cell的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,低电平触发
如果中断控制器有级联关系,下级的中断控制器还需要表明它的“interrupt-parent”是谁,用了interrupt-parent”中的哪一个“interrupts”,
设备树里使用中断
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>;
设备树里中断节点的示例
在 arch/arm/boot/dts
目录下可以看到2个文件:imx6ull.dtsi
、100ask_imx6ull-14x14.dts
,把里面有关中断的部分内容抽取出来
ARM GIC 正常可以有分为几类: Shared Peripheral Interrupts (SPIs), Private Peripheral Interrupts (PPIs), Software Generated Interrupts (SGIs), Locality-specific Peripheral Interrupts (LPIs).
上图中,可以看出:
- gpc 中的
interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
第一个是类别 GIC_SPI, 第二个是中断号,第三个是中断触发类型。 - gpio1 中没有
interrupt-parent
,所以继承父节点的interrupt-parent
也就是上级中断是 gpc - spidev 中,上级中断是 gpio1,
interrupts = <1 1>;
表示中断是 gpio1 中的 1号,上升沿触发。
从设备树反推IMX6ULL的中断体系,如下,比之前的框图多了一个“GPC INTC”:
GPC INTC的英文是:General Power Controller, Interrupt Controller。它提供中断屏蔽、中断状态查询功能,它还提供唤醒功能。
在代码中获得中断
设备树中的节点有些能被转换为内核里的platform_device,有些不能
- 根节点下含有compatile属性的子节点,会转换为platform_device
- 含有特定compatile属性的节点的子节点,会转换为platform_device. 如果一个节点的compatile属性,它的值是这4者之一:"simple-bus","simple-mfd","isa","arm,amba-bus", 那么它的子结点(需含compatile属性)也可以转换为platform_device。
- 总线I2C、SPI节点下的子节点:不转换为platform_device. 某个总线下到子节点,应该交给对应的总线驱动程序来处理, 它们不应该被转换为platform_device。
对于platform_device
一个节点能被转换为platform_device,如果它的设备树里指定了中断属性,那么可以从platform_device中获得“中断资源”,可以使用下列函数获得IORESOURCE_IRQ资源,即中断号, struct resource *platform_get_resource(struct platform_device *dev, unsigned int type, unsigned int num);
resource type 选择 IORESOURCE_IRQ
对于I2C设备、SPI设备
I2C总线驱动在处理设备树里的I2C子节点时,也会处理其中的中断信息。一个I2C设备会被转换为一个i2c_client结构体,中断号会保存在i2c_client的irq成员里. drivers/i2c/i2c-core.c
static int i2c_device_probe(struct device *dev)
{
...
irq = of_irq_get(dev->of_node, 0);
...
}
对于SPI设备节点,SPI总线驱动在处理设备树里的SPI子节点时,也会处理其中的中断信息。一个SPI设备会被转换为一个spi_device结构体,中断号会保存在spi_device的irq成员里,drivers/spi/spi.c
static int spi_drv_probe(struct device *dev)
{
...
spi->irq = of_irq_get(dev->of_node, 0);
...
}
调用of_irq_get获得中断号
如果你的设备节点既不能转换为platform_device,它也不是I2C设备,不是SPI设备,那么在驱动程序中可以自行调用of_irq_get函数去解析设备树,得到中断号。
对于GPIO
参考:drivers/input/keyboard/gpio_keys.c
可以使用 gpio_to_irq
或 gpiod_to_irq
获得中断号。
// 可以使用下面的函数获得引脚和flag
button->gpio = of_get_gpio_flags(pp, 0, &flags);
bdata->gpiod = gpio_to_desc(button->gpio);
// 再去使用gpiod_to_irq获得中断号
irq = gpiod_to_irq(bdata->gpiod);
编写使用中断的按键驱动程序
对于GPIO按键,我们并不需要去写驱动程序,使用内核自带的驱动程序 drivers/input/keyboard/gpio_keys.c
就可以,然后你需要做的只是修改设备树指定引脚及键值。
编程思路
设备树相关
gpio_keys_100ask {
compatible = "100ask,gpio_key";
gpios = <&gpio5 1 GPIO_ACTIVE_HIGH
&gpio4 14 GPIO_ACTIVE_HIGH>;
pinctrl-names = "default";
pinctrl-0 = <&key1_pinctrl
&key2_pinctrl>;
};
代码相关
count = of_gpio_count(node);
从上面的设备树中,gpios
这一项计算引脚数量。根据上级节点的 cells 数量,来计算 count.- 从设备树中获取 gpio 相关
gpio_keys_100ask[i].gpio = of_get_gpio_flags(node, i, &flag); gpio_keys_100ask[i].gpiod = gpio_to_desc(gpio_keys_100ask[i].gpio);
- 申请使用 gpio, 并配置是否低电平有效
err = devm_gpio_request_one(&pdev->dev, gpio_keys_100ask[i].gpio, flags, NULL);
- 设备树中没有中断号,只能通过 gpio 转为 irq,
gpio_keys_100ask[i].irq = gpio_to_irq(gpio_keys_100ask[i].gpio);
- 申请中断
err = request_irq(gpio_keys_100ask[i].irq, gpio_key_isr, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys_100ask[i]);
IMX6ULL设备树修改及上机实验
中断相关的其他驱动程序
中断的硬件框架
中断路径上的3个部件
中断源, 中断控制器, CPU.
GIC 能够统一管理中断,并且在多核环境下,能够选择发送给某个核或者所有核进行处理。
GIC介绍与编程
介绍
中断可以有多种不同的类型
- 软件触发中断(SGI,Software Generated Interrupt) 这是由软件通过写入专用仲裁单元的寄存器即软件触发中断寄存器(ICDSGIR)显式生成的。它最常用于CPU核间通信。SGI既可以发给所有的核,也可以发送给系统中选定的一组核心。中断号0-15保留用于SGI的中断号。用于通信的确切中断号由软件决定。
- 私有外设中断(PPI,Private Peripheral Interrupt) 这是由单个CPU核私有的外设生成的。PPI的中断号为16-31。它们标识CPU核私有的中断源,并且独立于另一个内核上的相同中断源,比如,每个核的计时器。
- 共享外设中断(SPI,Shared Peripheral Interrupt) 这是由外设生成的,中断控制器可以将其路由到多个核。中断号为32-1020。SPI用于从整个系统可访问的各种外围设备发出中断信号。
中断可以处于多种不同状态:
- 非活动状态(Inactive)–这意味着该中断未触发。
- 挂起(Pending)–这意味着中断源已被触发,但正在等待CPU核处理。待处理的中断要通过转发到CPU接口单元,然后再由CPU接口单元转发到内核。
- 活动(Active)–描述了一个已被内核接收并正在处理的中断。
- 活动和挂起(Active and pending)–描述了一种情况,其中CPU核正在为中断服务,而GIC又收到来自同一源的中断。
GIC控制器的逻辑结构
上图中,可以看出 SGI 和 PPI 是每个核自己的,SPI 是所有核共享的。
初始化
- Distributor和CPU interface在复位时均被禁用。复位后,必须初始化GIC,才能将中断传递给CPU核。
- 在Distributor中,软件必须配置优先级、目标核、安全性并启用单个中断;随后必须通过其控制寄存器使能。
- 对于每个CPU interface,软件必须对优先级和抢占设置进行编程。每个CPU接口模块本身必须通过其控制寄存器使能。
- 在CPU核可以处理中断之前,软件会通过在向量表中设置有效的中断向量并清除CPSR中的中断屏蔽位来让CPU核可以接收中断。
- 可以通过禁用Distributor单元来禁用系统中的整个中断机制;可以通过禁用单个CPU的CPU接口模块或者在CPSR中设置屏蔽位来禁止向单个CPU核的中断传递。也可以在Distributor中禁用(或启用)单个中断。
- 为了使某个中断可以触发CPU核,必须将各个中断,Distributor和CPU interface全部使能,并将CPSR中断屏蔽位清零
GIC的寄存器
GIC分为两部分:Distributor和CPU interface,它们的寄存器都有相应的前缀:“GICD”、“GICC”。这些寄存器都是映射为内存接口(memery map),CPU可以直接读写。
相关寄存器可以参考相关资料
GIC编程
使用cortex A7处理器的芯片,一般都是使用GIC v2的中断控制器。 处理GIC的基地址不一样外,对GIC的操作都是一样的。
在NXP官网可以找到IMX6ULL的SDK包。 下载后可以参考这个文件:core_ca7.h,里面含有GIC的初始化代码。
异常向量表的安装与调用
系统启动流程
- 引导加载程序(Bootloader):
- 负责加载内核镜像(如 zImage 或 vmlinux)到内存的指定位置,传递启动参数(如设备树、命令行参数)传递给内核。并跳转到内核的入口点,通常是 head.S 中的某个标签(如 stext)。
- 例如:U-Boot、GRUB、LILO 等。
- 内核解压缩阶段(如果使用压缩内核)
- 如果内核镜像被压缩(如 zImage),在 head.S 之前会运行解压缩代码(如
arch/arm/boot/compressed/head.S
)。 - 解压缩代码将内核解压到内存的指定位置,然后跳转到 head.S。
- 如果内核镜像被压缩(如 zImage),在 head.S 之前会运行解压缩代码(如
- 内核入口点:
- 体系结构相关的启动代码,通常是 head.S 或类似的汇编文件。通常位于
arch/<arch>/kernel/head.S
- 初始化 CPU(设置 CPU 的寄存器、模式、缓存等) 和内存(设置页表、启用 MMU(内存管理单元)),设置最小化的运行环境。并跳转到 C 代码:调用 start_kernel 函数,进入通用的内核初始化流程。
- 体系结构相关的启动代码,通常是 head.S 或类似的汇编文件。通常位于
- 内核初始化:
- 调用 start_kernel 函数,进入通用的内核初始化流程(如调度器、内存管理、中断处理等)。
- 用户空间初始化: 启动 init 进程,进入用户空间。
异常向量表的安装
复制向量表
// arch\arm\kernel\head.S
ldr r13, =__mmap_switched @ address to jump to after
...
1: b __enable_mmu
===
// arch\arm\kernel\head.S __enable_mmu
...
b __turn_mmu_on
===
// arch\arm\kernel\head.S __turn_mmu_on
// ret r3 是一个伪指令,用于将程序计数器设置为 r3 的值,从而实现跳转。ret r3 的等价指令是 bx r3,通常用于函数指针调用或间接跳转。
...
mov r3, r13
ret r3
===
// arch\arm\kernel\head-common.S __mmap_switched:
...
b start_kernel
asmlinkage __visible void __init start_kernel(void) // init\main.c
setup_arch(&command_line); // arch\arm\kernel\setup.c
paging_init(mdesc); // arch\arm\mm\mmu.c
devicemaps_init(mdesc); // arch\arm\mm\mmu.c
vectors = early_alloc(PAGE_SIZE * 2); // 1.分配新向量表
early_trap_init(vectors); // 2.从代码把向量表复制到新向量表
...
// 3. 映射新向量表到虚拟地址0xffff0000
/*
* Create a mapping for the machine vectors at the high-vectors
* location (0xffff0000). If we aren't using high-vectors, also
* create a mapping at the low-vectors virtual address.
*/
map.pfn = __phys_to_pfn(virt_to_phys(vectors));
map.virtual = 0xffff0000;
map.length = PAGE_SIZE;
#ifdef CONFIG_KUSER_HELPERS
map.type = MT_HIGH_VECTORS;
#else
map.type = MT_LOW_VECTORS;
#endif
create_mapping(&map);
向量表在哪
上面代码中可以看到代码中向量表位于__vectors_start
,它在arch/arm/kernel/vmlinux.lds
中定义, 可以用 grep 搜索。
__vectors_start = .;
.vectors 0xffff0000 : AT(__vectors_start) {
*(.vectors)
}
. = __vectors_start + SIZEOF(.vectors);
__vectors_end = .;
__stubs_start = .;
.stubs ADDR(.vectors) + 0x1000 : AT(__stubs_start) {
*(.stubs)
}
在代码里搜.vectors
,可以找到向量表:
// arch\arm\kernel\entry-armv.S
.section .vectors, "ax", %progbits
.L__vectors_start:
W(b) vector_rst
W(b) vector_und
W(ldr) pc, .L__vectors_start + 0x1000
W(b) vector_pabt
W(b) vector_dabt
W(b) vector_addrexcptn
W(b) vector_irq
W(b) vector_fiq
中断向量
发生中断时,CPU跳到向量表去执行b vector_irq
。
vector_irq函数使用宏来定义:
处理流程
处理函数
GIC驱动程序对中断的处理流程
参考资料:
irq_desc
对于irq_desc,内核有两种分配方法:
- 一次分配完所有的irq_desc, 把所有层级的硬件中断 hwirq,按照顺序平坦开来映射为 virq。
- 按需分配用到某个中断才分配它的irq_desc
一级中断控制器处理流程
- 假设要使用UART模块,它发出的中断连接到GIC的32号中断,分配的irq_desc序号为16
- 在GIC domain中会记录(32, 16)
- 那么注册中断时就是:
request_irq(16, ...)
- 发生UART中断时
- 程序从GIC中读取寄存器知道发生了32号中断,通过GIC irq_domain可以知道virq为16
- 调用irq_desc[16]中的handleA函数,它的作用是调用action链表中用户注册的函数
多级中断控制器处理流程
- 假设GPIO模块下有4个引脚,都可以产生中断,都连接到GIC的33号中断
- GPIO也可以看作一个中断控制器,对于它的4个中断
- 对于GPIO模块中0~3这四个hwirq,一般都会一下子分配四个irq_desc
- 假设这4个irq_desc的序号为100~103,在GPIO domain中记录(0,100) (1,101)(2,102) (3,103)
- 对于KEY,注册中断时就是:
request_irq(102, ...)
- 按下KEY时:
- 程序从GIC中读取寄存器知道发生了33号中断,通过GIC irq_domain可以知道virq为16
- 调用irq_desc[16]中的handleB函数
- handleB读取GPIO寄存器,确定是GPIO里2号引脚发生中断
- 通过GPIO irq_domain可以知道virq为102
- 调用irq_desc[102]中的handleA函数,它的作用是调用action链表中用户注册的函数
GIC驱动程序分析
参考资料:
- linux kernel的中断子系统之(七):GIC代码分析
- Linux 4.9.88内核源码
Linux-4.9.88\drivers\irqchip\irq-gic.c
Linux-4.9.88/arch/arm/boot/dts/imx6ull.dtsi
- Linux 5.4内核源码
Linux-5.4\drivers\irqchip\irq-gic.c
Linux-5.4/arch/arm/boot/dts/stm32mp151.dtsi
GIC中的重要函数和结构体
沿着中断的处理流程,GIC涉及这4个重要部分:
- CPU从异常向量表中调用handle_arch_irq,这个函数指针是有GIC驱动设置的
- GIC才知道怎么判断发生的是哪个GIC中断
- 从GIC获得hwirq后,要转换为virq:需要有GIC Domain
- 调用irq_desc[virq].handle_irq函数:这也应该由GIC驱动提供
- 处理中断时,要屏蔽中断、清除中断等:这些函数保存在irq_chip里,由GIC驱动提供
从硬件上看,GIC的功能是什么?
- 可以使能、屏蔽中断
- 发生中断时,可以从GIC里判断是哪个中断
在内核里,使用gic_chip_data结构体表示GIC,gic_chip_data里有什么?
- irq_chip:中断使能、屏蔽、清除,放在irq_chip中的各个函数里实现
// drivers\irqchip\irq-gic.c static struct irq_chip gic_chip = { .irq_mask = gic_mask_irq, .irq_unmask = gic_unmask_irq, .irq_eoi = gic_eoi_irq, .irq_set_type = gic_set_type, .irq_get_irqchip_state = gic_irq_get_irqchip_state, .irq_set_irqchip_state = gic_irq_set_irqchip_state, .flags = IRQCHIP_SET_TYPE_MASKED | IRQCHIP_SKIP_SET_WAKE | IRQCHIP_MASK_ON_SUSPEND, };
- irq_domain
- 申请中断时
- 在设备树里指定hwirq、flag,可以使用irq_domain的函数来解析设备树
- 根据hwirq可以分配virq,把(hwirq, virq)存入irq_domain中
- 发生中断时,从GIC读出hwirq,可以通过irq_domain找到virq,从而找到处理函数
所以,GIC用gic_chip_data来表示,gic_chip_data中重要的成员是:irq_chip、irq_domain。
GIC初始化过程
start_kernel (init\main.c)
init_IRQ (arch\arm\kernel\irq.c)
irqchip_init (drivers\irqchip\irqchip.c)
of_irq_init (drivers\of\irq.c)
desc->irq_init_cb = match->data;
...
ret = desc->irq_init_cb(desc->dev,
desc->interrupt_parent);
设备树
- 从 dtb 反编译为 dts
- 在 dts 中寻找 interrupt-controlls,找到最顶层的那个 gic
- 在 gic 节点中,找到
compatible="arm,cortex-a7-gic"
, 通过这个 compatible 搜索驱动,找到对应的 driver 源码。
流程
grep -nr "arm,cortex-a7-gic" --include="*.c" drivers/
找到源码drivers\irqchip\irq-gic.c
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
- 宏定义
// include\linux\irqchip.h #define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn) // include\linux\of.h #define OF_DECLARE_2(table, name, compat, fn) \ _OF_DECLARE(table, name, compat, fn, of_init_fn_2) // ... #if defined(CONFIG_OF) && !defined(MODULE) #define _OF_DECLARE(table, name, compat, fn, fn_type) \ static const struct of_device_id __of_table_##name \ __used __section(__##table##_of_table) \ = { .compatible = compat, \ .data = (fn == (fn_type)NULL) ? fn : fn } #else #define _OF_DECLARE(table, name, compat, fn, fn_type) \ static const struct of_device_id __of_table_##name \ __attribute__((unused)) \ = { .compatible = compat, \ .data = (fn == (fn_type)NULL) ? fn : fn } #endif
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
展开后得到如下: 对应的函数是 gic_of_init
, 并且在 __irqchip_of_table
这个 section 里面。这对应到 drivers\irqchip\irqchip.c
中的 void __init irqchip_init(void)
中的 of_irq_init(__irqchip_of_table);
static const struct of_device_id __of_table_cortex_a7_gic \
__used __section(__irqchip_of_table) \
= { .compatible = "arm,cortex-a7-gic", \
.data = gic_of_init }
// drivers\irqchip\irqchip.c
void __init irqchip_init(void)
{
of_irq_init(__irqchip_of_table);
acpi_probe_device_table(irqchip);
}
整体的流程:
gic_of_init
int __init gic_of_init(struct device_node *node, struct device_node *parent)
ret = gic_of_setup(gic, node);
根据设备树进行设置,包含寄存器地址等。ret = gic_of_setup(gic, node); gic->raw_dist_base = of_iomap(node, 0); gic->raw_cpu_base = of_iomap(node, 1);
ret = __gic_init_bases(gic, -1, &node->fwnode);
ret = __gic_init_bases(gic, -1, &node->fwnode); set_handle_irq(gic_handle_irq); // 设置 gic_handle_irq, 和前面的其他地方呼应起来。 gic_init_chip(gic, NULL, name, true); ret = gic_init_bases(gic, irq_start, handle); gic->domain = irq_domain_create_linear(handle, gic_irqs, &gic_irq_domain_hierarchy_ops, gic);
static inline struct irq_domain *irq_domain_create_linear(struct fwnode_handle *fwnode, unsigned int size, const struct irq_domain_ops *ops, void *host_data)
用到的irq_domain_ops
, 其中gic_irq_domain_translate
用于解析设备树中的中断相关的 hwirq 和 flag.gic_irq_domain_alloc
用于申请 irq_desc 数组中新的空闲 virq, 并设置相关信息,然后保存 hwirq 和 virq 的关联到 domain 中。static const struct irq_domain_ops gic_irq_domain_hierarchy_ops = { .translate = gic_irq_domain_translate, .alloc = gic_irq_domain_alloc, .free = irq_domain_free_irqs_top, };
申请GIC中断
设备树
内核对设备树的处理
为设备树节点分配设备
of_device_alloc (drivers/of/platform.c)
dev = platform_device_alloc("", -1); // 分配 platform_device
num_irq = of_irq_count(np); // 计算中断数
// drivers/of/irq.c, 根据设备节点中的中断信息, 构造中断资源
of_irq_to_resource_table(np, res, num_irq)
of_irq_to_resource // drivers\of\irq.c
int irq = irq_of_parse_and_map(dev, index); // 获得virq, 中断号
解析设备树映射中断: irq_of_parse_and_map
// drivers/of/irq.c, 解析设备树中的中断信息, 保存在of_phandle_args结构体中
of_irq_parse_one(dev, index, &oirq)
// kernel/irq/irqdomain.c, 创建中断映射
irq_create_of_mapping(&oirq);
irq_create_fwspec_mapping(&fwspec);
// 调用irq_domain->ops的translate或xlate,把设备节点里的中断信息解析为hwirq, type
irq_domain_translate(domain, fwspec, &hwirq, &type)
// 看看这个hwirq是否已经映射, 如果virq非0就直接返回
virq = irq_find_mapping(domain, hwirq);
// 否则创建映射
if (irq_domain_is_hierarchy(domain)) {
// 返回未占用的virq
// 并用irq_domain->ops->alloc函数设置irq_desc
virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec);
if (virq <= 0)
return 0;
} else {
/* Create mapping */
// 返回未占用的virq
// 并通过irq_domain_associate调用irq_domain->ops->map设置irq_desc
virq = irq_create_mapping(domain, hwirq);
if (!virq)
return virq;
}
分析
drivers\irqchip\irq-gic.c
中static int gic_irq_domain_translate(struct irq_domain *d, struct irq_fwspec *fwspec, unsigned long *hwirq, unsigned int *type)
因为设备树中的 GIC_SPI 是 0, 所以interrupts = <GIC_SPI 89 IRQ_TYPE_LEVEL_HIGH>;
中的 89 在内核中 hwirq = 89 + 16 + 16 = 121/* Get the interrupt number and add 16 to skip over SGIs */ *hwirq = fwspec->param[1] + 16; /* * For SPIs, we need to add 16 more to get the GIC irq * ID number */ if (!fwspec->param[0]) *hwirq += 16;
- 在开发板上,命令
cat /proc/interrupts
可以得到详细中断情况 - 当然也可以在
/sys/kernel/irq/
中,通过grep -nr "121"
来搜索 hwirq 121 号中断。在相应的 virq 子文件夹中,cat 相关的 hwirq、 chip_name 来获取更加详细的数据。chip_name
中断名hwirq
设备树中中断号加过 32 或者 16 之后的数值,用于内核per_cpu_count
每个核上面的中断次数actions
自己定义的函数type
中断引脚类型,水平触发还是边沿触发。