第 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)时,有两种典型拓扑:

  1. 所有 CPU 挂在根控制器上

    • 次级控制器作为根控制器的普通输入,其 SPI 中断固定路由到某 CPU。

    • 优点:软件简单;缺点:无法利用次级控制器的 PPI/SGI,且无法独立控制分发目标。

  2. 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,并注册链式 handler gic_handle_cascade_irq

中断到来时:

  1. 根 GIC 的 gic_handle_irqGIC_CPU_INTACK 获取 HW ID,映射为 IRQ number,调用 handle_IRQ

  2. handle_IRQ 进入该 IRQ 的 highlevel handler,如果是级联中断,handler 是被设定为 gic_handle_cascade_irq

  3. 在该 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 的两种管理方式
  1. 静态数组!CONFIG_SPARSE_IRQ):

    struct irq_desc irq_desc[NR_IRQS] __cacheline_aligned_in_smp;

    每个 IRQ number 对应一个描述符,通过数组索引直接访问。简单高效,但若平台 NR_IRQS 很大而实际用到的很少,会造成内存浪费。

  2. Radix TreeCONFIG_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_data vs chip_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 抽象:通过对 ackmaskeoi 等函数的组合调用,实现对电平/边沿触发、是否需要 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_irqhandle_edge_irq 等 flow handler,决定中断处理的“骨架”
action 指向 struct irqaction 链表。支持共享中断时,链表中会有多个节点
istate 内部状态,包含 IRQS_PENDINGIRQS_REPLAYIRQS_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):

    1. 中断控制器的 map 回调中设置。

    2. 外设驱动解析 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_lock vs irq_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 号

原理

  1. probe_irq_on 遍历所有可探测的中断描述符,设置 IRQS_AUTODETECT | IRQS_WAITING,并使能它们。

  2. 等待 100ms,期间任何意外中断会清除 IRQS_WAITING

  3. 触发目标硬件中断。该中断的 flow handler 会清除 IRQS_WAITING

  4. 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_unhandledlast_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);
}

设计精髓分析

  1. 为什么是 ack 而不是 mask+ack?

    • 边沿触发是一次性的。如果不 ack,中断控制器无法检测下一个边沿。但如果另一个 CPU 正在处理(in progress),则必须 mask+ack + 设 pending,委托处理。

  2. 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);
}

设计精髓

  1. 为什么必须先 mask 再 ack?

    • 电平中断的 pending 状态由电平本身决定。若不 mask,ack 后只要外设电平不撤,中断控制器立刻重新触发中断给 CPU,导致“中断风暴”。mask 后,电平虽然还在,但控制器不转发,CPU 可以安心处理。

  2. 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,允许在非临界区抢占内核代码,显著缩短了任务响应时间。但仍存在不确定性:

  1. 临界区:自旋锁持有、preempt_disable 等区间仍不可抢占。

  2. 中断上下文:中断 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);

handlerthread_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;
}

步骤详解

  1. irq_settings_can_request(desc) 的校验: 某些中断描述符被标记为 IRQ_NOREQUEST(例如用于级联的 IRQ),驱动不能直接申请。irq_settings_is_per_cpu_devid 检查是否为 per-CPU 类型且需要 per-CPU dev_id,此时应使用 request_percpu_irq

  2. 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 外设。

  3. 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_bhspin_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, &param);
    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 总结:从申请到处理的全路径

  1. 申请阶段: 驱动调用 request_threaded_irq → 分配 irqaction__setup_irq 执行嵌套/强制线程化/共享检查、线程创建、挂入链表 → IRQ 准备就绪。

  2. 中断触发阶段: 硬件中断 → CPU 异常 → handle_arch_irq → 控制器 driver 读取 HW ID,映射 IRQ number → handle_IRQdesc->handle_irq(highlevel flow handler) → mask/ack/eoi → handle_irq_event 调用 primary handler → primary 返回 IRQ_WAKE_THREAD → 唤醒 irq/%d-%s 内核线程 → 执行 thread_fn → 完成处理。

  3. 释放阶段: 驱动调用 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 信号后,会执行以下硬件自动行为:

  1. 模式切换:切换到 IRQ 模式(ARMv7)或 EL1_IRQ 异常级别(ARMv8)。

  2. 保存返回地址:将被打断指令的下一条地址保存到 LR_irq 中(PC + 4),并复制 CPSR 到 SPSR_irq。

  3. 跳转到异常向量: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_irqirq_set_affinity 等 API。

  • 根据硬件特性选择合适的 flow handler 和标志位(如 IRQF_ONESHOT)。

  • 在多核及级联控制器场景下合理配置中断路由与优先级。

  • 在调试中断风暴、丢失、响应延迟等问题时有据可依。

中断子系统是 Linux 内核中最具挑战性也最为核心的模块之一,理解其运作机理,是构建稳定、高效嵌入式/服务器系统的基石。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