任务调度策略:抢占式调度、时间片轮转与优先级反转

调度策略,说白了就是操作系统决定「下一个该谁跑」的规则。ThreadX 默认是抢占式调度,但实际项目中很少只用一种策略。我做过一个工业控制项目,电机控制和通信任务混在一起,光靠默认调度根本跑不顺。今天咱们就把抢占式、时间片轮转、优先级反转这几个核心问题掰开揉碎讲清楚。

抢占式调度原理

抢占式调度,核心就一句话:高优先级任务就绪时,立即打断当前任务。ThreadX 的调度器在以下时刻会检查是否需要抢占:

  • 任务调用 tx_thread_sleep() 主动挂起
  • 任务释放信号量、队列等同步对象
  • 外部中断触发,ISR 中调用了唤醒任务的 API
  • 时间片到期(如果启用了时间片轮转)

我刚开始用 ThreadX 时犯过一个低级错误:在 ISR 里调了 tx_thread_resume(),但忘了检查返回值。结果高优先级任务没被唤醒,低优先级任务一直跑,整个系统响应慢了 200ms。后来我养成了习惯——每次调用调度相关 API,都检查返回值。

抢占式调度的关键点:
  • 优先级数字越小,优先级越高(ThreadX 默认 0 为最高)
  • 抢占只发生在安全点(系统调用、中断返回时)
  • 同优先级任务默认按 FIFO 顺序执行

时间片轮转

同优先级任务如果一直占着 CPU 不放,其他同优先级任务就永远没机会跑。时间片轮转就是解决这个问题的——每个任务跑满一个时间片后,强制切换到下一个同优先级任务。

ThreadX 启用时间片轮转很简单,在创建任务时设置 time_slice 参数:

// 创建两个同优先级任务,各分配 5 个 tick 的时间片
tx_thread_create(&task1, "Task1", task1_entry, 0,
                 stack1, STACK_SIZE,
                 10, 10,          // 优先级 10,时间片 5 ticks
                 TX_NO_TIME_SLICE);

tx_thread_create(&task2, "Task2", task2_entry, 0,
                 stack2, STACK_SIZE,
                 10, 10,          // 同优先级 10,时间片 5 ticks
                 TX_NO_TIME_SLICE);

注意:time_slice 参数的单位是 ThreadX 的 tick。默认 tick 是 10ms,所以 5 个 tick 就是 50ms。我建议时间片不要设太短,否则上下文切换开销会吃掉 CPU。一般 5-20 个 tick 比较合理。

我的经验:时间片轮转只适用于同优先级任务。如果系统里优先级层次分明,其实用不上时间片。我一般只在调试阶段临时开时间片,用来观察多个任务是否都能得到执行机会。

优先级反转问题

优先级反转,是实时系统里最隐蔽的坑之一。简单说就是:高优先级任务被低优先级任务间接阻塞,而中优先级任务趁机抢跑。

典型的场景是这样的:

  1. 任务 L(低优先级)拿到信号量,开始访问共享资源
  2. 任务 H(高优先级)就绪,抢占 L,但发现信号量被 L 持有,于是挂起等待
  3. 任务 M(中优先级)就绪,它不需要信号量,于是抢跑执行
  4. 任务 L 因为被 M 抢占,一直无法释放信号量
  5. 任务 H 只能干等,直到 M 跑完,L 才能释放信号量

结果就是:最高优先级的任务 H,实际执行时间被中优先级任务 M 拖长了。这在实时系统里可能是致命的——比如电机控制任务错过控制周期,导致抖动甚至失控。

我曾经踩过的坑:在一个无人机飞控项目里,姿态解算任务(高优先级)偶尔会延迟 3-5ms,导致飞控不稳。查了两天才发现是通信任务(中优先级)在大量收发数据,把持有共享缓冲区的低优先级日志任务堵住了。最终靠优先级继承协议解决了问题。

优先级继承协议

优先级继承,是 ThreadX 内置的解决方案。它的原理很简单:当高优先级任务等待低优先级任务持有的资源时,低优先级任务临时继承高优先级任务的优先级。这样中优先级任务就无法抢占低优先级任务了,高优先级任务就能尽快拿到资源。

ThreadX 的互斥量(Mutex)默认支持优先级继承。使用方式:

TX_MUTEX mutex;

// 创建互斥量,启用优先级继承
tx_mutex_create(&mutex, "ProtectMutex", TX_INHERIT);

// 任务 L 获取互斥量
tx_mutex_get(&mutex, TX_WAIT_FOREVER);
// ... 访问共享资源 ...
tx_mutex_put(&mutex);

// 任务 H 获取互斥量(如果 L 持有,L 会临时继承 H 的优先级)
tx_mutex_get(&mutex, TX_WAIT_FOREVER);

创建互斥量时,第二个参数传 TX_INHERIT 就开启了优先级继承。如果传 TX_NO_INHERIT,就是普通互斥量,不处理优先级反转。

优先级继承 vs 优先级天花板:
特性 优先级继承 优先级天花板
原理 动态提升持有者的优先级 静态设定资源的最高优先级
实现复杂度 中等(需要跟踪等待关系) 低(只需设定一个值)
死锁风险 可能发生(需配合死锁检测) 较低
ThreadX 支持 ✅ 默认支持 ❌ 不直接支持

我个人更推荐优先级继承,因为它更灵活。但要注意:优先级继承不能完全避免死锁。如果两个任务互相等待对方持有的互斥量,还是会死锁。ThreadX 提供了 tx_mutex_get() 的超时参数,可以用来检测死锁——超时返回 TX_NOT_AVAILABLE。

调度策略对比总结

调度策略 适用场景 注意事项
纯抢占式 优先级层次分明的系统 注意优先级反转
时间片轮转 同优先级任务需要公平执行 时间片不宜过短
优先级继承 共享资源访问频繁的系统 注意死锁检测

核心知识结构图

下面这张图展示了 ThreadX 调度策略的核心关系:

ThreadX 任务调度策略核心结构 ThreadX 调度器 抢占式调度 时间片轮转 优先级继承协议 高优先级立即执行 同优先级 FIFO 顺序 每个任务固定时间片 到期强制切换 临时提升持有者优先级 防止中优先级任务抢占 ⚠️ 关键原则 抢占式调度保证实时性,时间片轮转保证公平性,优先级继承解决优先级反转

嗯,调度策略这块内容不少,但核心就三条:抢占保证实时、时间片保证公平、优先级继承解决反转。实际项目中,我建议先画清楚任务的优先级关系,再决定哪些任务用时间片,哪些资源用互斥量。别一上来就全开,容易出问题。