Linux 调度器实现:从 CFS 到 EEVDF

从 CFS 的红黑树与 vruntime 出发,讲解 Linux 6.6 的 EEVDF 调度器如何用 lag 与虚拟截止时间替代权重比例分配,剖析五大调度类与 nice 权重换算,深入调度域负载均衡、PELT 跟踪、CPU 亲和性与隔离,并结合 sched_ext 与 perf sched 给出排查清单。

进程调度器是操作系统内核中最核心、也最容易被误解的子系统。它决定了 CPU 时间如何在成百上千个可运行任务之间切分,直接影响延迟、吞吐、公平性与能效。从 2007 年随 2.6.23 进入主线的 CFS(Completely Fair Scheduler),到 2023 年 Linux 6.6 正式替换它的 EEVDF(Earliest Eligible Virtual Deadline First),Linux 的通用调度策略在近二十年间只发生过一次根本性更迭,而这次更迭恰恰暴露了 CFS 长期被掩盖的设计缺陷。

本文从 CFS 的红黑树与 vruntime 出发,逐层剖析 EEVDF 引入的 lag 与虚拟截止时间(virtual deadline)如何修正权重比例分配的偏差,随后梳理调度类与优先级体系、调度域负载均衡、PELT 负载跟踪、CPU 亲和性与隔离手段,并介绍 sched_ext 这一允许用户态 BPF 程序接管调度决策的新框架,最后给出基于 /proc/schedstat、perf sched 等工具的实测与排查方法。


一、CFS 的设计与 vruntime 机制

1.1 从 O(1) 调度器到 CFS

在 CFS 之前,Linux 使用 O(1) 调度器,其核心是两个按优先级组织的运行队列数组(active / expired),通过位图在常数时间内选出下一个任务。O(1) 调度器的问题在于:它依赖启发式规则判断任务是否「交互式」,而这些规则在负载形态变化时会失效,导致桌面场景出现明显卡顿。

CFS 的核心思想是把「公平」形式化为一个可计算的不变量:每个任务应获得与其权重成正比的 CPU 时间。为了做到这一点,CFS 不再直接使用真实运行时间,而是把它除以权重,得到虚拟运行时间 vruntime:

vruntime += delta_exec * (NICE_0_LOAD / weight)

其中 NICE_0_LOAD 为 nice=0 对应的权重(1024),weight 由 set_load_weight() 根据 nice 值查表得到。权重越大,同样的真实运行时间带来的 vruntime 增量越小,任务就越「慢」地被推进到队列后方,从而获得更多 CPU。

1.2 红黑树与最左节点

CFS 用一棵以 vruntime 为键的红黑树(struct cfs_rq 中的 tasks_timeline)组织就绪任务,调度决策退化为「取最左节点」:

static struct sched_entity *__pick_first_entity(struct cfs_rq *cfs_rq)
{
    struct rb_node *left = rb_first_cached(&cfs_rq->tasks_timeline);
    if (!left)
        return NULL;
    return rb_entry(left, struct sched_entity, run_node);
}

红黑树保证插入、删除、查找最左节点均为 O(log n),且 rb_leftmost 缓存让「取下一个」的查找接近 O(1)。每个 CPU 拥有独立的 cfs_rq,任务迁移时 vruntime 会随 cfs_rq->min_vruntime 做归一化,避免跨 CPU 后因时间基准不同而获得不合理的优先级。

1.3 min_vruntime 与时钟推进

CFS 不会让空闲 CPU 的 vruntime 基准无限落后。每个 cfs_rq 维护 min_vruntime,取队列中最小 vruntime 与上一次值的较大者,并在调度 tick 中单调推进。当新任务入队时,其 vruntime 被钳制到不小于 min_vruntime - sysctl_sched_latency 的水平,这就是著名的「新任务不能凭借极小的 vruntime 长期霸占 CPU」的补偿逻辑。

# 查看 CFS 的可调参数
sysctl kernel.sched_latency_ns kernel.sched_min_granularity_ns \
       kernel.sched_wakeup_granularity_ns kernel.sched_migration_cost_ns

1.4 sched_slice 与时间片分配

