先来先服务(FCFS)调度:算法原理、优缺点分析、适用场景、代码实现示例
一、算法原理:排队,就这么简单
先来先服务(First Come First Served,简称 FCFS)调度算法,说白了就是「谁先到谁先上」。
你想想看,去银行办业务、去食堂打饭,是不是都得排队?FCFS 就是这个道理。任务按照到达就绪队列的先后顺序,一个一个地被 CPU 执行。先来的任务先获得 CPU 使用权,直到它主动释放(比如任务完成、或者因为等待 I/O 而阻塞),下一个任务才能开始运行。
我记得刚入行那会儿,带我的老工程师跟我说:「别把调度想得太复杂,FCFS 就是最朴素的排队。」我当时还不太理解,后来自己写了几年代码才明白——简单,往往意味着可靠。
核心规则就两条:
- 非抢占式:一旦任务获得 CPU,就会一直运行到结束或主动阻塞。中途不会被其他任务打断。
- 按到达时间排序:调度器维护一个就绪队列,新来的任务排到队尾,调度器每次从队头取任务执行。
关键点:FCFS 的调度决策只依赖于任务的到达时间,不依赖任务的执行时间、优先级或其他任何属性。公平吗?公平。高效吗?不一定。
二、优缺点分析:公平背后的代价
优点
- 实现极其简单:只需要一个 FIFO 队列,不需要复杂的排序或优先级计算。代码量少,bug 也少。
- 公平性直观:每个任务都有机会被执行,不会出现「饿死」现象。你想想看,只要排队,总能轮到你。
- 适合长任务:如果任务执行时间都比较长且稳定,FCFS 的调度开销几乎可以忽略不计。
缺点
- 平均等待时间长:这是 FCFS 最大的硬伤。短任务如果排在长任务后面,等待时间会非常长。
- 不利于交互式系统:用户按一下键盘,可能要等好几秒才有响应。我在项目中遇到过这种情况——一个后台数据采集任务占着 CPU 不放,前台界面直接卡死,用户投诉电话都打爆了。
- 对突发任务不友好:紧急任务即使优先级再高,也得老老实实排队。这在实时系统中是致命的。
避坑指南:我曾经在一个工业控制项目中用了 FCFS,结果一个传感器数据采集任务因为排在长计算任务后面,导致控制指令延迟了 200ms,设备直接报警停机。从那以后,我对 FCFS 的适用场景变得非常谨慎。
三、适用场景:什么时候该用它?
FCFS 不是万能的,但在某些场景下,它反而是最优解。
| 场景 | 说明 | 推荐度 |
|---|---|---|
| 批处理系统 | 任务执行时间相近,且没有交互需求 | ★★★★★ |
| 单任务嵌入式设备 | 系统只运行一个主任务,偶尔处理中断 | ★★★★☆ |
| 实时性要求低的系统 | 对响应时间没有严格 deadline 要求 | ★★★☆☆ |
| 交互式系统 | 用户需要即时反馈 | ★☆☆☆☆ |
| 硬实时系统 | 任务必须在规定时间内完成 | ☆☆☆☆☆ |
我个人习惯在项目初期先用 FCFS 做原型验证。因为实现快、逻辑简单,等系统跑通了再根据实际需求换成更复杂的调度算法。你想想看,如果一开始就上优先级抢占调度,调试起来得多头疼?
四、代码实现示例
下面是一个用 C 语言实现的 FCFS 调度器示例。我尽量写得贴近实际嵌入式开发风格,而不是教科书上的伪代码。
#include <stdio.h>
#include <stdlib.h>
// 任务控制块
typedef struct {
int task_id; // 任务ID
int arrival_time; // 到达时间
int burst_time; // 执行时间
int start_time; // 开始时间
int finish_time; // 完成时间
int waiting_time; // 等待时间
int turnaround_time;// 周转时间
} Task;
// 按到达时间排序(冒泡排序,简单易懂)
void sort_by_arrival(Task tasks[], int n) {
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (tasks[j].arrival_time > tasks[j + 1].arrival_time) {
Task temp = tasks[j];
tasks[j] = tasks[j + 1];
tasks[j + 1] = temp;
}
}
}
}
// FCFS 调度核心
void fcfs_schedule(Task tasks[], int n) {
int current_time = 0;
for (int i = 0; i < n; i++) {
// 如果当前时间小于任务到达时间,CPU 空闲等待
if (current_time < tasks[i].arrival_time) {
current_time = tasks[i].arrival_time;
}
tasks[i].start_time = current_time;
tasks[i].finish_time = current_time + tasks[i].burst_time;
tasks[i].turnaround_time = tasks[i].finish_time - tasks[i].arrival_time;
tasks[i].waiting_time = tasks[i].start_time - tasks[i].arrival_time;
current_time = tasks[i].finish_time;
}
}
// 打印调度结果
void print_schedule(Task tasks[], int n) {
printf("任务ID | 到达时间 | 执行时间 | 开始时间 | 完成时间 | 等待时间 | 周转时间\n");
printf("----------------------------------------------------------------\n");
float avg_waiting = 0, avg_turnaround = 0;
for (int i = 0; i < n; i++) {
printf(" %2d | %2d | %2d | %2d | %2d | %2d | %2d\n",
tasks[i].task_id, tasks[i].arrival_time, tasks[i].burst_time,
tasks[i].start_time, tasks[i].finish_time,
tasks[i].waiting_time, tasks[i].turnaround_time);
avg_waiting += tasks[i].waiting_time;
avg_turnaround += tasks[i].turnaround_time;
}
printf("\n平均等待时间: %.2f\n", avg_waiting / n);
printf("平均周转时间: %.2f\n", avg_turnaround / n);
}
int main() {
// 模拟一组任务
Task tasks[] = {
{1, 0, 5},
{2, 2, 3},
{3, 4, 1},
{4, 6, 4}
};
int n = sizeof(tasks) / sizeof(tasks[0]);
// 先按到达时间排序
sort_by_arrival(tasks, n);
// 执行 FCFS 调度
fcfs_schedule(tasks, n);
// 输出结果
print_schedule(tasks, n);
return 0;
}
小提示:在实际嵌入式系统中,任务队列通常用链表实现,而不是数组。因为任务可能动态创建和销毁。我习惯用双向链表,这样插入和删除都方便。
五、核心逻辑流程图
下面我用一张 SVG 图来展示 FCFS 调度的核心流程。嗯,画图比写代码还费劲,但一张好图胜过千言万语。
从流程图可以看出,FCFS 的逻辑非常直白:循环判断队列是否为空,不为空就取队头任务执行,执行完再取下一個。没有优先级判断,没有时间片轮转,就这么简单粗暴。
总结一下:FCFS 是最基础的调度算法,理解它对于学习更复杂的调度策略(如短作业优先、时间片轮转、优先级调度)非常有帮助。我个人建议初学者先把 FCFS 吃透,再去看那些花里胡哨的调度算法。地基打不牢,楼盖得再高也得塌。
公众号:蓝海资料掘金营,微信deep3321