原图

中断控制器

ARM 公司提供了一种标准的中断控制器,称为通用中断控制器(Generic IntermurController,GIC)。目前 GIC 架构规范有 4 个版本:v1~v4。GICv2 最多支持 8 个处理器。GIC v3 最多支持 128 个处理器,GICv3 和 GICv4 只支持 ARM64 处理器。

GICv2 控制器的两个主要功能

  • 分发器(Distributor):系统中所有的中断源连接到分发器,分发器的寄存器用来控制单个中断的属性:优先级、状态、安全、转发信息(可以被发送到哪些处理器)和使能状态。分发器决定哪个中断应该通过处理器接口转发到哪个处理器
  • 处理器接口(CPU Interface):处理器通过处理器接口接收中断。处理器接口提供的寄存器用来屏蔽和识别中断,控制中断的状态。每个处理器有一个单独的处理器接口。软件通过中断号识别中断,每个中断号唯一对应一个中断源

中断有以下 4 种类型

  • 软件生成的中断(Software Generated Interrupt,SGI):中断号 0~15,通常用来实现处理器间中断(Inter-Processor Iterrupt,IPI)。这种中断是由软件写分发器的软件生成中断寄存器(GICD_SGIR)生成的
  • 私有外设中断(Private Peripheral Iterrupt,PPI):中断号 16~31。处理器私有的中断源,不同处理器的相同中断源没有关系,比如每个处理器的定时器
  • 共享外设中断(Shared Peripheral Iterrupt,SPI):中断号 32~1020。这种中断可以被中断控制器转发到多个处理器
  • 局部特定外设中断(Locality-specific Peripheral Interrunt,LPI),基于消息的中断 GIC, v1 和 GIC v2 不支持 LPI

中断有以下 4 种状态

  • Inactive:中断源没有发送中断
  • Pending:中断源已经发送中断,等待处理器外理
  • Active:处理器已经确认中断,正在处理
  • Active and pending:处理器正在处理中断,相同的中断源又发送了一个中断

中断域

一个大型系统可能有多个中断控制器,这些中断控制器可以级联,一个中断控制器作为中断源连接到另一个中断控制器,但只有一个中断控制器作为根控制器直接连接到处理器。如下图:

为了把每个中断控制器本地的硬件中断号映射到全局唯一的 Linux 中断号(虚拟中断号),内核定义了中断域 irq_domain,每个中断控制器有自己的中断域。

创建中断域

中断控制器的驱动程序使用分配函数 irq_domain_add_*() 创建和注册中断域。每种映射方法提供不同的分配函数,调用者必须给分配函数提供 irq_domain_ops 结构体,分配函在执行成功的时候返回 irq_domain 的指针。

中断域支持以下映射方法

  • 线性映射(linear map)

线性映射维护一个固定大小的表,索引是硬件中断号。如果硬件中断号的最大数量固定的,并且比较小(小于 256),那么线性映射是好的选择。对于线性映射,分配中断的函数如下:

1
2
3
4
static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node,
unsigned int size,
const struct irq_domain_ops *ops,
void *host_data)
  • 树映射(tree map)

树映射使用基数树(radix tree)保存硬件中断号到 Linux 中断号的映射。如果硬件中断号可能非常大,那么树映射是好的选择,因为不需要根据最大硬件中断号分配一个很大的表。对于树映射,分配中断域的函数如下:

1
2
3
static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node,
const struct irq_domain_ops *ops,
void *host_data)
  • 不映射(no map)

有些中断控制器很强,硬件中断号是可以配置的,例如 PowerPC 架构使用的 MPIC(Muti-PossorIterrupt Controller)。我们直接把 Linux 中断号写到硬件,硬件中断号就是 Linux 中断号,不需要映射。对于不映射,分配中断域的函数如下:

1
2
3
4
static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node,
unsigned int max_irq,
const struct irq_domain_ops *ops,
void *host_data)

分配函数把主要工作委托给函数__irq_domain_add()。函数__irq_domain_add() 的执行过是、分配一个 irq_domain 结构体,初始化成员,然后把中断域添加到全局链表 irq_domain_list 中。

创建映射