CFS 并非固定时间片,而是按队列总权重动态计算一个调度周期 sched_period,再把周期按权重比例切分给每个任务:

sched_slice(se) = period * se->load.weight / cfs_rq->load.weight

当队列中任务过多导致单个 sched_slice 小于 sched_min_granularity_ns(默认 0.75ms)时,周期不再按 sched_latency_ns 固定,而是按 nr_running * sched_min_granularity_ns 线性放大。这正是 CFS 在高并发下延迟劣化的根源:任务越多,每个任务等待被再次调度的时间越长,而权重比例公平性却依然被满足。


二、EEVDF 的 lag 与虚拟截止时间

2.1 CFS 遗留的延迟问题

CFS 保证的是长期比例公平,但它对延迟没有任何约束。设想一个 CPU 上有 100 个权重相同的任务,每个任务的时间片只有 0.75ms,而两次调度之间的间隔却可能达到 75ms。对交互式任务(如输入响应、音频线程)来说,这种抖动是不可接受的。

此外,CFS 的「最左 vruntime」策略存在一个隐蔽偏差:一个刚被唤醒的任务即使只运行了极短时间,也会因为 vruntime 最小而立刻抢占当前任务,造成频繁抢占;而一个已经运行很久的任务则会持续占据 CPU,直到其 vruntime 超过所有其他任务。两种行为都不理想。

2.2 EEVDF 的核心抽象:lag

EEVDF 源自 1995 年 Ion Stoica 与 Hussein Abdel-Wahab 的论文,其核心量是 lag,定义为任务「应得服务」与「已得服务」之差:

lag = (已运行时间按权重折算) - (理想公平份额)
  • lag > 0:任务欠服务(underserved),应尽快运行
  • lag < 0:任务超服务(overserved),应等待
  • lag = 0:任务处于理想状态

EEVDF 定义了**合格(eligible)**概念:只有 lag >= 0 的任务才允许被调度。这一条规则天然解决了 CFS 的抢占偏差——一个刚被唤醒但 lag 为负的任务不会立刻抢占当前运行的任务。

2.3 虚拟截止时间与 slice 分配

仅按 lag 调度会退化为「最短剩余时间优先」,导致短任务反复插队。EEVDF 因此引入虚拟截止时间(virtual deadline):每个任务在入队时被分配一个请求长度 slice,其虚拟截止时间为

vd = vruntime + slice / weight

调度器在所有合格任务中选择虚拟截止时间最小者。这一「合格性 + 最早截止」的组合,正是算法名称 EEVDF(Earliest Eligible Virtual Deadline First)的来源。

概念CFSEEVDF
排序键vruntime虚拟截止时间 vd
合格判定无(最左节点必选)lag >= 0
时间片sched_slice 由周期推导slice 由任务请求,受 sched_base_slice 限制
抢占条件唤醒即比较 vruntime需满足 lag 与 vd 双重条件
延迟可控性仅统计意义上公平可按 slice 显式控制

2.4 内核中的实现要点

EEVDF 在 Linux 6.6 中由 Peter Zijlstra 提交并合入,主要改动集中在 kernel/sched/fair.c:

  • struct sched_entity 新增 deadline 与 slice 字段
  • entity_eligible() 实现 lag >= 0 判定,使用 64 位定点运算处理权重倒数
  • pick_eevdf() 取代 __pick_first_entity(),在红黑树中查找满足合格性的最早截止任务
  • 唤醒抢占路径改为 wakeup_preempt_entity() 中的 deadline 比较
  • 相关可调参数变为 kernel.sched_base_slice_ns(默认 3ms)与 kernel.sched_min_slice_ns
# EEVDF 时代的核心可调参数
sysctl kernel.sched_base_slice_ns kernel.sched_min_slice_ns
# kernel.sched_base_slice_ns = 3000000
# kernel.sched_min_slice_ns = 750000

2.5 迁移注意事项

