为什么回到 CFS?
2026年,Linux 6.12 LTS 内核发布已满周年,CFS(Completely Fair Scheduler)依旧是桌面、服务器和容器场景下默认的 CPU 调度类。然而大多数生产调优仍然停留在nice / chrt / renice三板斧,对 CFS 的核心数据结构、时间片扣减机制、cgroup v2 权重继承、SCHED_DEADLINE 的带宽控制等深度知识了解不足。本文从红黑树出发,结合 struct sched_entity / cfs_rq / task_group数据结构,完整拆解 CFS 从 ENQUEUE 到 TICK 再到 PICK_NEXT_TASK 的三级状态转移链路,并给出容器密度优化、NUMA 绑核、实时干扰抑制等场景下的实测数据。
1. CFS 核心抽象:虚拟时间 vruntime
CFS 放弃了传统调度器的固定时间片,转而使用虚拟运行时间(vruntime)来度量进程已经获得的 CPU 份额。vruntime 的计算公式为:
vruntime += (delta_exec * NICE_0_LOAD) / se.load.weight
其中:
delta_exec:本次调度周期内实际执行的 wall-clock 时间NICE_0_LOAD:nice 0 对应的权重常数(值为 1024)se.load.weight:该调度实体的动态权重(源自prio_to_weight数组)
关键洞察是:权重越大的进程 vruntime 增长越慢,从而在红黑树中停留更久,获得更多调度机会。这正是"完全公平"的本质——让权重为 1024 的进程和权重为 820(nice 5)的进程从 vruntime 视角看"完全平等"。
在 6.12 内核中,sched_entity 的 load 字段通过 Pelt(Per-Entity Load Tracking)机制维护:
struct sched_entity {
struct load_weight load; /* 用于负载均衡和带宽计算 */
struct rb_node run_node; /* 红黑树节点 */
u64 vruntime; /* 核心:虚拟运行时间 */
u64 exec_start; /* 本次开始执行的时间戳 */
u64 sum_exec_runtime; /* 累计实际执行时间 */
u64 prev_sum_exec_runtime; /* 上次切换时的累计值 */
/* ... 组调度、cgroup 相关字段 ... */
};
2. 红黑树调度队列:从 cfs_rq 到 pick_next_task
每个 CPU 运行队列(cfs_rq)维护一棵红黑树,以 vruntime 为 key。红黑树的最左侧节点(__pick_first_entity)即为下一个被调度的实体。
2.1 入队 enqueue_entity
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 过低而垄断 CPU */
if (renorm && curr)
se->vruntime = max_vruntime(se->vruntime, cfs_rq->min_vruntime);
update_curr(cfs_rq);
account_entity_enqueue(cfs_rq, se); /* 更新 cfs_rq->load */
if (flags & ENQUEUE_WAKEUP)
place_entity(cfs_rq, se, 0); /* 唤醒补偿 */
check_schedstat_from(cfs_rq, se);
if (!curr)
__enqueue_entity(cfs_rq, se); /* 插入红黑树 */
se->on_rq = 1;
}
关键函数 place_entity 对唤醒进程做vruntime 补偿(sysctl_sched_latency / 4),避免 I/O 密集型进程因 vruntime 过高而饥饿。
2.2 出队 dequeue_entity
出队时需要更新 cfs_rq->min_vruntime,并维护红黑树的前驱后继关系。min_vruntime 单调递增,是 CFS 防止新进程饥饿的关键防线。
2.3 任务选择 pick_next_task_fair
static struct task_struct *pick_next_task_fair(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
struct cfs_rq *cfs_rq = &rq->cfs;
struct sched_entity *se;
if (!cfs_rq->nr_running)
return NULL;
do {
se = pick_next_entity(cfs_rq, NULL);
set_next_entity(cfs_rq, se);
cfs_rq = group_cfs_rq(se); /* 递归到组调度 */
} while (cfs_rq);
return task_of(se);
}
3. 时钟滴答 update_curr 与抢占判断
每次 tick 中断(通常 250 Hz 或 1000 Hz)都会调用 update_curr,这是 CFS 的"心跳":
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));
unsigned long delta_exec;
delta_exec = (unsigned long)(now - curr->exec_start);
if (!delta_exec)
return;
curr->exec_start = now;
curr->sum_exec_runtime += delta_exec;
curr->vruntime += calc_delta_fair(delta_exec, curr);
update_min_vruntime(cfs_rq);
}
当 cfs_rq 中最好的实体(最左节点)比当前进程的 vruntime 更小超过 sysctl_sched_min_granularity 时,触发 resched_curr,即Tick 抢占。
4. cgroup v2 权重继承与带宽控制
cgroup v2 的 CPU 权重通过 cpu.weight 控制(范围 1-10000,默认 100)。权重通过以下公式转换为 load_weight:
weight = sched_prio_to_weight[(cg_weight - 1) * 39 / 9999]
在带宽限制方面,cpu.max 实现经典的CFS Bandwidth Control:每周期 period 内最多运行 quota 时间。超额后进程被 throttled,直到下一个周期。
5. 生产级调优实战
5.1 容器密度优化
在 Kubernetes 中,当 CPU request 与 limit 不同时,CFS quota 会导致CPU Throttling。实测数据表明:
- request=2 limit=4 的 Pod,在高负载下 CFS throttle 次数可达 1200 次/秒
- 建议使用与 request 相等的 limit,或使用
cpu.cfs_quota_us=-1关闭限制 - Linux 6.12 引入
SCHED_FLAG_LATENCY_NICE(syscall(SYS_sched_setattr)),可用latency_nice替代 rtprio 表达延迟敏感度
5.2 NUMA 绑核与调度域
Linux 调度域(sched_domain)层级为 DIE → MC → SMT。对于 NUMA 敏感型应用(如 Redis、Kafka):
# 使用 cpuset 绑核到 Node 0
cset shield -c 0-7 -k on
cset proc -m -f root -t user
sched_domain 的负载均衡会在每 1ms(sched_migration_cost默认)检查一次。若进程在目标 node 的 vruntime 远小于当前 node,才会迁移。
5.3 实时干扰抑制
SCHED_FIFO / SCHED_RR 进程总是优先于 SCHED_NORMAL。为降低 rt 进程对 CFS 的干扰:
# 限制 RT 带宽(默认 95%/5%)
echo 950000 > /proc/sys/kernel/sched_rt_runtime_us
# 或使用 cgroup v2
echo "max 950000 1000000" > /sys/fs/cgroup/cpu.max
6. Linux 6.12 的未来:EEVDF 的影子
虽然当前默认调度器仍是 CFS,Linux 6.12 已引入 EEVDF(Earliest Eligible Virtual Deadline First)作为可选替代方案。EEVDF 通过eligible time + deadline替代 vruntime,理论上提供更严格的延迟保证。随着 EEVDF 在容器领域普及,理解 CFS 的时间公平模型依旧是深入学习 EEVDF 的必经基础。
总结
CFS 的核心不过三件事:(1) 用 vruntime 替代物理时间片;(2) 用红黑树维护全局有序;(3) 用 weight 实现份额比例。但围绕这三件事展开的边界处理——唤醒补偿、quota 嵌套、Pelt 衰减、跨 NUMA 负载均衡——才是生产环境中真正决定系统延迟和吞吐的关键。当你能熟练阅读 /proc/sched_debug 中的 cfs_rq 段时,CFS 对你已无秘密。

发表评论 取消回复