文章说明:
-
Linux内核版本:5.0
-
架构:ARM64
-
参考资料及图片来源:《奔跑吧Linux内核》
-
Linux 5.0内核源码注释仓库地址:
zhangzihengya/LinuxSourceCode_v5.0_study (github.com)
1. 中断控制器
Linux 内核支持众多的处理器架构,因此从系统角度来看,Linux内核的中断管理可以分成如下4层:
- 硬件层,如CPU和中断控制器的连接
- 处理器架构管理层,如CPU中断异常处理
- 中断控制器管理层,如IRQ号的映射
- Linux 内核通用中断处理器层,如中断注册和中断处理
不同的架构对中断控制器有着不同的设计理念:
- ARM架构采用通用中断控制器 (Generic Interrupt Controller, GIC)
- x86架构采用高级可编程中断控制器 (Advanced Programmable Interrupt Controller, APIC)
本文以 ARM Vexpress V2P-CAIS-CA7 平台为例来介绍中断管理的实现,它支持 Cortex-A15 和 Cortex-A7 两个CPU簇,中断控制器采用GIC-400,支持GIC Version 2 (GIC-V2),如下图所示:
对于一个中断来说,支持多个中断状态:
- 不活跃(inactive)状态:中断处于无效状态
- 等待(pending)状态:中断处于有效状态,但是等待CPU响应该中断
- 活跃(active)状态:GPU已经响应中断
- 活跃并等待(active and pending)状态:CPU正在响应中断,但是该中断源又发送中断过来
对于GIC来说,为每一个硬件中断源分配一个中断号,这就是硬件中断号。GIC会为支持的中断类型分配中断号范围,如下表所示:
- SGI通常用于多核之间的通信。GIC-V2最多支持16个SGI,硬件中断号范围为0~15。SGI通常在Linux内核中被用作处理器之间的中断 (Inter-Processor Interrupt, IPI),并会送达到系统指定的CPU上。
- PPI是每个处理器内核私有的中断。GIC-V2 最多支持16个PPI中断,硬件中断号范围为16~31。PPI通常会送达到指定的CPU 上,应用场景有CPU本地定时器(local timer)。
- SPI是公用的外设中断。GIC-V2最多可以支持988个外设中断,硬件中断号范围为32~1019。
- SGI和PPI是每个CPU私有的中断,而SPI是所有CPU内核共享的。
外设中断可以支持两种中断触发方式:
- 边沿触发(edge-triggered):当中断源产生一个上升沿或者下降沿时,触发一个中断
- 电平触发(level-sensitive):当中断信号线产生一个高电平或者低电平时,触发一个 中断
GIC主要由两部分组成,分别是仲裁单元(distributor)和CPU接口模块。仲裁单元为每一个中断源维护一个状态机,支持的状态有 inactive pending、active和active and pending。GIC检测中断的流程如下:
-
当GIC检测到一个中断发生时,会将该中断标记为pending状态
-
对于处于pending状态的中断,仲裁单元会确定目标CPU,将中断请求发送到这个CPU
-
对于每个CPU,仲裁单元会从众多处于pending状态的中断中选择一个优先级最高的中断,发送到目标CPU的CPU接口模块上
-
CPU接口模块会决定这个中断是否可以发送给CPU。如果该中断的优先级满足要求,GIC会发送一个中断请求信号给该CPU
-
当一个CPU进入中断异常后,会读取GICC_IAR来响应该中断(一般由Linux内核的中断处理程序来读寄存器)。寄存器会返回硬件中断号(hardware interrupt ID),对于SGI来说,返回源CPU的ID(source processor ID)。当GIC感知到软件读取了该寄存器后,又分为如下情况:
- 如果该中断处于pending状态,那么状态将变成active
- 如果该中断又重新产生,那么pending将状态变成active and pending状态
- 如果该中断处于active状态,将变成active and pending状态
-
当处理器完成中断服务,必须发送一个完成信号结束中断(End Of Interrupt, EOI)给GIC
2. 硬件中断号和 Linux 中断号的映射
在Linux中,注册中断接口函数request_irq()、request_threaded_irq() 使用Linux内核软件中断号(俗称软件中断号或IRQ号),而不是硬件中断号。接下来,以QEMU虚拟机的串口 0设备(它在主板上的序号为1,硬件中断号为33)为例,介绍硬件中断号是如何和Linux内核的IRQ号映射的。
-
首先介绍下前置知识
-
ARM64平台的设备描述基本上采用设备树(Device Tree)模式来描述硬件设备。QEMU虚拟机的设备树的描述脚本并没有实现在内核代码中,而实现在QEMU代码里。因此,可以通过DTC命令反编译出设备树脚本(Device Tree Script, DTS),反编译 DTS时,与串口中断相关的描述如下:
pl011@9000000 { clock-names = "uartclk\Oapb_pclk"; clocks = < 0x8000 0x8000 >; // interrupts 域描述相关的属性: // 中断类型,共享外设中断(GIC_SPI)在设备树中用0来表示,私有外设中断(GIC_PPI)在设备树中用1来表示,这里是0x00 // 中断 ID,这里是0x01 // 触发类型,(即IRQ_TYPE_LEVEL_HIGH,高电平触发) interrupts = < 0x00 0x01 0x04 >; reg = < 0x00 0x9000000 0x00 0x1000 >; // "arm,pl011\0arm,primecell" 外设的兼容字符串,用于和驱动程序进行匹配工作 compatible = "arm,pl011\0arm,primecell"; };
-
一个中断控制器用一个
irq_domain
数据结构来抽象描述,irq_domain数据结构的定义如下:// 一个中断控制器用一个 irq_domain 数据结构来抽象描述 struct irq_domain { // 用于将 irq_domain 连接到全局链表 irq_domain_list 中 struct list_head link; // irq_domain 的名称 const char *name; // irq_domain 映射操作使用的方法集合 const struct irq_domain_ops *ops; ... // 该 irq_domain 支持中断数量的最大值 irq_hw_number_t hwirq_max; unsigned int revmap_direct_max_irq; // 线性映射的大小 unsigned int revmap_size; // 基数树映射的根节点 struct radix_tree_root revmap_tree; struct mutex revmap_tree_mutex; // 线性映射用到的查找表 unsigned int linear_revmap[]; };
-
-
系统初始化时会查找DTS中定义的中断控制器,计算GIC最多支持的中断源的个数,GIC-V2规定最多支持1020个中断源,在SoC设计阶段就确定ARM SoC可以支持多少个中断源了,然后,调用
irq_domain_create_linear()->__irq_domain_add()
函数注册一个irq_domain
数据结构:struct irq_domain *__irq_domain_add(struct fwnode_handle *fwnode, int size, irq_hw_number_t hwirq_max, int direct_max, const struct irq_domain_ops *ops, void *host_data) { ... // 注册一个 irq_domain 数据结构 domain = kzalloc_node(sizeof(*domain) + (sizeof(unsigned int) * size), GFP_KERNEL, of_node_to_nid(of_node)); // 初始化 irq_domain 数据结构 ... // 把 irq_domain 数据结构加入全局的链表 irq_domain_list 中 list_add(&domain->link, &irq_domain_list); ... }
-
回到系统枚举阶段的中断号映射过程,在
of_amba_device_create()
函数中,irq_of_parse_and_map()
函数负责把硬件中断号映射到Linux内核的IRQ号// 把硬件中断号映射到 Linux 内核的 IRQ 号 unsigned int irq_of_parse_and_map(struct device_node *dev, int index) { struct of_phandle_args oirq; if (of_irq_parse_one(dev, index, &oirq)) return 0; return irq_create_of_mapping(&oirq); }
-
irq_of_parse_and_map()->of_irq_parse_one()
:主要用于解析DTS文件中设备定义的属性,如reg、interrupts等, 最后把DTS中的interrupts的值存放在oirq->args[]数组中 -
irq_of_parse_and_map()->irq_create_of_mapping->irq_create_fwspec_mapping()
:unsigned int irq_create_fwspec_mapping(struct irq_fwspec *fwspec) { ... // 查找外设所属的中断控制器的 irq_domain if (fwspec->fwnode) { domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_WIRED); if (!domain) domain = irq_find_matching_fwspec(fwspec, DOMAIN_BUS_ANY); } else { domain = irq_default_domain; } if (!domain) { pr_warn("no irq domain found for %s !\n", of_node_full_name(to_of_node(fwspec->fwnode))); return 0; } // 进行硬件中断号的转换 // hwirq 存储着这个硬件中断号,type存储该外设的中断类型 if (irq_domain_translate(domain, fwspec, &hwirq, &type)) return 0; ... // 如果这个硬件中断号已经映射过了,那么 irq_find_mapping() 函数可以找到映射后的中断号,在此情境下,该硬件中断号还没有映射 virq = irq_find_mapping(domain, hwirq); if (virq) { ... } if (irq_domain_is_hierarchy(domain)) { // 映射的核心函数 virq = irq_domain_alloc_irqs(domain, 1, NUMA_NO_NODE, fwspec); if (virq <= 0) return 0; } else { /* Create mapping */ virq = irq_create_mapping(domain, hwirq); if (!virq) return virq; } ... return virq; }
-
irq_of_parse_and_map()->irq_create_of_mapping->irq_create_fwspec_mapping()->irq_domain_alloc_irqs()->__irq_domain_alloc_irqs->irq_domain_alloc_descs()->__irq_alloc_descs()
:int __ref __irq_alloc_descs(int irq, unsigned int from, unsigned int cnt, int node, struct module *owner, const struct irq_affinity_desc *affinity) { ... // 在 allocated_irqs 位图中查找第一个包含连续 cnt 个 0 的位图区域 start = bitmap_find_next_zero_area(allocated_irqs, IRQ_BITMAP_BITS, from, cnt, 0); ... // 分配 irq_desc 数据结构 ret = alloc_descs(start, cnt, node, affinity, owner); ... }
irq_desc
数据结构:struct irq_desc { ... struct irq_data irq_data; ... } ____cacheline_internodealigned_in_smp;
irq_data数据结构:
struct irq_data { ... // 软件中断号 unsigned int irq; // 硬件中断号 unsigned long hwirq; ... };
-
irq_of_parse_and_map()->irq_create_of_mapping->irq_create_fwspec_mapping()->irq_domain_alloc_irqs()->__irq_domain_alloc_irqs()->irq_domain_alloc_descs()
函数返回 allocated_irqs 位图中第一个空闲位,这是软件中断号 -
irq_of_parse_and_map()->irq_create_of_mapping->irq_create_fwspec_mapping()->irq_domain_alloc_irqs()->__irq_domain_alloc_irqs()->irq_domain_alloc_irqs_hierarchy()->gic_irq_domain_alloc()
:static int gic_irq_domain_alloc(struct irq_domain *domain, unsigned int virq, unsigned int nr_irqs, void *arg) { ... // 解析出硬件中断号并存放在 hwirq 中 ret = gic_irq_domain_translate(domain, fwspec, &hwirq, &type); ... for (i = 0; i < nr_irqs; i++) { // 映射工作 ret = gic_irq_domain_map(domain, virq + i, hwirq + i); if (ret) return ret; } return 0; }
-
irq_of_parse_and_map()->irq_create_of_mapping->irq_create_fwspec_mapping()->irq_domain_alloc_irqs()->__irq_domain_alloc_irqs()->irq_domain_alloc_irqs_hierarchy()->gic_irq_domain_alloc()->gic_irq_domain_map()->irq_domain_set_info()->irq_domain_set_hwirq_and_chip()
:通过IRQ号获取irq_data数据结构,并把硬件中断号hwirq设置到irq_data数据结构中的hwirq成员中,就完成了硬件中断号到软件中断号的映射。 -
irq_of_parse_and_map()->irq_create_of_mapping->irq_create_fwspec_mapping()->irq_domain_alloc_irqs()->__irq_domain_alloc_irqs()->irq_domain_alloc_irqs_hierarchy()->gic_irq_domain_alloc()->gic_irq_domain_map()->irq_domain_set_info()->__irq_set_handler()
:设置中断描述符 desc->handle_irq 的回调函数 -
综上所述,硬件中断号和软件中断号的映射过程如下图所示: