一、为什么Linux需要CFS

在CFS(Completely Fair Scheduler)出现之前,Linux使用的是O(1)调度器,该调度器虽然在高负载下表现良好,但存在复杂的启发式交互判断和不公平的CPU时间分配问题。2007年,Linux 2.6.23引入了CFS,它彻底抛弃了传统的时间片和固定优先级分配方式,转而采用一种基于虚拟运行时的全新调度模型。

CFS的核心思想极其简洁:模拟一个理想的、精确多任务处理的CPU,在这个理想CPU上,每个任务都能在1/n的时间内获得CPU(n为可运行任务数)。现实中的CPU只能在某个时刻运行一个任务,因此CFS的目标就是尽可能逼近这个理想模型。

二、CFS核心数据结构:红黑树与vruntime

CFS使用红黑树(Red-Black Tree)来组织所有可运行任务,以任务的虚拟运行时间vruntime作为排序键值。红黑树保证了O(log n)的插入、删除和查找最左节点(最小vruntime)操作效率。

// 核心数据结构(简化)
struct sched_entity {
    u64 vruntime;          // 虚拟运行时间(核心字段)
    u64 exec_start;        // 本次开始执行的实际时间
    u64 sum_exec_runtime;  // 累计实际运行时间
    u64 prev_sum_exec_runtime; // 上次切换时的累计运行时间
};

struct cfs_rq {
    struct rb_root_cached runqueue;  // 红黑树根节点
    struct sched_entity *curr;       // 当前运行的任务
    unsigned int nr_running;         // 可运行任务数量
    u64 min_vruntime;                // 树中最小vruntime(用于归一化)
};

vruntime的计算公式:

vruntime += (delta_exec * NICE_0_LOAD) / se->load.weight

其中delta_exec是实际执行时间,NICE_0_LOAD是nice值为0的权重,load.weight是当前任务的权重。这意味着高优先级任务(nice值低)的vruntime增长更慢,因此能更频繁地被调度。

三、调度核心逻辑:pick_next_task_fair

当CPU需要选择下一个运行任务时,CFS调用pick_next_task_fair(),其核心步骤是:

  1. 从红黑树中找到vruntime最小的节点(最左节点)
  2. 将该任务从红黑树中移除(如果不在树上则为当前任务curr)
  3. 设置CFS运行队列的min_vruntime并更新
  4. 如果vruntime导致min_vruntime溢出,则归一化整个红黑树
static struct task_struct *pick_next_task_fair(struct rq *rq)
{
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;
    
    // 若无可运行任务,直接返回NULL
    if (!cfs_rq->nr_running)
        return NULL;
    
    // 获取最左节点(最小vruntime)
    se = pick_next_entity(cfs_rq);
    set_next_entity(cfs_rq, se);
    
    // 返回对应的task_struct
    return task_of(se);
}

四、任务入队与出队:enqueue_entity / dequeue_entity

入队操作enqueue_entity:当一个新任务变为可运行状态(TASK_RUNNING),需要将其插入到CPU对应的CFS红黑树中。

static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
    struct rb_node *parent = NULL;
    struct sched_entity *entry;
    int leftmost = 1;

    // 红黑树插入逻辑:按vruntime从左到右递增
    while (*link) {
        parent = *link;
        entry = rb_entry(parent, struct se_node, run_node);
        if (entity_before(se, entry)) {
            link = &parent->rb_left;
        } else {
            link = &parent->rb_right;
            leftmost = 0;
        }
    }
    
    rb_link_node(&se->run_node, parent, link);
    rb_insert_color(&se->run_node, &cfs_rq->tasks_timeline);
}

关键细节:新唤醒的线程的vruntime会被调整为max(se->vruntime, cfs_rq->min_vruntime - sysctl_sched_latency),防止饥饿问题——如果一个任务睡眠时间过长导致vruntime远小于其他任务,它会垄断CPU一段时间。

