为什么回到 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 对你已无秘密。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部