2. CFS调度器原理:完全公平调度器(CFS)的红黑树数据结构、虚拟运行时间(vruntime)计算、调度周期与最小粒度
完全公平调度器(Completely Fair Scheduler, CFS)是 Linux 内核自 2.6.23 版本引入的默认调度类,用于普通(SCHED_NORMAL / SCHED_OTHER)进程的 CPU 时间分配。CFS 的核心设计理念是:为每个进程提供 CPU 时间的“理想多任务处理器”模型,即假设存在一个无限快的 CPU,所有进程在同一时刻并行运行。在实际物理 CPU 上,CFS 通过精确的虚拟运行时间(vruntime)和红黑树数据结构,以 O(log n) 的复杂度选择下一个最需要 CPU 的进程。
2.1 红黑树数据结构:CFS 的运行队列
CFS 为每个 CPU 维护一个红黑树(Red-Black Tree),作为其就绪队列(cfs_rq)。红黑树是一种自平衡的二叉搜索树,其插入、删除和查找操作的时间复杂度均为 O(log n)。CFS 使用红黑树来组织所有可运行的 SCHED_NORMAL 进程,以进程的 vruntime 作为键值。
- 树节点:每个节点对应一个调度实体(struct sched_entity),通常内嵌在 task_struct 中。
- 排序规则:左子树的 vruntime 小于父节点,右子树的 vruntime 大于父节点。
- 最左节点:红黑树中最左侧的节点(vruntime 最小)即为下一个被调度执行的进程。
- 缓存指针:cfs_rq 中缓存了指向最左节点的指针(rb_leftmost),使得选择下一个进程的操作在大多数情况下为 O(1)。
2.2 虚拟运行时间(vruntime)计算
vruntime 是 CFS 调度决策的核心度量。它表示一个进程在理想多任务处理器上应该获得的运行时间。实际运行时间通过 权重(weight) 进行归一化,使得高优先级(低 nice 值)进程的 vruntime 增长更慢,从而获得更多 CPU 时间。
vruntime 的更新公式:
vruntime += delta_exec * (NICE_0_LOAD / se->load.weight)
- delta_exec:进程本次实际运行的物理时间(单位:ns)。
- NICE_0_LOAD:nice 值为 0 时的默认负载权重(1024)。
- se->load.weight:当前进程的调度实体权重,由 nice 值映射得到。
权重映射表(部分):
| nice 值 | 权重(weight) | vruntime 增长速率(相对 nice=0) | CPU 时间占比(2 个进程) |
|---|---|---|---|
| -20 | 88761 | 0.0115x(极慢) | ≈ 98.9% |
| -10 | 11051 | 0.0927x | ≈ 91.5% |
| 0 | 1024 | 1.0x(基准) | 50.0% |
| 10 | 304 | 3.368x | ≈ 22.9% |
| 19 | 15 | 68.267x(极快) | ≈ 1.4% |
2.3 调度周期与最小粒度
CFS 通过 调度周期(sched_latency) 和 最小粒度(min_granularity) 两个参数来控制进程切换的频率和公平性。
2.3.1 调度周期(sched_latency)
调度周期是 CFS 保证每个可运行进程至少获得一次 CPU 执行的时间窗口。在默认配置下,sched_latency 为 6ms(可通过内核参数 kernel.sched_latency_ns 调整)。
时间片计算:
timeslice = sched_latency * (se->load.weight / cfs_rq->load.weight)
- 当可运行进程数量较少时,每个进程获得的时间片较大,减少切换开销。
- 当进程数量增多时,CFS 会启用 nice 0 负载倍数(nr_running > sched_nr_latency) 机制,将调度周期按比例放大,避免时间片过小导致频繁切换。
2.3.2 最小粒度(min_granularity)
最小粒度是 CFS 允许进程连续运行的最小时间单位,默认值为 0.75ms(可通过 kernel.sched_min_granularity_ns 调整)。
- 作用:防止调度器过于频繁地抢占进程,避免上下文切换开销超过实际计算收益。
- 抢占条件:当当前进程的运行时间超过其时间片,或者新唤醒进程的 vruntime 比当前进程的 vruntime 小至少 min_granularity 时,触发调度。
- 边界情况:在实时性要求极高的场景(如音频处理),可以适当减小 min_granularity,但会增加 CPU 开销。
- 减少上下文切换:对于 CPU 密集型工作负载,适当增大 sched_latency(如 12ms~24ms),让进程运行更长时间。
- 降低延迟:对于交互式或延迟敏感型应用,减小 min_granularity(如 0.5ms),提高调度响应速度。
- 注意:过度减小 min_granularity 会导致大量上下文切换,增加 CPU 开销和缓存污染。
2.4 关键数据结构与代码路径
CFS 的核心数据结构定义在 kernel/sched/sched.h 和 kernel/sched/fair.c 中。以下是最关键的几个结构体:
| 数据结构 | 定义位置 | 关键字段 | 作用 |
|---|---|---|---|
| struct sched_entity | sched.h | vruntime, load, run_node, on_rq | 调度实体,内嵌在 task_struct 中,是红黑树的节点 |
| struct cfs_rq | sched.h | tasks_timeline (rb_root), rb_leftmost, min_vruntime | CFS 就绪队列,管理红黑树和调度统计 |
| struct sched_class | sched.h | enqueue_task, dequeue_task, pick_next_task, put_prev_task | 调度类接口,CFS 通过 fair_sched_class 实现 |
核心函数调用链(选择下一个进程):
pick_next_task_fair()
├── __pick_first_entity() // 获取 rb_leftmost 节点
├── set_next_entity() // 更新调度实体状态
│ └── __dequeue_entity() // 从红黑树中移除当前进程
└── return se->task // 返回 task_struct
/proc/sched_debug 查看每个 CPU 的 CFS 红黑树状态,包括当前最左节点的 vruntime、min_vruntime、nr_running 等信息。使用 perf sched 工具可以可视化调度延迟和切换频率。
2.5 总结:CFS 调度器的核心机制
| 机制 | 描述 | 性能影响 |
|---|---|---|
| 红黑树 | 以 vruntime 为键值排序,最左节点为下一个调度进程 | O(log n) 插入/删除,O(1) 选择最左节点 |
| vruntime | 加权虚拟运行时间,低 nice 值增长慢,高 nice 值增长快 | 实现公平性,nice 值差异影响 CPU 分配比例 |
| 调度周期 | 保证所有进程在 sched_latency 内至少运行一次 | 增大可减少切换,但增加延迟;减小则提高响应性 |
| 最小粒度 | 进程连续运行的最小时间单位,防止过度抢占 | 过小导致大量上下文切换,过大降低交互响应 |