前言
在现代计算机系统中,中断模块的硬件越来越复杂,有可能有多个中断控制器(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
)。
因此,内核在进行中断处理前需要识别不同的设备中断源的同时需要将设备的hwirq
转化为virq
,才能找到并执行设备的ISR
。本文介绍了中断子系统是如何对该部分进行抽象建模、屏蔽不同硬件之间的差异形成通用的中断处理模块的,在这个通用的中断处理模块中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
的创建和初始化,并将每一个能够发起中断的设备建立映射并添加到对应的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
有配置自己的翻译方法则进行翻译,否则就从设备的中断描述信息中获取。
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
。
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的翻译流程
通过以上的内容应该对内核对硬件层面的中断管理有了大致的了解,假设系统已经正常启动,内核完成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
,获取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
,该ISR
的入参是次级GIC
的中断描述符,在irq_desc
中可以找到次级GIC
的irq_domain
以及IC
寄存器的地址信息,可以读取次级GIC
的中断识别寄存器中的hwirq
并进行翻译。
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);
}
...
}
标签:Domain,struct,IRQ,virq,domain,hwirq,irq,IC,子系统
From: https://www.cnblogs.com/wodemia/p/18111318