7、中断管理:中断亲和性、中断负载均衡、中断嵌套与抢占

中断管理这个话题,在多核系统里其实挺有意思的。单核时代你写个中断服务函数,闭着眼睛都能搞定。但到了多核,事情就变得微妙了——你得考虑这个中断该由哪个核来处理?多个核同时收到中断怎么办?高优先级中断能不能打断低优先级的?

我个人习惯把中断管理拆成三个维度来看:亲和性决定了中断往哪儿送,负载均衡决定了怎么送才合理,嵌套与抢占则决定了处理过程中的优先级秩序。咱们一个一个聊。

7.1 中断亲和性:把中断绑定到指定核心

什么叫中断亲和性?说白了,就是给每个中断源指定一个“目标CPU”。你可以让某个外设的中断只发给CPU0,另一个外设的中断只发给CPU1。

为什么要这么做?我举个例子。你在做网络协议栈,数据包收发的处理逻辑里大量使用了CPU0的L1缓存。如果网卡中断随机跑到CPU1上,那CPU1就得去把数据从CPU0的缓存里搬过来,或者干脆重新加载——这性能损耗可不小。

在RT-Thread SMP中,中断亲和性是通过中断控制器的寄存器来配置的。以ARM GIC(Generic Interrupt Controller)为例,每个中断都有一个ICPIDR寄存器,你往里面写一个位掩码,就能指定哪些CPU可以响应该中断。

核心思路:把频繁交互的中断和对应的处理线程绑定在同一个核上,能最大化利用缓存局部性。

代码层面,RT-Thread提供了这样的接口:

/* 设置中断亲和性:将IRQn绑定到指定的CPU掩码 */
void rt_hw_interrupt_set_affinity(rt_uint32_t irq, rt_uint32_t cpu_mask);

/* 示例:将UART中断只绑定到CPU0 */
rt_hw_interrupt_set_affinity(UART_IRQn, 1 << 0);

这里cpu_mask是个位图。bit0对应CPU0,bit1对应CPU1,以此类推。如果你想中断在两个核之间轮转,就设置成(1 << 0) | (1 << 1)。不过我个人建议,除非你有明确的负载均衡需求,否则尽量绑定到一个核上——省得缓存乒乓。

我的经验:曾经有个项目,两个核都在处理同一个网卡的中断,结果性能反而下降了30%。查了半天发现是缓存一致性协议在疯狂同步数据。改成亲和性绑定后,问题立刻解决。

7.2 中断负载均衡:别让一个核累死

中断亲和性是把中断固定住,那负载均衡就是反过来——让中断在多个核之间“雨露均沾”。

你想想看,如果所有外设的中断都往CPU0上送,CPU0忙得冒烟,CPU1却在旁边喝茶。这显然不是SMP的初衷。中断负载均衡要解决的就是这个问题。

RT-Thread SMP里,中断负载均衡有两种实现方式:

  • 静态均衡:系统初始化时,根据中断优先级和预期负载,手动分配每个中断的目标CPU。这种方式简单可控,但不够灵活。
  • 动态均衡:运行时根据每个CPU的当前负载情况,动态调整中断的路由。RT-Thread目前主要支持静态方式,但你可以通过注册自定义的均衡策略来实现动态效果。

我一般建议这样分配:

中断类型 推荐策略 原因
高频率中断(如定时器、DMA) 绑定到专用核 避免频繁核间切换
低频率中断(如按键、GPIO) 随机分配或轮转 负载轻,影响不大
关键实时中断(如看门狗) 固定到主核 确保响应确定性

嗯,这里要注意:动态均衡虽然听起来很美好,但实现起来有坑。比如你在一个核上正在处理中断A,结果负载均衡器把中断B也扔过来了——如果中断B的优先级更高,那就会发生抢占。这其实是我们下一个要聊的话题。

7.3 中断嵌套与抢占:优先级说了算

中断嵌套,就是高优先级中断打断低优先级中断的处理。单核时代这很常见,多核时代呢?

其实多核系统里,中断嵌套有两种场景:

  • 同核嵌套:同一个CPU上,高优先级中断打断低优先级中断。这和单核完全一样。
  • 跨核抢占:CPU0在处理中断A,CPU1收到了更高优先级的中断B。这时候CPU1可以正常处理B,但不会去打断CPU0——因为两个核是独立的。

所以多核中断嵌套的核心问题变成了:如何保证共享资源的互斥访问?

举个例子,两个中断服务函数都要操作同一个链表。如果它们跑在不同的核上,那就得加锁。但中断里加锁有个讲究——你不能用普通的信号量,因为中断里不能阻塞。RT-Thread提供了rt_spin_lockrt_spin_unlock,专门用于中断上下文:

static struct rt_spinlock lock;

void irq_handler_a(void)
{
    rt_spin_lock(&lock);
    /* 操作共享链表 */
    rt_spin_unlock(&lock);
}

void irq_handler_b(void)
{
    rt_spin_lock(&lock);
    /* 操作同一个共享链表 */
    rt_spin_unlock(&lock);
}

这里有个细节:rt_spin_lock在单核系统里其实就是关中断,但在多核系统里,它会先关掉当前核的中断,然后自旋等待另一个核释放锁。这样既防止了同核嵌套,也解决了跨核竞争。

我曾经踩过的坑:在中断里用了rt_mutex,结果系统直接死锁。因为mutex会导致任务切换,而中断上下文不允许切换。记住:中断里只能用自旋锁,不能用任何可能引起阻塞的同步机制。

关于中断抢占的优先级配置,RT-Thread SMP沿用了ARM GIC的优先级模型。每个中断有0~255的优先级,数值越小优先级越高。你可以在系统初始化时统一配置:

/* 设置中断优先级 */
rt_hw_interrupt_set_priority(IRQ_UART, 10);   /* 高优先级 */
rt_hw_interrupt_set_priority(IRQ_TIMER, 20);  /* 低优先级 */

当UART中断和Timer中断同时到达同一个核时,UART会先被处理。如果UART正在处理时Timer来了,UART不会被抢占——因为GIC默认不支持中断嵌套,除非你手动在ISR里重新使能中断。我个人不建议在ISR里开嵌套,除非你非常清楚自己在做什么。嵌套层级一深,栈空间和调试难度都会爆炸。

7.4 实践建议:中断设计的几个原则

讲了这么多,最后总结几条我在项目中沉淀下来的原则:

  1. 中断服务函数要短。能做的事尽量少做,把耗时操作推到线程里去。中断里只做最必要的硬件操作和事件通知。
  2. 善用亲和性。高频中断绑定到专用核,低频中断可以随意。别让所有中断都挤在一个核上。
  3. 小心共享资源。多核中断之间访问全局变量、链表、缓冲区时,一定要用自旋锁保护。别偷懒。
  4. 避免中断嵌套。除非你有硬实时需求,否则保持中断的简单线性处理。嵌套带来的复杂度往往大于收益。
  5. 测试负载均衡效果。用性能计数器观察每个核的中断次数和处理时间。如果某个核的中断处理时间占比超过70%,就该考虑重新分配了。

中断管理在多核系统里,其实就是一个权衡的艺术。你要在响应速度、负载均衡、代码复杂度之间找到平衡点。没有银弹,但理解了亲和性、均衡和嵌套这三个概念,你至少有了正确的思考框架。