从 CFS 迁移到 EEVDF 不需要修改用户态程序,但两类负载可能观察到行为变化:

  1. 依赖 CFS 抢占特性的低延迟任务:EEVDF 更少发生抢占,若任务 slice 设置过大,唤醒延迟反而可能上升,可用 sched_setattr() 显式设置 sched_runtime
  2. CPU 密集批处理:EEVDF 在任务数少时与 CFS 行为接近,但任务数多时吞吐可能略有变化,因为合格性判定引入了额外的分支

三、调度类与优先级体系

3.1 五大调度类

Linux 采用「调度类链表」结构,pick_next_task() 从优先级最高的类开始遍历,第一个返回非空任务的类胜出:

调度类策略常量优先级范围典型用途
stop_sched_class无(内核内部)最高CPU 热插拔、migration 线程
dl_sched_classSCHED_DEADLINE按 deadline 排序硬实时周期性任务
rt_sched_classSCHED_FIFO / SCHED_RR1–99软实时、音视频线程
fair_sched_classSCHED_NORMAL / SCHED_BATCH / SCHED_IDLE100–139(nice -20–19)通用进程
idle_sched_classSCHED_IDLE(idle 任务)最低每 CPU 的空闲线程

3.2 SCHED_NORMAL / BATCH / IDLE

三者同属 fair_sched_class,共用 EEVDF 算法,差异在于调度器对它们的启发式处理:

  • SCHED_NORMAL:默认策略,唤醒时给予一定的 lag 补偿以降低交互延迟
  • SCHED_BATCH:标记为批处理,唤醒时不做交互补偿,sched_slice 更长,适合编译、渲染等吞吐型负载
  • SCHED_IDLE:权重被压到最低(WEIGHT_IDLEPRIO),只有 CPU 完全空闲时才运行,适合后台索引、日志压缩
# 以 SCHED_BATCH 运行一次大规模编译
chrt --batch 0 make -j$(nproc)

# 以 SCHED_IDLE 运行后台索引
chrt --idle 0 updatedb

3.3 SCHED_FIFO 与 SCHED_RR

实时调度类使用 1–99 的静态优先级(数值越大优先级越高),始终抢占任何 fair 类任务:

  • SCHED_FIFO:无时间片,任务运行直到主动阻塞或被更高优先级 RT 任务抢占
  • SCHED_RR:同优先级任务按固定时间片轮转,时间片由 sched_rr_timeslice_ms 控制(默认 100ms)
# 以 RT 优先级 50 运行,并设置 RR 时间片
sudo chrt -r 50 ./realtime-worker
sysctl kernel.sched_rr_timeslice_ms

风险提示:RT 任务若不主动让出 CPU,会完全饿死所有普通任务,甚至拖垮内核线程(如 ksoftirqd)导致系统无响应。Linux 从 3.14 起默认启用 RT throttling(kernel.sched_rt_runtime_us = 950000,即每周期 95%),留出 5% 给非 RT 任务。

3.4 SCHED_DEADLINE

SCHED_DEADLINE 基于 CBS(Constant Bandwidth Server)与 EDF(Earliest Deadline First),用三个参数描述任务:

sched_runtime   每次激活需要的 CPU 时间
sched_deadline  相对截止时间(<= sched_period)
sched_period    激活周期

带宽利用率 runtime / period 的求和不得超过 CPU 容量,否则 sched_setattr() 返回 EBUSY。这一准入控制(admission control)保证了实时任务集合的可调度性。

struct sched_attr attr = {
    .size = sizeof(attr),
    .sched_policy = SCHED_DEADLINE,
    .sched_runtime  = 2000000,   /* 2ms  */
    .sched_deadline = 10000000,  /* 10ms */
    .sched_period   = 10000000,  /* 10ms */
};
syscall(__NR_sched_setattr, 0, &attr, 0);

3.5 nice 值与权重换算表

nice 每降低 1,权重约增加 1.25 倍。内核使用 sched_prio_to_weight[] 查表完成换算:

nice权重CPU 占比(与 nice=0 竞争时)
-2088761约 45.6%
-109548约 15.0%
-53355约 6.6%
01024约 2.4%(100 个任务时)
5335约 0.8%
10110约 0.3%
1915约 0.04%

注意「占比」是相对于同队列所有任务之和而言的,实际占比随竞争者数量变化。nice 只影响相对比例,不构成任何时间保证。


