3、多核中断管理:中断控制器(GIC)原理、中断亲和性设置、核间中断(IPI)驱动开发

中断管理,说白了就是多核系统的“交通指挥中心”。你想想看,四个核同时跑,外设一窝蜂地发中断,谁来接?怎么接?接完怎么通知其他核?这些全得靠中断控制器(GIC)来调度。

我个人习惯把 GIC 看作一个“智能分拣器”。它不光要收中断,还得知道哪个中断该扔给哪个核去处理。今天我们就从 GIC 的架构讲起,一路聊到亲和性设置和 IPI 驱动怎么写。

3.1 GIC 架构:中断的“总枢纽”

ARM 多核系统里,GIC 是标配。目前主流的是 GICv2 和 GICv3。GICv2 最多支持 8 个核,GICv3 能支持上千个。咱们做嵌入式,GICv2 见得最多。

GIC 内部主要分两大块:Distributor(分发器)和CPU Interface(CPU 接口)。

  • Distributor:负责收中断、配优先级、决定把中断发给哪个核。
  • CPU Interface:每个核独享一个,负责把中断真正“怼”到核上。

我在项目中遇到过一个问题:某个外设的中断老是丢。查了半天,发现是 Distributor 配的优先级和 CPU Interface 的掩码没对齐。嗯,这种坑踩过一次就记住了。

核心要点:GIC 的 Distributor 做“路由决策”,CPU Interface 做“最终递送”。两者配置必须一致,否则中断会“迷路”。
GIC 架构与中断路由流程 外设中断源 外设中断源 外设中断源 Distributor 优先级管理 中断路由决策 亲和性配置 CPU Interface 0 → Core 0 CPU Interface 1 → Core 1 CPU Interface 2 → Core 2 说明: 1. 外设中断先汇聚到 Distributor,由它决定优先级和目标核。 2. Distributor 将中断转发给对应核的 CPU Interface。 3. CPU Interface 负责将中断信号递送给 CPU 核,并处理应答和结束信号。

3.2 中断亲和性:把中断“钉”在指定核上

什么叫亲和性?就是你可以告诉 GIC:“这个网卡的中断,只准发给 Core 0,别去烦 Core 1。” 这样做的好处很明显——减少核间切换,提高缓存命中率。

在 Linux 里,设置亲和性通常通过 /proc/irq/<irq_num>/smp_affinity 文件。写一个 bitmask 进去就行。比如只让 Core 0 处理,就写 1;让 Core 0 和 Core 1 处理,就写 3。

小技巧:我习惯在系统启动脚本里就把关键中断的亲和性配好。比如网卡中断绑在 Core 0,存储中断绑在 Core 1。这样能避免中断在核间“乱窜”,性能提升很明显。

亲和性配置示例

# 查看中断号
cat /proc/interrupts | grep eth0

# 设置亲和性(假设中断号为 47,绑定到 Core 0)
echo 1 > /proc/irq/47/smp_affinity

# 绑定到 Core 0 和 Core 2(bitmask = 101b = 5)
echo 5 > /proc/irq/47/smp_affinity

驱动里怎么配?通过 irq_set_affinity() 函数。我一般在 probe() 函数里就把亲和性设好,省得后面再调。

#include <linux/irq.h>

int my_dev_probe(struct platform_device *pdev)
{
    int irq = platform_get_irq(pdev, 0);
    struct cpumask mask;

    // 只绑定到 Core 0
    cpumask_clear(&mask);
    cpumask_set_cpu(0, &mask);
    irq_set_affinity(irq, &mask);

    return request_irq(irq, my_isr, 0, "my_dev", dev);
}
注意:亲和性设置不是万能的。如果某个核已经忙到冒烟,你还把中断全绑上去,那反而会拖垮系统。我曾经在一个项目里把四个网卡中断全绑在 Core 0 上,结果 Core 0 负载 100%,其他核闲得发慌。后来改成负载均衡模式,问题才解决。

3.3 核间中断(IPI):让核与核“说悄悄话”

