4、中断优先级与抢占:中断优先级配置、中断嵌套、抢占阈值设置

好,咱们接着聊中断。前面几章我们把中断的基本概念、向量表、ISR写法都过了一遍。这一章要聊的,是真正决定系统实时性上限的东西——中断优先级、嵌套,还有那个很多人容易忽略的抢占阈值。

说实话,我早年做QNX项目时,在优先级配置上栽过跟头。当时一个电机控制任务总是丢步,查了三天,最后发现是中断优先级配反了,低优先级的中断把高优先级的给堵死了。嗯,从那以后,我对这块就特别较真。

4.1 中断优先级:谁先跑,谁后跑

QNX的中断优先级,说白了就是给每个中断源排个队。硬件上,中断控制器(比如ARM GIC或x86 APIC)支持多个优先级。QNN Neutrino实时系统里,中断优先级范围通常是0到255,数字越小优先级越高。

你可能会问:那线程优先级呢?线程优先级是0到255,数字越大优先级越高。这两个方向是反的,千万别搞混了。我刚开始也经常记反,后来干脆在代码注释里写死:「中断优先级:0最高;线程优先级:255最高」。

核心原则: 中断优先级高于所有线程优先级。哪怕是最低优先级的中断,也能抢占最高优先级的线程。

配置中断优先级,通常是在系统启动时或驱动初始化阶段完成的。QNX提供了 InterruptAttach()InterruptAttachEvent() 两个函数,它们都支持指定优先级。

// 示例:绑定中断并设置优先级
int irq = 42;  // 假设是某个外设的中断号
int priority = 10;  // 高优先级

struct sigevent event;
SIGEV_PULSE_INIT(&event, my_chid, SIGEV_PULSE_PRIO_INHERIT, my_coid, 0);

int id = InterruptAttachEvent(irq, &event, _NTO_INTR_FLAGS_TRK_MSK, priority, 0);
if (id == -1) {
    // 处理错误
}

这里有个细节:_NTO_INTR_FLAGS_TRK_MSK 这个标志位,我建议你加上。它能让系统跟踪中断屏蔽状态,避免一些诡异的竞态问题。我在一个多核项目里遇到过不加这个标志导致的中断丢失,加了就稳了。

4.2 中断嵌套:高优先级打断低优先级

中断嵌套,就是高优先级的中断可以打断正在执行的低优先级中断ISR。QNX默认是支持嵌套的,但需要你显式地开启。

为什么默认不开启?因为嵌套会带来额外的栈开销和复杂度。如果你的ISR很短(比如几十微秒),其实没必要嵌套。但如果你有一个耗时较长的ISR(比如处理网络包),又不希望被其他中断阻塞太久,那就得开嵌套。

我的建议: ISR尽量短。如果必须做耗时操作,考虑用脉冲(pulse)或信号量把工作推到线程里做。中断里只做最紧急的事。

开启中断嵌套,需要在ISR里手动调用 InterruptEnable()。默认情况下,进入ISR时中断是关闭的。

const struct sigevent *my_isr(void *area, int id) {
    // 刚进来,中断是关的
    // 做一些紧急处理...

    // 开启中断,允许嵌套
    InterruptEnable();

    // 现在可以被打断了
    // 做一些不那么紧急的事...

    // 返回前,中断会自动恢复为关闭状态
    return NULL;
}

注意:InterruptEnable() 只影响当前CPU核。在多核系统里,其他核的中断不受影响。我记得有一次调试,发现一个核开了嵌套,另一个核没开,结果两个核的中断响应时间差了好几倍。排查了半天才发现是这个问题。

4.3 抢占阈值:精细控制谁可以抢谁

抢占阈值(Preemption Threshold)这个概念,很多人没听过。它其实是介于中断优先级和线程优先级之间的一个控制层。

简单说:你给一个中断设置一个「阈值优先级」。只有优先级高于这个阈值的中断,才能抢占当前ISR。优先级低于阈值的中断,得等当前ISR跑完才能执行。

为什么要这么干?你想想看,如果所有中断都能嵌套,那优先级低的中断可能永远得不到执行——被高优先级的中断反复抢占。这就是所谓的「中断饿死」。

我曾经踩过的坑: 在一个数据采集系统里,我把所有中断都设成了最高优先级,想着这样响应最快。结果低优先级的中断(比如按键)几乎不响应,因为每次它刚要执行,就被高优先级的数据采集中断抢走了。后来加了抢占阈值,才解决了这个问题。

QNX里设置抢占阈值,是通过 InterruptAttach()flags 参数实现的。具体来说,用 _NTO_INTR_FLAGS_PREEMPT_THRESHOLD 标志位。

// 设置抢占阈值
int id = InterruptAttach(irq, my_isr, NULL, size, 
                         _NTO_INTR_FLAGS_PREEMPT_THRESHOLD | 
                         _NTO_INTR_FLAGS_TRK_MSK, 
                         priority, threshold);
// priority: 中断优先级(0最高)
// threshold: 抢占阈值(只有优先级高于此值的中断才能抢占)

这里 threshold 的值怎么设?我一般遵循这个原则:

  • 关键中断(比如心跳、看门狗):优先级设0,阈值设0(不允许任何中断抢占)
  • 普通中断(比如UART、GPIO):优先级设10-20,阈值设5-10
  • 低优先级中断(比如按键、温度传感器):优先级设50以上,阈值设30左右

这样配置,关键中断不会被任何东西打断。普通中断可以被更关键的中断打断,但不会被低优先级的中断打扰。低优先级中断嘛,等大家都忙完了再处理也不迟。

4.4 实战中的优先级规划表

我习惯在项目开始前,先画一张中断优先级规划表。这样后面写代码时直接照着填,不容易出错。

中断源 优先级 抢占阈值 是否允许嵌套 说明
看门狗 0 0 必须立即响应,不能被任何中断打断
电机编码器 5 3 高精度控制,但允许看门狗打断
CAN总线接收 10 5 实时通信,但优先级低于电机
UART接收 30 20 数据接收,不要求极低延迟
GPIO按键 100 50 人机交互,延迟几十毫秒也能接受

这张表不是死的。实际调试时,你可能需要根据系统表现微调。比如发现CAN总线丢包了,那就把CAN的优先级提高一点。发现电机控制抖动,那就把电机的中断优先级再往高调。

小技巧: 用QNX的 sloginfotracelogger 工具,可以实时查看中断响应时间和嵌套次数。我每次调优先级时,都会开着这两个工具看效果。

4.5 总结一下

中断优先级和抢占,说白了就是给系统里的各种紧急事件排个序。优先级决定了谁先跑,嵌套决定了能不能打断别人,抢占阈值则是个精细调节的旋钮。

我个人习惯是:先画规划表,再写代码,最后用工具验证。这样一套流程下来,基本不会出大问题。如果你在项目中遇到中断响应异常、任务丢步、数据丢失这类问题,不妨先检查一下优先级配置——八成是这里出了问题。

下一章,我们会聊中断延迟的测量和优化。到时候我会分享一个我实际用过的测量方法,保证让你对系统的实时性心里有数。