四、调度域与负载均衡

4.1 sched_domain 层级

多核与 NUMA 系统中,CPU 之间的访问代价差异巨大。内核用调度域(sched_domain)描述这种拓扑层次,每个域包含若干调度组(sched_group),自底向上依次为:

SMT (超线程)
  └─ MC  (同一物理核簇)
       └─ PKG / DIE (同一插槽)
            └─ NUMA (同一 NUMA 节点)
                 └─ NUMA 之上:跨节点域
# 查看调度域拓扑
cat /proc/schedstat | head -20
ls /sys/kernel/debug/sched/domains/   # 需要 debugfs 挂载

每个 sched_domain 带有一组标志位,决定该层级允许的均衡行为,例如:

  • SD_LOAD_BALANCE:是否在该域执行负载均衡
  • SD_BALANCE_WAKE:唤醒时是否在该域寻找空闲 CPU
  • SD_SHARE_PKG_RESOURCES:域内是否共享 LLC 等资源(影响 cache-hot 迁移决策)
  • SD_NUMA:是否执行 NUMA 感知的迁移

4.2 负载均衡的三个触发点

内核在三种时机触发负载均衡,均由 fair_sched_class 实现:

  1. idle balance:当前 CPU 即将进入 idle 时,主动到更高层级的域「拉取」任务,避免 CPU 空转
  2. newidle balance:CPU 刚变为 idle(schedule() 选择了 idle 任务)时触发,比 idle balance 更轻量
  3. periodic balance:由 sched_balance_trigger() 在调度 tick 中周期性触发,从当前域向上逐级尝试迁移,直到负载平衡或到达顶层域

每次均衡的核心是 find_busiest_group() 与 find_busiest_queue(),通过 avg_load、group_capacity、group_util 等指标比较各组的「失衡程度」,再调用 detach_tasks() 从最忙队列摘取任务。

4.3 迁移代价与 cache-hot 判断

迁移并非无代价:任务的 cache 与 TLB 状态在目标 CPU 上需要重新建立。内核用 sysctl_sched_migration_cost_ns(默认 500000ns)作为启发式阈值——若任务最近在该 CPU 上运行且「cache hot」,则倾向于不迁移。

# 调整迁移代价阈值(高吞吐场景可适当提高)
sysctl kernel.sched_migration_cost_ns
sysctl kernel.sched_nr_migrate      # 单次均衡最多迁移的任务数(默认 32)

4.4 EAS 与 NUMA Balancing

在 ARM 大小核平台,EAS(Energy Aware Scheduling) 用能量模型(EM)替代传统的均衡启发式,在唤醒时直接把任务放到「刚好能容纳其 util 的最小核」上,兼顾性能与功耗。在 NUMA 平台,NUMA Balancing 通过定期取消映射(PROT_NONE)页表项捕获跨节点访问,并在 task_numa_fault() 中累积统计,触发 migrate_task_to() 把任务迁往访问更频繁的节点。

# 查看 NUMA 均衡状态
cat /proc/sys/kernel/numa_balancing
grep -E 'numa_(hint_faults|pte_updates|migrate)' /proc/vmstat

五、PELT 负载跟踪

5.1 从瞬时负载到指数衰减

早期调度器用「运行队列长度」衡量负载,但瞬时值噪声极大。PELT(Per-Entity Load Tracking) 从 Linux 3.8 引入,为每个调度实体(任务、cgroup、运行队列)维护指数衰减的平均值。

其核心是几何级数求和:把时间切成 1ms(LOAD_AVG_PERIOD)的窗口,每个窗口的贡献按 y^n 衰减,其中 y = 0.97857206(32 位定点近似),对应半衰期约 32ms:

load_sum(t) = load_sum(t-1) * y + 新增贡献

5.2 三个关键平均值

struct sched_avg 中最重要的三个量:

字段含义典型用途
load_avg按权重折算的负载(含 se->load.weight)负载均衡中判断「忙不忙」
runnable_avg可运行时间占比(含等待 CPU 的时间)判断 CPU 是否过载
util_avg实际占用 CPU 的时间占比(不含等待)EAS、schedutil 调频、CPU 容量比较

