进程调度原理

进程调度是操作系统的核心功能之一,决定了系统的响应性和公平性。

调度的本质

为什么需要调度

CPU 资源稀缺

  • 进程数量远大于 CPU 核心数
  • 需要多个进程"同时"运行(时分复用)
  • 调度器决定哪个进程获得 CPU

调度目标

  • 吞吐量:单位时间完成的任务数
  • 响应时间:从请求到响应的时间
  • 公平性:所有进程获得合理的 CPU 时间
  • 优先级:重要任务优先执行

调度时机

非抢占式调度(早期系统):

  • 进程主动放弃 CPU(阻塞或结束)
  • 简单但响应慢
  • 一个进程可能长期占用 CPU

抢占式调度(现代系统):

  • 时间片用完强制切换
  • 高优先级进程抢占低优先级
  • 提高响应性和公平性

触发调度的事件

  1. 时钟中断(时间片到期)
  2. 进程阻塞(I/O、睡眠)
  3. 进程创建或退出
  4. 优先级变化
  5. 系统调用返回

经典调度算法

FIFO(先来先服务)

原理

  • 按到达顺序排队
  • 最简单的调度算法

优点

  • 实现简单
  • 公平(先到先得)

缺点

  • 护航效应(Convoy Effect)
  • 长任务阻塞短任务
  • 平均等待时间长

SJF(最短作业优先)

原理

  • 选择预计运行时间最短的进程
  • 可以是抢占式(SRTF)或非抢占式

优点

  • 最优的平均等待时间(理论上)

缺点

  • 无法准确预测运行时间
  • 长任务可能饿死
  • 不公平

时间片轮转(Round Robin)

原理

  • 每个进程分配固定时间片(如 10ms)
  • 时间片用完放到队尾
  • 循环调度

时间片选择

  • 太大:退化为 FIFO,响应慢
  • 太小:上下文切换开销大
  • 经验值:10-100ms

优点

  • 公平性好
  • 响应时间可预测

缺点

  • 不区分优先级
  • I/O 密集型进程不利

多级反馈队列

设计思想

  • 多个优先级队列
  • 进程根据行为动态调整优先级
  • 兼顾响应性和公平性

工作机制

高优先级队列(时间片小)
  ↓ 用完时间片降级
中优先级队列(时间片中)
  ↓ 用完时间片降级
低优先级队列(时间片大)

特点

  • I/O 密集型进程(频繁阻塞)保持高优先级
  • CPU 密集型进程逐渐降级
  • 防止饿死(老化机制)

Linux CFS 调度器

设计哲学

CFS(Completely Fair Scheduler,完全公平调度器)是 Linux 2.6.23 引入的调度器。

核心思想

  • 理想情况下,每个进程应获得 1/n 的 CPU 时间(n 为进程数)
  • 记录每个进程的虚拟运行时间(vruntime)
  • 总是选择 vruntime 最小的进程运行

虚拟运行时间(vruntime)

计算公式

vruntime += 实际运行时间 × (NICE_0_LOAD / 进程权重)

关键概念

  • 实际运行时间:进程在 CPU 上的物理时间
  • 进程权重:由 nice 值决定,nice 值越低权重越大
  • nice 值:-20(最高优先级)到 19(最低优先级)

示例

进程 A:nice = 0,权重 = 1024
进程 B:nice = 5,权重 = 335

A 运行 10ms:vruntime += 10ms × (1024/1024) = 10ms
B 运行 10ms:vruntime += 10ms × (1024/335) ≈ 30.6ms

B 的 vruntime 增长更快,下次调度时 A 更可能被选中

红黑树调度队列

为什么用红黑树

  • 需要频繁查找最小 vruntime
  • 需要频繁插入和删除
  • 红黑树提供 O(log n) 的操作复杂度

工作流程

  1. 入队:进程进入就绪态,插入红黑树(按 vruntime 排序)
  2. 选择:选择最左节点(vruntime 最小)
  3. 运行:进程运行,vruntime 增加
  4. 出队:进程阻塞或时间片用完,从树中删除
  5. 重新入队:进程重新就绪,插入树中

