引言
Linux 内核的进程调度器是操作系统最核心的组件之一,它负责决定哪个进程获得 CPU 时间。从早期的 O(n) 调度器到 O(1) 调度器,Linux 2.6.23 内核引入的完全公平调度器(CFS, Completely Fair Scheduler)彻底改变了进程调度的设计理念。本文将深入剖析 CFS 的红黑树核心数据结构、虚拟运行时间(vruntime)计算机制、调度延迟与粒度平衡策略,并提供生产环境中的实战调优指南。
一、CFS 的设计哲学
CFS 的核心理念可以用一句话概括:让每个进程获得"公平"的 CPU 份额。与传统的基于时间片轮询的调度器不同,CFS 不直接使用时间片,而是通过以下方式实现公平性:
- 维护每个进程的虚拟运行时间(vruntime),记录进程已获得的加权 CPU 时间
- 使用红黑树按 vruntime 对所有可运行进程排序
- 始终选择 vruntime 最小的进程进行调度(最"亏欠" CPU 的进程)
- 通过权重(weight)差异化分配给不同 nice 值的进程
这种设计避免了传统调度器中时间片到期后的批量切换开销,系统随着进程增多仅以 O(log N) 增长。
二、核心数据结构
2.1 struct sched_entity(调度实体)
CFS 以调度实体而非进程为基本单位,这意味着它可以调度单个进程、进程组甚至整个用户会话。每个调度实体包含以下关键字段:
struct sched_entity {
struct load_weight load; // 基于 nice 值的权重
struct rb_node run_node; // 红黑树节点
u64 exec_start; // 本次调度开始时间
u64 sum_exec_runtime; // 总实际运行时间
u64 vruntime; // 虚拟运行时间
u64 prev_sum_exec_runtime; // 上次调度时的总运行时间
...
};
2.2 CFS 红黑树(struct cfs_rq)
每个 CPU 运行队列上维护一个 CFS 运行队列:
struct cfs_rq {
struct load_weight load; // 队列总权重
unsigned int nr_running; // 可运行进程数
u64 min_vruntime; // 树中最小 vruntime(基准值)
struct rb_root tasks_timeline; // 红黑树根
struct rb_node *rb_leftmost; // 最左节点(缓存,加速 pick_next)
struct sched_entity *curr, *next, *last;
};
红黑树的关键性质充分发挥:最左侧节点始终是 vruntime 最小的进程,通过缓存 rb_leftmost 可在 O(1) 时间内找到下一个调度目标。
三、虚拟运行时间(vruntime)的计算
vruntime 是 CFS 实现加权公平的核心。其计算公式为:
vruntime_delta = (实际运行时间 delta_exec * NICE_0_LOAD) / 进程权重
实际上内核使用以下高效实现(kernel/sched/fair.c):
static void update_curr(struct cfs_rq *cfs_rq)
{
struct sched_entity *curr = cfs_rq->curr;
u64 now = rq_clock_task(rq_of(cfs_rq));
u64 delta_exec;
delta_exec = now - curr->exec_start;
curr->exec_start = now;
curr->sum_exec_runtime += delta_exec;
// 核心:vruntime 增量 = delta_exec * (NICE_0_LOAD / weight)
curr->vruntime += calc_delta_fair(delta_exec, curr);
// 更新 min_vruntime 以保持基准单调递增
update_min_vruntime(cfs_rq);
}
关键点解读:
- nice 值越小(优先级越高),权重越大,vruntime 增长越慢,获得了更多实际 CPU 时间
calc_delta_fair()使用位移运算避免浮点操作,NICE_0_LOAD=1024 对应 nice=0- 权重每级差约 1.25 倍,即 nice 值相差 1 级时 CPU 份额差异约 10%
四、调度时机与核心操作
4.1 进程入队(enqueue_entity)
当进程被唤醒或新创建时,会被插入 CFS 红黑树。如果该进程之前正在运行或刚从睡眠中醒来,其 vruntime 可能被调整以避免长时间等待导致的"饥饿反转":
static void enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED);
bool curr = cfs_rq->curr == se;
// 处理 vruntime 归一化
if (renorm && curr)
se->vruntime += cfs_rq->min_vruntime;
update_curr(cfs_rq);
if (renorm && !curr)
se->vruntime += cfs_rq->min_vruntime;
// 实际入队:位置 = max(vruntime, min_vruntime - sysctl_sched_latency)
if (flags & ENQUEUE_WAKEUP)
place_entity(cfs_rq, se, 0);
account_entity_enqueue(cfs_rq, se);
if (flags & ENQUEUE_WAKEUP)
check_schedstat_required();
if (!curr)
__enqueue_entity(cfs_rq, se);
se->on_rq = 1;
}
4.2 进程出队(dequeue_entity)
当进程睡眠或停止时,从红黑树中移除,同时更新 min_vruntime:
static void dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
update_curr(cfs_rq);
// 保存当前 vruntime 到 prev,减去 min_vruntime 保持区间相对值
if (se != cfs_rq->curr)
__dequeue_entity(cfs_rq, se);
se->on_rq = 0;
account_entity_dequeue(cfs_rq, se);
// 出队时利用 se 的 vruntime 更新 min_vruntime
}
4.3 选择下一个进程(pick_next_entity)
static struct sched_entity *pick_next_entity(struct cfs_rq *cfs_rq)
{
// 直接从缓存的最左子节点获取
struct sched_entity *se = __pick_first_entity(cfs_rq);
struct sched_entity *left = se;
struct sched_entity *second;
if (cfs_rq->next && entity_before(cfs_rq->next, left))
second = exchange(&cfs_rq->next, left);
...
return left;
}
这就是为什么 rb_leftmost 缓存重要——在 tick 触发的抢占检查时,可以快速判断是否需要抢占当前进程。
五、调度延迟与粒度——精密的平衡
CFS 需要在两个目标间取得平衡:低延迟(快速响应) 和 高吞吐(减少切换开销)。内核通过以下 sysctl 参数控制:
# 默认值(内核 5.x+)
kernel.sched_latency_ns = 24000000 // 24ms,一个调度周期
kernel.sched_min_granularity_ns = 3000000 // 3ms,最小运行时间
kernel.sched_wakeup_granularity_ns = 4000000 // 4ms,唤醒抢占粒度
核心规则:
调度周期 = max(sched_latency_ns, nr_running * sched_min_granularity_ns)
进程时间片 = 调度周期 * (本进程权重 / 总权重)
这意味着当系统进程数少时(<8 个),所有进程能在 sched_latency 内至少运行一次;当进程数多时,通过 min_granularity 保证切换开销可控。
六、组调度与层级调度(cgroups 集成)
CFS 天然支持控制组(cgroup)调度,形成层次化的带宽分配:
- shares:定义组间 CPU 份额权重(默认 1024)
- cfs_quota/cfs_period:限制组的最大 CPU 使用率
- 层级限制:组的 quota 不能超过其父组的 quota
例如,将 Docker 容器的 CPU 限制为 50%:
docker run --cpus=0.5 ...
# 等价于 cpu.cfs_quota_us=50000 cpu.cfs_period_us=100000
// 内核校验逻辑:检查组的带宽是否超限
static int tg_set_cfs_bandwidth(struct task_group *tg, u64 period, u64 quota)
{
...
// 父组的可用 quota 检查
if (!cfs_bandwidth_can_quota(tg, quota))
return -EINVAL;
}
七、NUMA 感知调度
现代多核/多插槽系统的 NUMA 架构给 CPU 调度带来了新挑战。CFS 通过以下机制处理:
- NUMA balancing:自动将进程迁移到靠近其内存的 NUMA 节点
- sched_numa_balancing:通过缺页中断扫描进程的内存访问模式
- sched_scan_period_min/max:控制扫描频率(默认 100ms ~ 6s)
- 负载均衡:pull/push 跨节点迁移任务时考虑 NUMA 亲和性
八、实时调度策略
CFS 管理普通(SCHED_NORMAL)任务,而实时任务由独立的调度类处理。调度优先级链为:
stop_sched_class (最高,不可抢占)
dl_sched_class (SCHED_DEADLINE,基于 EDF 算法)
rt_sched_class (SCHED_FIFO / SCHED_RR,固定优先级)
fair_sched_class (CFS,SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE)
idle_sched_class (最低,仅空闲)
注意:当存在可运行的实时任务时,CFS 会被完全抢占,除非实时任务主动让出 CPU。
九、性能分析工具
分析 CFS 相关性能问题可参考以下工具:
perf sched record/report:记录和分析调度延迟perf sched latency:统计每个任务的调度延迟分布perf sched map:可视化 CPU 迁移和空闲时间/proc/<pid;/schedstat:进程级调度统计(运行时间/等待时间/切换次数)/sys/kernel/debug/sched/debug:调度器调试信息(需开启 CONFIG_SCHED_DEBUG)
$ perf sched record -- sleep 10
$ perf sched latency --sort max
-------------------------------------------------------------------------------
Task | Runtime ms | Switches | Average delay ms | Max delay ms |
-------------------------------------------------------------------------------
kworker/0:1H-(398) | 4.948 ms | 21 | 0.278 | 1.689 |
MainThread-(1586319) | 11.308 ms | 20 | 1.559 | 4.841 |
-------------------------------------------------------------------------------
十、生产环境调优指南
10.1 交互式桌面系统
# 降低调度延迟,提升响应速度
sysctl -w kernel.sched_latency_ns=12000000
sysctl -w kernel.sched_min_granularity_ns=1500000
sysctl -w kernel.sched_wakeup_granularity_ns=2000000
10.2 高吞吐服务器
# 增大调度周期,减少上下文切换
sysctl -w kernel.sched_latency_ns=48000000
sysctl -w kernel.sched_min_granularity_ns=6000000
sysctl -w kernel.sched_wakeup_granularity_ns=8000000
10.3 低延迟服务(数据库/交易系统)
# CPU 绑核 + 实时优先级 + 调度参数调优
chrt -f 99 ./trading_engine # SCHED_FIFO 优先级99
isocpus=4-7 nosoftlockup nohz_full=4-7 # 内核参数隔离核心
sysctl -w kernel.sched_rt_runtime_us=1000000 # 允许实时任务占满
# 非关键进程移到掩码外
cset shield --cpu=0-3 --kthread=on
10.4 排查调度抖动
当发现 CPU 使用率正常但执行时间抖动大时,按以下顺序排查:
- 检查
perf sched latency的 Max delay 是否异常 - 查看 kernel.watchdog 是否开启(NMI watchdog 引发中断延迟)
- 检查是否有过多的 timer tick 中断(CONFIG_HZ_PERIODIC vs CONFIG_NO_HZ_FULL)
- 排查 D 状态进程阻塞——
ps aux | awk '~/D/ {print}' - 检查 cgroup cpu.stat 的 throttled_time 是否增长
总结
CFS 通过红黑树和 vruntime 的优雅设计,在简洁的数据结构中实现了复杂的加权公平调度。作为 Linux 内核中最精妙的子系统之一,理解 CFS 不仅有助于系统性能调优,更能帮助开发者建立"公平调度"的系统设计思维。记住三个关键点:vruntime 决定公平、红黑树保证效率、粒度参数控制平衡——掌握这三者,方能驾驭 Linux 进程调度的精髓。
(本文基于 Linux 6.x 内核源码分析,适用于 5.x 及 6.x 系列版本)

发表评论 取消回复