往期内容
本专栏往期内容,interrtupr子系统:
pinctrl和gpio子系统专栏:
- 专栏地址:pinctrl和gpio子系统
- 编写虚拟的GPIO控制器的驱动程序:和pinctrl的交互使用–
末片,有专栏内容观看顺序input子系统专栏:
- 专栏地址:input子系统
- input角度:I2C触摸屏驱动分析和编写一个简单的I2C驱动程序
– 末片,有专栏内容观看顺序I2C子系统专栏:
- 专栏地址:IIC子系统
- 具体芯片的IIC控制器驱动程序分析:i2c-imx.c-CSDN博客
– 末篇,有专栏内容观看顺序总线和设备树专栏:
- 专栏地址:总线和设备树
- 设备树与 Linux 内核设备驱动模型的整合-CSDN博客
– 末篇,有专栏内容观看顺序
前言
在 Linux 内核中,IRQ(中断请求)处理机制是一个复杂而关键的组成部分。对于处理外部设备的中断需求,系统需要有一套有效的机制来管理和分发中断请求。Linux 内核通过 irq_desc、irq_data、irq_chip、irq_domain 等多个结构体来实现中断的管理和控制。这些结构体相互协作,不同层次地处理中断,从 GIC(通用中断控制器)到外部设备的中断请求,再到具体的设备驱动程序的中断处理函数。
在之前对中断描述符进行讲解的时候也有简单介绍过irq_desc的相关成员,本文主要要进一步扩展。之前的文章:深入解析Linux内核中断管理:从IRQ描述符到irq domain的设计与实现-CSDN博客
1. 总示意图
外部设备 1、外部设备 n 共享一个 GPIO 中断 B,多个 GPIO 中断汇聚到GIC(通用中断控制器)的 A 号中断,GIC 再去中断 CPU。那么软件处理时就是反过来,先读取 GIC 获得中断号 A,再细分出 GPIO 中断 B,最后判断是哪一个外部芯片发生了中断。
2. irq_desc
struct irq_desc {
struct irq_data irq_data;
unsigned int __percpu *kstat_irqs; // IRQ的统计信息
irq_flow_handler_t handle_irq; // (1)高层中断处理函数
struct irqaction *action; // (2)具体的中断处理函数链表
unsigned int status_use_accessors; // 中断描述符的状态
unsigned int core_internal_state__do_not_mess_with_it; // (3)内核内部状态
unsigned int depth; // (4)嵌套深度,用于enable和disable控制
unsigned int wake_depth; // (5)唤醒源嵌套深度
unsigned int irq_count; // (6)记录中断次数
unsigned long last_unhandled; // 上一次未处理的中断时间
unsigned int irqs_unhandled; // 记录未处理的中断次数
raw_spinlock_t lock; // (7)自旋锁
struct cpumask *percpu_enabled; // (8)每个 CPU 的中断使能情况
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint; // 中断亲和性提示
struct irq_affinity_notify *affinity_notify;
#ifdef CONFIG_GENERIC_PENDING_IRQ
cpumask_var_t pending_mask;
#endif
#endif
unsigned long threads_oneshot; // (9)与中断线程相关
atomic_t threads_active; // 活动线程计数
wait_queue_head_t wait_for_threads; // 等待队列
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir; // 该IRQ对应的proc文件系统接口
#endif
int parent_irq; // 父中断号
struct module *owner; // 所属模块
const char *name; // 中断名称
} ____cacheline_internodealigned_in_smp;
(1)irq_flow_handler_t handle_irq
handle_irq
是一个高层中断处理函数(high-level interrupt handler)。高层中断处理函数关注的是如何响应中断信号,而不涉及具体设备的中断细节。根据硬件配置的不同,中断控制器可以选择不同类型的 handle_irq
,如:
handle_level_irq
:用于电平触发的中断处理handle_edge_irq
:用于边沿触发的中断处理handle_simple_irq
:用于简单中断的处理handle_fasteoi_irq
:用于 EOI(End of Interrupt)类型的中断处理
handle_irq
可以按控制器类型、触发类型等动态调整。具体实现中:
(1)GIC 的处理函数:
假设 irq_desc[A].handle_irq 是 XXX_gpio_irq_handler(XXX 指厂家),这个函数需要读取芯片的
GPIO 控制器,细分发生的是哪一个 GPIO 中断(假设是B),再去调用 irq_desc[B]. handle_irq。注 意 : irq_desc[A].handle_irq 细分出中断后 B , 调 用 对 应
的irq_desc[B].handle_irq。显然中断 A 是 CPU 感受到的顶层的中断,GIC 中断 CPU 时,CPU 读取 GIC 状态得到中断 A。
(2)模块的中断处理函数:
比如对于 GPIO 模块向 GIC 发出的中断 B , 它 的 处 理 函 数 是irq_desc[B].handle_irq。
BSP 开发人员会设置对应的处理函数,一般是 handle_level_irq
或handle_edge_irq,从名字上看是用来处理电平触发的中断、边沿触发的中断。注意:导致 GPIO 中断 B
发生的原因很多,可能是外部设备 ,可能是外部设备n,可能只是某一个设备,也可能是多个设备。所以
irq_desc[B].handle_irq会调用某个链表里的函数,这些函数由外部设备提供。这些函数自行判断该中断是否自己产生,若是则处理。(3)外部设备提供的处理函数:
这里说的“外部设备”可能是芯片,也可能总是简单的按键。它们的处理函数由自己驱动程序提供,这是最熟悉这个设备的“人”:它知道如何判断设备是否发生了中断,如何处理中断。
对于共享中断,比如 GPIO 中断 B,它的中断来源可能有多个,每个中断源对应一个中断处理函数。所以
irq_desc[B]中应该有一个链表,存放着多个中断源的处理函数。一旦程序确定发生了 GPIO 中断 B,那么就会从链表里把那些函数取出来,一一执行。这个链表就是 action 链表。
(2)struct irqaction *action;
action指向一个struct irqaction的链表。如果一个interrupt request line允许共享,那么该链表中的成员可以是多个,否则,该链表只有一个节点。
action
是一个指向 struct irqaction
链表的指针,它存储了具体的中断处理函数。该链表中的每一个节点对应一个设备的中断处理函数,用于处理具体中断事务。当一个 IRQ 允许多个设备共享时(共享 IRQ),该链表可能包含多个节点,否则只包含一个节点。
调用 request_irq
或 request_threaded_irq
函数时,系统会自动构建一个 irqaction
结构体,并将其挂载到 action
链表中。
handler
:上半部中断处理函数,处理时间紧急的事务。thread_fn
:下半部中断处理函数,对应内核线程的操作,用于处理非紧急的事务。
可以有多种组合:
- 只提供
handler
:处理紧急的任务,不使用内核线程。 - 只提供
thread_fn
:完全使用内核线程来处理中断。 - 同时提供
handler
和thread_fn
:上半部和下半部分别处理紧急和非紧急任务。
共享中断时,irqaction
链表允许多个设备共享同一个 IRQ,并在卸载时通过 dev_id
标识每个设备。共享中断时,action
链表会遍历其中的每个处理函数,根据 dev_id
判断是否属于当前设备。
(3)unsigned int core_internal_state__do_not_mess_with_it;
这个有着很长名字的符号core_internal_state__do_not_mess_with_it在具体使用的时候被被简化成istate,表示internal state。就像这个名字定义的那样,我们最好不要直接修改它。
#define istate core_internal_state__do_not_mess_with_it
(4)unsigned int depth
depth
表示中断的嵌套深度。通过调用 enable_irq
和 disable_irq
函数可以控制 IRQ 的使能状态,当调用 disable_irq
时,depth
增加;调用 enable_irq
时,depth
减少。内核会检查 depth
值,以确保 IRQ 使能和禁用操作成对执行。
(5)unsigned int wake_depth
wake_depth
与系统电源管理相关。通过 irq_set_irq_wake
函数可以控制 IRQ 是否能够唤醒系统。当 IRQ 配置为唤醒源时,系统挂起后,若有该 IRQ 触发,则可唤醒系统。唤醒源的启用和禁用也可以嵌套,wake_depth
记录唤醒源的嵌套层数,以确保嵌套操作的平衡。
(6)unsigned int irq_count
irq_count
记录了中断的触发次数。配合 last_unhandled
和 irqs_unhandled
,这些字段用于检测异常中断(broken IRQ)。异常中断通常是由于硬件或固件故障导致中断处理程序没有处理完毕。内核可以根据这些计数信息进行故障诊断,并在必要时启动异常中断恢复机制。
(7)raw_spinlock_t lock
lock
是一个自旋锁,用于保护中断描述符的数据一致性。由于多个 CPU 可能同时访问同一个中断描述符,因此 lock
保障了并发访问的安全,避免竞态条件。
(8)struct cpumask *percpu_enabled
一个中断描述符可能会有两种情况,一种是该IRQ是global,一旦disable了该irq,那么对于所有的CPU而言都是disable的。还有一种情况,就是该IRQ是per CPU的,也就是说,在某个CPU上disable了该irq只是disable了本CPU的IRQ而已,其他的CPU仍然是enable的。percpu_enabled是一个描述该IRQ在各个CPU上是否enable成员。
percpu_enabled
用于管理每个 CPU 上的 IRQ 启用状态。当 IRQ 是全局共享的(global)时,禁用一个 CPU 的 IRQ 会同时禁用所有 CPU 上的该 IRQ。而如果是按 CPU 配置的(per-CPU),那么禁用某个 CPU 的 IRQ 只会影响该 CPU,不会影响其他 CPU。
(9)unsigned long threads_oneshot
threads_oneshot
及其配套字段 threads_active
、wait_for_threads
用于中断线程管理。IRQ 线程(IRQ thread)用于处理复杂和耗时的中断任务。threads_oneshot
标识是否启用了单次执行模式,threads_active
记录活动线程数量,而 wait_for_threads
是一个等待队列,用于在线程被中断处理程序唤醒后等待其完成工作。
3. irq_data
irq_data
是一个用于管理和保存中断描述符的结构体,它包含了中断的相关信息,如中断掩码、软件和硬件中断号、芯片指针等。具体定义如下:
struct irq_data {
u32 mask; // 中断掩码,用于指定中断的屏蔽状态
unsigned int irq; // 软件中断号,用于系统内部的软件中断识别
unsigned long hwirq; // 硬件中断号,代表硬件设备的中断 ID
unsigned int node; // NUMA节点索引,指定该中断描述符所在的内存节点
unsigned int state_use_accessors; // 中断状态标志,使用一系列宏定义 (IRQD_xxxx) 来描述不同的状态
struct irq_chip *chip; // 指向该中断的 IRQ 芯片结构体
struct irq_domain *domain; // 指向该中断所属的中断域 (irq_domain),用于映射硬件中断号到软件中断号
void *handler_data; // 与外设特定处理器相关的私有数据
void *chip_data; // 与中断控制器相关的私有数据
struct msi_desc *msi_desc; // 指向 MSI (Message Signaled Interrupts) 描述符
cpumask_var_t affinity; // 指定该中断的 CPU 亲和性,确定在哪个 CPU 上处理该中断
};
-
mask
:用于设置或获取该中断的掩码状态。掩码状态决定了中断是否会被忽略,当某些中断需要暂时被禁止时,可以设置对应的掩码。 -
irq
和hwirq
:irq
是软件中断号,用于在软件层识别中断,而hwirq
是硬件中断号,表示实际硬件设备的中断编号。irq_domain
用于建立irq
和hwirq
之间的映射关系。例如,GPIO 控制器的第 x 号中断可以映射为软件中的一个全局中断号。 -
node
:表示该中断描述符所在的 NUMA 节点。对于支持 NUMA 架构的系统,不同节点的内存访问速度可能不同,因此将中断描述符放在离处理器较近的节点内存中,有助于提高中断处理效率。 -
state_use_accessors
:表示中断的状态,使用一些宏定义(如IRQD_ACTIVE
等)来标识中断的不同状态。 -
chip
:指向一个irq_chip
结构体,该结构体定义了处理该中断的芯片操作。irq_chip
提供了各种函数指针,用于启动、关闭、屏蔽、解除屏蔽等中断操作。 -
domain
:指向irq_domain
结构体,该结构体负责建立硬件中断号 (hwirq
) 和软件中断号 (irq
) 的映射关系,支持不同域内的中断号映射到系统的全局中断号。 -
handler_data
和chip_data
:handler_data
是与外设相关的特定数据,chip_data
是与中断控制器相关的私有数据,这些数据可以帮助管理外设特性和中断控制。 -
msi_desc
:表示 MSI 中断的描述符。MSI 是一种基于消息的中断,不需要通过引脚的电平或边缘触发,而是通过发送消息来触发中断。 -
affinity
:用来设置中断亲和性,即指定该中断应在哪个 CPU 上处理。适当的中断亲和性可以减少跨 CPU 处理开销,提高中断处理的效率。
它就是个中转站,里面有 irq_chip 指针 irq_domain 指针,都是指向别的结构体。
比较有意思的是 irq、hwirq,irq 是软件中断号,hwirq 是硬件中断号。
比如上面我们举的例子,在 GPIO 中断 B 是软件中断号,可以找到 irq_desc[B]这个数组项;GPIO 里的第 x 号中断,这就是 hwirq。谁来建立 irq、hwirq 之间的联系呢?由 irq_domain 来建立。irq_domain会把本地的 hwirq 映射为全局的 irq,什么意思?比如 GPIO 控制器里有第 1 号中断,UART 模块里也有第 1 号中断,这两个“第 1 号中断”是不一样的,它们属于不同的“域”──irq_domain。
中断有两种形态,一种就是直接通过signal相连,用电平或者边缘触发。另外一种是基于消息的,被称为MSI (Message Signaled Interrupts)。msi_desc是MSI类型的中断相关。
node成员用来保存中断描述符的内存位于哪一个memory node上。 对于支持NUMA(Non Uniform Memory Access Architecture)的系统,其内存空间并不是均一的,而是被划分成不同的node,对于不同的memory node,CPU其访问速度是不一样的。如果一个IRQ大部分(或者固定)由某一个CPU处理,那么在动态分配中断描述符的时候,应该考虑将内存分配在该CPU访问速度比较快的memory node上。
4. irq_chip
irq_chip
是用于定义中断控制器的操作接口的数据结构。它包含了一系列函数指针,每个函数指针对应中断控制器的一个操作,如启动、停止、屏蔽和解除屏蔽等。该结构体的定义如下:
struct irq_chip {
const char *name; // 中断控制器名称,显示在 /proc/interrupts 中
int (*irq_startup)(struct irq_data *data); // 启动指定硬件中断
void (*irq_shutdown)(struct irq_data *data); // 关闭指定硬件中断
void (*irq_enable)(struct irq_data *data); // 启用指定硬件中断
void (*irq_disable)(struct irq_data *data); // 禁用指定硬件中断
void (*irq_ack)(struct irq_data *data); // 中断应答操作,告知硬件中断已被处理
void (*irq_mask)(struct irq_data *data); // 屏蔽指定硬件中断
void (*irq_unmask)(struct irq_data *data); // 解除指定硬件中断的屏蔽
void (*irq_eoi)(struct irq_data *data); // 中断结束
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest); // 设置中断亲和性
int (*irq_retrigger)(struct irq_data *data); // 重新触发中断
int (*irq_set_type)(struct irq_data *data, unsigned int flow_type); // 设置中断触发类型
int (*irq_set_wake)(struct irq_data *data, unsigned int on); // 设置中断唤醒
void (*irq_bus_lock)(struct irq_data *data); // 锁定慢速总线
void (*irq_bus_sync_unlock)(struct irq_data *data); // 解锁慢速总线
void (*irq_suspend)(struct irq_data *data); // 中断挂起
void (*irq_resume)(struct irq_data *data); // 中断恢复
void (*irq_pm_shutdown)(struct irq_data *data); // 电源管理关闭中断
};
-
name
:中断控制器的名称,会显示在/proc/interrupts
文件中,便于识别中断控制器来源。 -
irq_startup
和irq_shutdown
:用于启动和关闭硬件中断,初始化中断的硬件资源,并在中断不再使用时释放资源。 -
irq_enable
和irq_disable
:用于启用和禁用中断。当禁用时,中断不会被触发,也不会被处理。 -
irq_ack
:硬件应答操作,表示开始处理一个中断,通常会在中断处理函数中被调用,以告知硬件当前的中断已经开始被处理。 -
irq_mask
和irq_unmask
:用于屏蔽和解除屏蔽硬件中断。屏蔽操作可以暂时忽略中断信号,解除屏蔽后中断信号恢复。 -
irq_eoi
:中断结束操作,用于在处理完成后向中断控制器发送信号,表示该中断已处理完毕,系统可以继续处理其他任务。 -
irq_set_affinity
:设置中断的 CPU 亲和性,指定该中断优先在哪个 CPU 上处理,以提高系统性能和效率。 -
irq_set_type
:设置中断触发类型(电平触发或边沿触发)。不同硬件设备可能需要不同的中断触发方式。 -
irq_set_wake
:用于电源管理,设置中断是否能唤醒系统。在系统处于低功耗模式时,某些中断可以被配置为唤醒源。
irq_chip
提供了一系列函数指针,定义了如何控制硬件中断。这些函数在系统中请求和释放中断时被调用,完成中断的初始化、屏蔽和解除屏蔽等操作。request_irq
函数调用会自动调用 irq_chip
结构体中的相关函数,帮助我们启用中断和响应硬件事件,而不需要手动调用各个硬件的中断处理函数。此外,irq_chip
提供的灵活接口允许针对不同设备进行个性化的中断处理。
在 request_irq 后,并不需要手工去使能中断,原因就是系统调用对应的 irq_chip 里的函数帮我们使能了中断。
提供的中断处理函数中,也不需要执行主芯片相关的清中断操作,也是系统帮我们调用 irq_chip 中的相关函数。但是对于外部设备相关的清中断操作,还是需要我们自己做的。就像上面图里的“外部设备 1“、“外部设备 n”,外设备千变万化,内核里可没有对应的清除中断操作