Linux学习笔记--中断子系统
struct gpio_key {int gpio; // GPIO 引脚号int irq; // 中断号 enum of_gpio_flags flag; // GPIO 标志(如上下拉)
};中断处理函数
static irqreturn_t gpio_key_irq_100ask(int irq, void *dev_id)
{// 当按键触发中断时,打印按键状态printk("key %d val %d\n", irq, gpio_get_value(gpio_key->gpio));return IRQ_HANDLED;
}probe 函数
count = of_gpio_count(node); // 获取GPIO数量
gpio_keys = kzalloc(count * sizeof(struct gpio_key), GFP_KERNEL);for (i = 0; i < count; i++) {gpio = of_get_gpio_flags(node, i, &flags); // 获取GPIO号和标志irq = gpio_to_irq(gpio); // GPIO转中断号// 注册中断处理函数,双边沿触发(按下和释放都触发)request_irq(irq, gpio_key_irq_100ask, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys[i]);
}获取 GPIO 数量
count = of_gpio_count(node);作用:从设备树节点中统计 GPIO 属性的数量。
设备树:
gpio_keys {compatible = "100ask,gpio_key";gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>, /* 第0个GPIO */<&gpio0 13 GPIO_ACTIVE_HIGH>, /* 第1个GPIO */ <&gpio0 14 GPIO_ACTIVE_LOW>; /* 第2个GPIO */
};of_gpio_count() 会返回 3,因为有3个GPIO定义。
分配内存
gpio_keys = kzalloc(count * sizeof(struct gpio_key), GFP_KERNEL);作用:为所有 GPIO 按键分配连续的内存空间。
内存布局:
gpio_keys指针↓
[struct gpio_key] ← gpio_keys[0] (第1个按键)
[struct gpio_key] ← gpio_keys[1] (第2个按键)
[struct gpio_key] ← gpio_keys[2] (第3个按键)...kzalloc()分配并清零内存GFP_KERNEL表示在内核空间分配内存
循环处理每个 GPIO
for (i = 0; i < count; i++) {gpio = of_get_gpio_flags(node, i, &flags);irq = gpio_to_irq(gpio);// 保存信息和注册中断
}获取 GPIO 信息和标志
gpio = of_get_gpio_flags(node, i, &flags);参数:
node:设备树节点i:GPIO 的索引(0, 1, 2...)&flags:输出参数,获取 GPIO 标志
返回值:GPIO 编号,用于后续的 GPIO 操作
GPIO 标志可能包括:
GPIO_ACTIVE_HIGH:高电平有效GPIO_ACTIVE_LOW:低电平有效GPIO_PULL_UP:上拉GPIO_PULL_DOWN:下拉
GPIO 转中断号
irq = gpio_to_irq(gpio);作用:将 GPIO 引脚号转换为对应的中断号。
原理:在 Linux 中,每个 GPIO 都可以作为中断源,内核提供了 GPIO 号到中断号的映射。
注册中断处理函数
request_irq(irq, gpio_key_irq_100ask, IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING, "100ask_gpio_key", &gpio_keys[i]);参数详解:
| 参数 | 说明 |
|---|---|
irq | 中断号 |
gpio_key_irq_100ask | 中断处理函数指针 |
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | 双边沿触发 - 上升沿和下降沿都触发中断 |
"100ask_gpio_key" | 中断名称(用于 /proc/interrupts) |
&gpio_keys[i] | 传递给中断处理函数的设备ID |
中断触发方式说明
双边沿触发的作用:
IRQF_TRIGGER_RISING:电压从低到高变化时触发(按键按下)IRQF_TRIGGER_FALLING:电压从高到低变化时触发(按键释放)
这样无论是按下还是释放按键都会触发中断,可以检测完整的按键动作。
完整的数据流
设备树定义 → 2. 获取GPIO数量 → 3. 分配内存 → 4. 逐个配置GPIO → 5. 注册中断
实际执行示例
假设设备树定义如下:
gpio_keys {compatible = "100ask,gpio_key";gpios = <&gpio0 12 GPIO_ACTIVE_HIGH>,<&gpio0 13 GPIO_ACTIVE_LOW>;
};执行过程:
count = 2(2个GPIO)分配
2 * sizeof(struct gpio_key)的内存循环2次:
i=0: GPIO12 → 中断号A → 注册中断A
i=1: GPIO13 → 中断号B → 注册中断B
这样当任意按键按下或释放时,都会调用 gpio_key_irq_100ask 函数,并通过 dev_id 参数知道是哪个按键触发的。
中断控制器
中断控制器芯片定义
static struct irq_chip virtual_intc_irq_chip = {.name = "100ask_virtual_intc",.irq_ack = virtual_intc_irq_ack,.irq_mask = virtual_intc_irq_mask,.irq_mask_ack = virtual_intc_irq_mask_ack,.irq_unmask = virtual_intc_irq_unmask,.irq_eoi = virtual_intc_irq_eoi,
};中断域操作
static const struct irq_domain_ops virtual_intc_domain_ops = {.xlate = irq_domain_xlate_onetwocell, // 解析设备树.map = virtual_intc_irq_map, // 建立硬件中断到虚拟中断的映射
};核心函数解析
1. 中断映射函数
static int virtual_intc_irq_map(struct irq_domain *h, unsigned int virq,irq_hw_number_t hw)
{irq_set_chip_data(virq, h->host_data);irq_set_chip_and_handler(virq, &virtual_intc_irq_chip, handle_edge_irq);return 0;
}建立硬件中断号到Linux虚拟中断号的映射
设置中断芯片和处理函数(边沿触发)
2. 中断处理函数
static void virtual_intc_irq_handler(struct irq_desc *desc)
{struct irq_chip *chip = irq_desc_get_chip(desc);chained_irq_enter(chip, desc); // 进入链式中断处理,屏蔽中断/* 分辨是哪个硬件中断 */int hwirq = virtual_intc_get_hwirq(); // 随机获取0-3的硬件中断号/* 调用对应的中断处理函数 */generic_handle_irq(irq_find_mapping(virtual_intc_domain, hwirq));chained_irq_exit(chip, desc); // 退出链式中断处理,重新使能中断
}3. 探测函数
static int virtual_intc_probe(struct platform_device *pdev)
{// 1. 获取父中断控制器的中断号int irq_to_parent = platform_get_irq(pdev, 0);// 2. 设置链式中断处理函数irq_set_chained_handler_and_data(irq_to_parent, virtual_intc_irq_handler, NULL);// 3. 分配4个连续的中断描述符int irq_base = irq_alloc_descs(-1, 0, 4, numa_node_id());// 4. 创建中断域virtual_intc_domain = irq_domain_add_legacy(np, 4, irq_base, 0,&virtual_intc_domain_ops, NULL);return 0;
}1. 获取父中断控制器的中断号
int irq_to_parent = platform_get_irq(pdev, 0);作用:
从设备树中获取虚拟中断控制器连接到父中断控制器(如GIC)的中断号
platform_get_irq(pdev, 0)获取设备树中定义的第一个中断号这个中断号是虚拟中断控制器向父中断控制器"汇报"中断时使用的中断线
设备树示例:
virtual_intc: virtual-interrupt-controller {compatible = "100ask,virtual_intc";interrupt-parent = <&gic>; // 父控制器是GICinterrupts = <0 100 4>; // 这是platform_get_irq(pdev, 0)获取的中断// 含义:SPI类型,中断号100,高电平触发
};2. 设置链式中断处理函数
irq_set_chained_handler_and_data(irq_to_parent, virtual_intc_irq_handler, NULL);作用:
建立中断处理链:当父中断控制器收到
irq_to_parent中断时,会调用virtual_intc_irq_handler参数说明:
irq_to_parent:父中断控制器的中断号virtual_intc_irq_handler:链式中断处理函数NULL:传递给处理函数的私有数据(这里不需要)
中断处理流程:
父中断控制器(GIC)收到中断↓
调用 irq_desc[irq_to_parent].handle_irq↓
执行 virtual_intc_irq_handler (因为设置了链式处理)↓
虚拟中断控制器处理具体的中断源3. 分配中断描述符
int irq_base = irq_alloc_descs(-1, 0, 4, numa_node_id());作用:
为虚拟中断控制器的4个硬件中断源分配连续的Linux虚拟中断号
参数详解:
-1:起始中断号,-1表示由内核自动分配0:要分配的硬件中断号的起始值(这里从0开始)4:要分配的中断数量(虚拟中断控制器有4个中断源)numa_node_id():当前NUMA节点ID
分配结果示例:
假设 irq_base = 100
那么分配的中断号范围是:100, 101, 102, 103
对应硬件中断号:0, 1, 2, 34. 创建中断域
virtual_intc_domain = irq_domain_add_legacy(np, 4, irq_base, 0,&virtual_intc_domain_ops, NULL);作用:
创建并注册一个中断域,建立硬件中断号与Linux虚拟中断号的映射关系
参数详解:
np:设备节点指针4:中断域包含的中断数量irq_base:分配的起始虚拟中断号0:硬件中断号的起始值&virtual_intc_domain_ops:中断域操作函数集NULL:宿主数据
建立的映射关系:
硬件中断号 → Linux虚拟中断号0 → irq_base + 01 → irq_base + 1 2 → irq_base + 23 → irq_base + 3完整的中断处理流程
// 当硬件中断发生时:
1. 父中断控制器(GIC)收到中断信号
2. 调用 virtual_intc_irq_handler
3. virtual_intc_irq_handler 中:- 调用 virtual_intc_get_hwirq() 获取具体硬件中断号(0-3)- 通过 irq_find_mapping(virtual_intc_domain, hwirq) 找到对应的虚拟中断号- 调用 generic_handle_irq() 执行该虚拟中断号的处理函数
4. 最终调用到设备驱动中注册的中断处理函数这个probe函数完成了虚拟中断控制器的完整初始化:
建立与父控制器的连接 - 获取父中断号
注册中断处理机制 - 设置链式中断处理函数
分配系统资源 - 分配虚拟中断号范围
建立映射关系 - 创建中断域,连接硬件中断号与虚拟中断号
这样,当虚拟中断控制器产生中断时,就能正确地通过父中断控制器传递到Linux内核的中断处理子系统,最终调用到相应设备驱动的中断处理函数。
工作原理
层次结构:这个虚拟中断控制器作为子中断控制器连接到父中断控制器(如GIC)
中断流程:
父中断控制器收到中断
调用
virtual_intc_irq_handler随机选择一个硬件中断号(0-3)
通过中断域找到对应的虚拟中断号
调用该虚拟中断号的处理函数
中断管理:
使用
chained_irq_enter/exit来管理中断屏蔽状态支持中断的确认、屏蔽、取消屏蔽等操作
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。
在Linux内核中,gic_chip_data 结构体用于描述通用中断控制器(GIC)的硬件信息和状态,它是GIC驱动的核心数据结构
| 成员名 | 功能描述 |
|---|---|
version | GIC控制器的硬件版本号 |
irq_base | 该GIC控制器所管理的虚拟中断号(virq)的起始编号 |
irq_nr | 该GIC控制器管理的中断数量 |
dist_base | 分发器(Distributor)寄存器区的内核虚拟地址 |
cpu_base | CPU接口(CPU Interface)寄存器区的内核虚拟地址 |
irq_chip | 指向irq_chip结构体的指针,包含对GIC的底层硬件操作函数 |
irq_domain | 指向irq_domain结构体的指针,负责硬件中断号(hwirq)与虚拟中断号(virq)的映射 |
在表格列出的成员中,有几个扮演着尤为重要的角色:
寄存器基地址 (
dist_base与cpu_base)
GIC硬件主要由两大模块组成:分发器(Distributor) 和 CPU接口(CPU Interface)。dist_base和cpu_base分别是这两个模块寄存器在内核地址空间中的映射地址。驱动通过操作这些地址的寄存器,来完成中断的使能、配置优先级以及应答等具体硬件操作。中断控制核心 (
irq_chip与irq_domain)
这是gic_chip_data与Linux内核中断子系统交互的关键。irq_chip:它包含了一组函数指针,定义了如何操作GIC控制器本身,例如屏蔽一个中断(irq_mask)、应答一个中断(irq_ack)、设置中断触发类型(irq_set_type)等。你可以把它理解为GIC的"设备驱动程序"。irq_domain:它的核心作用是建立硬件中断号(hwirq) 和Linux内核内部使用的虚拟中断号(virq) 之间的映射关系。当设备树被解析时,正是通过irq_domain来找到或分配对应的虚拟中断号,从而让驱动程序可以使用request_irq来注册中断处理函数。
GIC驱动的初始化
了解gic_chip_data是如何被创建和初始化的,有助于更深入地理解其作用。这个过程主要由 gic_of_init 驱动初始化函数完成,一般包括以下步骤:
分配结构体:内核为
gic_chip_data分配内存空间。映射寄存器:解析设备树,将GIC的分发器和CPU接口的物理地址映射到内核虚拟地址空间,并赋值给
dist_base和cpu_base成员。初始化
irq_chip:准备好一个irq_chip结构体实例,填充其各类操作函数(如mask、unmask、ack等),并将gic_chip_data中的irq_chip指针指向它。创建
irq_domain:创建一个irq_domain,并设置好其操作函数集(irq_domain_ops),特别是用于翻译设备树中断信息的xlate函数和建立映射的map函数。然后将其赋值给gic_chip_data的irq_domain成员。
中断域操作结构体
static const struct irq_domain_ops virtual_intc_domain_ops = {.translate = virtual_intc_domain_translate, // 翻译设备树中断说明符.alloc = virtual_intc_domain_alloc, // 分配和设置中断描述符
};中断芯片操作
static struct irq_chip virtual_intc_chip = {.name = "virtual_intc",.irq_mask = virtual_intc_irq_mask,.irq_unmask = virtual_intc_irq_unmask,.irq_eoi = virtual_intc_irq_eoi,
};设备树翻译函数
static int virtual_intc_domain_translate(struct irq_domain *d,struct irq_fwspec *fwspec,unsigned long *hwirq,unsigned int *type)
{if (is_of_node(fwspec->fwnode)) {if (fwspec->param_count != 2)return -EINVAL;*hwirq = fwspec->param[0]; // 硬件中断号*type = fwspec->param[1]; // 中断触发类型return 0;}return -EINVAL;
}作用:从设备树的中断说明符中提取硬件中断号和触发类型。
设备树示例:
device_node {interrupts = <0 4>; // hwirq=0, type=4 (IRQ_TYPE_LEVEL_HIGH)
};中断分配函数
static int virtual_intc_domain_alloc(struct irq_domain *domain,unsigned int virq, unsigned int nr_irqs, void *data)
{struct irq_fwspec *fwspec = data;struct irq_fwspec parent_fwspec;irq_hw_number_t hwirq;int i;/* 1. 设置当前中断域的中断描述符 */hwirq = fwspec->param[0];for (i = 0; i < nr_irqs; i++)irq_domain_set_hwirq_and_chip(domain, virq + i, hwirq + i,&virtual_intc_chip, NULL);/* 2. 设置父中断域的中断 */parent_fwspec.fwnode = domain->parent->fwnode;parent_fwspec.param_count = 3;parent_fwspec.param[0] = 0; // GIC_SPI类型parent_fwspec.param[1] = hwirq + upper_hwirq_base; // 父中断号parent_fwspec.param[2] = fwspec->param[1]; // 触发类型return irq_domain_alloc_irqs_parent(domain, virq, nr_irqs, &parent_fwspec);
}作用:
步骤1:为每个虚拟中断号设置硬件中断号和中断芯片
步骤2:向父中断控制器申请对应的中断资源
中断芯片操作函数
static void virtual_intc_irq_unmask(struct irq_data *d)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);irq_chip_unmask_parent(d); // 调用父控制器的unmask
}static void virtual_intc_irq_mask(struct irq_data *d)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);irq_chip_mask_parent(d); // 调用父控制器的mask
}static void virtual_intc_irq_eoi(struct irq_data *d)
{printk("%s %s %d\n", __FILE__, __FUNCTION__, __LINE__);irq_chip_eoi_parent(d); // 调用父控制器的EOI
}特点:所有操作都委托给父中断控制器,体现了层次化设计
探测函数
static int virtual_intc_probe(struct platform_device *pdev)
{struct irq_domain *parent_domain;struct device_node *parent;int err;/* 1. 读取设备树中的upper_hwirq_base属性 */err = of_property_read_u32(pdev->dev.of_node, "upper_hwirq_base", &upper_hwirq_base);/* 2. 找到父中断控制器和父中断域 */parent = of_irq_find_parent(pdev->dev.of_node);parent_domain = irq_find_host(parent);/* 3. 创建层次化中断域 */virtual_intc_domain = irq_domain_add_hierarchy(parent_domain, 0, 4,pdev->dev.of_node, &virtual_intc_domain_ops, NULL);return 0;
}层次化中断映射机制
映射关系:
设备驱动
↓ 使用虚拟中断号
虚拟中断控制器 (virtual_intc)
↓ 硬件中断号 0-3
父中断控制器 (如GIC)
↓ 硬件中断号 (upper_hwirq_base + 0) 到 (upper_hwirq_base + 3)
硬件中断线
设备树配置示例:
virtual_intc: virtual-interrupt-controller {compatible = "100ask,virtual_intc";interrupt-parent = <&gic>; // 父控制器是GICupper_hwirq_base = <100>; // 在GIC中的起始中断号#interrupt-cells = <2>; // 硬件中断号 + 触发类型
};my_device {compatible = "100ask,my_device";interrupt-parent = <&virtual_intc>; // 使用虚拟中断控制器interrupts = <1 IRQ_TYPE_LEVEL_HIGH>; // 硬件中断号1,高电平触发
};中断处理流程
当硬件中断发生时:
GIC收到中断:中断号 =
upper_hwirq_base + hwirqGIC调用虚拟中断控制器的处理函数(自动设置)
虚拟中断控制器处理:
调用
virtual_intc_irq_mask(委托给GIC)调用设备驱动注册的中断处理函数
调用
virtual_intc_irq_eoi(委托给GIC)
与传统方法的区别
| 方面 | 传统方法 (irq_domain_add_legacy) | 层次化方法 (irq_domain_add_hierarchy) |
|---|---|---|
| 中断映射 | 手动建立映射 | 自动通过设备树建立 |
| 父中断处理 | 需要手动设置链式处理函数 | 自动委托给父控制器 |
| 代码复杂度 | 较高,需要管理链式中断 | 较低,内核自动处理层次关系 |
| 适用场景 | 传统、简单的中断控制器 | 现代、复杂的中断控制器层次结构 |
Linux内核中层次化中断控制器的现代实现方式:
自动层次管理:通过
irq_domain_add_hierarchy自动建立父子关系操作委托:所有中断操作自动委托给父中断控制器
设备树驱动:完全通过设备树配置中断映射
简化开发:无需手动处理链式中断和中断屏蔽逻辑
这种方式更适合复杂的SoC设计,其中多个中断控制器可能形成层次结构。
在层次化中断控制器架构中调用父级的chip函数是非常重要的设计,主要原因如下:
1. 硬件操作的必要性
实际硬件控制
虚拟中断控制器只是软件抽象,没有实际的硬件寄存器
父中断控制器(如GIC) 才是真正控制物理中断线的硬件
所有对中断的使能、禁用、确认等操作最终都需要作用于物理硬件
// 这些操作最终都要落实到GIC的硬件寄存器
static void virtual_intc_irq_mask(struct irq_data *d)
{printk("虚拟控制器执行mask\n");irq_chip_mask_parent(d); // 实际调用GIC的mask函数来操作硬件
}中断状态管理
状态一致性
父中断控制器需要维护准确的中断状态:
| 操作 | 父控制器需要知道的原因 |
|---|---|
| Mask | 记录哪些中断被禁用,避免错误处理 |
| Unmask | 知道哪些中断已启用,可以正常响应 |
| EOI | 确认中断处理完成,可以接受新中断 |
中断流控(Flow Control)
电平触发中断的特别需求
对于电平触发中断,处理流程必须严格:
中断发生↓
GIC检测到高电平,触发中断↓
调用虚拟控制器,虚拟控制器mask中断↓
执行设备驱动中断处理函数↓
设备清除中断源(电平变低)↓
虚拟控制器unmask中断↓
GIC可以再次检测中断如果不调用父级的mask:
电平保持高电平期间,GIC会持续触发中断
导致中断风暴,系统崩溃
层次化中断处理流程
完整的中断处理链条
// 当中断发生时:
1. GIC硬件检测到中断
2. GIC调用 irq_desc[parent_irq].handle_irq
3. 自动调用虚拟控制器的处理函数(内核自动设置)
4. 虚拟控制器处理:- 调用 irq_chip_mask_parent() // 屏蔽GIC中的中断- 调用设备驱动的中断处理函数- 调用 irq_chip_eoi_parent() // 通知GIC中断处理完成具体场景分析
场景1:中断屏蔽
// 设备驱动调用 disable_irq(irq)
disable_irq(irq)↓
调用 virtual_intc_irq_mask(irq_data)↓
调用 irq_chip_mask_parent(irq_data) // 调用GIC的mask函数↓
GIC操作硬件寄存器,真正屏蔽中断线场景2:中断处理完成
// 中断处理完成后
中断处理函数返回↓
调用 virtual_intc_irq_eoi(irq_data)↓
调用 irq_chip_eoi_parent(irq_data) // 调用GIC的EOI函数↓
GIC硬件确认中断处理完成,可以接受新中断软件架构优势
责任分离
| 组件 | 职责 |
|---|---|
| 虚拟中断控制器 | 中断映射、多路复用、软件逻辑 |
| 父中断控制器 | 硬件操作、状态管理、物理控制 |
GIC的特殊要求
一些中断控制器(如GIC)有严格的编程模型要求:
// GICv2的典型操作序列
gic_mask_irq(irq); // 屏蔽中断
handle_irq_event(); // 处理中断
gic_eoi_irq(irq); // 结束中断// 如果跳过任何步骤,GIC可能进入错误状态调用父级chip函数的主要原因:
硬件操作:只有父控制器能操作物理硬件寄存器
状态一致性:父控制器需要准确的中断状态信息
流控要求:电平触发中断需要严格的mask/unmask序列
架构设计:层次化中断控制器的标准模式
代码质量:复用稳定代码,避免重复实现
这种设计确保了虚拟中断控制器能够正确集成到Linux内核的中断子系统中,同时保持与真实硬件的正确交互。
