Linux 内核中断子系统深度解析之设计精髓
第 1 章 中断子系统综述与 IRQ Domain
1.1 中断子系统综述
1.1.1 前言
-
正确使用
request_threaded_irq等 API; -
选用合适的同步机制保护临界区;
-
利用 softirq、tasklet、workqueue 等下半部机制。
1.1.2 硬件抽象:外设、中断控制器与 CPU
一个典型中断硬件系统包含三个角色:
-
外设:产生中断请求(IRQ request line)。
-
中断控制器:汇聚多个外设的中断信号,进行优先级管理、分发,并提供控制接口。
-
CPU:接收中断信号(如 ARM 的 nIRQ/nFIQ),跳转到异常向量表执行处理。
设计精髓:
-
中断控制器之上可以级联,形成树状拓扑。
-
CPU 仅感知“有无中断”,不关心优先级与来源;中断控制器负责将物理中断映射到逻辑信号,并分发给指定 CPU。
-
这种分层将硬件的灵活性与软件的简洁性解耦,驱动工程师只需面对 IRQ number。
1.1.3 多中断控制器与多 CPU 的拓扑选择
当系统中存在多个支持多核的中断控制器(如两级 GIC)时,有两种典型拓扑:
-
所有 CPU 挂在根控制器上
-
次级控制器作为根控制器的普通输入,其 SPI 中断固定路由到某 CPU。
-
优点:软件简单;缺点:无法利用次级控制器的 PPI/SGI,且无法独立控制分发目标。
-
-
CPU 分别挂接不同控制器
-
例如根 GIC 接 CPU0‑3,次级 GIC 接 CPU4‑7。
-
优点:各控制器可独立分发;缺点:整体看起来像两个独立域,影响负载均衡。
-
场景调试:在多核系统中,中断分发策略直接影响实时性。需要对每个中断设置合理的 affinity 并监控统计信息(/proc/interrupts),防止热点 CPU。
1.1.4 中断分发策略
中断控制器通常提供 Interrupt Processor Target Register(如 GIC 的 ITARGETSR),每个 bit 表示是否将该中断发送给对应 CPU。软件可在 handler 中动态修改该寄存器,实现简单的轮流处理。
更复杂的需求(如负载均衡、中断聚合)由 irqbalance 守护进程或内核线程 irq/... 在软件层完成。
1.2 IRQ Domain:硬件中断 ID 到 IRQ 号的映射
1.2.1 问题的提出
系统中有两种中断标识:
-
IRQ number:软件视角的虚拟中断号,驱动用于注册 handler。
-
HW interrupt ID:中断控制器内部对连接在其上的中断源的物理编码。
当存在多个级联控制器时,仅靠 HW interrupt ID 无法唯一标识一个外设中断(不同控制器可能重复编号)。于是内核引入 irq domain 机制,将 HW interrupt ID 映射到全局唯一的 IRQ number。
1.2.2 映射数据库的类型
内核根据硬件特性抽象出三种映射方式:
| 类型 | 描述 | 使用场景 |
|---|---|---|
| 线性映射 (linear) | 使用 lookup table,HW ID 直接作为索引。要求 HW ID 空间小且紧凑。 | 绝大多数 SOC 内嵌控制器,如 GIC |
| Radix Tree 映射 | 动态建立 radix tree 保存映射。HW ID 稀疏或很大时使用。 | PowerPC、MIPS 平台 |
| 无映射 (nomap) | HW ID 可由软件配置,直接令 HW ID 等于 IRQ number。 | 如 MPIC 控制器 |
设计精髓:
-
抽象 irq_domain_ops:每个 irq domain 通过回调函数(
.map、.xlate等)实现自己的翻译逻辑,上层代码完全透明。 -
统一入口:
irq_create_mapping(domain, hwirq)动态创建映射并返回 IRQ number。
1.2.3 关键数据结构
/**
* @brief 中断域结构,表示一个中断控制器及其映射能力。
*/
struct irq_domain {
struct list_head link; /**< 全局 irq_domain_list 链表节点 */
const char *name;
const struct irq_domain_ops *ops; /**< 回调函数集 */
void *host_data; /**< 控制器私有数据 */
struct device_node *of_node; /**< 对应设备树节点 */
/* 反向映射数据 */
irq_hw_number_t hwirq_max;
unsigned int revmap_size; /**< 线性映射表大小 */
struct radix_tree_root revmap_tree; /**< radix tree 根 */
unsigned int linear_revmap[]; /**< 线性映射查找表 */
};
1.2.4 映射建立流程
1. 中断控制器初始化时注册 domain 例如 GIC 驱动调用 irq_domain_add_legacy() 或 irq_domain_add_linear(),在全局链表中挂入新 domain,并为每个有效 HW ID 预分配 IRQ number。
2. 外设驱动初始化时解析设备树
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)) // 解析 interrupts 属性
return 0;
return irq_create_of_mapping(&oirq); // 创建映射并返回 IRQ number
}
3. irq_create_of_mapping 内部流程
-
根据
interrupt-parent找到目标irq_domain。 -
调用 domain 的
ops->xlate解析interrupts属性(如<&intc 37 4>),得到 HW ID 和触发类型。 -
调用
irq_create_mapping(domain, hwirq)在 mapping DB 中建立关联。
4. irq_domain_associate 具体动作
int irq_domain_associate(struct irq_domain *domain, unsigned int virq,
irq_hw_number_t hwirq)
{
struct irq_data *irq_data = irq_get_irq_data(virq);
irq_data->hwirq = hwirq;
irq_data->domain = domain;
if (domain->ops->map)
domain->ops->map(domain, virq, hwirq); // 设置 irq chip、handler 等
// 存入反向映射表或 radix tree
if (hwirq < domain->revmap_size)
domain->linear_revmap[hwirq] = virq;
else
radix_tree_insert(&domain->revmap_tree, hwirq, irq_data);
return 0;
}
设计精髓:
-
反向映射表不仅用于通过 HW ID 查找 IRQ number,还能在中断处理时快速由硬件 ID 转为软件 handler。
-
map回调是最关键的填充点:它会调用irq_set_chip_and_handler()等,将中断描述符与实际的 irq chip、flow handler 绑定。
1.2.5 级联场景下的转换过程
以两级 GIC 为例:
-
根 GIC 初始化:
gic_init_bases创建 domain,预分配 IRQ 号。 -
次级 GIC 初始化:同样创建 domain,同时调用
irq_of_parse_and_map将自己作为外设接入根 GIC,并注册链式 handlergic_handle_cascade_irq。
中断到来时:
-
根 GIC 的
gic_handle_irq读GIC_CPU_INTACK获取 HW ID,映射为 IRQ number,调用handle_IRQ。 -
handle_IRQ进入该 IRQ 的highlevel handler,如果是级联中断,handler 是被设定为gic_handle_cascade_irq。 -
在该 handler 中重复步骤 1 的过程,最终递送到外设的 specific handler。
场景调试: 如果外设中断被级联,可以通过 /proc/interrupts 观察父中断的计数增加,但子中断的驱动 handler 不执行,往往是域映射或 xlate 解析错误,需要检查设备树及 interrupt-controller 相关属性。
第 2 章 IRQ Number 与中断描述符
2.1 基本概念
2.1.1 通用中断处理流程
在 Linux 内核中,每个外设中断都由 struct irq_desc 来描述,称为中断描述符。内核维护一个关于所有 IRQ 的描述符数据库。当发生硬件中断时,处理流程如下:
外设触发中断 | v 中断控制器采集 HW interrupt ID | v 通过 irq domain 翻译为 IRQ number | v 在描述符数据库中查找对应的 struct irq_desc | v 调用描述符中的 highlevel irq-events handler | +---> (1) 调用底层 irq chip callback 进行 flow control (mask, ack, eoi...) | +---> (2) 调用 action list 上的 specific handler 处理具体中断
2.1.2 中断的打开与关闭
本章涉及两种“开关中断”概念:
| 类型 | 含义 | 影响范围 |
|---|---|---|
| CPU 中断 | 打开/关闭本地 CPU 响应中断的能力。ARM 通过 CPSR 的 I/F 位控制。 | 仅本 CPU |
| IRQ mask/unmask | 控制中断控制器对特定 IRQ 的屏蔽。 | 所有 CPU 对该中断的可见性 |
关键设计演进:
-
旧内核(2.6.35 之前):区分 fast handler(关 CPU 中断执行)和 slow handler(开 CPU 中断执行)。slow handler 耗时较长,关中断会影响系统性能。
-
新内核:废弃了
IRQF_DISABLED标志。所有 specific handler 都在关闭本 CPU 中断的状态下执行,耗时操作推送到 threaded handler 或 bottom half 中。这避免了中断嵌套过深,并简化了设计。
2.1.3 IRQ Number 的两种管理方式
-
静态数组(
!CONFIG_SPARSE_IRQ):struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp;
每个 IRQ number 对应一个描述符,通过数组索引直接访问。简单高效,但若平台 NR_IRQS 很大而实际用到的很少,会造成内存浪费。
-
Radix Tree(
CONFIG_SPARSE_IRQ):// 中断描述符通过 radix tree 管理,irq number 作为 key
描述符动态分配,内存更紧凑。查找需遍历 radix tree,有一定性能开销,但内存利用率高。
设计精髓:IRQ number 在引入 irq domain 后已成为纯粹的“虚拟编号”,与硬件引脚彻底解耦。这让内核可以在不修改驱动代码的情况下,适应不同的硬件拓扑。
2.2 中断描述符数据结构
2.2.1 struct irq_data:底层 irq chip 相关数据
/**
* @brief 每个中断的底层硬件相关数据。
*
* 将 irq chip、domain、hwirq 等硬件相关信息打包在一起。
*/
struct irq_data {
u32 mask; /**< 对 irq chip 的掩码 */
unsigned int irq; /**< 软件 IRQ number */
unsigned long hwirq; /**< 硬件中断 ID */
unsigned int node; /**< NUMA 节点 */
unsigned int state_use_accessors; /**< 状态位 (IRQD_xxx) */
struct irq_chip *chip; /**< 所属中断控制器接口 */
struct irq_domain *domain; /**< 所属 IRQ domain */
void *handler_data; /**< specific handler 私有数据 */
void *chip_data; /**< irq chip 私有数据 */
struct msi_desc *msi_desc; /**< MSI 中断描述符 */
cpumask_var_t affinity; /**< CPU affinity 设置 */
};
-
node字段:在 NUMA 系统中,中断描述符所在的内存节点会影响访问速度。如果一个中断固定由某 CPU 处理,在初始化时尽量将该描述符分配在该 CPU 的本地节点上。 -
handler_datavschip_data:前者供 specific handler 使用(通过irq_get_handler_data获取),后者供底层 irq chip 驱动使用。
2.2.2 struct irq_chip:中断控制器操作接口
/**
* @brief 中断控制器操作集。
*
* 每个中断控制器(如 GIC、GPIO 控制器)要实现一组回调函数,
* 供 highlevel irq handler 调用以进行 flow control。
*/
struct irq_chip {
const char *name; /**< /proc/interrupts 显示的名称 */
unsigned 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); /**< 确认中断,通常清除 pending */
void (*irq_mask)(struct irq_data *data); /**< 屏蔽中断源 */
void (*irq_unmask)(struct irq_data *data); /**< 取消屏蔽 */
void (*irq_eoi)(struct irq_data *data); /**< 中断结束通知 (GIC 必需) */
int (*irq_set_affinity)(struct irq_data *data, const struct cpumask *dest, bool force);
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);
// ... power management callbacks ...
};
设计精髓:
-
Flow Control 抽象:通过对
ack、mask、eoi等函数的组合调用,实现对电平/边沿触发、是否需要 EOI 等不同硬件行为的统一处理。上层 flow handler 不关心硬件细节,只调用这些接口。 -
irq_bus_lock/irq_bus_sync_unlock:为挂在 I2C/SPI 等慢速总线上的中断控制器设计。避免长时间持自旋锁等待总线操作。
2.2.3 struct irq_desc:中断描述符
/**
* @brief 完整的中断描述符。
*
* 它是中断处理流程的核心枢纽,连接软件 handler 与硬件控制接口。
*/
struct irq_desc {
struct irq_data irq_data; /**< 硬件相关数据 */
unsigned int __percpu *kstat_irqs; /**< 各 CPU 中断计数 */
irq_flow_handler_t handle_irq; /**< highlevel flow handler */
struct irqaction *action; /**< specific handler 链表 */
unsigned int status_use_accessors; /**< 描述符状态 (IRQ_xxx) */
unsigned int core_internal_state__do_not_mess_with_it; /**< 内部状态,别名 istate */
unsigned int depth; /**< disable 嵌套深度 */
unsigned int wake_depth; /**< wakeup source 使能嵌套深度 */
unsigned int irq_count; /**< 中断触发次数 (用于 broken IRQ 检测) */
unsigned long last_unhandled; /**< 上次未处理中断时间 */
unsigned int irqs_unhandled; /**< 累计未处理次数 */
raw_spinlock_t lock; /**< 保护本描述符的自旋锁 */
struct cpumask *percpu_enabled; /**< per-CPU 使能状态 */
#ifdef CONFIG_SMP
const struct cpumask *affinity_hint; /**< 用户建议的 affinity */
#endif
unsigned long threads_oneshot; /**< oneshot 线程掩码 */
atomic_t threads_active; /**< 活跃线程计数 */
wait_queue_head_t wait_for_threads; /**< 线程同步等待队列 */
#ifdef CONFIG_PROC_FS
struct proc_dir_entry *dir; /**< /proc/irq/xxx 目录 */
#endif
int parent_irq; /**< 父中断号 (级联场景) */
const char *name; /**< /proc/interrupts 显示名称 */
};
关键字段详解:
| 字段 | 作用 |
|---|---|
handle_irq |
指向 handle_level_irq、handle_edge_irq 等 flow handler,决定中断处理的“骨架” |
action |
指向 struct irqaction 链表。支持共享中断时,链表中会有多个节点 |
istate |
内部状态,包含 IRQS_PENDING、IRQS_REPLAY、IRQS_AUTODETECT 等标志,用于中断重发和自动探测 |
depth |
enable_irq/disable_irq 的嵌套深度,depth > 0 时中断被屏蔽 |
percpu_enabled |
对于 per-CPU 类型中断,记录每个 CPU 上的使能状态 |
2.3 中断描述符的初始化
2.3.1 静态数组场景(!CONFIG_SPARSE_IRQ)
int __init early_irq_init(void)
{
int i;
for (i = 0; i < NR_IRQS; i++) {
desc[i].kstat_irqs = alloc_percpu(unsigned int); // per-CPU 统计计数
alloc_masks(&desc[i], GFP_KERNEL, node); // CPU mask 内存分配
raw_spin_lock_init(&desc[i].lock); // 自旋锁初始化
desc_set_defaults(i, &desc[i], node, NULL); // 填充默认值
}
return arch_early_irq_init();
}
2.3.2 Radix Tree 场景(CONFIG_SPARSE_IRQ)
int __init early_irq_init(void)
{
int initcnt = arch_probe_nr_irqs(); // 体系结构确定预分配数量
if (initcnt > nr_irqs)
nr_irqs = initcnt;
for (i = 0; i < initcnt; i++) {
struct irq_desc *desc = alloc_desc(i, node, NULL); // 动态分配
set_bit(i, allocated_irqs); // 标记已分配
irq_insert_desc(i, desc); // 插入 radix tree
}
}
2.4 与中断控制器相关的设置接口
2.4.1 调用时机的变迁
-
传统方式(静态 IRQ):在 machine driver 初始化时一次性对所有已知 IRQ 设置 chip、type、handler。
-
现代方式(DT + irq domain):
-
中断控制器的
map回调中设置。 -
外设驱动解析 DT 后调用
irq_create_mapping,该函数触发 domain 的map回调。
-
2.4.2 主要接口分析
irq_set_chip
int irq_set_chip(unsigned int irq, struct irq_chip *chip)
{
// 获取描述符锁,防止并发修改
struct irq_desc *desc = irq_get_desc_lock(irq, &flags, 0);
desc->irq_data.chip = chip;
irq_put_desc_unlock(desc, flags);
irq_reserve_irq(irq); // For CONFIG_SPARSE_IRQ,标记 IRQ 已使用
return 0;
}
irq_set_irq_type
int irq_set_irq_type(unsigned int irq, unsigned int type)
{
// 注意:使用 irq_get_desc_buslock 而非 irq_get_desc_lock
struct irq_desc *desc = irq_get_desc_buslock(irq, &flags, IRQ_GET_DESC_CHECK_GLOBAL);
int ret = __irq_set_trigger(desc, irq, type & IRQ_TYPE_SENSE_MASK);
irq_put_desc_busunlock(desc, flags);
return ret;
}
设计精髓:
-
irq_get_desc_lockvsirq_get_desc_buslock:前者仅持自旋锁;后者先调用irq_bus_lock(例如 I2C mutex),然后关中断+持自旋锁。对于慢速总线中断控制器,这避免了长时间自旋。 -
IRQ_GET_DESC_CHECK_GLOBAL:对于 per-CPU 中断(N-N mode),不能设置 trigger type,因为每个 CPU 有独立的寄存器。底层代码会检查并拒绝此类操作。
irq_set_chip_and_handler_name
void irq_set_chip_and_handler_name(unsigned int irq, struct irq_chip *chip,
irq_flow_handler_t handle, const char *name)
{
irq_set_chip(irq, chip);
__irq_set_handler(irq, handle, 0, name);
}
__irq_set_handler 的级联参数
void __irq_set_handler(unsigned int irq, irq_flow_handler_t handle, int is_chained, const char *name)
{
desc->handle_irq = handle;
if (handle != handle_bad_irq && is_chained) {
irq_settings_set_noprobe(desc); // 禁止自动探测
irq_settings_set_norequest(desc); // 禁止 driver request 此 IRQ
irq_settings_set_nothread(desc); // 禁止线程化
irq_startup(desc, true);
}
}
is_chained=1 用于级联中断线,该 IRQ 被标记为不可探测/不可申请/不可线程化,确保只有级联的父控制器 handler 能用它。
第 3 章 High Level IRQ Event Handler
3.1 进入 High Level Handler 的路径
以 ARM 为例,中断处理的调用链如下:
IRQ 异常向量 -> irq_handler (汇编宏) -> handle_arch_irq (函数指针,在 GIC 初始化时设置) -> gic_handle_irq (读取 GIC_CPU_INTACK 获取 HW ID) -> irq_find_mapping (HW ID -> IRQ number) -> handle_IRQ -> generic_handle_irq -> desc->handle_irq() // 进入 high level handler
/**
* @brief ARM 通用中断分发入口。
*/
void handle_IRQ(unsigned int irq, struct pt_regs *regs)
{
generic_handle_irq(irq);
}
int generic_handle_irq(unsigned int irq)
{
struct irq_desc *desc = irq_to_desc(irq);
generic_handle_irq_desc(irq, desc);
return 0;
}
static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)
{
desc->handle_irq(irq, desc);
}
3.2 预备知识:几种特殊机制
3.2.1 自动探测 IRQ(Auto Probing)
某些老式硬件的驱动不知道自己的 IRQ 号,需要遍历探测:
unsigned long irqs; int irq; irqs = probe_irq_on(); // 启动探测,返回可能 IRQ 的掩码 // 触发硬件产生一次中断 irq = probe_irq_off(irqs); // 结束探测,返回确定的 IRQ 号
原理:
-
probe_irq_on遍历所有可探测的中断描述符,设置IRQS_AUTODETECT | IRQS_WAITING,并使能它们。 -
等待 100ms,期间任何意外中断会清除
IRQS_WAITING。 -
触发目标硬件中断。该中断的 flow handler 会清除
IRQS_WAITING。 -
probe_irq_off检查所有仍在自动探测状态的描述符,找到那个IRQS_WAITING被清除的。若找到唯一一个,返回该 IRQ 号;否则探测失败。
3.2.2 中断重发(Resend)
场景:CPU A 在处理 x 外设的中断时,x 外设又触发了中断,但被 CPU B 捕捉、mask 并设置 IRQS_PENDING 标志,委托 CPU A 处理。在 CPU A 结束 handler 前,CPU B 由于某些原因 disable 了该 IRQ,导致 CPU A 没来得及处理这个 pending 中断。当该 IRQ 再次 enable 时,需要 resend 机制来补发。
void check_irq_resend(struct irq_desc *desc, unsigned int irq)
{
if (irq_settings_is_level(desc)) // 电平中断不需要 resend
return;
if (desc->istate & IRQS_REPLAY) // 已经在 resend 过程中
return;
if (desc->istate & IRQS_PENDING) {
desc->istate &= ~IRQS_PENDING;
desc->istate |= IRQS_REPLAY;
if (!chip->irq_retrigger || !chip->irq_retrigger(data))
// 硬件 retrigger 失败则尝试软件方式
sw_resend_irq(desc, irq);
}
}
3.2.3 Broken IRQ 检测与处理
当内核收到一个中断却没有任何 handler 认领它(返回 IRQ_NONE)时:
-
记录
irqs_unhandled和last_unhandled时间。 -
如果 100,000 次中断中有 99,900 次未被处理,则判定该 IRQ 为 broken,disable 之。
-
启动一个 timer(
poll_spurious_irq_timer),定期轮询该 IRQ,试图让其他 handler 认领。
void note_interrupt(unsigned int irq, struct irq_desc *desc, irqreturn_t action_ret)
{
if (unlikely(action_ret == IRQ_NONE)) {
if (time_after(jiffies, desc->last_unhandled + HZ/10))
desc->irqs_unhandled = 1; // 时间间隔够长,重新计数
else
desc->irqs_unhandled++;
desc->last_unhandled = jiffies;
}
desc->irq_count++;
if (desc->irq_count >= 100000) {
desc->irq_count = 0;
if (desc->irqs_unhandled > 99900) {
__report_bad_irq(irq, desc, action_ret);
desc->istate |= IRQS_SPURIOUS_DISABLED;
desc->depth++;
irq_disable(desc);
mod_timer(&poll_spurious_irq_timer,
jiffies + POLL_SPURIOUS_IRQ_INTERVAL);
}
desc->irqs_unhandled = 0;
}
}
3.3 硬件信号模型
3.3.1 CPU 与中断控制器之间的信号
| 信号 | 含义 | ARM 表现 |
|---|---|---|
| 中断触发信号 | 通知 CPU 有中断待处理 | nIRQ/nFIQ 引脚 |
| Ack 信号 | CPU 确认中断,获取中断信息 | 读 GIC_CPU_INTACK 寄存器 |
| EOI 信号 | CPU 通知中断处理完毕 | 写 GIC_CPU_EOI 寄存器 |
3.3.2 中断控制器与外设之间的信号
只有一根 Interrupt Request Line。外设通过电平或边沿变化,向中断控制器宣告中断事件。
3.4 几种典型的 High Level Flow Handler
3.4.1 边沿触发处理器:handle_edge_irq
硬件行为假设:
_______ IRQ_| |________________________________ (上升沿后保持高电平) | | ack -> 清除 pending 状态,拉低信号线
代码流程:
void handle_edge_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
// (1) 检查是否应跳过处理
if (irqd_irq_disabled(&desc->irq_data) ||
irqd_irq_inprogress(&desc->irq_data) || !desc->action) {
if (!irq_check_poll(desc)) {
desc->istate |= IRQS_PENDING; // 设置 pending,委托处理
mask_ack_irq(desc); // mask 并 ack
goto out_unlock;
}
}
kstat_incr_irqs_this_cpu(irq, desc);
// (2) Ack 中断,清除硬件 pending 状态
desc->irq_data.chip->irq_ack(&desc->irq_data);
do {
if (unlikely(!desc->action)) {
mask_irq(desc);
goto out_unlock;
}
// (3) 处理其他 CPU 委托的 pending
if (unlikely(desc->istate & IRQS_PENDING) &&
!irqd_irq_disabled(&desc->irq_data) &&
irqd_irq_masked(&desc->irq_data))
unmask_irq(desc); // unmask 让其他 CPU 也能响应
// (4) 调用 specific handler
handle_irq_event(desc);
} while ((desc->istate & IRQS_PENDING) &&
!irqd_irq_disabled(&desc->irq_data));
out_unlock:
raw_spin_unlock(&desc->lock);
}
设计精髓分析:
-
为什么是 ack 而不是 mask+ack?
-
边沿触发是一次性的。如果不 ack,中断控制器无法检测下一个边沿。但如果另一个 CPU 正在处理(
in progress),则必须 mask+ack + 设 pending,委托处理。
-
-
do-while 循环的作用:
-
在
handle_irq_event执行期间,锁被释放了(避免长时间持锁)。此时其他 CPU 可能再次上报该中断并设IRQS_PENDING。do-while 循环让本 CPU 处理完这些“积压”中断,避免中断丢失。
-
3.4.2 电平触发处理器:handle_level_irq
硬件行为假设:
IRQ ____________________________ (有效电平一直保持) | | mask -> 即使有电平也不转发 ack -> 清除控制器状态 只有外设 specific handler 清除外设状态寄存器后,电平才消失
代码流程:
void handle_level_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
mask_ack_irq(desc); // (1) 先 mask 再 ack
if (irqd_irq_inprogress(&desc->irq_data))
if (!irq_check_poll(desc))
goto out_unlock;
desc->istate &= ~(IRQS_REPLAY | IRQS_WAITING);
kstat_incr_irqs_this_cpu(irq, desc);
if (unlikely(!desc->action || irqd_irq_disabled(&desc->irq_data))) {
desc->istate |= IRQS_PENDING;
goto out_unlock;
}
handle_irq_event(desc); // (2) 调用 specific handler
cond_unmask_irq(desc); // (3) 有条件 unmask
out_unlock:
raw_spin_unlock(&desc->lock);
}
设计精髓:
-
为什么必须先 mask 再 ack?
-
电平中断的 pending 状态由电平本身决定。若不 mask,ack 后只要外设电平不撤,中断控制器立刻重新触发中断给 CPU,导致“中断风暴”。mask 后,电平虽然还在,但控制器不转发,CPU 可以安心处理。
-
-
cond_unmask_irq的条件是什么?-
对于 threaded handler 且标记了
IRQF_ONESHOT,在 threaded handler 执行完之前不能 unmask,否则嵌套会破坏 oneshot 语义。此函数内部检查该条件。
-
3.4.3 EOI 处理器:handle_fasteoi_irq
适用于 GIC 这类需要显式 End-Of-Interrupt 的中断控制器:
void handle_fasteoi_irq(unsigned int irq, struct irq_desc *desc)
{
raw_spin_lock(&desc->lock);
// 不 ack 也不 mask,直接检查状态
if (irqd_irq_disabled(&desc->irq_data) ||
irqd_irq_inprogress(&desc->irq_data) || !desc->action) {
if (!irq_check_poll(desc)) {
desc->istate |= IRQS_PENDING;
goto out;
}
}
kstat_incr_irqs_this_cpu(irq, desc);
handle_irq_event(desc);
desc->irq_data.chip->irq_eoi(&desc->irq_data); // 处理完才发 EOI
out:
raw_spin_unlock(&desc->lock);
}
设计精髓:
-
GIC 的中断处理模型是:读
IAR时自动 ack,写EOI表示处理完成。在写 EOI 之前,该 CPU 不会再收到该中断,也不需要 mask。因此 flow handler 最简洁。
第 4 章 驱动申请中断 API:request_threaded_irq
4.1 接口规格
/** * @brief 向内核注册一个中断处理函数(支持线程化)。 * * @param irq IRQ number(通过 irq_of_parse_and_map 等获取)。 * @param handler Primary handler(在硬中断上下文执行)。可为 NULL。 * @param thread_fn Threaded handler(在内核线程中执行)。可为 NULL。 * @param irqflags 中断标志(IRQF_TRIGGER_xxx, IRQF_SHARED, IRQF_ONESHOT 等)。 * @param devname 设备名称(/proc/interrupts 中显示)。 * @param dev_id 设备唯一标识,共享中断时必须非 NULL,`free_irq` 时用于精确释放。 * @return 0 成功,负值失败。 */ int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id);
| primary handler | threaded handler | 行为 |
|---|---|---|
| NULL | NULL | 错误,返回 -EINVAL |
| 设定 | 设定 | 正常流程:primary handler 做轻量工作,然后唤醒 threaded handler |
| 设定 | NULL | 全部工作在 primary handler 中完成(传统方式) |
| NULL | 设定 | 内核提供 irq_default_primary_handler,仅返回 IRQ_WAKE_THREAD |
4.2 关键标志位
| 标志 | 说明 |
|---|---|
IRQF_SHARED |
允许多个设备共享一个中断线。共享中断的 dev_id 必须非 NULL。 |
IRQF_ONESHOT |
保证 threaded handler 执行期间该中断不会被再次触发(mask 住)。适用于 I2C/SPI 等慢速外设。 |
IRQF_PERCPU |
标记为 per-CPU 中断。 |
IRQF_NO_THREAD |
强制该中断不被线程化,即使开启了 threadirqs 内核参数。 |
IRQF_NO_SUSPEND |
系统 suspend 时不 disable 该中断,确保能唤醒系统。 |
第 5 章 驱动申请中断 API:request_threaded_irq 深度解析
5.1 中断线程化与 Linux 实时性
5.1.1 非抢占式内核的实时性痛点
在 Linux 2.4 及更早内核中,内核态代码不可抢占。下图展示了一个典型的延迟场景:
高优先级任务 [睡眠] .................... [唤醒] ...... [执行] ^ ^ 低优先级任务 [运行(用户)] [系统调用(内核)] [返回用户] ^ ^ 中断事件 | T0: 中断产生,但因关中断而延迟 | T1: 开中断,进入 handler | T2: handler 执行 | T3: 唤醒高优先任务,但仍需等待 | T4: 系统调用返回用户态,调度发生
任务响应时间 = T3 → T4 的延迟,可能长达整个系统调用执行时间。 中断延迟 = T0 → T2,包含关中断时间。
根本原因:内核态执行路径无法被高优先级任务抢占。
5.1.2 抢占式内核的改善与局限
2.6 内核引入 CONFIG_PREEMPT,允许在非临界区抢占内核代码,显著缩短了任务响应时间。但仍存在不确定性:
-
临界区:自旋锁持有、
preempt_disable等区间仍不可抢占。 -
中断上下文:中断 handler、softirq、tasklet 仍然可以抢占进程上下文,延迟半底的执行也可能阻塞调度。
结论:即使打开抢占,最坏情况任务响应时间仍不可预测。
5.1.3 中断线程化:借鉴 RTOS 的设计
RTOS 中常将中断处理简化为“发送一条消息给驱动任务”,从而保证中断上下文的极短执行。Linux 借鉴此思路,引入 threaded interrupt handler:
-
Primary Handler:在硬件中断上下文中执行,只做最关键、最快的处理(如 ack、唤醒线程),必须全程关中断。
-
Threaded Handler:在内核线程中执行,可以睡眠、使用互斥锁、耗时操作等。
核心优势:
-
将耗时的中断处理推后到内核线程,与普通进程公平调度,改善了系统实时性。
-
驱动开发者可以自然地使用互斥锁等睡眠锁,无需考虑中断上下文限制。
-
配合
IRQF_ONESHOT标志,可阻止 handler 重入,简化驱动设计。
5.2 request_threaded_irq 接口规格
/** * @brief 向内核注册一个中断处理函数(支持线程化)。 * * @param irq IRQ number。通常通过 irq_of_parse_and_map 等获取。 * @param handler Primary handler,在硬中断上下文中执行。可为 NULL。 * @param thread_fn Threaded handler,在内核线程中执行。可为 NULL。 * @param irqflags 中断标志位,控制共享、触发类型、oneshot 等行为。 * @param devname 设备名,显示于 /proc/interrupts。 * @param dev_id 设备唯一标识符。共享中断时必须非 NULL,供 free_irq 精确释放。 * @return 0 表示成功,负值表示错误码。 */ int request_threaded_irq(unsigned int irq, irq_handler_t handler, irq_handler_t thread_fn, unsigned long irqflags, const char *devname, void *dev_id);
handler 和 thread_fn 的组合:
| handler | thread_fn | 行为 |
|---|---|---|
| NULL | NULL | 错误,返回 -EINVAL |
| 非 NULL | 非 NULL | 正常线程化模式:handler 执行后返回 IRQ_WAKE_THREAD 唤醒 thread_fn |
| 非 NULL | NULL | 传统模式:全部工作在 handler 中完成,不创建内核线程 |
| NULL | 非 NULL | 内核使用默认 primary handler(只返回 IRQ_WAKE_THREAD) |
关键 irqflags 说明:
| 标志位 | 功能 |
|---|---|
IRQF_TRIGGER_XXX |
指定中断触发方式(电平/边沿),必须与硬件匹配 |
IRQF_SHARED |
允许多个设备共享此中断线。此时 dev_id 必须非 NULL |
IRQF_ONESHOT |
保证 threaded handler 执行期间该中断不会被再次触发(中断源被 mask) |
IRQF_NO_THREAD |
禁止强制线程化(即使内核启动参数指定了 threadirqs) |
IRQF_NO_SUSPEND |
系统挂起时不关闭该中断,用于唤醒源 |
IRQF_PERCPU |
标记为 per-CPU 中断 |
设计精髓: dev_id 参数看似多余,但在共享中断场景下至关重要。free_irq(irq, dev_id) 必须依赖它来精确定位要移除的 irqaction 节点,因为多个设备共享同一个 IRQ number。驱动应在 handler 中首先读取自己的硬件状态寄存器,判断中断是否属于自己的设备,若非则快速返回 IRQ_NONE。
5.3 request_threaded_irq 主流程分析
int request_threaded_irq(unsigned int irq, irq_handler_t handler,
irq_handler_t thread_fn, unsigned long irqflags,
const char *devname, void *dev_id)
{
struct irq_desc *desc;
struct irqaction *action;
int retval;
// (1) 共享中断必须提供 dev_id,否则后续释放无法定位
if ((irqflags & IRQF_SHARED) && !dev_id)
return -EINVAL;
// (2) 根据 IRQ number 获取中断描述符
desc = irq_to_desc(irq);
if (!desc)
return -EINVAL;
// (3) 检查该 IRQ 是否可被驱动申请
if (!irq_settings_can_request(desc) ||
WARN_ON(irq_settings_is_per_cpu_devid(desc)))
return -EINVAL;
// (4) 如果未提供 handler,但提供了 thread_fn,则使用默认 primary handler
if (!handler) {
if (!thread_fn)
return -EINVAL;
handler = irq_default_primary_handler;
}
// (5) 分配并填充 irqaction 结构
action = kzalloc(sizeof(struct irqaction), GFP_KERNEL);
if (!action)
return -ENOMEM;
action->handler = handler;
action->thread_fn = thread_fn;
action->flags = irqflags;
action->name = devname;
action->dev_id = dev_id;
// (6) 获取慢速总线锁(如果需要),然后执行核心注册
chip_bus_lock(desc);
retval = __setup_irq(irq, desc, action);
chip_bus_sync_unlock(desc);
if (retval)
kfree(action);
return retval;
}
步骤详解:
-
irq_settings_can_request(desc)的校验: 某些中断描述符被标记为IRQ_NOREQUEST(例如用于级联的 IRQ),驱动不能直接申请。irq_settings_is_per_cpu_devid检查是否为 per-CPU 类型且需要 per-CPU dev_id,此时应使用request_percpu_irq。 -
irq_default_primary_handler的实现:static irqreturn_t irq_default_primary_handler(int irq, void *dev_id) { return IRQ_WAKE_THREAD; }它不做任何硬件操作,仅返回唤醒标记。潜在风险:如果是电平触发中断,该 handler 没有清除外设的中断状态,电平会保持,导致中断一打开就重新触发,形成死循环。因此必须配合
IRQF_ONESHOT使用,或者在 handler 中手动 ack 外设。 -
chip_bus_lock/chip_bus_sync_unlock: 针对挂在 I2C/SPI 等慢速总线上的中断控制器。lock 操作可能是一个 mutex,避免长时间持有自旋锁。
5.4 __setup_irq 实现细节
__setup_irq 是注册中断的核心,完成了中断线程创建、共享检查、标志处理等大量工作。
5.4.1 嵌套 IRQ(Nested IRQ)处理
背景:当一个中断控制器通过慢速总线(如 I2C GPIO 扩展器)级联时,其自身的线程化 handler 中需要处理子中断。这种场景不适合再用 highlevel flow handler + primary handler 的模式,因为整个路径会长时间关中断。内核提供 nested IRQ 机制:
-
父中断的 threaded handler 中调用
handle_nested_irq(irq)来处理子中断。 -
子中断的描述符标记为
IRQ_NESTED_THREAD,不拥有自己的 highlevel handler,也不创建独立线程,而是借用父中断的线程执行。
// __setup_irq 中的 nested 处理片段
nested = irq_settings_is_nested_thread(desc);
if (nested) {
if (!new->thread_fn) {
ret = -EINVAL;
goto out_mput;
}
new->handler = irq_nested_primary_handler; // 仅用于调试打印
} else {
// 非 nested 的正常处理...
}
设计精髓:nested IRQ 避免了“双重关中断”——慢速总线操作已在父线程中,子中断处理只需直接调用,不再经过硬件 flow control,大幅降低了中断延迟。
5.4.2 强制中断线程化(Forced IRQ Threading)
当内核命令行参数 threadirqs 启用时,系统会尝试将所有未明确标记 IRQF_NO_THREAD 的中断强制转化为线程化模式。
static void irq_setup_forced_threading(struct irqaction *new)
{
if (!force_irqthreads)
return;
if (new->flags & (IRQF_NO_THREAD | IRQF_PERCPU | IRQF_ONESHOT))
return;
new->flags |= IRQF_ONESHOT;
if (!new->thread_fn) {
set_bit(IRQTF_FORCED_THREAD, &new->thread_flags);
new->thread_fn = new->handler; // 原来的 handler 变为 thread_fn
new->handler = irq_default_primary_handler; // primary 换为默认
}
}
场景分析与注意事项:
-
强制线程化后,原本在中断上下文执行的
handler被移到了内核线程(进程上下文)。这可能导致:-
原先使用
spin_lock_irq保护的数据现在需要用spin_lock_bh或spin_lock(因为已经不在硬中断上下文)。 -
依赖硬中断上下文时序的代码可能失效。
-
-
内核通过自动添加
IRQF_ONESHOT确保 threaded handler 不会重入,模拟原来 primary handler 的不可重入特性。 -
调试技巧:若怀疑某个中断被强制线程化导致问题,可检查
/proc/interrupts,对应 IRQ 行会显示[threaded]标记。
5.4.3 创建中断处理线程
如果提供了 thread_fn 且非 nested,则创建一个专属内核线程。
if (new->thread_fn && !nested) {
struct task_struct *t;
static const struct sched_param param = {
.sched_priority = MAX_USER_RT_PRIO/2, // 默认实时优先级 50
};
t = kthread_create(irq_thread, new, "irq/%d-%s", irq, new->name);
if (IS_ERR(t)) { ... }
sched_setscheduler_nocheck(t, SCHED_FIFO, ¶m);
get_task_struct(t); // 增加引用计数,防止线程意外退出后 task_struct 被释放
new->thread = t;
set_bit(IRQTF_AFFINITY, &new->thread_flags);
}
设计细节:
-
线程命名规则为
irq/%d-%s,如irq/37-mmc0,便于ps命令查看。 -
线程的调度策略被设为
SCHED_FIFO,优先级为MAX_USER_RT_PRIO/2,保证中断处理线程具有一定的实时性。 -
get_task_struct保证即使线程函数异常退出,中断描述符中thread指针所指向的task_struct不会被释放,避免内核访问野指针。
5.4.4 共享中断的检查与链表插入
old_ptr = &desc->action;
old = *old_ptr;
if (old) {
// (a) 检查共享条件是否相符
if (!((old->flags & new->flags) & IRQF_SHARED) ||
((old->flags ^ new->flags) & IRQF_TRIGGER_MASK) ||
((old->flags ^ new->flags) & IRQF_ONESHOT))
goto mismatch;
if ((old->flags & IRQF_PERCPU) != (new->flags & IRQF_PERCPU))
goto mismatch;
// (b) 追加到 action 链表末尾
do {
thread_mask |= old->thread_mask;
old_ptr = &old->next;
old = *old_ptr;
} while (old);
shared = 1;
}
关键的共享约束:
-
所有共享该中断线的
irqaction必须都设置IRQF_SHARED。 -
触发类型(
IRQF_TRIGGER_MASK)必须一致,否则硬件行为不可预期。 -
IRQF_ONESHOT状态必须一致,以确保整个中断线的 mask 行为统一。 -
per-CPU 属性必须一致。
5.4.5 One-Shot 中断的 thread_mask 分配
对于 oneshot 中断,需要为每个 irqaction 分配一个唯一的掩码位,以区分唤醒哪个线程:
if (new->flags & IRQF_ONESHOT) {
if (thread_mask == ~0UL) { // 超过系统支持的最大共享数
ret = -EBUSY;
goto out_mask;
}
new->thread_mask = 1 << ffz(thread_mask); // 找到第一个未设置的位
} else if (new->handler == irq_default_primary_handler &&
!(desc->irq_data.chip->flags & IRQCHIP_ONESHOT_SAFE)) {
ret = -EINVAL;
goto out_mask;
}
设计逻辑:
-
若未提供 primary handler 且非 oneshot,则系统无法阻止中断在 threaded handler 执行前再次触发,这是危险的不安全组合,故返回错误。
-
若底层的 irq chip 自身已经保证了 oneshot 行为(
IRQCHIP_ONESHOT_SAFE),则允许省去IRQF_ONESHOT标志。
5.5 总结:从申请到处理的全路径
-
申请阶段: 驱动调用
request_threaded_irq→ 分配irqaction→__setup_irq执行嵌套/强制线程化/共享检查、线程创建、挂入链表 → IRQ 准备就绪。 -
中断触发阶段: 硬件中断 → CPU 异常 →
handle_arch_irq→ 控制器 driver 读取 HW ID,映射 IRQ number →handle_IRQ→desc->handle_irq(highlevel flow handler) → mask/ack/eoi →handle_irq_event调用 primary handler → primary 返回IRQ_WAKE_THREAD→ 唤醒irq/%d-%s内核线程 → 执行thread_fn→ 完成处理。 -
释放阶段: 驱动调用
free_irq(irq, dev_id)→ 从 action 链表中移除指定irqaction,若无其他共享者则 shutdown 中断线。
场景调试要点:
-
若
thread_fn未被调用,检查 primary handler 返回值是否为IRQ_WAKE_THREAD。 -
若中断风暴,检查电平中断是否未在 primary handler 中清除外设状态;检查 oneshot 标志是否正确。
-
若
/proc/interrupts计数增加但驱动无响应,检查共享中断中IRQF_SHARED未设置导致注册失败
第 6 章 ARM 中断处理过程
6.1 异常向量与现场保存
ARM 处理器在收到 IRQ 信号后,会执行以下硬件自动行为:
-
模式切换:切换到 IRQ 模式(ARMv7)或 EL1_IRQ 异常级别(ARMv8)。
-
保存返回地址:将被打断指令的下一条地址保存到 LR_irq 中(
PC + 4),并复制 CPSR 到 SPSR_irq。 -
跳转到异常向量:PC 被设置为
0xFFFF0018(高向量)或0x00000018(低向量),执行 IRQ 向量处的指令。
在 Linux 内核中,异常向量表被放置于与内核地址空间紧密耦合的位置,且由汇编代码实现快速分发。
6.1.1 vector_stub 宏
/**
* @brief 通用异常入口桩。
*
* 该宏完成从异常进入点到 C 处理函数的过渡工作:
* - 修正 LR(某些模式需要减 4 或 8)
* - 保存 r0, lr 到栈上
* - 调用指定的 C 函数
* - 返回前恢复寄存器,并执行异常返回指令
*/
.macro vector_stub, name, mode, correction=0
.align 5
vector_\name:
.if \correction
sub lr, lr, #\correction
.endif
@ 保存 r0, lr 到 SVC 模式栈(因为接下来可能切换模式)
stmia sp, {r0, lr} @ 将 r0, lr 存入 sp 开始的位置
mrs lr, spsr @ 读取被中断时的 CPSR
str lr, [sp, #8] @ 保存 spsr 到栈上
@ 准备调用 C 函数的参数
mov r0, sp @ pt_regs 结构指针
mov r1, #\mode @ 异常模式
b bad_mode @ 暂时跳转(后续会通过 c 函数分发)
.endm
在实际实现中(arch/arm/kernel/entry-armv.S),IRQ 入口使用 vector_stub irq, IRQ_MODE, 4,因为 IRQ 的 LR 需要减去 4 才能指向被打断的指令。该桩负责将寄存器保存到栈上,构造出 struct pt_regs,然后调用 __irq_svc(若中断发生在 SVC 模式)或 __irq_usr(发生在用户模式)。
6.2 irq_handler 宏:从平台无关分发到具体控制器
内核为了支持多平台,抽象出一个 irq_handler 宏,该宏在配置 CONFIG_MULTI_IRQ_HANDLER 时,会跳转到一个全局函数指针 handle_arch_irq。
/** * @brief 平台无关的 IRQ 分发宏。 * * 如果配置了 MULTI_IRQ_HANDLER,则通过 handle_arch_irq 函数指针 * 调用具体的中断控制器处理函数;否则直接调用 arch_irq_handler_default。 */ .macro irq_handler #ifdef CONFIG_MULTI_IRQ_HANDLER ldr r1, =handle_arch_irq mov r0, sp @ pt_regs 作为参数 adr lr, BSYM(9997f) @ 设定返回地址 ldr pc, [r1] @ 跳转到 handle_arch_irq #else arch_irq_handler_default #endif 9997: .endm
handle_arch_irq 的设置时机:
-
对于单一 GIC 系统,在 GIC 初始化函数中直接设置:
set_handle_irq(gic_handle_irq);
-
对于多类型中断控制器的系统,可在
machine_desc->handle_irq回调中设置,setup_arch()函数负责调用,从而实现根据设备树或机器 ID 按需选择。
这种设计使得一个内核镜像可以支持多种平台,在运行时选择正确的中断处理入口,而无需为每个平台编译独立内核。
第 7 章 GIC 中断控制器驱动分析
7.1 GIC 硬件架构概述
ARM Generic Interrupt Controller (GIC) 是 ARM 生态中标准的中断控制器,目前广泛使用 GICv2 和 GICv3。以 GICv2 为例,其主要组成部分:
-
Distributor:负责所有中断的管理,包括优先级、分发到各 CPU、触发方式配置等。包含以下寄存器:
-
GICD_CTLR:Enable/Disable 整个 Distributor。 -
GICD_ISENABLER/GICD_ICENABLER:设置/清除中断使能。 -
GICD_ITARGETSR:为 SPI 指定目标 CPU。 -
GICD_ICFGR:配置触发类型(电平/边沿)。
-
-
CPU Interface:每个 CPU 各有一组私有寄存器,用于与自身的 CPU 交互:
-
GICC_CTLR:Enable/Disable CPU Interface。 -
GICC_IAR:Interrupt Acknowledge Register,读取获得当前中断 ID 并自动 ack。 -
GICC_EOIR:End Of Interrupt Register,写入中断 ID 表示处理完成。
-
-
中断 ID 号空间:
-
SGI (0‑15):软件产生中断,用于核间通信。
-
PPI (16‑31):私有外设中断,每个 CPU 独立。
-
SPI (32‑1019):共享外设中断,所有 CPU 可见。
-
7.2 核心数据结构
/**
* @brief GIC 芯片私有数据。
*
* 每个 GIC 实例(包括级联的次级 GIC)对应一个该结构体。
*/
struct gic_chip_data {
union {
void __iomem *dist_base; /**< Distributor 基地址 */
void __iomem *raw_dist_base;
};
void __iomem *cpu_base; /**< CPU Interface 基地址 */
struct irq_domain *domain; /**< 所属 irq domain */
unsigned int gic_irqs; /**< 本 GIC 支持的中断数目 */
// ...
};
每个 GIC(无论是根还是次级)均维护一个 gic_chip_data 实例,并通过 gic_data[] 数组统一管理。
7.3 GIC 初始化流程
7.3.1 主入口:gic_of_init()
/**
* @brief GIC 的设备树初始化入口。
*
* 解析设备树节点,获取基地址等信息,并调用 gic_init_bases() 完成软硬件初始化。
*/
int __init gic_of_init(struct device_node *node, struct device_node *parent)
{
void __iomem *dist_base, *cpu_base;
// 从 reg 属性读取 Distributor 和 CPU Interface 地址
dist_base = of_iomap(node, 0);
cpu_base = of_iomap(node, 1);
// gic_cnt 是全局计数器,每个 GIC 得到唯一编号
gic_init_bases(gic_cnt, -1, dist_base, cpu_base, 0, node);
// 如果不是根 GIC,则将自己挂到父 GIC 上
if (parent) {
irq = irq_of_parse_and_map(node, 0); // 获取父 GIC 的中断号
gic_cascade_irq(gic_cnt, irq); // 设置级联 handler
}
gic_cnt++;
return 0;
}
设计精髓: parent 参数的判断将根 GIC 与次级 GIC 的处理统一在同一函数中。次级 GIC 除了初始化自身的中断控制器能力外,还会作为“外设”向父 GIC 申请一个中断号,以便父 GIC 能将其中断事件传递给 CPU。
7.3.2 gic_init_bases():核心初始化
void __init gic_init_bases(unsigned int gic_nr, int irq_start,
void __iomem *dist_base, void __iomem *cpu_base,
u32 percpu_offset, struct device_node *node)
{
irq_hw_number_t hwirq_base;
struct gic_chip_data *gic = &gic_data[gic_nr];
int gic_irqs, irq_base, i;
// (1) 根据 GIC 类型确定硬件中断号起始值和中断总数
if (gic_nr == 0) { // 根 GIC
hwirq_base = 16; // 前 16 个为 SGI,不应被映射
gic_irqs = readl_relaxed(gic->dist_base + GIC_DIST_CTR) & 0x1f;
gic_irqs = (gic_irqs + 1) * 32;
if (gic_irqs > 1020) gic_irqs = 1020;
gic_irqs -= 16;
} else { // 次级 GIC
hwirq_base = 32; // 跳过 SGI 和 PPI
gic_irqs = ... ;
gic_irqs -= 32;
}
// (2) 从系统动态分配 IRQ 号
irq_base = irq_alloc_descs(irq_start, 16, gic_irqs, numa_node_id());
gic->gic_irqs = gic_irqs;
// (3) 创建 legacy irq domain,一次性建立所有映射
gic->domain = irq_domain_add_legacy(node, gic_irqs, irq_base,
hwirq_base, &gic_irq_domain_ops, gic);
// ...
}
irq_domain_add_legacy 的设计意图: 在传统平台(或在未能提供完整设备树映射信息的场合),内核采用“一次性建立整块映射”的方式:给定起始硬件中断号和起始软件 IRQ 号,逐个建立线性映射。这保证了所有 SPI(或 PPI)在初始化后立刻拥有可用的 IRQ number。
7.4 中断处理流程:gic_handle_irq()
这是整个 GIC 驱动中最核心的函数,运行在硬中断上下文中。
/**
* @brief GIC 的中断入口函数,由 handle_arch_irq 调用。
*
* 该函数读取 GICC_IAR,获得 HW 中断 ID,映射到 IRQ number,
* 并调用 handle_IRQ 进入通用中断处理栈。
*/
static asmlinkage void __exception_irq_entry gic_handle_irq(struct pt_regs *regs)
{
u32 irqstat, irqnr;
struct gic_chip_data *gic = &gic_data[0]; // 根 GIC
void __iomem *cpu_base = gic_data_cpu_base(gic);
do {
irqstat = readl_relaxed(cpu_base + GIC_CPU_INTACK); // 读取 IAR
irqnr = irqstat & GICC_IAR_INT_ID_MASK; // 提取中断 ID
if (likely(irqnr > 15 && irqnr < 1021)) {
irqnr = irq_find_mapping(gic->domain, irqnr); // 转为 IRQ number
handle_IRQ(irqnr, regs); // 通用处理
continue;
}
if (irqnr < 16) { // IPI (SGI) 处理
writel_relaxed(irqstat, cpu_base + GIC_CPU_EOI); // 先写 EOI
#ifdef CONFIG_SMP
handle_IPI(irqnr, regs);
#endif
continue;
}
break; // 无效中断号,退出循环
} while (1);
}
设计细节与精髓:
-
do-while循环:同一 CPU 可能连续收到多个中断请求,GICC_IAR 在读完一个中断后,如果还有其他 pending 中断,会立即返回下一个中断 ID。通过循环连续处理,可以减少异常入口/出口的开销,提升吞吐量。 -
EOI 的顺序:对于普通 SPI/PPI,EOI 在调用
handle_IRQ之后,由 highlevel handler 视类型决定写入时机。但对于 SGI,因为没有标准的 flow handler,直接在入口处写入 EOI,确保 IPI 尽快完成,避免阻塞其他 SGI。 -
无效中断号的保护:读取到的 IRQ ID 为 1022 或 1023 通常表示无挂起中断或硬件错误,循环退出,避免无限循环。
7.5 级联处理:gic_handle_cascade_irq()
次级 GIC 通过父 GIC 的一条 SPI 线连接到系统。当该 SPI 触发时,将进入级联处理函数。
/**
* @brief 次级 GIC 的级联处理函数。
*
* 作为父 GIC 的一个特定 IRQ 的 handler,它负责读取次级 GIC 的 IAR,
* 映射并调用 handle_IRQ。
*/
static void gic_handle_cascade_irq(unsigned int irq, struct irq_desc *desc)
{
struct gic_chip_data *chip_data = irq_desc_get_handler_data(desc);
void __iomem *cpu_base = chip_data->cpu_base;
u32 cascade_irqnr;
raw_spin_lock(&desc->lock);
cascade_irqnr = readl_relaxed(cpu_base + GIC_CPU_INTACK) & GICC_IAR_INT_ID_MASK;
if (cascade_irqnr > 15 && cascade_irqnr < 1021) {
unsigned int cascade_irq = irq_find_mapping(chip_data->domain, cascade_irqnr);
generic_handle_irq(cascade_irq); // 递归进入通用中断处理
}
raw_spin_unlock(&desc->lock);
}
调试场景:如果次级 GIC 的外设中断不能送达,应先检查 /proc/interrupts 中父 GIC 对应 SPI 的计数是否增加。若父级计数正常但子级无反应,可能是级联 handler 未注册或 domain 映射出错。
总结
本系列文档从硬件抽象、IRQ Domain 映射、中断描述符、high‑level flow handler 到驱动 API 实现,以及 ARM+GIC 的具体处理流程,系统地剖析了 Linux 中断子系统的设计精髓与实现细节:
-
准确使用
request_threaded_irq、irq_set_affinity等 API。 -
根据硬件特性选择合适的 flow handler 和标志位(如
IRQF_ONESHOT)。 -
在多核及级联控制器场景下合理配置中断路由与优先级。
-
在调试中断风暴、丢失、响应延迟等问题时有据可依。
中断子系统是 Linux 内核中最具挑战性也最为核心的模块之一,理解其运作机理,是构建稳定、高效嵌入式/服务器系统的基石。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)