4. 任务调度算法:时间片轮转调度、优先级抢占调度、协作式调度、调度点分析

调度算法,说白了就是RTOS的「大脑」。它决定哪个任务该跑,哪个任务该等。我做了这么多年嵌入式,见过太多系统崩溃,十有八九是调度策略没选对。

今天咱们就把这几种调度算法掰开揉碎讲清楚。你想想看,一个游戏机里同时要处理画面渲染、音频播放、手柄输入、网络通信……这些任务谁先谁后?调度算法就是干这个的。

4.1 时间片轮转调度(Round-Robin)

时间片轮转,我习惯叫它「轮流坐庄」。每个任务分到固定长度的时间片,时间到了就换下一个。公平,但不一定高效。

核心思想:所有同优先级任务,按FIFO顺序轮流执行固定时长。

我在做早期掌机项目时用过这个算法。当时有3个任务:画面刷新、音频缓冲、省电管理。每个任务分5ms时间片,大家轮流跑。简单,但有个问题——画面刷新任务经常跑不完就被切走了,导致屏幕闪烁。

时间片大小的选择是个艺术。太短了,上下文切换开销大;太长了,响应延迟高。我个人习惯是:

  • 时间片 = 系统心跳的 1~5 倍
  • 一般取 10~50ms
  • 游戏机里我常用 16.67ms(对应60fps的帧周期)
// 时间片轮转调度伪代码
void RR_Scheduler(void) {
    Task_t *current = GetCurrentTask();
    Task_t *next = GetNextTaskInReadyList(current);
    
    if (next != NULL) {
        // 保存当前任务上下文
        SaveContext(current);
        // 加载下一个任务上下文
        LoadContext(next);
        // 设置时间片定时器
        SetTimer(TIME_SLICE_MS);
    }
}

我的经验:时间片轮转适合所有任务重要性差不多的场景。游戏机的后台任务(日志、统计)用这个挺好,但关键任务别用。

4.2 优先级抢占调度(Priority-Preemptive)

这个才是游戏机系统的重头戏。高优先级任务可以随时打断低优先级任务。说白了,就是「重要的事先办」。

我记得有一次做街机主板,画面渲染任务优先级设低了,结果手柄输入任务一忙,画面就卡顿。后来把渲染任务提到最高优先级,问题立刻解决。

核心规则:

  • 高优先级任务就绪时,立即抢占低优先级任务
  • 同优先级任务之间,按时间片轮转
  • 优先级可以静态分配,也可以动态调整

优先级怎么定?我一般这么分:

优先级等级 典型任务 响应要求
最高(0-1) 中断处理、紧急事件 < 1ms
高(2-3) 画面渲染、音频输出 1-16ms
中(4-6) 手柄输入、网络通信 16-50ms
低(7-9) 存档、日志、统计 > 100ms

注意优先级反转:我曾经在一个项目中踩过这个坑。低优先级任务持有锁,高优先级任务等锁,中优先级任务趁机抢跑。结果高优先级任务被活活饿死。解决方案是优先级继承协议。

4.3 协作式调度(Cooperative)

协作式调度,也叫非抢占式调度。任务自己决定什么时候让出CPU。你想想看,这就像大家排队上厕所,前面的人不出来,后面的人就得等着。

这种调度方式在游戏机里用得不多,但有个场景特别合适——内存紧张的小型掌机。因为没有抢占,不需要保存那么多上下文,RAM占用少。

协作式调度的关键点:

  • 任务必须主动调用 yield() 或阻塞等待
  • 一个任务跑死,整个系统就挂了
  • 没有优先级概念,大家平等
// 协作式调度示例
void Task_GameLogic(void) {
    while(1) {
        UpdateGameState();  // 更新游戏逻辑
        task_yield();       // 主动让出CPU
    }
}

void Task_Render(void) {
    while(1) {
        RenderFrame();      // 渲染一帧
        task_yield();       // 主动让出CPU
    }
}

我的建议:除非你非常确定每个任务的执行时间,否则别用纯协作式。我见过有人把协作式和中断结合用,效果还不错——中断里做时间片管理,任务里协作运行。

4.4 调度点分析

调度点,就是系统检查「要不要切换任务」的时刻。这个知识点容易被忽略,但恰恰是调度的核心。

常见的调度点有:

  1. 时间片耗尽——定时器中断触发,检查是否需要切换
  2. 任务阻塞——任务调用 delay()、等待信号量、等待消息队列
  3. 任务唤醒——更高优先级的任务变为就绪状态
  4. 中断返回——中断处理完后,检查是否有更高优先级任务就绪
  5. 系统调用——调用 RTOS API 时,可能触发调度

我当年优化一个游戏引擎时,发现帧率不稳定。排查了好久,最后发现是调度点设置太频繁了。每次中断返回都做调度检查,导致上下文切换开销占了CPU的30%。

优化策略:

  • 减少不必要的调度点检查
  • 批量处理中断,减少中断返回次数
  • 使用「延迟调度」技术,把调度决策推迟到安全时刻

这里有个实用技巧——「调度点合并」。比如游戏机里,画面渲染任务每16.67ms跑一次。你可以把调度点对齐到这个周期,而不是每次系统心跳都检查。这样上下文切换次数能减少一半以上。

// 调度点优化示例
void SysTick_Handler(void) {
    static uint32_t tick_count = 0;
    tick_count++;
    
    // 每5个tick才做一次调度检查
    if (tick_count % 5 == 0) {
        ScheduleCheck();  // 检查是否需要切换任务
    }
    
    // 更新系统时间
    UpdateSystemTime();
}

避坑指南:我曾经把调度点间隔设得太大(100ms),结果手柄输入延迟高得离谱。玩家按了按键,角色要等100ms才有反应。嗯,这种体验谁受得了?调度点间隔要根据最紧急任务的响应需求来定。

最后总结一下我的经验:

  • 游戏机主控任务用优先级抢占,保证关键任务实时响应
  • 后台任务用时间片轮转,公平分配CPU
  • 内存受限场景可以考虑协作式,但要谨慎
  • 调度点要精心设计,别让上下文切换吃掉性能

实际项目中,我通常把三种调度方式混着用。比如FreeRTOS就支持混合调度策略——同优先级用时间片轮转,不同优先级用抢占式。你想想看,这不就是取长补短吗?