4、中断共享与负载均衡:多核中断分发、中断亲和性设置、中断负载均衡策略

好,咱们接着聊中断。前面几章我们把中断的基本机制、ISR怎么写、DMA怎么配合都讲透了。但有个问题我一直憋着没说——在多核系统里,中断到底该往哪个核上送?

你想想看,如果所有中断都往CPU0上砸,那其他核闲着,CPU0忙死。这跟公司里活都让一个人干有什么区别?所以,中断的负载均衡,说白了就是让每个核都干点活,别让一个核累趴下。

4.1 多核中断分发机制

QNX的中断分发,底层依赖硬件的中断控制器。ARM的GIC、x86的IOAPIC,各有各的玩法。但QNX在软件层面做了统一抽象。

我个人习惯把中断分发分为三种模式:

  • 固定分发:中断永远发给指定CPU。适合某些对延迟极度敏感的外设。
  • 轮询分发:中断轮流发给各个CPU。简单粗暴,但效果还行。
  • 动态分发:根据当前CPU负载,把中断发给最闲的那个核。这个最智能,但开销也最大。

嗯,这里要注意:QNX默认用的是固定分发。所有中断默认都发到CPU0。你如果不做任何配置,那就是CPU0扛下所有。

警告: 我在项目中遇到过,一个4核系统,CPU0的中断响应延迟飙到200微秒以上,其他三个核闲得发慌。查了半天,就是中断分发没配。所以,多核系统一定要主动配置中断亲和性。

4.2 中断亲和性设置

中断亲和性,就是告诉中断控制器:这个中断,你发给哪个核(或哪几个核)。

在QNX里,设置中断亲和性主要通过 InterruptAttach()InterruptCpu() 这两个函数。

先看个例子:

#include <sys/neutrino.h>
#include <inttypes.h>

int attach_interrupt_to_cpu(int intr, int cpu_mask) {
    int rc;
    struct sigevent event;
    int id;

    // 设置事件
    SIGEV_INTR_INIT(&event);

    // 绑定中断
    id = InterruptAttach(intr, &event, NULL, 0, 0, 0);
    if (id == -1) {
        return -1;
    }

    // 设置亲和性:只允许在指定的CPU上触发
    rc = InterruptCpu(id, cpu_mask, NULL);
    if (rc != EOK) {
        InterruptDetach(id);
        return -1;
    }

    return id;
}

// 使用示例:把中断17绑定到CPU2和CPU3
int id = attach_interrupt_to_cpu(17, (1 << 2) | (1 << 3));

这里 cpu_mask 是个位掩码。bit0对应CPU0,bit1对应CPU1,以此类推。你想让中断发给哪个核,就把对应的bit置1。

小技巧: 我曾经调试过一个网卡驱动,发现中断总是被CPU0处理,导致网络吞吐量上不去。后来我把中断亲和性设成了CPU1和CPU2,吞吐量直接翻倍。说白了,中断亲和性设置得好,性能提升立竿见影。

4.3 中断负载均衡策略

光设置亲和性还不够。你想想看,如果每个中断都固定绑死在一个核上,那负载还是不均衡。比如CPU0绑了10个中断,CPU1只绑了1个,那CPU0还是累。

所以,我们需要一个策略来动态调整中断的分配。QNX提供了几种思路:

4.3.1 手动均衡

你自己写个监控线程,定期检查每个CPU的中断处理次数。如果发现某个核太忙,就调用 InterruptCpu() 把部分中断挪到其他核上。

代码大概长这样:

void* load_balancer(void* arg) {
    while (1) {
        // 获取每个CPU的中断计数
        // 判断哪个核负载过高
        // 把部分中断迁移到空闲核
        // 睡个几秒再检查
        sleep(5);
    }
    return NULL;
}

这个方法灵活,但需要你自己写逻辑。我早期做的一个项目就是这么干的,效果还行,就是代码量大了点。

3.2 使用QNX的自动均衡

QNX其实内置了一个中断负载均衡机制。你只需要在构建系统时启用 procnto-smp-instr 这个内核,然后配置一下 /proc/boot/startup.cmd 里的参数。

举个例子:

# 在startup.cmd中启用中断负载均衡
set irq_balancer=1
set irq_balancer_interval=100  # 每100毫秒重新分配一次

启用后,内核会自动把中断分散到各个CPU上。我个人觉得,大部分场景用这个就够了,省心。

核心要点: 自动均衡适合通用场景,手动均衡适合对延迟有特殊要求的场景。比如音频驱动,你可能希望中断永远在同一个核上处理,避免缓存抖动。这时候手动绑定反而更好。

4.3.3 混合策略

我实际项目中用得最多的,其实是混合策略。把关键中断手动绑定到特定核,其他中断交给自动均衡去处理。

比如:

  • 网卡中断:绑定到CPU1和CPU2(手动)
  • 磁盘中断:绑定到CPU3(手动)
  • 其他外设中断:交给自动均衡(自动)

这样既保证了关键业务的延迟可控,又让其他中断不会扎堆。

4.4 避坑指南

讲几个我踩过的坑:

  • 中断风暴:我曾经把一个高频中断绑到了单个核上,结果那个核的中断响应延迟飙升到毫秒级。后来才发现,中断频率太高,单个核处理不过来。解决方案是把中断分散到多个核上。
  • 缓存亲和性:如果你把中断和对应的数据处理线程绑在不同的核上,那缓存命中率会很低。我建议把中断和数据处理线程绑在同一个核上,或者至少绑在共享L2缓存的核上。
  • 不要过度均衡:中断迁移本身有开销。如果每100微秒就重新分配一次,那系统资源都花在迁移上了,得不偿失。我一般把均衡间隔设在50-200毫秒之间。

重要提醒: 中断负载均衡不是银弹。如果你的系统中断数量很少(比如只有两三个),那手动绑定比自动均衡更高效。别为了用而用。

4.5 实战建议

最后,给各位几个实操建议:

  1. 先测量,再优化:用 toppidin 看看每个CPU的中断处理时间占比。哪个核的 intr 时间高,就优先处理哪个核。
  2. 从简单开始:先试试QNX的自动均衡,不行再上手写手动均衡。别一上来就搞复杂方案。
  3. 留好调试接口:我习惯在驱动里加个procfs接口,运行时可以动态查看和修改中断亲和性。这样调试起来方便很多。

嗯,中断负载均衡这块,说白了就是让每个CPU都别闲着,也别太累。把握好这个度,你的系统性能就能上一个台阶。下一章我们聊聊中断调试的那些破事,保证都是实战干货。