引言

Linux 内核调度器是操作系统最核心的组件之一,它决定了 CPU 时间如何在多个进程之间分配。自 Linux 2.6.23 引入 CFS(Completely Fair Scheduler,完全公平调度器)以来,Linux 调度器经历了从简单时间片轮转模型到完全公平理念的革命性转变。而随着移动设备和数据中心对能耗的日益关注,EAS(Energy-Aware Scheduling,能耗感知调度)的出现进一步将能效优化纳入了调度决策的核心。

本文将深入剖析 CFS 调度器的核心算法与实现细节,并在此基础上阐释 EAS 能耗感知调度如何在异构多核架构(ARM big.LITTLE/DynamIQ)上实现性能与功耗的最佳平衡。

第一部分:CFS 调度器的设计哲学

1.1 核心思想——虚拟运行时间(vruntime)

CFS 摒弃了传统的时间片分配模型,转而采用一个简洁而优雅的抽象:虚拟运行时间(virtual runtime, vruntime)。每个进程维护一个 vruntime 值,表示该进程在虚拟时间轴上已经"运行"了多久。CFS 的核心原则是选择 vruntime 最小的进程优先执行,从而保证所有可运行进程的 vruntime 趋向一致,实现"完全公平"。

vruntime 的计算公式如下:

vruntime += (实际运行时间 × NICE_0_LOAD) / 进程权重

其中 NICE_0_LOAD 是优先级为 0 的进程权重常量(1024),进程权重 由进程的 nice 值映射而来。这意味着低优先级(nice 值高)的进程权重较小,同样的实际运行时间会累积更多的 vruntime,从而被"惩罚"而获得较少的 CPU 时间;高优先级进程则相反。

1.2 红黑树——高效选择最小 vruntime

CFS 使用红黑树(Red-Black Tree)作为调度队列的数据结构。红黑树是一种自平衡二叉搜索树,所有可运行进程以 vruntime 为键值插入树中。树的最左侧节点即为 vruntime 最小的进程,可以被 O(1) 时间找到。插入和删除操作均为 O(log N),在成千上万个进程的场景下依然保持高效。

内核源码中,CFS 的红黑树由struct rq(运行队列)中的cfs_rq管理:

struct cfs_rq {
    struct load_weight load;       // 队列总权重
    unsigned int nr_running;       // 可运行进程数
    u64 min_vruntime;              // 最小 vruntime 基准
    struct rb_root_cached tasks_timeline;  // 红黑树根节点(含最左节点缓存)
    struct rb_node *rb_leftmost;   // 快速访问最左节点
    // ...
};

1.3 调度粒度与抢占策略

CFS 没有固定时间片,而是通过sched_latency(目标延迟)和min_granularity(最小粒度)参数动态计算每个进程的应得时间片:

time_slice = sched_latency / nr_running(不低于 min_granularity)

默认 sched_latency 为 6ms,min_granularity 为 0.75ms。当可运行进程增多时,每个进程的时间片自动缩小,保证响应延迟可控。

CFS 采用隐式抢占策略:当新进程的 vruntime 与当前进程的 vruntime 差距小于 sched_min_granularity 时不触发抢占,避免因频繁切换导致的缓存抖动。

第二部分:CFS 的核心操作

2.1 入队与出队(enqueue/dequeue)

进程被唤醒或创建时调用enqueue_task_fork()/enqueue_task(),将进程的调度实体struct sched_entity嵌入红黑树。进程睡眠或被抢占时调用dequeue_task(),从树中移除。

关键代码路径(kernel/sched/fair.c):

static void enqueue_task_fair(struct rq *rq, struct task_struct *p, int flags) {
    struct cfs_rq *cfs_rq;
    struct sched_entity *se = &p->se;
    
    // 遍历各级调度组
    for_each_sched_entity(se) {
        if (se->on_rq)
            goto enqueue;
        cfs_rq = cfs_rq_of(se);
        enqueue_entity(cfs_rq, se, flags);  // 实际入队操作
        cfs_rq->h_nr_running++;             // 队列计数器递增
        flags = ENQUEUE_WAKEUP;
    }
}

enqueue_entity()函数会更新进程的 vruntime(取 min_vruntime 与自身 vruntime 的较大值,防止新进程因 vruntime 过小而"饥饿"老进程),然后调用__enqueue_entity()执行红黑树插入。

2.2 进程选择(pick_next_task)

调度发生时,pick_next_task_fair()从红黑树中取出最左节点:

static struct task_struct *pick_next_task_fair(struct rq *rq) {
    struct cfs_rq *cfs_rq = &rq->cfs;
    struct sched_entity *se;
    struct task_struct *p;
    
