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

异常向量表

_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.dtsi100ask_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_irqgpiod_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。
  • 内核入口点:
    • 体系结构相关的启动代码,通常是 head.S 或类似的汇编文件。通常位于 arch/<arch>/kernel/head.S
    • 初始化 CPU(设置 CPU 的寄存器、模式、缓存等) 和内存(设置页表、启用 MMU(内存管理单元)),设置最小化的运行环境。并跳转到 C 代码:调用 start_kernel 函数,进入通用的内核初始化流程。
  • 内核初始化:
    • 调用 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驱动程序分析

参考资料:

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.cstatic 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 中断引脚类型,水平触发还是边沿触发。

两类中断控制器处理流程_链式和层级

发表评论