IPI,全称 Inter-Processor Interrupt。说白了就是一个核给另一个核发中断。多核系统里,IPI 的用途很广:比如 TLB 刷新、任务调度、核间同步。

在 ARM 上,IPI 是通过 GIC 的 SGI(Software Generated Interrupt)机制实现的。SGI 是 GIC 里的一种特殊中断,由软件触发,直接发给指定核。

IPI 驱动开发步骤

  1. 注册 IPI 中断处理函数:每个核都要注册,因为谁都有可能收到 IPI。
  2. 触发 IPI:通过写 GIC 的 SGI 寄存器,指定目标核和中断 ID。
  3. 处理 IPI:在中断处理函数里,根据 IPI 类型做相应操作。

Linux 内核里,IPI 已经封装好了。你不需要直接操作 GIC 寄存器。用 smp_call_function() 或 arch_send_call_function_single_ipi() 就行。

关键点:IPI 处理函数里不能做太多事。它通常只设置一个标志位,然后唤醒一个工作队列或 tasklet。因为 IPI 是在中断上下文跑的,时间长了会影响系统响应。

IPI 驱动示例(伪代码)

// 定义 IPI 中断号(通常由平台定义)
#define IPI_IRQ 29

// IPI 中断处理函数
static irqreturn_t ipi_handler(int irq, void *dev_id)
{
    // 读取 IPI 类型寄存器
    u32 ipi_type = readl(ipi_status_reg);

    switch (ipi_type) {
    case IPI_RESCHEDULE:
        // 重新调度标志
        set_need_resched();
        break;
    case IPI_CALL_FUNC:
        // 执行回调函数
        generic_smp_call_function_interrupt();
        break;
    default:
        break;
    }

    // 清除 IPI 状态
    writel(ipi_type, ipi_status_reg);

    return IRQ_HANDLED;
}

// 初始化 IPI
void ipi_init(void)
{
    int ret;

    // 每个核都要注册
    ret = request_percpu_irq(IPI_IRQ, ipi_handler, "my_ipi", &ipi_data);
    if (ret) {
        pr_err("Failed to register IPI IRQ\n");
        return;
    }

    // 使能 IPI 中断
    enable_percpu_irq(IPI_IRQ, 0);
}

// 发送 IPI 给 Core 1
void send_ipi_to_core1(void)
{
    // 使用 Linux 封装函数
    smp_call_function_single(1, my_ipi_callback, NULL, 0);
}

3.4 避坑指南:我踩过的那些坑

问题 现象 原因 解决方法
中断丢失 外设触发中断,但 CPU 没响应 GIC 优先级配置错误,或 CPU Interface 掩码没开 检查 GICD_IPRIORITYR 和 GICC_PMR 寄存器
中断风暴 CPU 一直进中断,系统卡死 中断处理函数没清中断标志,或电平触发中断没处理完 确保 ISR 里清中断源,并考虑用线程化中断
IPI 死锁 两个核互相发 IPI,谁也不让谁 IPI 处理函数里拿了锁,而发送方也在等同一把锁 IPI 处理函数里尽量不用锁,或用 spin_lock 的变体
亲和性不生效 设置了亲和性,中断还是跑到其他核 中断控制器不支持亲和性,或驱动没调用 irq_set_affinity 确认 GIC 版本,检查驱动是否实现了 set_affinity 回调

我曾经在一个项目里,IPI 死锁搞了整整两天。最后发现是 IPI 处理函数里调了一个需要睡眠的函数。嗯,中断上下文里不能睡眠,这个教训太深刻了。

3.5 小结

多核中断管理,核心就三件事:GIC 怎么分中断、中断绑在哪个核、核之间怎么通信。GIC 的 Distributor 和 CPU Interface 是硬件基础,亲和性设置是软件策略,IPI 是核间协作的“信使”。

实际开发中,我建议你先用 /proc/interrupts 看看中断分布,再根据负载情况调整亲和性。IPI 驱动尽量用内核封装好的接口,别自己造轮子。记住,中断管理做得好,多核性能才能发挥出来。