    // 如果红黑树为空,返回 NULL
    if (!cfs_rq->nr_running)
        return NULL;
    
    // 获取最左节点(最小 vruntime)
    se = pick_next_entity(cfs_rq, NULL);
    // 处理组调度,获取实际的任务
    set_next_entity(cfs_rq, se);
    p = task_of(se);
    return p;
}

2.3 组调度(CGroup Integration)

CFS 天然支持 Linux CGroup 机制,每个 CGroup 拥有独立的cfs_rq和权重。嵌套 CGroup 形成树状结构,顶层 CGroup 内的进程通过"比例共享"模型分配 CPU:

  • 每个 CGroup 的 CPU 份额 = 自身权重 / 同级所有 CGroup 权重之和 × 父 CGroup 可用 CPU
  • 用户可通过 cpu.shares(默认 1024)调整 CGroup 的相对权重
  • cpu.cfs_quota_us 和 cpu.cfs_period_us 可设置 CGroup 的硬上限

第三部分:多核调度与负载均衡

3.1 每 CPU 运行队列

在多核系统中,每个 CPU 拥有独立的struct rq和cfs_rq。这避免了全局红黑树的锁竞争,但也引入了负载不均的问题。

3.2 负载均衡机制

Linux 通过SMP 负载均衡在 CPU 间迁移任务,核心逻辑在load_balance()中实现:

  1. 空闲负载均衡(Idle Load Balance):当 CPU 空闲时,主动从最繁忙的 CPU 迁移任务
  2. 周期性负载均衡(Periodic Load Balance):通过时钟中断定期检查 CPU 间负载差异
  3. 唤醒负载均衡(Wakeup Load Balance):进程唤醒时选择最空闲的 CPU
  4. NUMA 平衡(NUMA Balancing):在 NUMA 架构下,将任务迁移到靠近其内存的 CPU 节点

负载的计算采用指数移动平均(EMA)模型,避免短期抖动影响:

load_avg = load_avg × decay + new_load × (1 - decay)

3.3 Sched Domain

Linux 使用sched_domain层次化组织 CPU,自底向上通常为:

  • MC 域(Multi-Core):同一物理核上的逻辑 CPU(SMT/超线程)
  • DIE 域(Die):同一芯片上的所有物理核
  • NUMA 域:同一节点的所有芯片

负载平衡从最底层开始,先在 MC 域内平衡,再逐步向上到 NUMA 域,减少跨节点迁移。

第四部分:EAS 能耗感知调度

4.1 为什么需要 EAS?

传统 CFS 只关注公平性,不关心功耗。但在 ARM big.LITTLE / DynamIQ 架构中,高性能核(big)与高能效核(LITTLE)的功耗差异可能达到 5-10 倍。将计算密集型任务放到 LITTLE 核上运行会因执行时间大幅延长而"得不偿失",而将轻量级后台任务放到 big 核上则是纯粹的能源浪费。

EAS 的核心目标:在保证性能满足的前提下,选择功耗最小的 CPU 配置。

4.2 EAS 的核心模型

EAS 引入了CPU 能效模型(Energy Model, EM),为每个 CPU 频率点定义功耗与容量:

struct em_perf_state {
    unsigned int frequency;    // 频率(kHz)
    unsigned int power;        // 功耗(mW)
    unsigned int cost;         // 能效代价(用于 OPP 排序)
};

struct em_perf_domain {
    struct em_perf_state *table;
    unsigned int nr_perf_states;
    unsigned long cpus[];
};

每个 CPU 的功耗 = idle_power + dynamic_power,其中:

  • idle_power:CPU 空闲时的静态功耗
  • dynamic_power = capacity² × power / max_capacity² × load / max_load(动态近似)

4.3 EAS 调度决策

EAS 在每次任务选取 CPU 时,计算放置在不同 CPU 上的能耗代价,选择代价最小的方案。其决策流程:

  1. 评估每个 CPU 域的能量影响:将任务放在某个 CPU 后,该 CPU 需要升频到什么程度?增加的功耗是多少?
  2. 选择总功耗最低的方案:比较迁移到不同 CPU 所带来的额外功耗
  3. 利用 LITTLE 核优先:当任务不需要高性能时,优先放到高能效的小核
  4. 动态升频大核:当小核频率利用率超过 up_threshold(默认 85%)时,触发升频或迁移到大核

关键判断逻辑(简化):

