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