调度算法基础:调度器的职责、调度时机与性能指标
各位同学,今天我们聊聊调度算法的基础。说实话,调度器这东西,在实时系统里就是“大脑”一样的存在。我刚开始做嵌入式时,总觉得调度嘛,不就是轮着来?后来被项目坑过几次,才明白这里面的门道有多深。
一、调度器的职责——它到底在忙什么?
调度器的核心任务,说白了就三件事:选谁跑、跑多久、跑完怎么办。
- 任务选择:从就绪队列里挑一个最合适的任务。嗯,这里要注意,不是随便挑,得看优先级、截止时间、资源占用等等。
- 上下文切换:把当前任务的状态保存好,再把新任务的状态恢复出来。我见过不少新手在这里栽跟头——寄存器没保存全,回来就崩了。
- 资源管理:协调共享资源,防止死锁。我曾经在一个多任务项目中,因为忘了释放互斥锁,导致三个任务互相等待,系统直接“死机”。
我个人习惯:在设计调度器时,先把任务分类。周期性的、突发性的、紧急的,分清楚再定策略。别一上来就写代码,否则后面改起来想哭。
二、调度时机——什么时候触发调度?
调度不是随时都能做的。触发调度的时机,主要有两种模式:
1. 事件驱动调度
事件来了,才调度。比如外部中断、定时器溢出、消息到达。这种方式的优点是响应快,缺点是如果事件太多,调度器会被频繁打断。
举个例子:你在做一个按键响应系统。用户按一下键,触发中断,调度器立刻把按键处理任务拉进来跑。跑完继续干别的。这就是事件驱动。
避坑指南:我曾经在一个工业控制器项目里,事件驱动调度做得太“积极”,结果中断嵌套了五层,栈空间直接爆了。后来我加了个中断优先级屏蔽,才稳住。
2. 时间片轮转调度
每个任务分一个固定时间片,轮流跑。时间片到了,强制切换。这种方式公平,但实时性差——你想想看,如果紧急任务刚好轮到最后一个时间片,那得等多久?
时间片大小的选择是个学问。太短,上下文切换开销大;太长,响应延迟高。我一般建议:时间片 = 系统时钟滴答周期的 10~100 倍。具体多少,得看你任务的执行时间分布。
// 一个简单的时间片轮转调度伪代码
void scheduler() {
while (1) {
task = get_next_task(); // 轮转取任务
set_timer(TIME_SLICE); // 设置时间片
task->run(); // 执行任务
// 时间片到,自动触发调度
}
}
三、调度性能指标——怎么评价调度器好不好?
调度器好不好,不能光靠感觉。我一般用三个指标来衡量:
| 指标 | 定义 | 我的经验 |
|---|---|---|
| 吞吐量 | 单位时间内完成的任务数 | 吞吐量高不代表实时性好。我曾经优化过一个批处理系统,吞吐量翻倍了,但紧急任务反而被堵死了。 |
| 响应时间 | 从任务就绪到开始执行的时间 | 这是实时系统的命门。我一般要求响应时间不超过任务周期的 10%。 |
| 截止时间 | 任务必须在规定时间内完成 | 硬实时系统里,错过截止时间就是事故。软实时系统可以偶尔超时,但不能频繁。 |
注意:这三个指标往往是矛盾的。想提高吞吐量,就得让任务跑久一点,但响应时间就长了。想保证截止时间,就得预留资源,但吞吐量就下来了。没有完美的调度器,只有适合场景的调度器。
四、知识体系结构图
下面这张图,是我自己总结的调度算法基础结构。你看一眼,就能明白今天讲的内容是怎么串起来的。
五、一点个人感悟
调度算法这东西,理论看着简单,实际用起来全是坑。我做了十几年嵌入式,换过七八种调度策略,最后还是觉得:理解你的任务特性,比背调度算法更重要。
你想想看,如果你的任务都是周期性的、执行时间稳定的,那时间片轮转就挺好。但如果任务有突发性、有紧急程度差异,那事件驱动加优先级调度更靠谱。别死磕一种策略,灵活点。
最后说一句:调度器是实时系统的“心脏”。心脏跳得好,系统才稳。多花点时间在设计上,后面调试能省一半的功夫。