target_cpu = min_energy_cpu(possible_cpus) {
    for each cpu in pd_cpus:
        // 执行容量 = 任务利用率 × CPU 最大容量
        cpu_cap = task_util × cpu.max_capacity / 1024
        
        // 需要的频率 = cpu_cap × max_capacity / cpu_capacity × max_freq
        req_freq = cpu_cap × cpu.max_freq / cpu.capacity
        
        // 活跃的功耗
        active_power = em->power(req_freq) × cpu_util
        
        // 空闲功耗(未被使用的核心)
        idle_power = em->idle_power(other_busy_cpus)
        
        energy[cpu] = active_power + idle_power
    return min(energy)
}

4.4 EAS 与 IPA 的协同

EAS 通常与 IPA(Intelligent Power Allocation)协同工作:

  • EAS 选择任务在哪个 CPU 上运行
  • IPA 根据 EAS 的决策,动态调整 CPU 频率和电压(DVFS),确保在满足性能需求的前提下最小化功耗
  • IPA 还管理 SoC 的散热预算(power budget),在温度过高时限制 CPU 频率

4.5 实际能效收益

在典型使用场景中,EAS 的能效收益表现为:

  • 视频播放场景:相比纯 CFS 调度,电池续航提升 15-25%
  • 社交媒体应用:轻量交互任务被智能路由到 LITTLE 核,降低平均功耗 30%
  • 游戏场景:EAS 能更精准地将渲染线程分配到 big 核,避免因 LITTLE 核偶发满载导致的掉帧
  • 后台服务(服务器):在数据中心场景,EAS 模型延伸出的 Energy-Aware Scheduling 可帮助节省机房整体能耗

第五部分:实战—内核参数调优与观测

5.1 CFS 相关调优参数

参数路径默认值说明
sched_latency_ns/proc/sys/kernel/sched_latency_ns24,000,000 ns (24ms)目标调度延迟
sched_min_granularity_ns/proc/sys/kernel/sched_min_granularity_ns3,000,000 ns (3ms)最小抢占粒度
sched_wakeup_granularity_ns/proc/sys/kernel/sched_wakeup_granularity_ns4,000,000 ns (4ms)唤醒抢占粒度
sched_migration_cost_ns/proc/sys/kernel/sched_migration_cost_ns500,000 µs进程迁移成本(决定缓存热迁移阈值)

5.2 EAS 相关调优参数

参数路径说明
schedutilscaling governorEAS 默认的频率调节 governor,根据调度负载直接选择频率
rate_limit_us/dev/policy*/rate_limit_usschedutil 的频率转换间隔
up_threshold/sys/devices/system/cpu/cpufreq/升频阈值(小核利用率超过此值触发升频或迁移)
energy_performance_preference/sys/devices/system/cpu/cpufreq/policy*/energy_performance_preference能耗性能偏好(performance/balance_performance/balance_power/power)

5.3 观察与诊断工具

# 查看进程的 vruntime
cat /proc/[pid]/sched | grep vruntime

# 查看 CPU 频率与调度器状态
turbostat --show CPU,Busy%,Bzy_MHz,TSC_MHz,CorWatt -i 1

# 查看 EAS 能耗模型
cat /sys/devices/system/cpu/cpufreq/policy*/scaling_available_frequencies
cat /sys/kernel/debug/energy_model/energy_model

# perf sched 查看调度延迟
perf sched record -- sleep 5
perf sched latency

# eBPF 工具追踪调度事件
bpftrace -e 'tracepoint:sched:sched_switch { printf("%s -> %s\n", args->prev_comm, args->next_comm); }'

第六部分:总结与展望

从 CFS 到 EAS,Linux 内核调度器的演进体现了开源社区对计算效率的追求永无止境。CFS 用 vruntime 和红黑树实现了简洁优雅的公平调度,EAS 则在此基础上引入能效模型,让调度决策从"谁更公平"升级为"谁更节能"。

面向未来,Linux 调度器还将面临以下挑战与方向:

  • 异构计算:随着 NPU、DPU、GPU 的加入,调度器需要管理更复杂的执行单元
  • 虚拟化场景:Host 与 Guest 的调度器协同,避免双重调度带来的效率损失
  • 实时性增强:SCHED_DEADLINE、SCHED_EXT(6.12 引入的外挂调度器框架)让实时性和可扩展性双双提升
  • AI 驱动的调度决策:利用机器学习预测任务行为,实现更智能的功耗-性能平衡

Linux 内核调度器始终在不断进化——从毫秒级的公平共享到微瓦级的能效感知,它持续证明着一件事:底层基础设施的创新,是上层应用体验最坚实的基础。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部