深入解析GIC中断处理与内核初始化:基于Linux 4.9.88内核的详细分析
往期内容
本专栏往期内容,interrtupr子系统:
- 深入解析Linux内核中断管理:从IRQ描述符到irq domain的设计与实现
- Linux内核中IRQ Domain的结构、操作及映射机制详解
- 中断描述符irq_desc成员详解
- Linux 内核中断描述符 (irq_desc) 的初始化与动态分配机制详解
- 中断的硬件框架
- GIC介绍
- GIC寄存器介绍
- ARM架构中断与异常向量表机制解析
pinctrl和gpio子系统专栏:
专栏地址:pinctrl和gpio子系统
编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用
– 末片,有专栏内容观看顺序
input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有专栏内容观看顺序
目录
前言
建议先看一下往期文章关于中断相关结构体的讲解:中断描述符irq_desc成员详解-CSDN博客
-
Linux 4.9.88内核源码
Linux-4.9.88\drivers\irqchip\irq-gic.c
Linux-4.9.88/arch/arm/boot/dts/imx6ull.dtsi
本文以Linux 4.9.88内核为基础,系统解析了通用中断控制器(GIC)的中断处理与初始化机制。介绍了一级和多级中断控制器的处理流程,包括中断号映射(hwirq与virq)和中断服务函数的调用。梳理了GIC驱动的关键结构体与核心函数,阐述了GIC的功能及其内核表示方法。以设备树为线索,详细剖析了GIC初始化过程,解读了IRQCHIP_DECLARE
宏及其如何与设备树节点匹配并触发初始化。通过源码分析与注释,逐步讲解了GIC的域操作和分层中断处理流程。
1.GIC中断处理流程
1.1 一级中断控制器处理流程
对于irq_desc,内核有两种分配方法:
- 一次分配完所有的irq_desc
- 按需分配(用到某个中断才分配它的irq_desc
现在的内核基本使用第1种方法。
-
假设GIC可以向CPU发出161019号中断,这些数字被称为hwirq。015用于Process之间通信,比较特殊。
-
假设要使用UART模块,它发出的中断连接到GIC的32号中断,分配的irq_desc序号为16
-
在GIC domain中会记录(32, 16)
-
那么注册中断时就是:
request_irq(16, ...)
//虚拟中断号or软件中断号 -
发生UART中断时
- 程序从GIC中读取寄存器知道发生了32号中断,通过GIC irq_domain可以知道virq为16
- 调用irq_desc[16]中的handleA函数(GIC提供的handle函数),它的作用是屏蔽中断、调用action链表中用户注册的函数handler和thread_fn、清除中断
1.2 多级中断控制器处理流程
-
假设GPIO模块下有4个引脚,都可以产生中断,都连接到GIC的33号中断
-
GPIO也可以看作一个中断控制器,对于它的4个中断
-
对于GPIO模块中0~3这四个hwirq,一般都会一下子分配四个irq_desc
-
假设这4个irq_desc的序号为100~103,在GPIO domain中记录(0,100) (1,101)(2,102) (3,103)
-
对于KEY,注册中断时就是:
request_irq(102, ...)
-
按下KEY时:
-
程序从GIC中读取寄存器知道发生了33号中断,通过GIC irq_domain可以知道virq为17
-
调用irq_desc[17]中的handleB函数
-
mask GIC33号中断
- handleB读取GPIO寄存器,确定是GPIO里2号引脚发生中断
- 通过GPIO irq_domain可以知道virq为102
- 调用irq_desc[102]中的handleA函数,它的作用是mask GPIO2号中断、调用action链表中用户注册的函数、clear GPIO2号中断
- clear GIC33 号中断
-
2.GIC中的重要函数和结构体
沿着中断的处理流程,GIC涉及这4个重要部分:
-
CPU从异常向量表中调用handle_arch_irq,这个函数指针是有GIC驱动设置的
- GIC才知道怎么判断发生的是哪个GIC中断
-
从GIC获得hwirq后,要转换为virq:需要有GIC Domain
-
调用irq_desc[virq].handle_irq函数:这也应该由GIC驱动提供
-
处理中断时,要屏蔽中断、清除中断等:这些函数保存在irq_chip里,由GIC驱动提供
从硬件上看,GIC的功能是什么?
- 可以使能、屏蔽中断
- 发生中断时,可以从GIC里判断是哪个中断
在内核里,使用gic_chip_data结构体表示GIC,gic_chip_data里有什么?
- irq_chip:中断使能、屏蔽、清除,放在irq_chip中的各个函数里实现
- irq_domain
-
申请中断时
- 在设备树里指定hwirq、flag,可以使用irq_domain的函数来解析设备树
- 根据hwirq可以分配virq,把(hwirq, virq)存入irq_domain中
-
发生中断时,从GIC读出hwirq,可以通过irq_domain找到virq,从而找到处理函数
-
所以,GIC用gic_chip_data来表示,gic_chip_data中重要的成员是:irq_chip、irq_domain。
3.GIC初始化过程
Linux-4.9.88\drivers\irqchip\irq-gic.c
📎irq-gic.c
(将dtb文件反汇编为dts后,在里面搜索Interrupt-contorller找到中断控制器的节点,复制其compatible属性,在内核源码中grep该属性搜索一下就可以找到该文件)
3.1 内核支持多种GIC
按照设备树的套路:
- 驱动程序注册platform_driver
- 它的of_match_table里有多个of_device_id,表示能支持多个设备
- 有多种版本的GIC,在内核为每一类GIC定义一个结构体of_device_id,并放在一个段里:
// drivers\irqchip\irq-gic.c
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
IRQCHIP_DECLARE(arm11mp_gic, "arm,arm11mp-gic", gic_of_init);
IRQCHIP_DECLARE(arm1176jzf_dc_gic, "arm,arm1176jzf-devchip-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
IRQCHIP_DECLARE(msm_8660_qgic, "qcom,msm-8660-qgic", gic_of_init);
IRQCHIP_DECLARE(msm_qgic2, "qcom,msm-qgic2", gic_of_init);
IRQCHIP_DECLARE(pl390, "arm,pl390", gic_of_init);
把宏IRQCHIP_DECLARE
展开:
// include\linux\irqchip.h
#define IRQCHIP_DECLARE(name, compat, fn) OF_DECLARE_2(irqchip, name, compat, fn)
#define OF_DECLARE_2(table, name, compat, fn) \
_OF_DECLARE(table, name, compat, fn, of_init_fn_2)
#define _OF_DECLARE(table, name, compat, fn, fn_type)\
static const struct of_device_id __of_table_##name\
__used __section(__irqchip_of_table)\
= { .compatible = compat,\
.data = (fn == (fn_type)NULL) ? fn : fn }
展开示例:
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
展开后得到:
static const struct of_device_id __of_table_cortex_a7_gic\
__used __section(__irqchip_of_table)\
= { .compatible = "arm,cortex-a7-gic",\
.data = gic_of_init }
3.2 初始化调用
// drivers\irqchip\irq-gic.c
IRQCHIP_DECLARE(gic_400, "arm,gic-400", gic_of_init);
IRQCHIP_DECLARE(arm11mp_gic, "arm,arm11mp-gic", gic_of_init);
IRQCHIP_DECLARE(arm1176jzf_dc_gic, "arm,arm1176jzf-devchip-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a15_gic, "arm,cortex-a15-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
IRQCHIP_DECLARE(msm_8660_qgic, "qcom,msm-8660-qgic", gic_of_init);
IRQCHIP_DECLARE(msm_qgic2, "qcom,msm-qgic2", gic_of_init);
IRQCHIP_DECLARE(pl390, "arm,pl390", gic_of_init);
像5.3.1中上述的宏,表示支持的GIC的种类,比如要使用IRQCHIP_DECLARE(cortex_a7_gic, “arm,cortex-a7-gic”, gic_of_init);,那它是如何去选择和初始化的?如下:
start_kernel (init\main.c)
init_IRQ (arch\arm\kernel\irq.c)
irqchip_init (drivers\irqchip\irqchip.c)
of_irq_init (drivers\of\irq.c)
desc->irq_init_cb = match->data;
ret = desc->irq_init_cb(desc->dev,
desc->interrupt_parent);
如何知道of_irq_init在哪里被调用的:
选择某一GIC和进行初始化:
\Linux-4.9.88\drivers\irqchip\irq-gic.c
void __init of_irq_init(const struct of_device_id *matches)
{
const struct of_device_id *match; // 用于存放匹配的设备节点的指针
struct device_node *np, *parent = NULL; // np 指向当前中断控制器节点,parent 指向父节点
struct of_intc_desc *desc, *temp_desc; // 中断控制器描述符指针,用于保存控制器相关信息
struct list_head intc_desc_list, intc_parent_list; // 初始化中断控制器列表和父节点列表
INIT_LIST_HEAD(&intc_desc_list); // 初始化描述符列表,存储扫描到的中断控制器
INIT_LIST_HEAD(&intc_parent_list); // 初始化父节点列表,存储待初始化的父节点
// 1. 遍历设备树中与 `matches` 匹配的节点
for_each_matching_node_and_match(np, matches, &match) {
// 检查节点是否有 "interrupt-controller" 属性且可用
if (!of_find_property(np, "interrupt-controller", NULL) ||
!of_device_is_available(np))
continue;
// 如果匹配项的 `data` 字段为空,发出警告并跳过
if (WARN(!match->data, "of_irq_init: no init function for %s\n",
match->compatible))
continue;
// 2. 为每个匹配的中断控制器分配并填充一个 `of_intc_desc` 结构体
desc = kzalloc(sizeof(*desc), GFP_KERNEL);
if (WARN_ON(!desc)) {
of_node_put(np);
goto err;
}
desc->irq_init_cb = match->data; // 初始化回调函数
desc->dev = of_node_get(np); // 保存节点信息
desc->interrupt_parent = of_irq_find_parent(np); // 查找中断父节点
if (desc->interrupt_parent == np) // 若节点为根中断控制器,设置为 NULL
desc->interrupt_parent = NULL;
list_add_tail(&desc->list, &intc_desc_list); // 添加到中断控制器描述符列表
}
/*
* 3. 逐层初始化中断控制器,优先初始化根节点(即无父节点的控制器)
*/
while (!list_empty(&intc_desc_list)) {
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
int ret;
// 跳过非当前父节点的子节点
if (desc->interrupt_parent != parent)
continue;
list_del(&desc->list); // 从描述符列表中移除当前描述符
of_node_set_flag(desc->dev, OF_POPULATED); // 标记节点已初始化
pr_debug("of_irq_init: init %s (%p), parent %p\n",
desc->dev->full_name,
desc->dev, desc->interrupt_parent);
// 调用中断控制器的初始化回调函数
ret = desc->irq_init_cb(desc->dev,
desc->interrupt_parent);
if (ret) {
of_node_clear_flag(desc->dev, OF_POPULATED); // 若初始化失败,清除标记
kfree(desc);
continue;
}
/*
* 初始化成功,将此节点加入到父节点列表中,
* 以便后续初始化其子节点
*/
list_add_tail(&desc->list, &intc_parent_list);
}
// 获取下一个待处理的父节点
desc = list_first_entry_or_null(&intc_parent_list,
typeof(*desc), list);
if (!desc) {
pr_err("of_irq_init: children remain, but no parents\n");
break;
}
list_del(&desc->list);
parent = desc->dev;
kfree(desc);
}
// 释放未使用的描述符节点
list_for_each_entry_safe(desc, temp_desc, &intc_parent_list, list) {
list_del(&desc->list);
kfree(desc);
}
err:
// 清理和释放资源,释放未初始化的描述符节点
list_for_each_entry_safe(desc, temp_desc, &intc_desc_list, list) {
list_del(&desc->list);
of_node_put(desc->dev);
kfree(desc);
}
}
- 匹配中断控制器节点:遍历设备树中所有符合
matches
条件的节点,通过检查节点是否有interrupt-controller
属性,筛选出所有中断控制器节点,并分配of_intc_desc
结构体保存节点相关信息(包括初始化回调函数、节点指针、父节点指针等)。 - 初始化中断控制器:从根中断控制器(无父节点)开始,每次处理当前父节点的所有子节点,并递归初始化,确保初始化顺序由根至叶子。
- 清理和释放:初始化完成后,释放未使用的描述符,确保资源不被浪费。
IRQCHIP_DECLARE
宏如何完成与设备树的挂钩:
宏 IRQCHIP_DECLARE(cortex_a9_gic, "arm,cortex-a9-gic", gic_of_init);
的作用是声明并注册一个中断控制器,这样在调用 of_irq_init
时能够识别设备树中的 "arm,cortex-a9-gic"
中断控制器节点。具体过程如下:
IRQCHIP_DECLARE
定义了一个of_device_id
类型的结构体实例,其中包含"arm,cortex-a9-gic"
兼容字符串与gic_of_init
初始化函数的关联。of_irq_init
遍历设备树时,识别出符合"arm,cortex-a9-gic"
的节点,匹配后调用gic_of_init
进行中断控制器初始化,从而与设备树节点挂钩。
3.3 在设备树里指定GIC
在设备树中指定GIC,内核驱动程序根据设备树来选择、初始化GIC。
drivers\irqchip\irqchip.c
中并没有定义一个platform_driver,但是套路是一样的。
调用过程:
of_irq_init:
-
内核有一个__irqchip_of_table数组,里面有多个of_device_id,表示多种GIC
-
要使用哪类GIC?在设备树里指定
-
根据设备树,找到__irqchip_of_table树组中对应的项,调用它的初始化函数
IRQCHIP_DECLARE(cortex_a7_gic, "arm,cortex-a7-gic", gic_of_init);
3.4 gic_of_init分析
先记住上面的中断处理流程,再来分析gic_of_init:
申请中断的过程(irq_domain_ops域操作结构体):
static const struct irq_domain_ops gic_irq_domain_hierarchy_ops = {
.translate = gic_irq_domain_translate,
.alloc = gic_irq_domain_alloc,
.free = irq_domain_free_irqs_top,
};
- 在设备树中声明要使用哪个GIC控制器下的哪个中断
- 用gic_irq_domain_translate来解析设备树,知道其hwirq以及flag
- 为hwirq在irq_desc数组中找到空闲的位置(数组下标就是其virq)
- 记录hwirq和virq在domain中(建立联系)
- 使用gic_irq_domain_alloc处理,提供handle中断函数等
关键词:gic_of_setup、__gic_init_bases、gic_init_chip、gic_init_bases、irq_domain_create_linear
int __init
gic_of_init(struct device_node *node, struct device_node *parent)
{
struct gic_chip_data *gic; // 用于存储当前GIC的配置信息
int irq, ret;
// 检查设备树节点的有效性,如果为空则返回错误码 -ENODEV
if (WARN_ON(!node))
return -ENODEV;
// 检查当前GIC控制器数量是否已达到最大允许数量,若超限则返回错误码 -EINVAL
if (WARN_ON(gic_cnt >= CONFIG_ARM_GIC_MAX_NR))
return -EINVAL;
// 获取用于当前GIC的数据结构,并使用 gic_data 数组存储多实例的 GIC
gic = &gic_data[gic_cnt];
// 调用 gic_of_setup 函数根据设备树节点 `node` 初始化 GIC 的基本信息
ret = gic_of_setup(gic, node);
if (ret)
return ret;
/*
* 如果 hypervisor 模式(HYP)不可用或 CPU 接口寄存器太小,禁用分离的
* End Of Interrupt/Deactivate (EOI/Deactivate) 模式。
*/
if (gic_cnt == 0 && !gic_check_eoimode(node, &gic->raw_cpu_base))
static_key_slow_dec(&supports_deactivate);
// 初始化 GIC 的寄存器基地址、GIC 中断域等基本配置
ret = __gic_init_bases(gic, -1, &node->fwnode);
if (ret) {
// 若初始化失败,调用 gic_teardown 清理当前 GIC 配置
gic_teardown(gic);
return ret;
}
// 若是第一个 GIC 控制器,进行物理地址初始化并设置 KVM (Kernel-based Virtual Machine) 信息
if (!gic_cnt) {
gic_init_physaddr(node);
gic_of_setup_kvm_info(node);
}
// 若存在父节点,则解析并映射第一个中断号并进行级联中断配置
if (parent) {
irq = irq_of_parse_and_map(node, 0); // 解析并映射设备树中第一个中断
gic_cascade_irq(gic_cnt, irq); // 配置级联中断
}
// 如果启用了 GIC V2M(GIC Version 2 Memory Mapped)支持,初始化 GIC V2M
if (IS_ENABLED(CONFIG_ARM_GIC_V2M))
gicv2m_init(&node->fwnode, gic_data[gic_cnt].domain);
// 增加 GIC 控制器计数器,表明一个新的 GIC 已初始化完成
gic_cnt++;
return 0;
}
3.4.1gic_of_setup
gic_of_init
函数中调用的,主要是初始化 GIC 的基本信息,和设备树挂钩。
reg中,第一行就是分发器Distributro所对应寄存器的地址,第二行则是cpu interface所对应寄存器的地址,既然是物理地址,那就得映射,就是由该函数实现的:
static int gic_of_setup(struct gic_chip_data *gic, struct device_node *node)
{
// 检查传入参数 gic 和 node 是否为空,以确保有效性
if (!gic || !node)
return -EINVAL; // 如果为空,返回 -EINVAL 表示无效参数
// 使用设备树节点 `node` 映射分发器(distributor)寄存器基地址
gic->raw_dist_base = of_iomap(node, 0);
// 如果映射失败,输出错误信息,并跳转到 error 标签清理已分配资源
if (WARN(!gic->raw_dist_base, "unable to map gic dist registers\n"))
goto error;
// 使用设备树节点 `node` 映射 CPU 接口(CPU interface)寄存器基地址
gic->raw_cpu_base = of_iomap(node, 1);
// 如果映射失败,输出错误信息,并跳转到 error 标签清理已分配资源
if (WARN(!gic->raw_cpu_base, "unable to map gic cpu registers\n"))
goto error;
// 读取设备树节点中的 `cpu-offset` 属性,用于定义 CPU 接口偏移量
// 如果未定义 `cpu-offset` 属性,默认偏移量设为0
if (of_property_read_u32(node, "cpu-offset", &gic->percpu_offset))
gic->percpu_offset = 0;
return 0; // 成功初始化时返回 0 表示无错误
error:
// 如果映射失败,调用 gic_teardown 释放已分配的 GIC 资源
gic_teardown(gic);
return -ENOMEM; // 返回 -ENOMEM 表示内存分配失败
}
3.4.2 __gic_init_bases
要目的是为 GIC(Generic Interrupt Controller)初始化基础数据结构和相关属性,为后续中断处理和分发提供支持。它包含了对第一个 GIC 实例的特殊处理(主要是主 GIC 的初始化),并设置了对 SMP(对称多处理)系统的支持。
static int __init __gic_init_bases(struct gic_chip_data *gic,
int irq_start,
struct fwnode_handle *handle)
{
char *name;
int i, ret;
// 检查传入的 gic 是否为空或已经初始化,若是则返回 -EINVAL 表示无效参数
if (WARN_ON(!gic || gic->domain))
return -EINVAL;
// 检查当前是否为主 GIC,即 gic_data[0]
if (gic == &gic_data[0]) {
/*
* 初始化 CPU 接口映射,将其默认设置为所有 CPU。
* 每个 CPU 在探测其 ID 时将此映射进一步细化。
* 仅主 GIC 需要该操作。
*/
for (i = 0; i < NR_GIC_CPU_IF; i++)
gic_cpu_map[i] = 0xff; // 0xff 表示所有 CPU
#ifdef CONFIG_SMP
// 设置跨 CPU 的中断调用函数
set_smp_cross_call(gic_raise_softirq);
#endif
// 设置 CPU 热插拔的回调状态,用于在 CPU 启动时初始化 GIC
cpuhp_setup_state_nocalls(CPUHP_AP_IRQ_GIC_STARTING,
"AP_IRQ_GIC_STARTING",
gic_starting_cpu, NULL);
// 注册 GIC 的中断处理函数
set_handle_irq(gic_handle_irq);
// 如果支持 EOI/Deactivate 模式,输出提示信息
if (static_key_true(&supports_deactivate))
pr_info("GIC: Using split EOI/Deactivate mode\n");
}
// 初始化 GIC 芯片名称和类型
if (static_key_true(&supports_deactivate) && gic == &gic_data[0]) {
// 主 GIC 使用分离的 EOI/Deactivate 模式,名称设置为 "GICv2"
name = kasprintf(GFP_KERNEL, "GICv2");
gic_init_chip(gic, NULL, name, true); // EOI/Deactivate 为 true
} else {
// 其他 GIC 使用默认名称格式 "GIC-x",x 是 GIC 索引
name = kasprintf(GFP_KERNEL, "GIC-%d", (int)(gic - &gic_data[0]));
gic_init_chip(gic, NULL, name, false); // EOI/Deactivate 为 false
}
// 调用 gic_init_bases 初始化 GIC 基础结构
ret = gic_init_bases(gic, irq_start, handle);
if (ret)
kfree(name); // 若初始化失败,释放为名称分配的内存
return ret; // 返回初始化结果
}
set_handle_irq
:
gic_init_chip
:
gic_init_bases
:
原文地址:https://blog.csdn.net/caiji0169/article/details/143809794
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!