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 的掩码没对齐。嗯,这种坑踩过一次就记住了。
3.2 中断亲和性:把中断“钉”在指定核上
什么叫亲和性?就是你可以告诉 GIC:“这个网卡的中断,只准发给 Core 0,别去烦 Core 1。” 这样做的好处很明显——减少核间切换,提高缓存命中率。
在 Linux 里,设置亲和性通常通过 /proc/irq/<irq_num>/smp_affinity 文件。写一个 bitmask 进去就行。比如只让 Core 0 处理,就写 1;让 Core 0 和 Core 1 处理,就写 3。
亲和性配置示例
# 查看中断号
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);
}
3.3 核间中断(IPI):让核与核“说悄悄话”
IPI,全称 Inter-Processor Interrupt。说白了就是一个核给另一个核发中断。多核系统里,IPI 的用途很广:比如 TLB 刷新、任务调度、核间同步。
在 ARM 上,IPI 是通过 GIC 的 SGI(Software Generated Interrupt)机制实现的。SGI 是 GIC 里的一种特殊中断,由软件触发,直接发给指定核。
IPI 驱动开发步骤
- 注册 IPI 中断处理函数:每个核都要注册,因为谁都有可能收到 IPI。
- 触发 IPI:通过写 GIC 的 SGI 寄存器,指定目标核和中断 ID。
- 处理 IPI:在中断处理函数里,根据 IPI 类型做相应操作。
Linux 内核里,IPI 已经封装好了。你不需要直接操作 GIC 寄存器。用 smp_call_function() 或 arch_send_call_function_single_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 驱动尽量用内核封装好的接口,别自己造轮子。记住,中断管理做得好,多核性能才能发挥出来。