3、任务调度算法:时间片轮转、优先级抢占、合作式调度

好,咱们今天聊聊任务调度。这是RTOS的核心中的核心。

你想想看,一个单片机就一个核,但你要让它同时处理电机控制、传感器读取、通信协议……怎么做到的?说白了,就是靠调度器在背后「翻牌子」。它决定哪个任务先跑,哪个任务等一等。

我个人习惯把调度算法分成三大类:时间片轮转、优先级抢占、合作式调度。这三种我都用过,各有各的脾气。咱们一个一个说。

3.1 时间片轮转调度

这个最直观。就像几个人轮流用一台电脑,每人用固定时长,时间一到就换人。

每个任务分配一个固定的时间片,比如5毫秒。任务A跑5ms,挂起;任务B跑5ms,挂起;然后回到任务A。周而复始。

核心特点:

  • 所有任务地位平等,没有优先级之分
  • 每个任务都能得到CPU时间,不会饿死
  • 时间片大小直接影响系统响应速度

我在项目中遇到过一个问题:时间片设得太小,比如1ms,结果任务切换开销占了CPU的30%以上。设得太大,比如100ms,按键按下去要等100ms才有反应,用户体验极差。

我的经验:时间片一般设在5-20ms之间比较稳妥。如果任务对响应要求高,就别用纯时间片轮转了。

代码实现其实不复杂。核心就是一个定时器中断,每次中断触发就切换到下一个任务:

// 时间片轮转调度核心
void scheduler_tick(void) {
    // 保存当前任务上下文
    save_context(current_task);
    
    // 切换到下一个任务
    current_task = (current_task + 1) % MAX_TASKS;
    
    // 恢复新任务上下文
    restore_context(current_task);
}

嗯,这里要注意:时间片轮转不解决「紧急任务」的问题。如果电机过流了,你还让它等5ms?那电机可能就烧了。

3.2 优先级抢占调度

这个就聪明多了。每个任务有个优先级,高优先级的任务可以随时打断低优先级的任务。

你想想看,电机过流检测任务的优先级设为最高,一旦触发,立刻打断正在运行的LED闪烁任务。这才是实时系统该有的样子。

核心特点:

  • 任务有明确的优先级等级
  • 高优先级任务可以抢占低优先级任务
  • 系统响应时间可预测,适合硬实时场景

我曾经踩过一个坑:把通信任务优先级设得太高,结果电机控制任务被频繁打断,导致电机运行不平顺。后来我学乖了——电机控制任务的优先级一定要高于通信任务。

避坑指南:我曾经把两个任务的优先级设成一样,结果系统行为变得不可预测。记住:同优先级任务用时间片轮转,不同优先级才用抢占。别混为一谈。

优先级抢占的调度逻辑是这样的:

// 优先级抢占调度核心
void scheduler_preemptive(void) {
    task_t *highest = NULL;
    
    // 找到就绪态中优先级最高的任务
    for (int i = 0; i < MAX_TASKS; i++) {
        if (task_ready[i] && 
            (highest == NULL || task_priority[i] > highest->priority)) {
            highest = &tasks[i];
        }
    }
    
    // 如果当前任务不是最高优先级,就切换
    if (current_task != highest) {
        save_context(current_task);
        current_task = highest;
        restore_context(current_task);
    }
}

这里有个经典问题:优先级反转。低优先级任务拿着高优先级任务需要的资源,结果高优先级任务被阻塞。嗯,这个问题在后面的章节会专门讲。

3.3 合作式调度

这个最古老,但也最简单。任务自己决定什么时候让出CPU。

说白了,就是每个任务主动调用一个yield()函数,告诉调度器:「我干完了,你让别人跑吧。」

核心特点:

  • 没有抢占,任务自己控制执行时间
  • 不需要定时器中断,资源开销极小
  • 适合任务简单、执行时间确定的场景

我记得早期做8位单片机项目时,RAM只有128字节,ROM只有2KB。跑抢占式RTOS?想都别想。合作式调度是唯一的选择。

合作式调度的代码极其简洁:

// 合作式调度核心
void task_yield(void) {
    // 保存当前任务状态
    save_context(current_task);
    
    // 切换到下一个就绪任务
    current_task = get_next_ready_task();
    
    // 恢复新任务上下文
    restore_context(current_task);
}

// 每个任务必须主动让出CPU
void task_led(void) {
    while (1) {
        toggle_led();
        task_yield();  // 主动让出
    }
}

重要提醒:合作式调度最怕「流氓任务」。如果一个任务不调用yield(),其他任务永远得不到CPU。我曾经见过一个同事写的循环里忘了加yield(),结果整个系统卡死。排查了整整两天。

3.4 三种调度算法的对比

咱们用一张表总结一下:

特性 时间片轮转 优先级抢占 合作式调度
任务优先级
响应时间 取决于时间片 可预测,短 取决于任务执行时间
资源开销 中等
适用场景 任务重要性相同 硬实时系统 资源受限系统
风险 紧急任务响应慢 优先级反转 任务阻塞系统

我个人建议:做机器人控制器,优先考虑优先级抢占调度。因为机器人对实时性要求高,电机控制、力传感器读取这些任务必须优先响应。

但如果你在做简单的传感器采集、LED控制,合作式调度完全够用,而且代码简单、bug少。

嗯,三种调度算法就讲到这里。下一章咱们聊聊任务同步与通信——信号量、互斥锁、消息队列。这些东西在实际项目中天天用,不搞清楚会出大问题。