三者的区别至关重要:util_avg 衡量「真正吃掉的 CPU」,runnable_avg 衡量「想要但没拿到的 CPU」。当 runnable_avg >> util_avg 时,说明该 CPU 存在排队,是扩容或迁移的信号。

5.3 读取 PELT 数据

# 全局 PELT 可见性开关(部分内核需要显式开启)
cat /proc/sys/kernel/sched_debug 2>/dev/null
mount -t debugfs none /sys/kernel/debug
cat /sys/kernel/debug/sched/debug | head -40

/proc/sched_debug 输出中,每个 cfs_rq 会打印 .avg_load、.avg_util、.avg_runnable,可以直接观察某个 CPU 的负载形态:

cpu#0, 2400.000 MHz
  .nr_running                    : 3
  .load                          : 3072
  .avg_load                      : 2841
  .avg_util                      : 2103
  .avg_runnable                  : 2950

5.4 容量感知与利用率钳制

现代内核为每个 CPU 定义 cpu_capacity(归一化到 1024),并引入 uclamp(utilization clamping):任务可以通过 sched_setattr() 设置 sched_util_min / sched_util_max,向调频器与 EAS 表达「我至少需要多少算力」。Android 大量使用 uclamp 来提升前台应用的频率下限,同时压制后台任务。

# 查看 uclamp 相关 sysctl
sysctl kernel.sched_util_clamp_min kernel.sched_util_clamp_max
# 默认 1024 / 1024 表示不钳制

六、CPU 亲和性与隔离

6.1 taskset 与 sched_setaffinity

CPU 亲和性(affinity)把任务限制在特定 CPU 集合上运行,是降低 cache 抖动与调度噪声最直接的手段:

# 让进程只在 CPU 2、3 上运行
taskset -c 2,3 ./my-worker

# 查看某个进程当前的亲和性掩码
taskset -p $(pgrep -f my-worker)

# 修改运行中进程的亲和性
taskset -cp 4,5 12345

在代码中对应 sched_setaffinity() / sched_getaffinity() 系统调用,cpu_set_t 结构承载掩码。注意亲和性只是软约束:sched_setaffinity 成功不代表任务一定不迁移,内核仍可能在掩码范围内做负载均衡。

6.2 isolcpus 与 nohz_full

若要真正把 CPU 从通用调度中「摘出来」,需要内核启动参数配合:

isolcpus=domain,managed_irq,2-3
nohz_full=2-3
rcu_nocbs=2-3
参数作用
isolcpus=把指定 CPU 从调度域中移除,普通任务不会被自动均衡上去
nohz_full=在这些 CPU 上关闭调度时钟中断(除一个任务外),减少 tick 干扰
rcu_nocbs=把 RCU 回调卸载到其他 CPU,避免目标 CPU 被软中断打断
irqaffinity=把中断默认亲和性移出隔离 CPU
# 验证隔离是否生效
cat /sys/devices/system/cpu/isolated
cat /proc/cmdline | tr ' ' '\n' | grep -E 'isolcpus|nohz_full|rcu_nocbs'
# 查看目标 CPU 上的任务数(应为 0 或仅剩 kthread)
ps -eLo psr,pid,comm | awk '$1==2'

6.3 cpuset 控制组

isolcpus 是全局的、静态的;cpuset 则提供了运行时可变的、层级化的 CPU 与内存节点划分,是容器与实时场景更推荐的方案:

# 创建 cpuset 并把任务限定到 CPU 4-7
mkdir /sys/fs/cgroup/rt-pool
echo "+cpuset" > /sys/fs/cgroup/cgroup.subtree_control
echo "4-7" > /sys/fs/cgroup/rt-pool/cpuset.cpus
echo "0" > /sys/fs/cgroup/rt-pool/cpuset.mems
echo $$ > /sys/fs/cgroup/rt-pool/cgroup.procs

6.4 隔离场景下的完整配置清单

