第三章 实时系统基础:RTOS与Linux PREEMPT_RT对比、中断延迟、调度延迟、优先级反转

各位好,我是老张。今天咱们聊聊实时系统的基础。说实话,很多做座舱开发的工程师,一上来就怼着应用层写代码,结果系统一卡一卡的,根本不知道问题出在哪。我当年刚入行时也犯过这毛病。

实时系统,说白了就是「在规定时间内必须完成指定任务」的系统。座舱里,你按一下方向盘上的接听键,如果等半秒才响应,那体验就完蛋了。嗯,这里要注意,实时 ≠ 快速,而是「可预测的响应时间」。

3.1 RTOS vs Linux PREEMPT_RT:两种实时方案

MTK8678 这个平台很有意思。它既有 Cortex-A78 大核跑 Linux,又有 Cortex-M4 核跑 RTOS。我习惯把这种架构叫「大小核混搭」。为什么会这样?因为两种方案各有千秋。

对比项 传统RTOS(如FreeRTOS) Linux PREEMPT_RT
中断延迟 微秒级(1-10μs) 几十微秒级(10-100μs)
调度延迟 确定性强,抖动小 抖动较大,但可优化
优先级反转 需手动处理 有优先级继承机制
生态与驱动 弱,需自己写 强,Linux驱动丰富
典型应用 传感器、电机控制 多媒体、HMI、网络

核心观点:在MTK8678上,我建议把硬实时任务(如CAN总线读取、按键消抖)放在M4核的RTOS上,把软实时任务(如导航渲染、语音识别)放在A78核的PREEMPT_RT Linux上。各司其职,别混着用。

3.2 中断延迟:为什么你的按键有时不灵?

中断延迟,就是从硬件产生中断信号,到CPU开始执行中断服务程序(ISR)之间的时间差。我在项目中遇到过,某次客户反馈说「方向盘上的音量键偶尔没反应」。查了半天,发现是Linux内核在关中断做内存拷贝,导致按键中断被延迟了200多微秒。

中断延迟的组成,你想想看:

  • 硬件延迟:信号从外设传到CPU的时间,一般固定
  • CPU响应延迟:CPU完成当前指令,保存上下文的时间
  • 软件延迟:内核关中断的时间(这是大头!)

避坑指南:我曾经在PREEMPT_RT上调试时,发现一个驱动里用了 spin_lock_irqsave(),这个函数会关本地中断。如果锁持有时间过长,中断延迟就会飙升。后来我改成 raw_spin_lock() + 中断线程化,延迟从80μs降到了15μs。

3.3 调度延迟:系统卡顿的元凶

调度延迟,指的是一个高优先级任务就绪后,到它真正获得CPU执行的时间。说白了,就是「排队等CPU的时间」。

在RTOS里,调度器是抢占式的。高优先级任务一就绪,立马抢CPU。但在Linux PREEMPT_RT里,情况复杂一些:

  • 即使打了PREEMPT_RT补丁,内核仍然有不可抢占的临界区
  • 中断处理函数默认优先级高于任何用户态任务
  • 如果中断处理写得不好,会阻塞所有用户态实时任务

注意:在MTK8678上,GPU驱动和显示驱动的中断处理函数特别容易「拖后腿」。我建议把关键的中断处理函数线程化,用chrt命令设置实时优先级。比如:

# 查看当前中断线程的优先级
ps -eo pid,pri,rtprio,cmd | grep irq

# 设置显示中断线程为实时优先级99
chrt -f -p 99 $(pgrep -f "irq/.*display")

3.4 优先级反转:一个经典坑

优先级反转,就是低优先级任务占着资源不放,导致高优先级任务被阻塞。我当年第一次遇到这个问题时,debug了整整三天。

举个例子:

  1. 任务A(高优先级)和任务C(低优先级)共享一个互斥锁
  2. 任务C先拿到锁,然后被任务B(中优先级)抢占了CPU
  3. 任务B一直运行,任务C无法释放锁
  4. 任务A虽然优先级最高,但拿不到锁,只能干等

结果就是:高优先级任务被中优先级任务「间接阻塞」了。在座舱里,这会导致触摸响应被后台音乐解码任务拖死。

解决方案:

  • RTOS方案:使用优先级继承协议(PIP)。当高优先级任务被阻塞时,临时提升持有锁的低优先级任务的优先级。
  • Linux PREEMPT_RT方案:使用实时互斥锁(rt_mutex),它自带优先级继承机制。
// Linux PREEMPT_RT 中使用实时互斥锁
#include <linux/rtmutex.h>

struct rt_mutex my_lock;
rt_mutex_init(&my_lock);

// 在实时任务中
rt_mutex_lock(&my_lock);
// 临界区代码
rt_mutex_unlock(&my_lock);

个人经验:在MTK8678的M4核上跑FreeRTOS时,我习惯把所有共享资源的任务优先级都设计成「同一优先级」,然后用时间片轮转。这样虽然牺牲了一点实时性,但彻底避免了优先级反转。嗯,有时候「简单粗暴」反而更可靠。

3.5 实战建议:MTK8678上的实时调优

最后,我总结几条在MTK8678上做实时调优的实战建议:

  • 中断负载均衡:把不同外设的中断绑定到不同CPU核上,避免一个核被中断淹没。用 echo 2 > /proc/irq/xxx/smp_affinity 设置亲和性
  • 关中断时间监控:用 ftrace 的 irqsoff tracer 监控关中断时间。我一般要求关中断时间不超过50μs
  • 实时任务隔离:把关键实时任务绑到某个专用核上,用 isolcpus 内核参数隔离
  • 避免动态内存分配:在实时任务中,别用 malloc/free。用预分配的内存池

好了,这一章就聊到这儿。实时系统调优是个细活,需要耐心和工具。下一章咱们会深入讲「中断线程化与优先级继承」的具体实现,到时候我会带大家看MTK8678上的实际代码。