中断子系统(一)IRQ Domain
前言#
在现代计算机系统中,中断模块的硬件越来越复杂,有可能有多个中断控制器(Interrupt Controller, IC
)之间进行级联从而拓展可以管理的中断源数量。这就会产生几个问题,每个IC
上都连接着多个设备,IC
会给irq line
连接的每一个设备分配一个硬件中断请求号(HW interrupt number,hwirq
),不同的IC
之间是独立的,因此hwirq
会有可能复用,CPU
就无法仅依赖连接的root IC
的hwirq
来区分中断源。
此外,内核并不理解hwirq
,内核通过中断描述符表( interrupt descriptor table, IDT
)来管理所有的中断处理函数(Interrupt Service Routine, ISR
),每一个中断描述符中包含了该中断的描述信息和处理函数,而定位一个中断描述符依赖于IRQ number
,这是一个逻辑中断号(下文缩写为virq
)。
因此,内核在进行中断处理前需要识别不同的设备中断源才能找到并执行设备的ISR
,这就需要将接收到的hwirq
转化为virq
。本文介绍了中断子系统是如何对该部分进行抽象建模、屏蔽不同硬件之间的差异形成通用的中断处理模块的,在这个通用的中断处理模块中hwirq
又是如何翻译为virq
的。
Note: 本文避免讨论与硬件或体系结构相关的细节,专注于通用的中断处理模块,另外本文的源码解读基于Linux 5.10
本文涉及的相关名词缩写如下:
IC
:Interrupt Controller
, 中断控制器hwirq
:Hardware Interrupt Number
, 硬件中断请求号virq
:Virtual Interrupt Number
, 虚拟中断请求号ISR
:Interrupt Service Routine
, 中断服务程序GIC
:Generic Interrupt Controller
, 通用中断控制器
中断源识别的例子#
为了帮助理解内核代码,首先我们先梳理一下CPU
和IC
之间是如何连接的,以及在这个架构下内核是如何识别中断源的,有了这个概念以后再去理解内核代码就更加容易。
如图所示,有三个IC
进行级联(级联呈现树状结构),root IC
作为根IC
连接到CPU
上,假设设备Device-D
发起了一个中断请求(如图中虚线所示),此时CPU
会检测到root IC
的电平变化并从root IC
的寄存器中取出硬件中断号hwirq-1
(对应IC-B
连接到root IC
的irq line
),此时CPU
根据hwirq-1
找到并执行IC-B
的中断处理函数handler-B
,handler-B
是一个特殊的ISR
,他处理的是IC
的中断请求而不是普通设备的中断请求,handler-B
会从IC-B
的寄存器中找到此时真正发起中断请求的设备的hwirq
(即hwirq-2
),并将hwirq-2
翻译为Device-D
对应的virq
,并执行对应的ISR
,至此就完成了一次中断请求的识别和执行。
在这个例子中可以很清晰的看到找到Device-D
的ISR
前需要逐级地对hwirq
进行转化,最终转化为设备中断的virq
,定位到设备的中断描述符,执行相应的ISR
。显然在每一级转化中(hwirq
转化为virq
)需要依赖于一个映射表,每一个IC
需要维护一个自己的映射表,维护的这个映射表在内核中由结构体struct irq_domain
实现。
irq_domain#
struct irq_domain 结构体#
irq_domain
可以理解为一个KV
数据库,专门用于在某个IC
内部查询hwirq
对应的virq
。
struct irq_domain {
// 链表节点,所有的irq_domain会放在一个全局的链表中
struct list_head link;
// irq_domain name
const char *name;
// irq_domain的操作函数集合
const struct irq_domain_ops *ops;
// `IC`私有数据,不同控制器类型自定义
void *host_data;
/* Optional data */
// 对应的`IC`设备信息
struct fwnode_handle *fwnode;
// 存储KV的数据结构
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[];
};
irq_domain
结构体其中有一些关键的成员变量:
link
:irq_domain
会被放置在一个全局的链表irq_domain_list
中进行管理ops
:ops
中定义了一系列的callback
函数,这些函数是与具体的硬件相关的,比如在mapping
(建立映射)的过程中除了要在irq_domian
中记录KV
关系之外还需要进行一些硬件相关的操作,一个具体的例子就是IC
可以依据hwirq
的范围设置不同的handler
等等,在这些IC
驱动自定义的callback
函数中可以进行一些非通用的操作。
struct irq_domain_ops {
int (*match)(struct irq_domain *d, struct device_node *node,
enum irq_domain_bus_token bus_token);
int (*select)(struct irq_domain *d, struct irq_fwspec *fwspec,
enum irq_domain_bus_token bus_token);
int (*map)(struct irq_domain *d, unsigned int virq, irq_hw_number_t hw);
void (*unmap)(struct irq_domain *d, unsigned int virq);
int (*xlate)(struct irq_domain *d, struct device_node *node,
const u32 *intspec, unsigned int intsize,
unsigned long *out_hwirq, unsigned int *out_type);
};
host_data
:存放了一些IC
的私有数据,由IC
驱动进行自定义,可能会在callback
函数中使用fwnode
:IC
设备信息revmap*
:真正存储KV
映射的数据结构,有线性映射和raidx-tree
两种模式,根据映射关系是否稀疏可以选择其中一种,revmap_size
和linear_revmap
用于线性存储,revmap_tree
用于radix-tree
,还有一种直接映射的场景(hwirq
即virq
),此时使用revmap_direct_max_irq
字段。
irq_domain的创建和初始化#
irq_domain
有一系列的创建函数,用于linear
、nomap
、legacy
、radix-tree
各种模式,这些函数会在IC
驱动程序初始化相关的代码中被调用,这些函数都是通过调用__irq_domain_add()
实现,但是在参数上有一些差异,可以结合上一小节中关于revmap*
变量的说明进行阅读。
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)
{
return __irq_domain_add(of_node_to_fwnode(of_node), size, size, 0, ops, host_data);
}
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)
{
return __irq_domain_add(of_node_to_fwnode(of_node), 0, max_irq, max_irq, ops, host_data);
}
static inline struct irq_domain *irq_domain_add_legacy_isa(
struct device_node *of_node,
const struct irq_domain_ops *ops,
void *host_data)
{
return irq_domain_add_legacy(of_node, NUM_ISA_INTERRUPTS, 0, 0, ops,
host_data);
}
static inline struct irq_domain *irq_domain_add_tree(struct device_node *of_node,
const struct irq_domain_ops *ops,
void *host_data)
{
return __irq_domain_add(of_node_to_fwnode(of_node), 0, ~0, 0, ops, host_data);
}
系统的启动流程 - irq_domain的创建和映射添加#
在系统启动时会创建好所有的irq_domain
,但是在此之前内核需要知道硬件之间的拓扑结构,掌握触发中断的设备和IC
之间是如何连接的,这部分信息需要通过DTS
文件(Device Tree Source
)或者ACPI
文件(Advanced Configuration and Power Interface
)来描述,DTS
常用于嵌入式系统,DTS
文件存在于内核源码之中,在进行内核编译时会编译成.dtb
文件,放置在固定的目录,而ACPI
用于传统PC
和服务器,ACPI
文件放置在BIOS
或者UEFI
固件之中。总之系统在启动时能够获取到硬件之间的拓扑信息,在这个过程中会进行irq_doamin
的创建和初始化,并对每一个能够发起中断的设备分配一个virq
,然后建立映射并添加到对应的irq_domain
。这里对具体的初始化流程不做深入分析。
对于设备驱动来说,创建中断映射前并不知道自身在连接的IC
中对应的hwirq
,但映射的建立的需要irq_domain
、hwirq
、virq
三个参数,建立映射相关的API
如下,分别用于创建一个映射和创建多个连续映射。
int irq_domain_associate(struct irq_domain *domain, unsigned int virq,
irq_hw_number_t hwirq);
void irq_domain_associate_many(struct irq_domain *domain, unsigned int irq_base,
irq_hw_number_t hwirq_base, int count);
因此在设备驱动中创建一个映射一般是调用irq_of_parse_and_map()
函数,该函数对irq_domain_associate()
进行了多层的封装,以device_node
作为参数输入尝试建立一个映射并返回virq
,。
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);
}
EXPORT_SYMBOL_GPL(irq_of_parse_and_map);
irq_of_parse_and_map
完成映射建立需要三步:
- 获取设备对应的
hwirq
:需要由IC
的irq_domain
来识别设备树节点信息,得到对应hwirq
,这个过程由irq_domain_translate()
函数完成,涉及到ops->xlate()
这个callback
函数,如果IC
有配置自己的翻译方法则进行使用IC
自己的方式,否则就从设备的中断描述信息中获取(在DTS
或者ACPI
中定义)。
static int irq_domain_translate(struct irq_domain *d,
struct irq_fwspec *fwspec,
irq_hw_number_t *hwirq, unsigned int *type)
{
if (d->ops->xlate)
return d->ops->xlate(d, to_of_node(fwspec->fwnode),
fwspec->param, fwspec->param_count,
hwirq, type);
/* If domain has no translation, then we assume interrupt line */
*hwirq = fwspec->param[0];
return 0;
}
- 另外还需要从内核中分配一个有效的中断描述符,通过
irq_domain_alloc_descs()
函数完成,该函数可以为一个指定的virq
或者由内核分配的virq
创建中断描述符,总之如果分配成功可以获取一个有效的virq
。
int irq_domain_alloc_descs(int virq, unsigned int cnt, irq_hw_number_t hwirq,
int node, const struct irq_affinity_desc *affinity)
{
unsigned int hint;
if (virq >= 0) {
virq = __irq_alloc_descs(virq, virq, cnt, node, THIS_MODULE,
affinity);
} else {
hint = hwirq % nr_irqs;
if (hint == 0)
hint++;
virq = __irq_alloc_descs(-1, hint, cnt, node, THIS_MODULE,
affinity);
if (virq <= 0 && hint > 1) {
virq = __irq_alloc_descs(-1, 1, cnt, node, THIS_MODULE,
affinity);
}
}
return virq;
}
- 最后通过
irq_domain_associate()
将hwirq
和virq
间的映射添加到irq_domain
中。
hwirq的翻译流程#
通过以上的内容应该对内核对硬件层面的中断管理有了大致的了解,假设系统已经正常启动,内核将root IC
中的hwirq
翻译为最底层设备的virq
还需要依赖于次级IC
(Secondary IC
)的ISR
。root IC
不连接到到任何其他的IC
上,因此仅作为IC
使用,但是次级IC
除了接受其他设备的连接以外自身还需要连接到其他的IC
上,因此就具备了设备和IC
两重身份,不仅需要管理一个irq_domain
,还需要注册自己的ISR
。
这里以GIC
级联为例,看看root GIC
和Secondary GIC
之间的处理是如何联动的。首先是root GIC
的处理函数gic_handle_irq
,该函数在CPU
收到了来自中断分发器(Interrupt Distributor
)的中断时执行,该函数会读取GIC
的中断识别寄存器(Interrupt ACKnowledge Register, IAR
)并赋值给irqstat
,然后从irqstat
中获取hwirq
,调用handle_domain_irq()
进行处理。
static void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0];
void __iomem *cpu_base = gic_data_cpu_base(gic);
...
do {
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK);
irqnr = irqstat & GICC_IAR_INT_ID_MASK;
if (unlikely(irqnr >= 1020))
break;
....
handle_domain_irq(gic->domain, irqnr, regs);
} while (1);
}
handle_domain_irq()
调用了__handle_domain_irq()
,该函数增加了一个参数lookup=true
,表示需要通过查找映射关系将hwirq
转化为virq
,irq_find_mapping()
就是从irq_domain
中查找映射关系,得到virq
后调用generic_handle_irq()
找到virq
对应的中断描述符并执行对应的ISR
,在当前场景下也就是执行次级GIC
的ISR
。
int __handle_domain_irq(struct irq_domain *domain, unsigned int hwirq,
bool lookup, struct pt_regs *regs)
{
...
if (lookup)
irq = irq_find_mapping(domain, hwirq);
if (unlikely(!irq || irq >= nr_irqs)) {
ack_bad_irq(irq);
ret = -EINVAL;
} else {
generic_handle_irq(irq);
}
...
}
次级GIC注册的ISR
如下,它以次级GIC
的中断描述符作为参数,该函数会获取寄存器的地址信息,读取次级GIC
的中断识别寄存器并得到hwirq
,然后通过次级GIC
的irq_domain
进行翻译得到下一级设备的virq
,假设下一级设备就是最底层的网卡设备,此时根据该virq
就能执行网卡的ISR
,如果不是则继续转到次次级的GIC
中断处理函数中。
static void gic_handle_cascade_irq(struct irq_desc *desc)
{
struct gic_chip_data *chip_data = irq_desc_get_handler_data(desc);
struct irq_chip *chip = irq_desc_get_chip(desc);
unsigned int cascade_irq, gic_irq;
unsigned long status;
...
status = readl_relaxed(gic_data_cpu_base(chip_data) + GIC_CPU_INTACK);
gic_irq = (status & GICC_IAR_INT_ID_MASK);
if (gic_irq == GICC_INT_SPURIOUS)
goto out;
cascade_irq = irq_find_mapping(chip_data->domain, gic_irq);
if (unlikely(gic_irq < 32 || gic_irq > 1020)) {
handle_bad_irq(desc);
} else {
isb();
generic_handle_irq(cascade_irq);
}
...
}
总结#
内核对IC
的建模和抽象实现了多级级联场景下依然能够正确的识别中断设备,执行正确的中断处理函数,其中最关键的设计就是irq_domain
和virq
。本文的内容主要分析了内核在中断处理函数执行之前做的一些通用工作,后续将会对中断处理相关的内容进行分析,包括中断描述符、软中断等内容。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)