创建中断域以后,需要向中断域添加硬件中断号到 Linux 中断号的映射,内核提供了函数 irq_create_mapping:

1
2
static inline unsigned int irq_create_mapping(struct irq_domain *host,
irq_hw_number_t hwirq)

输入参数是中断域和硬件中断号,返回 Linux 中断号,该函数首先分配 Linux 中断号然后把硬件中断号到 Linux 中断号的映射添加到中断域。

查找映射

中断处理程序需要根据硬件中断号查找 Linux 中断号,内核提供了函数 irq_find_mapping 函数:

1
2
3
4
5
6
7
/**
* irq_find_mapping() - Find a linux irq from a hw irq number.
* @domain: domain owning this hardware interrupt
* @hwirq: hardware irq number in that domain space
*/
static inline unsigned int irq_find_mapping(struct irq_domain *domain,
irq_hw_number_t hwirq)

输入参数是中断域和硬件中断号,返回 Linux 中断号。

Linux 中断处理

对于中断控制器的每个中断源,向中断域添加硬件中断号到 Linux 中断号的映射时内核分配一个 Linux 中断号和一个中断描述符 irq_desc,中断描述符有两个层次的中断处理函数。

  • 第一层处理函数是中断描述符的成员 handle_irq()。
  • 第二层处理函数是设备驱动程序注册的处理函数。中断描述符有一个中断处理链表(irq_desc.action),每个中断处理描述符(irq_action)保存设备驱动程序注册的处理函数。因为多个设备可以共享同一个硬件中断号,所以中断处理链表可能挂载多个中断处理描述符。

怎么存储 Linux 中断号到中断描述符的映射关系?有两种实现方式。

  • 如果中断编号是稀疏的(即不连续),那么使用基数树(radix tree)存储。需要开启配置宏 CONFIG_SPARSE_IRQ。
  • 如果中断编号是连续的,那么使用数组存储。

设备驱动程序可以使用函数 request_irq() 注册中断处理函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* request_irq - Add a handler for an interrupt line
* @irq: The interrupt line to allocate
* @handler: Function to be called when the IRQ occurs.
* Primary handler for threaded interrupts
* If NULL, the default primary handler is installed
* @flags: Handling flags
* @name: Name of the device generating this interrupt
* @dev: A cookie passed to the handler function
*
* This call allocates an interrupt and establishes a handler; see
* the documentation for request_threaded_irq() for details.
*/
static inline int __must_check
request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)
  • 参数 irq 是 Linux 中断号。
  • 参数 handler 是处理函数。
  • 参数 flags 是标志位,可以是 0 或者以下标志位的组合。

    IRQF_SHARED:允许多个设备共享同一个中断号 IRQF_TIMER:定时器中断 IRQF_PERCPU:中断是每个处理器私有的 IRQF_NOBALANCING:不允许该中断在处理器之间负载均衡。 IRQF_NO_THREAD:中断不能线程化

  • 参数 name 是设备名称。
  • 参数 dev 是传给处理函数(由参数 handler 指定)的参数。

在上一节我们分析了 ARM64 架构下的异常处理流程,当异常发生时会跳转到异常向量表中执行异常处理函数,中断是异常的一种,因此当中断发生时也会跳转到异常向量表中执行中断处理函数,ARM64 对应有不同的异常级别,其中内核运行在 EL1 级别,对于这种情况,根据我们上一节的分析中断会跳转到 el1h_64_irq 标号处运行程序,该标号处的处理在不同的异常下都是类似的,不同的是跳转的处理函数不同,对于 el1h_64_irq 标号会跳转到 el1h_64_irq_handle 函数处处理,下面我们详细看下该函数的具体处理工作是怎样的吧:

同样由于函数调用层次较多,这里采用流程图的方式展现,当然这必然会忽略掉很多细节,详细的读者可以自己追踪源码查看。 原图

通过上图我们大概就了解了中断处理的整体流程。当中断发生时跳转到异常向量表,然后跳转到对应的标号处执行,然后执行中断控制器的处理函数,再调用外部中断注册的中断处理函数。

中断控制器驱动初始化

以一张图来整理这些函数之间的调用关系,如下: 原图

参考文献

《Linux 内核深度解析》