参考: 中国科学院大学 2025年秋《高级操作系统教程》课件
调度器的目标:
- 降低周转时间
- 降低响应时间
- 实时性:在任务的截止时间内完成任务
- 公平性:每个任务都应该有机会执行,不能饿死
越公平,周转时间越差 - 开销低:调度器是为了优化系统,而非制造性能 BUG
- 可扩展:随着任务数量增加,仍能正常工作
- 周转时间(turnaround time) = 完成时间 - 进入系统时间
- 响应时间(reponse time) = 首次运行时间 - 进入系统时间
调度的挑战:
- 缺少信息(没有先知)
- 工作场景动态变化
- 线程/任务间的复杂交互
- 调度目标多样性
- 不同的系统可能关注不一样的调度指标
- 许多方面存在取舍
- 调度开销 V.S. 调度效果
- 优先级 V.S. 公平
- 能耗 V.S. 性能
非抢占式调度 (Non-Preemptive)
FIFO 调度/FCFS 先到先服务(First Come First Served)
- 优点:简单,易于实现
- 缺点:
- 平均周转、响应时间较长
- 存在护航效应
护航效应 convoy effect:耗时较少的潜在资源消费者被排在重量级的消费者之后
SJF 最短任务优先(Shortest Job First)
- 优点:平均周转时间短
- 缺点:
- 不公平,可能导致任务饿死
- 平均响应时间过长
- 并不能解决护航效应的问题
- 需要预知任务时间
抢占式调度 (Preemptive)
每次任务执行一定时间后会被切换到下一任务,而非执行至终止
通过定时触发的时钟中断实现
大量的 OS 机制是时间驱动的
- 更新系统在线时间: uptime
- 更新日历时间
- SMP 系统中,对运行队列进行负载均衡
- 运行已到期的定时器
- 更新资源和进程统计信息
- 如
time ls;对时间进行剖析
最短完成时间优先 (STCF, Shortest Time-to-Complete First)
相当于抢占式 SJF:
- 只考虑周转时间,且已知任务时间,任务只使用cpu 的情况下,是最优的
- 无法预知任务时间
Round-Robin 轮转调度
基本思路: 轮询,一个时间片运行一个工作,然后切换
- 优点:公平、平均响应时间短
- 缺点:周转时间很差
- 当每个任务的执行时间差不多时最为明显
trade-off: 时间片(time slice) 的选择
- 长度必须是时钟中断周期的倍数
- 时间片长度越短: 响应时间越好, 上下文切换开销越高
- 保存恢复寄存器现场
- cache,TLB,分支预测器等的刷新
- 时间片长度越长: 类似于 FCFS
多级反馈队列 (Multi-level Feedback Queue, MLFQ)
- 起源: 1962,Corbato,时分共享系统 CTSS
- 解决的问题:
- 优化周转时间:先执行短工作
- 降低响应时间
- 结构: 独立的队列
- 一个工作只能出现在一个队列里
- 每个队列可以有多个工作
- 优先级决定执行
- 同一队列的工作有相同的优先级:采用轮转调度
- 每个队列有不同的优先级:优先执行高优先级任务
关键:如何设置优先级? 动态设定优先级
- 工作进入系统时,最高优先级
- 根据进程消耗时间降低优先级 accounting
- 周期性提升所有工作优先级
根据进程消耗时间降低优先级 accounting
方式 1:
- 进程主动释放 CPU(在时间片内),保持优先级不变;
- 进程长时间占用 CPU (用完时间片),降低优先级
- 问题:容易被欺骗,进程在时间片即将用完之前释放 cpu
方式 2: 进程用完在某一层的时间配额时,降低优先级
周期性提升所有工作优先级
- 解决进程的 starvation 饿死
- 可以正确对待 CPU 密集型进程与交互型(IO 密集型)进程之间的变化
- 困难:如何确定周期(voo-doo constant)?
MLFP 变体
- 大多数MLFP变体:支持不同队列可变的时间片长度
- 高优先级时间片短,切换更快
- Solaris:提供一组表决定如何调整优先级
- FreeBSD:使用量衰减 (delay-usage) 算法
特殊使用
- 有些系统将最高优先级队列留给操作系统使用
- 有些系统允许用户给出优先级设置建议(UNIX Niceness)
- -20 .. 19 的整数,越 nice 越让别人得到 CPU
- -20: 极坏
- 19: 极好
比例份额调度/公平份额调度 (proportional-share / fair-share)
最终目的: 确保每个工作获得一定比例的 CPU 时间(解决进程饿死的问题),而非优化周转时间和响应时间
彩票调度 (lottery scheduling)
基本思路: 每个进程拥有不同的份额,每次调度从总份额里随机抽取一个数,
落到哪个进程就调度哪个进程。(从概率上满足期望的比例,但不确保)
- 占有份额的多少决定进程的优先级高低
- 转让(ticket transfer):进程可以临时将自己的份额转让给其他进程
- 应用于服务器/客户端交互,客户端可以加速服务器执行自己的请求
- 通胀(ticket inflation):进程可以临时提升自己的份额数量
- 适用于进程之间互相信任的环境
评估:
- 避免特殊的工作负载的影响
- 实现简单,几乎不需要记录任何状态,只需要一个随机数生成器
- 速度很快
- 不确定性:偶尔不能产生正确的比例
- 困难:如何为每个进程分配份额?
步长调度 (stride schedulingg)
应用于Linux Complete Fair Scheduling (CFS)
基本思路:
- 步长 stride:与份额数成反比
- 行程值 pass:计数器,累加步长,记录总体进展
- 每次调度时,选择拥有最小行程值的进程
评估:
- 解决了彩票调度的不确定性问题
- 需要为每个进程记录全局状态
- 问题:如何加入新进程?
如果行程值设为0,则其独占CPU。
多处理器调度 (Multiprocessor Scheduling)
单队列多处理器调度 (Single Queue Multiprocessor Scheduling, SQMS)
单处理器调度策略直接加锁实现
trade-off:
- 简单,不需要太多修改
- 缺乏可扩展性(scalability)
- 不能很好地保持缓存亲和度
- 大多数系统都引入一些亲和度机制:
- 保持一些工作的亲和度的同时,牺牲一部分工作的亲和度
多队列多处理器调度(Multi-Queue Multiprocessor Scheduling, MQMS)
基本框架: 采用多个调度队列
- 每个 CPU 都有自己的调度队列,每个 CPU 调度之间互相独立
- 每个任务进入系统后根据一些启发性规则进入某个调度队列
- 每个队列可以使用不同的调度规则
trade-off:
- 具备可扩展性 (scalability)
- 具有良好的缓存亲和度
- 存在负载不均 (load imbalance) 问题
应对负载不均问题:迁移(migration)
- 不断迁移任务到其他 CPU 以实现负载均衡
- 工作窃取(work stealing):任务少的队列检查其他队列的任务是不是更多,是则窃取任务
- trade-off:寻找合适的检查频率
- 检查太频繁:带来较高开销
- 检查间隔太长:带来负载不均
- trade-off:寻找合适的检查频率
Linux 多处理器调度
未达成共识:(Linux 3种调度算法)
| O(1)调度 | CFS 完全公平调度 | BFS调度 |
|---|---|---|
| 多队列 | 多队列 | 单队列 |
| 基于优先级调度 | 基于比例调度 | 基于比例调度 |
| 类似 MLFQ | 类似步长调度 | 最早最合适虚拟截止时间优先算法(EEVEF) |
多处理器调度的困难之处
- 负载不均 (load imbalance) 与缓存亲和度 (cache affinity) 的 trade-off
- 优先级反转问题:
进程持有锁时被切换到中间优先级进程,再切换到高优先级需要同样的锁的进程 - 多用户:单线程的A和10,000线程的B在CFS上共享CPU
- 异构处理器:大小核性能差异
- 分布式系统:无法避免的节点失效和网络延迟
- NUMA (Non-uniform memory access)
共享内存密集型程序在远/近 CPU 上性能差达到数倍 - 优先级反转问题
优先级反转问题
应对:
- Linux:摆烂
- 实时系统
- 优先级继承 (Priority Inheritance)/ 优先级提升 (Priority Ceiling)
持有 mutex 的线程/进程会继承 block 在该 mutex 上进程的最高优先级,但也不是万能的 (例如条件变量唤醒) - 对潜在的优先级反转进行预警 (lockdep)
避免高/低优先级的任务争抢资源
- 优先级继承 (Priority Inheritance)/ 优先级提升 (Priority Ceiling)