在 Linux 操作系统的复杂体系中,中断管理起着至关重要的作用。它就像是系统运行的 “调度员”,确保各种硬件设备的请求能够及时得到处理,维持系统的高效稳定运行。而在 Linux 中断管理中,中断号的映射是一个关键而又神秘的环节。本文将深入探索 Linux 中断管理中中断号的映射机制,带你揭开其神秘面纱。
一、引言
在 Linux 内核中,中断管理涉及多个层面。不同的处理器架构对中断控制器有着不同的设计理念,例如 ARM 架构采用通用中断控制器(Generic Interrupt Controller,GIC),而 x86 架构采用高级可编程中断控制器(Advanced Programmable Interrupt Controller,APIC)。
Linux 内核的中断管理可以分为四层:硬件层,如 CPU 和中断控制器的连接;处理器架构管理层,如 CPU 中断异常处理;中断控制器管理层,如 IRQ 号的映射;Linux 内核通用中断处理器层,如中断注册和中断处理。
对于中断控制器而言,它需要对外设中断进行编码,用硬件中断号(HW interrupt ID)来标识外设的中断。而对于 CPU 来说,它需要为每一个外设中断编号,即 IRQ number,这个 IRQ number 是一个虚拟的中断 ID,和硬件无关,仅被 CPU 用来标识一个外设中断。在中断控制器级联的情况下,仅用 HW interrupt ID 不能唯一标识一个外设中断,还需要知道该 HW interrupt ID 所属的中断控制器。因此,Linux 内核中的中断子系统需要提供一个将 HW interrupt ID 映射到 IRQ number 上来的机制。
以 ARM 架构为例,ARM Vexpress V2P-CAIS-CA7 平台的中断控制器采用 GIC-400,支持 GIC Version 2(GIC-V2)。对于一个中断来说,支持多个中断状态,包括不活跃(inactive)状态、等待(pending)状态、活跃(active)状态和活跃并等待(active and pending)状态。GIC 会为支持的中断类型分配中断号范围,如 SGI 通常用于多核之间的通信,硬件中断号范围为 0~15;PPI 是每个处理器内核私有的中断,硬件中断号范围为 16~31;SPI 是公用的外设中断,硬件中断号范围为 32~1019。
外设中断可以支持两种中断触发方式:边沿触发(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 来响应该中断,寄存器会返回硬件中断号,对于 SGI 来说,返回源 CPU 的 ID;当 GIC 感知到软件读取了该寄存器后,如果该中断处于 pending 状态,那么状态将变成 active;如果该中断又重新产生,那么 pending 将状态变成 active and pending 状态;如果该中断处于 active 状态,将变成 active and pending 状态;当处理器完成中断服务,必须发送一个完成信号结束中断(End Of Interrupt,EOI)给 GIC。
二、中断控制器
Linux 内核支持众多的处理器架构,因此从系统角度来看,Linux内核的中断管理可以分成如下4层:
硬件层,如CPU和中断控制器的连接
处理器架构管理层,如CPU中断异常处理
中断控制器管理层,如IRQ号的映射
Linux 内核通用中断处理器层,如中断注册和中断处理
2.1不同架构的中断控制器设计
ARM 架构采用通用中断控制器 (GIC),x86 架构采用高级可编程中断控制器 (APIC)。
ARM 架构下的通用中断控制器(GIC)在不同的平台上发挥着重要作用。以 ARM Vexpress V2P-CAIS-CA7 平台为例,该平台采用 GIC-400,支持 GIC Version 2(GIC-V2)。而 x86 架构则采用高级可编程中断控制器(APIC),其设计和功能与 ARM 架构的 GIC 有所不同。
本文以 ARM Vexpress V2P-CAIS-CA7 平台为例来介绍中断管理的实现,它支持 Cortex-A15 和 Cortex-A7 两个CPU簇,中断控制器采用GIC-400,支持GIC Version 2 (GIC-V2),如下图所示:
ARM Vexpress V2P-CAIS-CA7 平台的中断控制器采用 GIC-400,对于一个中断来说,支持多个中断状态,包括不活跃(inactive)状态、等待(pending)状态、活跃(active)状态和活跃并等待(active and pending)状态。
例如,当 GIC 检测到一个中断发生时,会将该中断标记为 pending 状态;对于处于 pending 状态的中断,仲裁单元会确定目标 CPU,将中断请求发送到这个 CPU;当一个 CPU 进入中断异常后,会读取 GICC_IAR 来响应该中断,寄存器会返回硬件中断号,对于 SGI 来说,返回源 CPU 的 ID;
当 GIC 感知到软件读取了该寄存器后,如果该中断处于 pending 状态,那么状态将变成 active;如果该中断又重新产生,那么 pending 将状态变成 active and pending 状态;如果该中断处于 active 状态,将变成 active and pending 状态;当处理器完成中断服务,必须发送一个完成信号结束中断(End Of Interrupt,EOI)给 GIC。
对于一个中断来说,支持多个中断状态:
不活跃(inactive)状态:中断处于无效状态
等待(pending)状态:中断处于有效状态,但是等待CPU响应该中断
活跃(active)状态:GPU已经响应中断
活跃并等待(active and pending)状态:CPU正在响应中断,但是该中断源又发送中断过来
对于GIC来说,为每一个硬件中断源分配一个中断号,这就是硬件中断号。GIC会为支持的中断类型分配中断号范围,如下表所示:
中断类型中断号范围软件触发中断(SGI)0~15私有外设中断(PPI)16~31共享外设中断(SPI)32~1019
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内核共享的。
2.2GIC 中断控制器的组成和功能
GIC主要由两部分组成,分别是仲裁单元(distributor)和CPU接口模块。仲裁单元为每一个中断源维护一个状态机,支持的状态有 inactive、pending、active 和 active and pending。CPU 接口模块通过这些核心收到中断,该模块主机寄存器屏蔽,识别和控制中断转发到内核。为每一个硬件中断源分配中断号,支持多种中断状态。
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.3GIC 中断类型和触发方式
SGI 用于多核通信,PPI 是每个处理器内核私有的中断,SPI 是公用外设中断。
GIC 中断类型分为 SGI(软件触发中断)、PPI(专用外设中断)和 SPI(共享外设中断)。SGI 用于多核通信,软件生成的中断,通过写入专用仲裁单元的寄存器即软件触发中断寄存器(ICDSGIR)显式生成,最多支持 16 个 SGI 中断,硬件中断号从 ID0~ID15。PPI 是由单个 CPU 核私有的外设生成的,中断号为 16~31,标识 CPU 核私有的中断源。SPI 是由外设生成的,中断控制器可以将其路由到多个核,中断号为 32~1020。
外设中断可以支持两种中断触发方式:边沿触发(edge-triggered)和电平触发(level-sensitive)。当中断源产生一个上升沿或者下降沿时,触发一个中断为边沿触发;当中断信号线产生一个高电平或者低电平时,触发一个中断为电平触发。
三、硬件中断号和Linux中断号的映射
在Linux中,注册中断接口函数request_irq()、request_threaded_irq() 使用Linux内核软件中断号(俗称软件中断号或IRQ号),而不是硬件中断号。接下来,以QEMU虚拟机的串口 0设备(它在主板上的序号为1,硬件中断号为33)为例,介绍硬件中断号是如何和Linux内核的IRQ号映射的。
3.2前置知识:设备树模式描述硬件设备
ARM64 平台设备描述采用设备树模式。在 ARM64 架构下,设备描述基本上采用设备树(Device Tree)模式来描述硬件设备。例如,QEMU 虚拟机的设备树的描述脚本并没有实现在内核代码中,而是实现在 QEMU 代码里。因此,可以通过 DTC 命令反编译出设备树脚本(Device Tree Script, 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";
};
3.2irq_domain 数据结构抽象描述中断控制器
一个中断控制器用一个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 的回调函数
3.3映射机制的三种方式
linear map(线性映射):维护固定大小的表,索引是硬件中断号。如果硬件中断最大数量固定,并且数值不大,可以选择线性映射。其优点是简单直接,查找速度快,能够快速地将硬件中断号映射为软件中断号。缺点是不够灵活,当硬件中断数量较多或变化较大时,可能会浪费较多的内存空间,或者无法满足映射需求。
tree map(树映射):当硬件中断号范围很大时,可以选择树映射。它通过树状结构来组织和管理中断号的映射关系,能够更有效地利用内存空间,并且对于大量中断号的映射具有更好的扩展性和灵活性。不过,其实现相对复杂,查找中断号时需要遍历树结构,可能会导致查找速度稍慢于线性映射。
no map(不映射):即硬件中断号直接就是 Linux 的中断号,这种方式适用于硬件中断号本身已经具有唯一性,不需要额外的映射操作的情况。优点是简单高效,无需进行复杂的映射处理,能够直接使用硬件中断号进行中断处理。缺点是缺乏灵活性,如果硬件中断号存在冲突或不满足 Linux 中断管理的要求,则无法使用这种方式。
四、映射过程分析
4.1映射入口函数 irq_of_parse_and_map
⑴函数原型和调用过程
irq_of_parse_and_map函数的原型为unsigned int irq_of_parse_and_map(struct device_node *dev, int index)。这个函数在 Linux 内核中用于处理设备树中的中断映射,它主要的作用是将设备树中定义的中断资源解析并映射到一个 IRQ 号,以便内核能够识别和管理这些中断。
该函数的调用过程较为复杂。首先,它会根据传入的设备节点和索引,查找设备树中相应的中断控制器信息。如果找到了中断信息,函数会调用中断控制器的相关回调函数,将设备树中的中断描述符映射成内核能够处理的 IRQ 号。最后,映射成功后,函数返回对应的 IRQ 号,这个 IRQ 号可以用于后续的中断处理,比如request_irq等。
⑵结合调用框图分析映射流程
irq_of_parse_and_map函数的映射流程可以通过调用框图来更好地理解。首先,of_irq_parse_one函数被调用,用于解析设备树中设备定义的属性,如 "reg" 和 "interrupt",并将设备树中的 "interrupts" 属性存放在out_irq->args[1]中。接着,irq_create_of_mapping函数被调用,它将数据转移并进行映射操作。在映射过程中,会根据设备树中的中断信息,调用中断控制器的相关回调函数,将硬件中断号映射为虚拟中断号。
具体来说,会通过irq_domain_set_hwirq_and_chip函数设置硬件中断号和芯片信息,然后通过irq_domain_get_irq_data获取中断数据。如果中断经过中断控制器到达 CPU 后,Linux 会首先通过irq_find_mapping函数,根据物理中断号 "hwirq" 的值,查找包含映射关系的基数树(radix tree)或者线性数组,得到 "hwirq" 对应的虚拟中断号 "irq"。
4.2中断管理核心数据结构
⑴irq_desc:描述 Linux 中断的结构体
在 Linux 内核中,对于每一个外设的 IRQ 都用struct irq_desc来描述,我们称之中断描述符。系统中每一个连接外设的中断线用一个中断描述符来描述,每一个外设的中断请求线分配一个中断号,系统中有多少个中断线就有多少个中断描述符。struct irq_desc中包含了许多重要的成员,如handle_irq是中断的处理函数入口,action是用户注册的中断处理函数链表,status是中断描述符的状态等。
⑵irq_data:包含软件中断号、硬件中断号等信息
struct irq_data结构体为中转站,里面有irq_chip结构体指针、irq_domain结构体指针。irq_data结构体中包含了软件中断号irq和硬件中断号hwirq,以及与中断相关的其他信息,如中断芯片数据结构指针chip、中断控制器抽象描述数据结构指针domain等。通过irq_domain结构体,irq_data建立了硬件中断号和软件中断号之间的联系,irq_domain会把本地的hwirq映射为全局的irq。
⑶irq_chip:对硬件中断器的操作函数集合
struct irq_chip结构体包含了一系列对硬件中断器的操作函数,如irq_startup用于启动中断,irq_shutdown用于关闭中断,irq_enable用于使能中断,irq_disable用于禁止中断等。在中断处理过程中,系统会调用这些函数来对硬件中断器进行操作。
⑷irq_domain:抽象描述中断控制器,完成映射任务
一个中断控制器用一个irq_domain数据结构来抽象描述。irq_domain数据结构包含了许多重要的成员,如link用于将irq_domain连接到全局链表irq_domain_list中,name是irq_domain的名称,ops是中断控制器映射操作使用的方法集合,hwirq_max是该irq_domain支持中断数量的最大值,revmap_size是线性映射的大小,revmap_tree是基数树映射的根节点等。
irq_domain_ops结构体是irq_domain的映射操作方法集合,其中包含了多个操作函数,如match用于判断一个指定的设备节点是否和一个irq_domain匹配,select功能与match类似,map用于创建或更新硬件中断号和软件中断号的映射关系,unmap与map操作相反,xlate用于将指定设备上的中断属性翻译成硬件中断号和中断触发类型等。
中断管理中的这些核心数据结构相互协作,完成了 Linux 中断号的映射任务,使得内核能够有效地管理和处理各种硬件中断。
五、全文总结
Linux 中断管理中的中断号映射机制复杂而重要,通过对中断控制器的了解和映射过程的分析,我们可以更好地理解 Linux 操作系统的中断管理机制。
中断号的映射在 Linux 系统中起着关键作用,它将硬件中断号与 Linux 内核的软件中断号进行关联,使得内核能够有效地处理来自各种外设的中断请求。
不同架构的中断控制器设计各异,如 ARM 架构采用通用中断控制器(GIC),x86 架构采用高级可编程中断控制器(APIC)。以 ARM 架构为例,GIC 在中断管理中发挥着重要作用,通过对中断状态的管理和中断类型的划分,实现了高效的中断处理。
硬件中断号和 Linux 中断号的映射通过 irq_domain 数据结构来实现,该结构抽象描述了中断控制器,并提供了三种映射方式:线性映射、Radix Tree map 和 no map。这些映射方式根据不同的硬件平台和中断控制器特点进行选择,以满足系统的中断管理需求。
映射过程分析中,我们了解了映射入口函数irq_of_parse_and_map的作用和调用过程,以及中断管理核心数据结构的相互协作。这些数据结构包括irq_desc、irq_data、irq_chip和irq_domain,它们共同完成了 Linux 中断号的映射任务,使得内核能够有效地管理和处理各种硬件中断。