五、时间片与调度粒度:sched_slice与sched_period

CFS不使用固定时间片,而是动态计算每个任务应运行的时长(称为slice):

static u64 sched_slice(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
    unsigned int nr_running = cfs_rq->nr_running;
    u64 slice = __sched_period(nr_running);
    // 按权重分配slice
    slice = div_u64(slice * se->load.weight, cfs_rq->load.weight);
    return slice;
}

调度周期sysctl_sched_period默认为6ms(SCHED_PERIOD),最小粒度min_granularity一般为0.75ms。当运行任务数越多,每个任务的slice越小,保证公平性同时兼顾交互响应。

六、组调度与带宽控制:task_group与cfs_bandwidth

从Linux 2.6.24开始,CFS支持组调度(Group Scheduling),允许将任务分组并限制每个组的CPU使用率:

struct cfs_bandwidth {
    raw_spinlock_t lock;
    ktime_t period;           // 周期长度(默认100ms)
    u64 quota;                // 周期内可用时间(-1为无限)
    u64 runtime;              // 剩余可用时间
    struct hlist_head throttled_cfs_rq; // 被限流的CFS队列
};

通过cfs_period_us和cfs_quota_us两个cgroup参数,可以精确控制某个组(如Docker容器)的CPU使用上限。例如设置quota为period的0.5倍,则该组最多使用50%的CPU。

七、多核负载均衡:sched_domain与性能优化

在现代多核系统中,CFS的管理在硬件上扩展到多个CPU核心。每个CPU维护独立的CFS运行队列,但当某个CPU空闲时,会从繁忙CPU的任务队列中拉取任务,这就是负载均衡。

负载均衡的层级结构(sched_domain):

  • MC(Multi-Core):同一物理CPU内部的多核心之间
  • SYS(System):不同NUMA节点之间

负载均衡触发时机包括:定时tick检查、CPU进入空闲时、新fork子进程时。尽量在同一CPU的睡眠中上唤醒被同一运行队列管理的任务(smt亲和性),减少IPI(处理器间中断)开销。

八、CFS调优实践与内核参数

参数默认值说明
sched_latency_ns24ms调度周期(默认让每个任务至少运行一次所需时间)
sched_min_granularity_ns3ms最小调度粒度(保证交互任务不被过度抢占)
sched_wakeup_granularity_ns4ms唤醒抢占粒度(控制新唤醒任务抢占当前任务的倾向)
sched_migration_cost_ns500000缓存热迁移成本(防止不必要的迁移)

交互密集型场景优化建议:减小sched_min_granularity_ns和sched_wakeup_granularity_ns可以提升桌面响应速度,但会增加上下文切换开销。服务器场景则可以适当增大这些值来减少切换开销。

九、最新进展:sched_ext与可拓展调度器

Linux 6.12引入了sched_ext(Scheduling Extension),允许用户空间自定义调度策略并加载到内核中。这为特殊场景(如游戏引擎的帧同步、AI推理的突发任务)提供了更灵活的调度能力,而无需修改内核代码。

基于调度分级(sched_class)的框架,CFS仍然是默认的公平调度器,但sched_ext的引入意味着Linux调度器进入了一个可扩展的新时代——用户可以根据自己的工作负载特征编写定制化的eBPF程序来实现特定的调度策略。

十、总结

CFS是Linux内核中设计最优雅的调度子系统之一。它通过虚拟运行时间的概念将公平用数学方式精确量化,利用红黑树高效地维护了O(log n)复杂度的任务选择,并通过负载均衡策略适配多核架构。理解CFS不仅有助于系统性能调优,更是深入学习操作系统调度原理的必经之路。

在当今容器化和云原生时代,CFS的组调度能力(cgroup cpu控制)和带宽管理功能成为了Kubernetes等容器编排平台CPU资源管理的底层基石,对理解整个云原生基础设施有着不可替代的价值。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部