目标手段检查点
排除通用任务isolcpus= 或 cpuset/sys/devices/system/cpu/isolated
关闭周期 ticknohz_full=/proc/cmdline、tick_nohz_full_mask
卸载 RCU 回调rcu_nocbs=ps -eLo psr,comm | grep rcuo
移出设备中断irqaffinity=/proc/irq/*/smp_affinity
移出内核线程手动 taskset/proc/sched_debug 中的 nr_running
锁定频率cpupower frequency-set -g performancescaling_governor
禁用节能状态cpupower idle-set -D 0cpuidle 目录

注意:即使配置完整,nohz_full 的 CPU 上仍可能因 migration/N 内核线程、timerfd 或未绑定中断而被唤醒,务必用 perf sched latency 与 tracing 逐项确认。


七、sched_ext 可扩展调度器

7.1 设计动机

传统的调度器策略全部硬编码在内核中,任何实验性策略都需要重新编译内核并承担极高的验证成本。sched_ext(Linux 6.12 正式合入,CONFIG_SCHED_CLASS_EXT)把调度决策委托给 BPF 程序,让调度器成为一个可以在运行时加载、卸载、替换的组件。

其核心机制:

  • 新增 ext_sched_class,优先级介于 stop 与 dl 之间(可配置)
  • BPF 程序通过 struct sched_ext_ops 实现回调:enqueue、dispatch、pick_task、running、stopping、tick 等
  • 内核提供 BPF_MAP_TYPE_STRUCT_OPS 承载调度器状态
  • 支持 DSQ(Dispatch Queue) 抽象,用户态可自定义多级队列并实现优先级、抢占、负载均衡

7.2 一个最小可用的 scx 调度器

/* minimal_scx.bpf.c —— 基于 FIFO 的最简 sched_ext 调度器 */
#include <scx/common.bpf.h>

char _license[] SEC("license") = "GPL";

s32 BPF_STRUCT_OPS_SLEEPABLE(minimal_init)
{
    return 0;
}