时间片计算

CFS 不使用固定时间片,而是动态计算:

调度周期

  • 所有进程轮流运行一次的时间
  • 默认:6ms(进程数 ≤ 8)
  • 进程数 > 8:进程数 × 0.75ms

进程时间片

时间片 = 调度周期 × (进程权重 / 总权重)

最小粒度

  • 保证每次至少运行 1ms(默认)
  • 防止时间片过小导致频繁切换

组调度

问题

  • 用户 A:1 个进程
  • 用户 B:100 个进程
  • 每个进程公平 → 用户 B 获得 100 倍 CPU

解决方案

  • 先在用户组间公平分配
  • 再在组内进程间公平分配

层次结构

总 CPU
  ├─ 用户 A(50%)
  │    └─ 进程 A1(50%)
  └─ 用户 B(50%)
       ├─ 进程 B1(0.5%)
       ├─ 进程 B2(0.5%)
       └─ ... (100个进程)

实时调度

实时进程特点

硬实时

  • 必须在截止时间内完成
  • 错过截止时间视为失败
  • 例:飞行控制、工业控制

软实时

  • 应尽量在截止时间内完成
  • 错过截止时间降低服务质量
  • 例:音视频播放

Linux 实时调度策略

SCHED_FIFO

  • 先进先出
  • 没有时间片限制
  • 运行到完成或阻塞或被更高优先级抢占

SCHED_RR

  • 时间片轮转
  • 同优先级进程轮流运行
  • 保留抢占特性

SCHED_DEADLINE

  • 基于截止时间调度
  • 最复杂但最精确
  • 保证任务在截止时间前完成

优先级

实时进程(优先级 0-99)
  ↓ 高于
普通进程(优先级 100-139)

多核调度

负载均衡

目标

  • 各 CPU 负载均衡
  • 减少进程迁移(缓存友好)

时机

  • 定期检查(tick 中断)
  • CPU 空闲时
  • fork 新进程时
  • exec 执行时

CPU 亲和性

概念

  • 进程倾向于在同一 CPU 上运行
  • 提高缓存命中率

设置方式

  • 硬亲和性:强制绑定
  • 软亲和性:优先但非强制

应用场景

  • 性能敏感应用
  • NUMA 系统优化
  • 实时系统

NUMA 感知

NUMA(非一致性内存访问)

  • 每个 CPU 有本地内存
  • 访问远程内存更慢

调度策略

  • 优先在进程内存所在节点调度
  • 减少跨节点内存访问
  • 权衡负载均衡和局部性

调度性能分析

关键指标

延迟(Latency)

  • 进程从就绪到运行的等待时间
  • 影响响应性

吞吐量(Throughput)

  • 单位时间完成的任务数
  • 影响整体效率

公平性(Fairness)

  • 各进程获得 CPU 时间的偏差
  • CFS 的核心目标

性能调优

nice 值调整

  • 降低 nice 值提高优先级
  • 适用于关键服务

实时优先级

  • 谨慎使用
  • 可能导致系统无响应

CPU 亲和性

  • 绑定关键进程到特定 CPU
  • 隔离干扰

cgroup 限制

  • 限制进程组 CPU 使用
  • 防止资源垄断

调度器的演进

O(1) 调度器(2.6.0-2.6.22)

特点

  • 常数时间调度(O(1))
  • 两个优先级数组(活动和过期)
  • 复杂的启发式规则

问题

  • 交互性判断不准确
  • 公平性不佳
  • 难以维护

CFS(2.6.23-至今)

改进

  • 简单优雅的设计
  • 真正的公平调度
  • 更好的交互性

持续优化

  • PELT(Per-Entity Load Tracking)
  • 频率和能耗感知调度
  • 组调度改进

思考题

  1. 为什么 CFS 使用虚拟运行时间而非实际运行时间?
  2. 实时进程优先级高,为什么不会饿死普通进程?
  3. 多核系统中,如何平衡负载均衡和缓存局部性?