#进程调度算法
面试官问:"操作系统如何决定下一个运行哪个进程?"
小王说:"用调度算法...有先来先服务、短作业优先..."
面试官继续追问:"那什么是时间片?为什么时间片太大会导致系统响应变慢?"
小王说:"因为...进程要等很久?"
面试官又问:"Linux用的CFS调度器是什么原理?为什么它能保证公平性?"
小王彻底卡住了。
进程调度是操作系统的核心功能之一,看似简单,背后涉及的权衡和优化多得让人眼花缭乱。今天,我们把常见的调度算法全部讲透。
#一、从一个问题开始
先想象一个餐厅的场景:
你是餐厅经理,需要决定:
- 现在有5桌客人等位
- 1号桌:点了10道菜,需要30分钟
- 2号桌:点了2道菜,需要5分钟
- 3号桌:点了5道菜,需要15分钟
- 4号桌:点了1道菜,需要2分钟
- 5号桌:点了3道菜,需要8分钟
你选择按什么顺序做菜?
选择1:按点菜顺序(先来先服务)
结果:
- 1号桌等30分钟完成
- 2号桌等35分钟完成
- ...
- 平均等待时间 = (30 + 35 + 50 + 55 + 63) / 5 = 46.6分钟
选择2:先做快的(短作业优先)
结果:
- 4号桌等2分钟完成
- 2号桌等7分钟完成
- 5号桌等15分钟完成
- ...
- 平均等待时间 = (2 + 7 + 15 + 30 + 35) / 5 = 17.8分钟
短作业优先的等待时间减少了62%!这就是调度算法要解决的问题:如何分配CPU时间,让整体效率最高。
#【直观类比】
#进程调度 = 餐厅出餐顺序
┌─────────────────────────────────────────────────┐
│ 不同的调度策略 │
├─────────────────────────────────────────────────┤
│ 先来先服务(FCFS):按排队顺序做 │
│ - 公平,但效率可能不高 │
│ - 长作业会拖住后面的短作业 │
├─────────────────────────────────────────────────┤
│ 短作业优先(SJF):先做快的 │
│ - 平均等待时间最短 │
│ - 但长作业可能"饿死" │
├─────────────────────────────────────────────────┤
│ 时间片轮转(RR):轮着来,每人做一会儿 │
│ - 响应时间快 │
│ - 但切换开销大 │
├─────────────────────────────────────────────────┤
│ 优先级调度:VIP优先 │
│ - 重要任务先执行 │
│ - 但低优先级可能饿死 │
└─────────────────────────────────────────────────┘#调度算法的评价指标
| 指标 | 说明 | 优化目标 |
|---|---|---|
| CPU利用率 | CPU忙碌时间占比 | 越高越好 |
| 吞吐量 | 单位时间完成进程数 | 越高越好 |
| 周转时间 | 进程从提交到完成的时间 | 越短越好 |
| 等待时间 | 进程在就绪队列等待的时间 | 越短越好 |
| 响应时间 | 从提交到首次响应的时间 | 越短越好 |
周转时间 = 完成时间 - 到达时间
等待时间 = 开始时间 - 到达时间
周转时间 = 等待时间 + 执行时间#二、核心原理
#1. 先来先服务(FCFS, First Come First Served)
最简单的调度算法:
def fcfs_schedule(processes):
"""
processes: [(arrival_time, burst_time), ...]
按到达时间排序
"""
# 按到达时间排序
sorted_procs = sorted(processes, key=lambda x: x[0])
current_time = 0
waiting_time = 0
for arrival, burst in sorted_procs:
if current_time < arrival:
current_time = arrival
waiting_time += current_time - arrival
current_time += burst
return waiting_time / len(processes)
# 示例:进程P1-P4
# P1: 到达0,执行4
# P2: 到达1,执行3
# P3: 到达2,执行2
# P4: 到达3,执行1
# 执行顺序:P1 → P2 → P3 → P4
# Gantt图:
# | P1 | P2 | P3 | P4 |
# 0 4 7 9 10
# 等待时间:
# P1: 0 - 0 = 0
# P2: 4 - 1 = 3
# P3: 7 - 2 = 5
# P4: 9 - 3 = 6
# 平均等待时间 = (0 + 3 + 5 + 6) / 4 = 3.5优点:
- 简单,容易实现
- 公平(按顺序)
缺点:
- 护航效应(Convoy Effect):长作业在前面会导致后面短作业等待很久
- 平均等待时间不是最优
#2. 短作业优先(SJF, Shortest Job First)
优先执行预计执行时间最短的作业:
def sjf_schedule(processes):
"""
非抢占式SJF
"""
processes = sorted(processes, key=lambda x: x[1]) # 按执行时间排序
# 注意:这里简化了,实际还需要考虑到达时间
current_time = 0
total_waiting = 0
# 使用优先级队列
import heapq
ready_queue = []
idx = 0
while idx < len(processes) or ready_queue:
# 添加所有已到达的进程
while idx < len(processes) and processes[idx][0] <= current_time:
arrival, burst = processes[idx]
# 按burst时间排序
heapq.heappush(ready_queue, (burst, arrival))
idx += 1
if ready_queue:
burst, arrival = heapq.heappop(ready_queue)
current_time += burst
total_waiting += current_time - arrival - burst
else:
# 没有可运行的进程,跳到下一个到达时间
current_time = processes[idx][0]
return total_waiting / len(processes)SJF vs FCFS(假设所有进程同时到达):
进程:P1=10, P2=4, P3=3, P4=2
FCFS顺序:P1 → P2 → P3 → P4
- P1等待:0,周转:10
- P2等待:10,周转:14
- P3等待:14,周转:17
- P4等待:17,周转:19
- 平均等待:10.25
SJF顺序:P4 → P3 → P2 → P1
- P4等待:0,周转:2
- P3等待:2,周转:5
- P2等待:5,周转:9
- P1等待:9,周转:19
- 平均等待:4.0SJF的问题:
- 需要预先知道作业执行时间(实际很难准确预测)
- 长作业可能"饿死"
#3. 最短剩余时间优先(SRTF, Shortest Remaining Time First)
抢占式版本的SJF:
def srtf_schedule(processes):
"""
最短剩余时间优先(抢占式SJF)
"""
import heapq
current_time = 0
ready_queue = []
completed = []
idx = 0
n = len(processes)
while len(completed) < n:
# 添加新到达的进程
while idx < n and processes[idx][0] <= current_time:
arrival, burst = processes[idx]
# (remaining_time, arrival_time, original_burst)
heapq.heappush(ready_queue, (burst, arrival, burst))
idx += 1
if ready_queue:
remaining, arrival, original = heapq.heappop(ready_queue)
current_time += 1
remaining -= 1
# 检查是否有新进程到达
while idx < n and processes[idx][0] <= current_time:
new_arrival, new_burst = processes[idx]
heapq.heappush(ready_queue, (new_burst, new_arrival, new_burst))
idx += 1
if remaining > 0:
heapq.heappush(ready_queue, (remaining, arrival, original))
else:
completed.append((current_time - arrival - original, arrival, original))
else:
current_time = processes[idx][0]
return sum(p[0] for p in completed) / len(completed)#4. 时间片轮转(RR, Round Robin)
每个进程执行一个时间片,然后切换到下一个:
def round_robin_schedule(processes, time_slice=2):
"""
时间片轮转调度
"""
from collections import deque
current_time = 0
ready_queue = deque()
remaining = {} # pid -> remaining_time
idx = 0
# 初始化
while idx < len(processes) and processes[idx][0] <= current_time:
arrival, burst = processes[idx]
ready_queue.append(arrival)
remaining[arrival] = burst
idx += 1
total_waiting = 0
while ready_queue:
arrival = ready_queue.popleft()
burst = remaining[arrival]
if burst <= time_slice:
# 进程完成
current_time += burst
waiting = current_time - arrival - burst
total_waiting += waiting
# 添加新到达的进程
while idx < len(processes) and processes[idx][0] <= current_time:
new_arrival, new_burst = processes[idx]
ready_queue.append(new_arrival)
remaining[new_arrival] = new_burst
idx += 1
else:
# 时间片用完,放回队列
current_time += time_slice
remaining[arrival] = burst - time_slice
ready_queue.append(arrival)
# 添加新到达的进程
while idx < len(processes) and processes[idx][0] <= current_time:
new_arrival, new_burst = processes[idx]
ready_queue.append(new_arrival)
remaining[new_arrival] = new_burst
idx += 1
return total_waiting / len(processes)时间片大小的影响:
时间片太大:
- 响应时间变慢(像FCFS)
- 但上下文切换开销小
时间片太小:
- 响应时间快
- 但上下文切换开销大(CPU时间浪费在切换上)
最优时间片 ≈ 80%的进程都在这段时间内完成
通常选择 10-100ms#5. 优先级调度
给每个进程分配优先级,高优先级先执行:
def priority_schedule(processes):
"""
优先级调度(数值越小优先级越高)
"""
import heapq
current_time = 0
ready_queue = []
idx = 0
while idx < len(processes) or ready_queue:
# 添加所有已到达的进程
while idx < len(processes) and processes[idx][0] <= current_time:
arrival, burst, priority = processes[idx]
heapq.heappush(ready_queue, (priority, arrival, burst))
idx += 1
if ready_queue:
priority, arrival, burst = heapq.heappop(ready_queue)
current_time += burst
else:
current_time = processes[idx][0]优先级反转问题:
场景:
- 进程A(低优先级):获取了锁
- 进程B(中优先级):抢占CPU,进程A无法运行
- 进程C(高优先级):等待锁,但锁被A持有
结果:高优先级进程反而等最久!
解决方案:
1. 优先级继承:持有锁的低优先级进程继承等待者的高优先级
2. 优先级天花板:访问资源前提升优先级#6. 多级反馈队列(MLFQ)
Linux CFS之前广泛使用的调度器:
┌─────────────────────────────────────────────────┐
│ 多级反馈队列示意图 │
├─────────────────────────────────────────────────┤
│ 队列0(最高优先级) RR时间片=8ms │
│ ┌─────────────────────────┐ │
│ │ P5 │ P6 │ P7 │ ... │ │
│ └─────────────────────────┘ │
│ ↓ 队列1(次高)RR时间片=16ms │
│ ┌─────────────────────────┐ │
│ │ P3 │ P4 │ ... │ │
│ └─────────────────────────┘ │
│ ↓ 队列2(中)RR时间片=32ms │
│ ┌─────────────────────────┐ │
│ │ P1 │ P2 │ ... │ │
│ └─────────────────────────┘ │
│ ↓ ... │
│ 队列n(最低) RR时间片=更大 │
└─────────────────────────────────────────────────┘
规则:
1. 新进程进入最高优先级队列
2. 如果时间片用完,降级到下一队列
3. 如果没时间片用完就阻塞,提升到上一队列
4. 最高队列为空才运行次高队列
优点:
- I/O密集型进程保持在高优先级(响应快)
- CPU密集型进程自动降级(避免饿死)#三、CFS调度器(Linux)
Linux 2.6.23之后使用CFS(Completely Fair Scheduler):
// CFS的核心思想:给每个进程分配"虚拟运行时间"
// 谁的实际运行时间最少,谁下次优先运行
struct sched_entity {
struct load_weight load; // 权重(Nice值影响)
struct rb_node run_node; // 红黑树节点
unsigned int on_rq; // 是否在就绪队列
u64 exec_start; // 上次开始执行时间
u64 sum_exec_runtime; // 总执行时间
u64vruntime; // 虚拟运行时间
u64 last_sum_exec_runtime; // 上次总执行时间
};
// 优先级映射
// Nice值 -20 → 权重 1024
// Nice值 0 → 权重 1024
// Nice值 +19 → 权重 15
// 权重比例 ≈ 1.25^(nice)
static const int prio_to_weight[40] = {
/* -20 */ 88761, 71755, 56483, 46273, 36291,
/* -15 */ 29154, 23254, 18705, 14949, 11916,
/* -10 */ 9548, 7620, 6100, 4904, 3906,
/* -5 */ 3121, 2501, 1991, 1586, 1277,
/* 0 */ 1024, 820, 655, 526, 423,
/* 5 */ 335, 272, 215, 172, 137,
/* 10 */ 110, 87, 70, 56, 45,
/* 15 */ 36, 29, 23, 18, 15,
};CFS的红黑树结构:
就绪队列中的进程按vruntime排列:
vruntime最小(最应该运行)
↓
┌───┴───┐
P3 P5
/ \ / \
P1 - - P7
/
P2
新进程加入时:
- 计算其vruntime
- 插入到红黑树中的正确位置
- 最左边的节点最先运行
进程运行时:
- vruntime += 运行时间 * ( NICE_0_LOAD / 进程权重 )
- Nice值越高(优先级越低),vruntime增长越快#四、边界与特例
#1. 调度算法的选择标准
| 场景 | 推荐算法 | 原因 |
|---|---|---|
| 批处理系统 | SJF/SRTF | 最大化吞吐量 |
| 交互系统 | RR/MLFQ | 最小化响应时间 |
| 实时系统 | 优先级/EDF | 确保deadline |
| 通用Linux | CFS | 公平+高效 |
#2. 实时调度算法
EDF(Earliest Deadline First):
// 最晚截止时间优先
// 每次调度最紧急(最早截止)的任务
struct task {
int id;
long period; // 周期
long execution; // 执行时间
long deadline; // 下次截止时间
long next_deadline; // 绝对截止时间
};
// 调度器总是选择deadline最近的任务
// 条件:如果 sum(execution_i / period_i) <= 1,则可调度Rate Monotonic(周期单调):
周期越短的任务优先级越高
例如:
- Task A:周期20ms,执行10ms → 利用率 50%
- Task B:周期50ms,执行25ms → 利用率 50%
总利用率 = 100%,可调度
RM优先级:A > B#3. 多核调度
多核CPU的调度更复杂:
┌─────────────────────────────────────────────────┐
│ 多核调度问题 │
├─────────────────────────────────────────────────┤
│ 1. 负载均衡 │
│ - 不同核心的负载可能不均 │
│ - 需要定期迁移进程 │
│ │
│ 2. 亲和性 │
│ - 进程最好在同一核心运行(缓存友好) │
│ - 但不能为了亲和性牺牲负载均衡 │
│ │
│ 3. 迁移成本 │
│ - 迁移后缓存失效 │
│ - 需要权衡迁移收益和成本 │
└─────────────────────────────────────────────────┘#五、常见误区
#❌ 误区一:SJF是最优调度算法
SJF在平均等待时间上是最优的,但:
1. 需要预先知道执行时间(实际不可能准确预测)
2. 长作业可能饿死
3. 实际系统用的是预测算法(如指数平均)#❌ 误区二:时间片越小越好
时间片太小的问题:
- 上下文切换开销占比增加
- 如果切换开销是1ms,时间片也是1ms
- 50%的时间浪费在切换上!
实际测试:
- 时间片 4ms,上下文切换开销 0.1ms → 开销占比 2.5%
- 时间片 1ms,上下文切换开销 0.1ms → 开销占比 10%#❌ 误区三:Linux只用CFS
Linux实际上有多种调度器:
- CFS:普通进程的默认调度器
- BFS:desktop版本,曾经是CFS的替代
-实时调度器:SCHED_FIFO, SCHED_RR, SCHED_DEADLINE#❌ 误区四:响应时间和吞吐量是同义词
响应时间:用户提交到首次响应的时间
- 关注交互体验
- 需要RR等调度器
吞吐量:单位时间完成的任务数
- 关注系统效率
- 需要SJF等调度器
可能矛盾:
- RR响应快但吞吐量低(切换开销)
- SJF吞吐高但响应慢(长作业在前)#六、记忆技巧
#一句话总结
调度算法就是在"公平性"和"效率"之间找平衡
#对比速记表
| 算法 | 等待时间 | 吞吐量 | 响应时间 | 复杂度 |
|---|---|---|---|---|
| FCFS | 中 | 中 | 差 | O(n) |
| SJF | 最短 | 高 | 差 | O(n log n) |
| RR | 中 | 中 | 好 | O(n) |
| 优先级 | 不确定 | 不确定 | 依赖优先级 | O(n log n) |
| MLFQ | 好 | 高 | 好 | 中 |
| CFS | 好 | 高 | 好 | O(log n) |
#口诀
"批处理用SJF,交互用RR" "响应要快时间片短,吞吐要高上下文少" "高优先级防饿死,负载均衡多核跑"
#七、实战检验
#自检题目
题目1:有4个进程同时到达,执行时间分别为8、4、4、4。用FCFS和SJF计算平均等待时间。
点击查看答案
FCFS(按顺序执行):
顺序:P1(8) → P2(4) → P3(4) → P4(4)
等待时间:
P1: 0
P2: 8
P3: 12
P4: 16
平均等待:36/4 = 9
周转时间:
P1: 8
P2: 12
P3: 16
P4: 20
平均周转:56/4 = 14SJF(短作业优先):
顺序:P2(4) → P3(4) → P4(4) → P1(8)
等待时间:
P2: 0
P3: 4
P4: 8
P1: 16
平均等待:28/4 = 7
SJF比FCFS减少2个单位等待时间题目2:时间片轮转中,时间片大小对系统性能有什么影响?
点击查看答案
时间片大小的权衡:
| 时间片 | 优点 | 缺点 |
|---|---|---|
| 太小 | 响应时间短,交互性好 | 上下文切换开销大,吞吐量下降 |
| 太大 | 切换开销小,吞吐高 | 响应时间长,像FCFS |
最佳选择:
- 大多数进程是IO密集型 → 时间片可以大一些
- 大多数进程是CPU密集型 → 时间片要小一些
经验公式:
最优时间片 ≈ 80% * 最长执行时间
或者
最优时间片 ≈ 上下文切换时间的10-100倍#面试追问预测
| 问题 | 考察点 | 进阶追问 |
|---|---|---|
| CFS原理 | Linux调度器 | 虚拟时间怎么计算 |
| 优先级反转 | 实时调度 | 优先级继承怎么实现 |
| 多核调度 | 扩展性 | 负载均衡怎么做 |
#八、生产实战案例
#案例:Nginx的连接调度
Nginx使用多种调度策略:
# upstream配置
upstream backend {
# 默认:轮询
server 192.168.1.1:8080;
server 192.168.1.2:8080;
# 加权轮询
server 192.168.1.1:8080 weight=3;
server 192.168.1.2:8080 weight=1;
# 最少连接
least_conn;
# IP哈希(会话保持)
ip_hash;
}Nginx调度算法:
- 轮询(Round Robin):默认,适合负载均衡
- 加权轮询:按权重分配请求
- 最少连接:分配给连接数最少的服务器
- IP哈希:同一IP的请求发到同一服务器
#案例:Java线程池的队列选择
线程池本质上也是一种调度:
public class ThreadPoolExecutor {
// 核心线程数
private final int corePoolSize;
// 最大线程数
private final int maximumPoolSize;
// 队列选择策略
// 1. LinkedBlockingQueue:无界队列,可能堆积
// 2. ArrayBlockingQueue:有界队列,拒绝策略
// 3. SynchronousQueue:直接提交,不排队
public void execute(Runnable command) {
if (command == null) throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 队列选择决定调度策略
if (isRunning(c) && workQueue.offer(command)) {
// ...
} else if (!addWorker(command, false)) {
// 拒绝策略
}
}
}调度算法的选择,本质上是在"公平性"、"响应速度"、"吞吐量"之间做权衡。没有完美的算法,只有适合场景的算法。理解这些权衡,才能在系统设计中做出正确的选择。