字符设备驱动-9-中断子系统-中断结构体

0 引入SPARSE_IRQ#

如果内核配置了 CONFIG_SPARSE_IRQ,那么它就会用 基数树(radix tree) 来代替 irq_desc 数组。
SPARSE 的意思是“稀疏”,假设大小为 1000 的数组中只用到 2 个数组项,那不是浪费嘛?当中断比较“稀疏”时可以用基数树来代替数组。
image

1 irq_desc 数组#

位于include/linux/irqdesc.h

image

在这里插入图片描述

内核中记录一个irq_desc的数组,数组的每一项对应一个中断或者一组中断(使用同一中断号)。irq_desc几乎记录所有中断相关的东西,这个结构是中断的核心。每一个irq_desc数组项中都有一个函数:handle_irq,还有一个action链表

irq_desc 数组结构链路如下图:

image

1.1 中断处理函数handle_irq#

1.1.1 共享中断概念引入#


image

  1. 上图一个gpio按键连接gpio模块第一个引脚1,可以设置该引脚,当电平发生变化时,让该引脚产生中断,那么gpio模块会上报中断到gic模块, gic模块继续中断cpu。
  2. 同理当一个外部设备网卡和该gpio按键可以共享一个中断,也接到gpio模块第一个引脚1,gpio模块会上报中断到gic模块, gic模块继续中断cpu。这里就用到了共享中断的概念。

可以看到中断的触发时从左到右的过程,那么cpu进行响应中断请求时就是从右到左的过程。

  1. cpu读取GIC控制器,判段中断号,如果是A号中断说明是来源于gpio模块,如果是A'中断,说明来源于其他模块

  2. A号中断的来源有很多种,有gpio0,gpio1...., 又会从gpio控制寄存器来辨别倒是是哪一个gpio产生的中断,比如是B号中断

  3. B号中断的来源有很多种,有按键,网卡...

1.1.2 中断的处理函数来源#

中断处理函数来源有三:

  1. GIC 的处理函数:
    GIC 中断 CPU 时,CPU 读取 GIC 状态得到中断 A。假设 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家),这个函数需要读取芯片的 GPIO 控制器,细分发生的是哪一个 GPIO 中断(假设是B),再去调用 irq_desc[B]. handle_irq

CPU从异常向量表中调用handle_arch_irq,这个函数指针是有GIC驱动设置的.调用irq_desc[virq].handle_irq函数:这也应该由GIC驱动提供。

  1. 模块的中断处理函数:
    对于 GPIO 模块向 GIC 发出的中断 B , 它 的 处 理 函 数 是irq_desc[B].handle_irq
    导致 GPIO 中断 B 发生的原因很多,可能是外部设备 1,可能是外部设备n,可能只是某一个设备,也可能是多个设备。所以 irq_desc[B].handle_irq会调用链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。

  2. 外部设备提供的处理函数:(也就是action里面的函数)
    这里说的“外部设备”可能是芯片,也可能是简单的按键。它们的处理函数由自己驱动程序提供。对于共享中断,比如 GPIO 中断 B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以 irq_desc[B]中应该有一个链表, 这个链表就是 action 链表。一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,一一执行。

1.2 irqaction#

irqaction 结构体在include/linux/interrupt.h

image

image

irq_desc[A]这里对应的action一般为NULL, 而irq_desc[B]handle_irq会调用链表里的函数,这些函数就是对应不同的irqaction

当调用request_irq、request_threaded_irq 注册中断处理函数时,内核就会构造一个 irqaction 结构体。在里面保存name、dev_id等,最重要的是 handler、thread_fn、thread
函数原型为:

1.2.1 request_threaded_irq#

1.2.2 request_irq#

1.2.3 devm_request_irq#

image
image
image
这里irq编号使用的虚拟中断号,虚拟中断号怎么来?详见后面1.4 irq_domain

handler :是中断处理的上半部函数,用来处理紧急的事情。
thread_fn :对应一个内核线程 thread,当 handler 执行完毕,Linux 内核会唤醒对应的内核线程。在内核线程里,会调用 thread_fn 函数。
  1. 可以提供 handler 而不提供 thread_fn,就退化为一般的 request_irq 函数。
  2. 可以不提供 handler 只提供 thread_fn,完全由内核线程来处理中断。
  3. 也可以既提供 handler 也提供 thread_fn,这就是中断上半部、下半部。

在 reqeust_irq 时可以传入 dev_id,为何需要 dev_id?作用有 2:

  1. 中断处理函数执行时,可以使用 dev_id
  2. 卸载中断时要传入 dev_id,这样才能在action链表中根据 dev_id 找到对应项(所以在共享中断中必须提供 dev_id,非共享中断可以不提供)

1.3 irq_data#

定义再include/linux/irq.h

image
irq_data就是个中转站,里面有 irq_chip 指针 irq_domain 指针,irq 是软件中断号,hwirq 是硬件中断号。
比如GPIO 中断 B 就是软件中断号,可以找到 irq_desc[B]这个数组项;GPIO 里的第 x 号中断,这就是 hwirq

irq、hwirq 之间的联系呢?由 irq_domain 来建立。下面介绍irq_domain

image

1.4 irq_domain#

include/linux/irqdomain.h 中定义该结构。

image

img

设备树中你会看到这样的属性:

1
2
interrupt-parent = <&gpio1>;
interrupts = <5 IRQ_TYPE_EDGE_RISING>;

表示使用gpio1_5作为中断,hwirq 就是 5。当我们在驱动中会使用 request_irq(irq, handler)这样的函数来注册中断,irq编号就是虚拟中断,那么虚拟中断号(软件中断号)要怎么得到?
就是gpio1对应的irq_domain 结构体。irq_domain 结构体中有一个 irq_domain_ops 结构体,里面有各种操作函数。

1.4.0 中断控制器注册 irq_domain#

img

通过 __irq_domain_add 初始化irq_domain数据结构,然后把 irq_domain 添加到全局的链表irq_domain_list中。

1.4.1 irq_domain_ops#

1.4.1.1 xlate函数#

image

xlate函数
用来解析设备树的中断属性,提取出 hwirq、type 等信息。

1.4.1.2 map函数#

hwirq 转换为 irq

1.5 irq_chip#

irq_chip 结构体在include/linux/irq.h中定义

image

1
2
3
4
5
6
7
8
9
* @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_enable函数帮我们使能了中断。
我们提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用irq_chip中的相关函数。
但是对于外部设备相关的清中断操作,还是需要我们自己做的。就像上面图里的“外部设备 1“、“外部设备 n”,外设备千变万化,内核里没有对应的清除中断操作。

2 中断不同结构体之间的关系框图#

image-20240810191140464