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里手动调用 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的优先级提高一点。发现电机控制抖动,那就把电机的中断优先级再往高调。
sloginfo 或 tracelogger 工具,可以实时查看中断响应时间和嵌套次数。我每次调优先级时,都会开着这两个工具看效果。
4.5 总结一下
中断优先级和抢占,说白了就是给系统里的各种紧急事件排个序。优先级决定了谁先跑,嵌套决定了能不能打断别人,抢占阈值则是个精细调节的旋钮。
我个人习惯是:先画规划表,再写代码,最后用工具验证。这样一套流程下来,基本不会出大问题。如果你在项目中遇到中断响应异常、任务丢步、数据丢失这类问题,不妨先检查一下优先级配置——八成是这里出了问题。
下一章,我们会聊中断延迟的测量和优化。到时候我会分享一个我实际用过的测量方法,保证让你对系统的实时性心里有数。