Linux 中断子系统(三)Linux 中断处理过程
原图
中断控制器
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 | static inline struct irq_domain *irq_domain_add_linear(struct device_node *of_node, |
- 树映射(tree map)
树映射使用基数树(radix tree)保存硬件中断号到 Linux 中断号的映射。如果硬件中断号可能非常大,那么树映射是好的选择,因为不需要根据最大硬件中断号分配一个很大的表。对于树映射,分配中断域的函数如下:
1 | static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node, |
- 不映射(no map)
有些中断控制器很强,硬件中断号是可以配置的,例如 PowerPC 架构使用的 MPIC(Muti-PossorIterrupt Controller)。我们直接把 Linux 中断号写到硬件,硬件中断号就是 Linux 中断号,不需要映射。对于不映射,分配中断域的函数如下:
1 | static inline struct irq_domain *irq_domain_add_nomap(struct device_node *of_node, |
分配函数把主要工作委托给函数__irq_domain_add()。函数__irq_domain_add() 的执行过是、分配一个 irq_domain 结构体,初始化成员,然后把中断域添加到全局链表 irq_domain_list 中。
创建映射
创建中断域以后,需要向中断域添加硬件中断号到 Linux 中断号的映射,内核提供了函数 irq_create_mapping:
1 | static inline unsigned int irq_create_mapping(struct irq_domain *host, |
输入参数是中断域和硬件中断号,返回 Linux 中断号,该函数首先分配 Linux 中断号然后把硬件中断号到 Linux 中断号的映射添加到中断域。
查找映射
中断处理程序需要根据硬件中断号查找 Linux 中断号,内核提供了函数 irq_find_mapping 函数:
1 | /** |
输入参数是中断域和硬件中断号,返回 Linux 中断号。
Linux 中断处理
对于中断控制器的每个中断源,向中断域添加硬件中断号到 Linux 中断号的映射时内核分配一个 Linux 中断号和一个中断描述符 irq_desc,中断描述符有两个层次的中断处理函数。
- 第一层处理函数是中断描述符的成员 handle_irq()。
- 第二层处理函数是设备驱动程序注册的处理函数。中断描述符有一个中断处理链表(irq_desc.action),每个中断处理描述符(irq_action)保存设备驱动程序注册的处理函数。因为多个设备可以共享同一个硬件中断号,所以中断处理链表可能挂载多个中断处理描述符。
怎么存储 Linux 中断号到中断描述符的映射关系?有两种实现方式。
- 如果中断编号是稀疏的(即不连续),那么使用基数树(radix tree)存储。需要开启配置宏 CONFIG_SPARSE_IRQ。
- 如果中断编号是连续的,那么使用数组存储。
设备驱动程序可以使用函数 request_irq() 注册中断处理函数:
1 | /** |
- 参数 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 内核深度解析》