引言
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()中实现:
- 空闲负载均衡(Idle Load Balance):当 CPU 空闲时,主动从最繁忙的 CPU 迁移任务
- 周期性负载均衡(Periodic Load Balance):通过时钟中断定期检查 CPU 间负载差异
- 唤醒负载均衡(Wakeup Load Balance):进程唤醒时选择最空闲的 CPU
- 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 上的能耗代价,选择代价最小的方案。其决策流程:
- 评估每个 CPU 域的能量影响:将任务放在某个 CPU 后,该 CPU 需要升频到什么程度?增加的功耗是多少?
- 选择总功耗最低的方案:比较迁移到不同 CPU 所带来的额外功耗
- 利用 LITTLE 核优先:当任务不需要高性能时,优先放到高能效的小核
- 动态升频大核:当小核频率利用率超过
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_ns | 24,000,000 ns (24ms) | 目标调度延迟 |
| sched_min_granularity_ns | /proc/sys/kernel/sched_min_granularity_ns | 3,000,000 ns (3ms) | 最小抢占粒度 |
| sched_wakeup_granularity_ns | /proc/sys/kernel/sched_wakeup_granularity_ns | 4,000,000 ns (4ms) | 唤醒抢占粒度 |
| sched_migration_cost_ns | /proc/sys/kernel/sched_migration_cost_ns | 500,000 µs | 进程迁移成本(决定缓存热迁移阈值) |
5.2 EAS 相关调优参数
| 参数 | 路径 | 说明 |
|---|---|---|
| schedutil | scaling governor | EAS 默认的频率调节 governor,根据调度负载直接选择频率 |
| rate_limit_us | /dev/policy*/rate_limit_us | schedutil 的频率转换间隔 |
| 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 内核调度器始终在不断进化——从毫秒级的公平共享到微瓦级的能效感知,它持续证明着一件事:底层基础设施的创新,是上层应用体验最坚实的基础。

发表评论 取消回复