void BPF_STRUCT_OPS(minimal_enqueue, struct task_struct *p, u64 enq_flags)
{
    /* 所有任务进入全局 FIFO 队列 */
    scx_bpf_dispatch(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
}

SCX_OPS_DEFINE(minimal_ops,
               .enqueue = (void *)minimal_enqueue,
               .init    = (void *)minimal_init,
               .name    = "minimal");
# 编译并加载(需要 clang 与 sched_ext 头文件)
clang -O2 -target bpf -c minimal_scx.bpf.c -o minimal_scx.bpf.o
scx_loader minimal_scx.bpf.o

# 观察是否生效
cat /sys/kernel/sched_ext/state      # enabled
cat /sys/kernel/sched_ext/root/ops   # minimal

7.3 生态与实战调度器

社区已在 sched-ext/scx 仓库中提供了多个生产可用调度器:

调度器策略特点适用场景
scx_rusty多域、按权重分发、支持负载均衡通用服务器
scx_lavd面向延迟的虚拟截止时间策略游戏、桌面、手持设备
scx_bpfland简单全局 FIFO + 唤醒抢占低延迟通用
scx_layered用户态定义分层规则多租户、混合负载
scx_flatcg扁平化 cgroup 调度容器密度优化
# 使用 scx_lavd 并启用自动模式
sudo scx_lavd --autopilot

# 查看当前调度器统计
sudo scx_lavd --monitor 5

7.4 安全边界与回退机制

sched_ext 设计了多道保险,避免有缺陷的 BPF 调度器把系统卡死:

  1. 看门狗(watchdog):若某个任务在 scx_bpf_error() 之前长时间未被调度,内核会打印警告并中止该调度器
  2. 超时回退:/sys/kernel/sched_ext/ 下的 enable_seq 与 state 反映状态,出错时自动切回 CFS/EEVDF
  3. 禁用开关:启动参数 sched_ext=disabled 或 sysctl kernel.sched_ext_enable=0 可全局关闭
  4. 验证器约束:BPF 验证器限制程序复杂度与内存访问,禁止睡眠上下文中的非法操作

生产环境使用前,务必在相同负载模型下与 EEVDF 做 A/B 对比,并保留 sysrq 与带外管理通道,以便调度器异常时快速恢复。


八、调度观测与性能排查

8.1 /proc/schedstat

/proc/schedstat 提供每个 CPU、每个调度域以及每个任务的累计统计,是分析调度行为的低成本入口:

# 每个 CPU 一行:yield_count, schedule_count, ttwu_count...
head -n $(nproc) /proc/schedstat

# 每个调度域的负载均衡统计(load_balance 次数、失败原因)
grep -A1 '^domain' /proc/schedstat | head -30

# 每个任务的运行与等待时间(第 7、8 列为 run_delay 与 pcount)
grep '^cpu#' -A2 /proc/schedstat

关键字段解读:

字段含义异常信号
ttwu_count唤醒次数异常高说明存在频繁阻塞/唤醒循环
ttwu_local本地 CPU 唤醒占比占比低说明跨 CPU 唤醒多,缓存失效严重
run_delay在运行队列中等待的累计时间高值说明 CPU 饱和或亲和性配置不当
pcount迁移次数高值说明负载均衡过于激进

8.2 perf sched

perf sched 是定位调度延迟与迁移问题的主力工具:

# 采集 10 秒的调度事件
sudo perf sched record -- sleep 10

# 按任务汇总等待延迟
sudo perf sched latency --sort max

# 查看唤醒链与迁移路径
sudo perf sched timehist --highlight-only
sudo perf sched map --color-parts 0.5

# 统计迁移次数
sudo perf sched stats

perf sched latency 输出中的 Maximum 与 Average 列直接反映「任务被唤醒后多久才真正拿到 CPU」,是判断调度噪声最直接的指标。

8.3 chrt / schedtool / sched_debug

# 查看进程调度策略与优先级
chrt -p $(pgrep -f my-worker)
# pid 12345's current scheduling policy: SCHED_OTHER
# pid 12345's current scheduling priority: 0

# schedtool 可一次性设置策略与亲和性
schedtool -B -n -5 -a 2-3 -e ./my-worker

# 内核调度器内部状态快照
cat /proc/sched_debug | head -60

8.4 常见问题排查清单

现象可能原因排查手段
交互任务卡顿队列过长导致 slice 过小perf sched latency、sched_base_slice_ns
吞吐低于预期过度迁移导致 cache 失效perf sched stats 迁移次数、sched_migration_cost_ns
RT 任务抖动被其他 RT 任务或中断抢占perf sched record、rt_runtime_us
隔离 CPU 仍有任务内核线程或中断未移出ps -eLo psr,comm、/proc/irq/*/smp_affinity
NUMA 性能差跨节点访存频繁numastat、numa_balancing 统计
调频不及时util_avg 偏低或 uclamp 过松schedutil trace、sched_util_clamp_min
唤醒延迟高亲和性掩码过窄taskset -p、/proc/sched_debug
负载不均调度域标志配置不当/proc/schedstat 域统计

排查时建议按「全局是否饱和 → 哪类任务排队 → 调度延迟分布 → 迁移与 NUMA → 中断干扰」的顺序推进,每一步用上一节列出的工具交叉验证,避免直接跳到调参。


相关阅读

  • https://plumephp.com/os-cpu-scheduling/ —— 调度策略与算法的入门梳理,与本文的实现视角互为补充
  • https://plumephp.com/os-realtime-scheduling/ —— 实时调度、优先级反转与 PREEMPT_RT 的完整分析
  • https://plumephp.com/os-performance-tools/ —— perf、ftrace、bcc 等性能观测工具的系统性使用指南

延伸阅读

  1. Linux Kernel Documentation: Documentation/scheduler/sched-design-CFS.rst
  2. Linux Kernel Documentation: Documentation/scheduler/sched-eevdf.rst
  3. Linux Kernel Documentation: Documentation/scheduler/sched-stats.rst
  4. LWN.net, “An EEVDF CPU scheduler for Linux”(Jonathan Corbet,2023)
  5. LWN.net, “The extensible scheduler class”(2023–2024 系列报道)
  6. Stoica I., Abdel-Wahab H., “Earliest Eligible Virtual Deadline First: A Flexible and Accurate Mechanism for Proportional Share Resource Allocation”(1995)
  7. 《Linux Kernel Development》(Robert Love 著)—— 调度器章节
  8. 《Understanding the Linux Kernel》(Bovet & Cesati 著)—— 进程调度与 SMP 均衡
  9. Linux 源码:kernel/sched/fair.c、kernel/sched/core.c、kernel/sched/pelt.c、kernel/sched/ext.c
  10. sched-ext 官方仓库:https://github.com/sched-ext/scx

#!/usr/bin/env bash
# ============================================================
# 完整可运行示例:调度器观测与隔离实验
# 需要 root 权限(部分步骤在受限容器中不可用)
# 建议在 x86_64 或 arm64 的 Linux 6.6+ 上运行
# ============================================================
set -uo pipefail

echo "=== 步骤 0: 环境与内核版本 ==="
uname -r
grep -E 'isolcpus|nohz_full|rcu_nocbs' /proc/cmdline || echo "(未配置隔离参数)"

echo
echo "=== 步骤 1: 当前调度策略与优先级 ==="
for pid in 1 $$; do
    chrt -p "$pid" 2>/dev/null || true
done

echo
echo "=== 步骤 2: EEVDF / CFS 可调参数 ==="
for k in sched_base_slice_ns sched_min_slice_ns \
         sched_latency_ns sched_min_granularity_ns \
         sched_migration_cost_ns sched_nr_migrate \
         sched_rt_runtime_us sched_rr_timeslice_ms; do
    v=$(sysctl -n "kernel.$k" 2>/dev/null || echo "N/A")
    printf '  kernel.%-28s = %s\n' "$k" "$v"
done

echo
echo "=== 步骤 3: 每 CPU 调度统计 ==="
head -n "$(nproc)" /proc/schedstat | awk '{printf "  cpu%-3s yield=%-8s sched=%-10s ttwu=%-10s\n", $1, $5, $6, $7}'

echo
echo "=== 步骤 4: 亲和性与隔离验证 ==="
echo "  isolated CPUs : $(cat /sys/devices/system/cpu/isolated 2>/dev/null || echo none)"
echo "  nohz_full     : $(cat /sys/devices/system/cpu/nohz_full 2>/dev/null || echo none)"
echo "  NUMA balancing: $(cat /proc/sys/kernel/numa_balancing 2>/dev/null || echo N/A)"

echo
echo "=== 步骤 5: 用 taskset + chrt 跑一次受控负载 ==="
TARGET_CPU=0
[ "$(nproc)" -gt 1 ] && TARGET_CPU=1
echo "  在 CPU ${TARGET_CPU} 上以 SCHED_BATCH 运行 3 个 busy loop(2 秒)"
for i in 1 2 3; do
    taskset -c "$TARGET_CPU" chrt --batch 0 \
        bash -c 'end=$((SECONDS+2)); while [ $SECONDS -lt $end ]; do :; done' &
done
wait

echo
echo "=== 步骤 6: perf sched 快速采样 ==="
if command -v perf >/dev/null 2>&1; then
    perf sched record -- sleep 3 2>/dev/null || true
    perf sched latency --sort max 2>/dev/null | head -15 || echo "  (需安装 linux-tools)"
else
    echo "  perf 未安装,跳过"
fi

echo
echo "=== 步骤 7: sched_ext 状态(Linux 6.12+) ==="
if [ -d /sys/kernel/sched_ext ]; then
    echo "  state : $(cat /sys/kernel/sched_ext/state 2>/dev/null)"
    echo "  ops   : $(cat /sys/kernel/sched_ext/root/ops 2>/dev/null || echo none)"
else
    echo "  sched_ext 未启用"
fi

echo
echo "=== 步骤 8: 调度器内部快照 ==="
if [ -r /sys/kernel/debug/sched/debug ]; then
    head -30 /sys/kernel/debug/sched/debug
else
    head -30 /proc/sched_debug 2>/dev/null || echo "  (需 root 或 CONFIG_SCHED_DEBUG)"
fi

echo
echo "=== 完成 ==="

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「os」更多文章

  1. ARM64 体系结构与内核实现
  2. 内核网络栈:sk_buff、NAPI 与 XDP
  3. eBPF 开发实战:CO-RE 与 libbpf