引言
Linux 内核的进程调度器是操作系统的核心组件之一,它决定了哪个进程在何时获得 CPU 时间。自 2.6.23 版本以来,完全公平调度器(Completely Fair Scheduler, CFS) 取代了传统的 O(1) 调度器,成为 Linux 默认的进程调度器。CFS 的设计理念——"尽可能公平地分配 CPU 时间给所有可运行进程"——看似简单,但其实现却精妙而深刻。
本文将从 CFS 的核心概念出发,深入剖析其底层数据结构、虚拟运行时间计算机制、组调度支持、实时调度策略,以及生产环境中的调优实践。
1. CFS 核心设计理念
CFS 的核心思想是维护一个虚拟运行时间(Virtual Runtime, vruntime)。每个进程都有一个 vruntime 值,表示该进程在虚拟时钟下已经运行的时间。调度器总是选择 vruntime 最小的进程执行,从而在宏观上保证所有进程获得"公平"的 CPU 份额。
关键原则:
- 理想多处理器模型:假设有 N 个相同的进程,每个应获得 1/N 的 CPU 时间
- 红黑树组织:所有可运行进程按 vruntime 排列在红黑树中,最左侧节点即为下一个调度的目标
- 细粒度补偿:被唤醒的进程会根据当前最小 vruntime 进行调整,避免长时间睡眠后"霸占"CPU
2. 核心数据结构
2.1 sched_entity(调度实体)
每个进程的调度信息存储在 sched_entity 结构中:
struct sched_entity {
struct load_weight load; // 负载权重(与 nice 值相关)
struct rb_node run_node; // 红黑树节点
u64 vruntime; // 虚拟运行时间 ⭐ 核心字段
u64 exec_start; // 本次开始执行的时间
u64 sum_exec_runtime; // 总实际运行时间
u64 prev_sum_exec_runtime; // 上一次切换时的总运行时间
// ...
};
load 字段的权重由 nice 值映射表决定。nice 值每降低 1 级(优先级提高),权重增加约 25%;每升高 1 级,权重减少约 10%。这意味着 nice -20 的进程获得的 CPU 时间约为 nice 19 的进程的 ~3300 倍。
2.2 cfs_rq(CFS 运行队列)
每个 CPU 核心维护一个 CFS 运行队列:
struct cfs_rq {
struct load_weight load; // 队列总权重
unsigned long runnable_weight; // 可运行总权重
unsigned int nr_running; // 可运行进程数
u64 min_vruntime; // 队列最小 vruntime(单调递增)
struct rb_root_cached tasks_timeline; // 红黑树根节点 ⭐
// 负载跟踪(PELT 机制)
struct sched_avg avg;
// ...
};
min_vruntime 是整个队列的基准线。新创建进程的 vruntime 会被设置为当前 min_vruntime,而不是 0,从而防止新进程"霸占"CPU。
2.3 进程描述符中的调度相关字段
struct task_struct {
const struct sched_class *sched_class; // 调度类
struct sched_entity se; // 普通调度实体
struct sched_rt_entity rt; // 实时调度实体
int policy; // 调度策略
int static_prio; // 静态优先级 (100-139)
int normal_prio; // 动态优先级
int rt_prio; // 实时优先级 (0-99)
// ...
};
3. 虚拟运行时间(vruntime)的精确计算
CFS 的公平性依赖于 vruntime 的精确计算:
delta_vruntime = delta_exec × (NICE_0_LOAD / curr->load)
其中:
delta_exec:进程实际运行的时钟时间NICE_0_LOAD:nice 0 进程的权重基准(1024)curr->load:当前进程的权重
关键洞察:高权重进程(低 nice 值)的 vruntime 增长更慢,因此获得更多 CPU 时间;低权重进程(高 nice 值)的 vruntime 增长更快,被调度频率更低。
例如,两个进程 A(nice 0, weight=1024)和 B(nice 5, weight 335):
- A 运行 10ms → vruntime 增加 10ms × (1024/1024) = 10ms
- B 运行 10ms → vruntime 增加 10ms × (1024/335) ≈ 30.6ms
- 结果:A 与 B 的 CPU 时间比 ≈ 3:1
4. 组调度(CGroup 支持)
CFS 支持层级调度:用户组(task group)内部再运行 CFS,组间再进行公平分配。这在容器和 cgroup 场景中至关重要。
// 层级调度实体
struct task_group {
struct sched_entity **se; // 每个 CPU 一个调度实体
struct cfs_rq **cfs_rq; // 每个 CPU 一个运行队列
unsigned long shares; // 组权重(cpu.shares)
// Linux CGroup v2: cpu.max (quota/period)
};
容器场景示例:两个容器分别设置 cpu.shares 为 1024 和 512,当它们同时争抢 CPU 时,获得的 CPU 时间比为 2:1。若使用 CGroup v2 的 cpu.max 设置 100000 100000,则该容器最多使用 1 个 CPU 核心。
5. 实时调度策略
除了 CFS 处理的 SCHED_NORMAL(普通进程),Linux 还支持两种实时策略:
| 策略 | 优先级 | 说明 |
|---|---|---|
| SCHED_FIFO | 1-99 | 先进先出,高优先级可抢占低优先级,不进行时间片轮转 |
| SCHED_RR | 1-99 | 轮转策略,同优先级进程按时间片轮转执行 |
实时进程 vs 普通进程:实时进程(优先级 0-99)永远优先于普通进程(优先级 100-139)。即使普通进程的 vruntime 很小,只要有可运行的实时进程,就会立即抢占。
SCHED_DEADLINE(自 3.14 内核新增)则基于 EDF(最早截止时间优先)算法,适用于严格的实时任务。进程声明 (runtime, period, deadline),内核保证每个周期内至少获得 runtime 的 CPU 时间。
6. PELT:负载跟踪机制
CFS 使用 Per-Entity Load Tracking (PELT) 来跟踪进程和运行队列的负载。该机制通过衰减累加器追踪 CPU 利用率:
load_avg = load_avg × decay + active × (1 - decay)
衰减因子确保历史数据的影响随时间指数衰减,窗口约为 32ms。PELT 的输出用于:
- 负载均衡:将任务从繁忙 CPU 迁移到空闲 CPU
- CPU 频率调节:cpufreq governor 感知负载变化
- 容量感知调度(CAS):大小核架构中选择合适的核心
2023 年 Linux 6.6 引入了 CQW(Concurrent Queue Workload) 补丁,改进了多线程工作负载的 Pelt 响应延迟问题。
7. 生产环境调优实践
7.1 关键 sysctl 参数
# 调度粒度(默认 1ms,越低响应越快但上下文切换开销越大)
kernel.sched_min_granularity_ns = 1000000
# 唤醒抢占粒度(默认 2ms,降低可被唤醒进程抢占的阈值)
kernel.sched_wakeup_granularity_ns = 2000000
# 迁移成本(影响负载均衡决策,默认 500000ns)
kernel.sched_migration_cost_ns = 500000
# NUMA 负载均衡间隔
kernel.numa_balancing = 1 # 启用 NUMA 自动均衡
7.2 进程优先级控制
# 设置进程 nice 值(-20 最高,19 最低)
nice -n -10 ./cpu_intensive_task
renice -n -5 -p 12345
# 设置实时优先级
chrt -f -p 50 12345 # SCHED_FIFO 优先级50
chrt -r -p 30 12345 # SCHED_RR 优先级30
# 使用 taskset 绑定 CPU
taskset -c 0-3 ./pinned_task
# 使用 cgroup v2 限制容器资源
mkdir /sys/fs/cgroup/mycontainer
echo "50000 100000" > /sys/fs/cgroup/mycontainer/cpu.max
echo 12345 > /sys/fs/cgroup/mycontainer/cgroup.procs
7.3 延迟敏感型应用优化
数据库、消息队列等低延迟应用常见配置:
- 设置
SCHED_RR或SCHED_FIFO实时优先级 - 通过
isolcpus隔离 CPU 核心供独占使用 - 将中断绑定到非隔离核心(
/proc/irq/*/smp_affinity) - 使用
NO_FULL_NOHZ减少 tick 中断对计算核心的影响 - 启用
SCHED_BATCH降低批处理任务的调度频率
7.4 容器密度优化
高密部署场景下:
- CFS 带宽控制(cpu.cfs_budelta/kuota)限制 CPU 突发
- 设置
sched_latency_ns与sched_min_granularity_ns的比例为 8:1,在公平性和切换开销间平衡 - 使用
cpu.weight(CGroup v2, 1-10000)替代 shares,控制更直观
8. 调度器的未来方向
Linux 内核社区持续改进调度器:
- Core Scheduling:解决 L1TF/Meltdown 漏洞的副作用,限制同一组进程共享物理核心,同时减少跨核侧信道风险
- 调度器扩展(sched_ext):Linux 6.12 引入的可扩展调度框架,允许用户空间实现自定义调度策略,无需修改内核源码
- 异构架构优化:Intel Thread Director 和 ARM big.LITTLE 的协同调度改进
- NUMA 感知增强:自动页面迁移与进程放置的联合优化
总结
CFS 通过虚拟运行时间和红黑树数据结构,在简洁的设计中实现了高效的公平调度。理解 vruntime 计算机制、PELT 负载跟踪、组调度层级和实时策略优先级,是进行系统级性能调优的基础。随着 sched_ext 等用户可编程调度框架的成熟,Linux 调度器的灵活性和可扩展性将进一步提升,为云原生、实时计算和异构硬件场景提供更精细的控制能力。

发表评论 取消回复