进程调度器是操作系统内核中最核心、也最容易被误解的子系统。它决定了 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)的来源。
| 概念 | CFS | EEVDF |
|---|---|---|
| 排序键 | 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 不需要修改用户态程序,但两类负载可能观察到行为变化:
- 依赖 CFS 抢占特性的低延迟任务:EEVDF 更少发生抢占,若任务
slice设置过大,唤醒延迟反而可能上升,可用sched_setattr()显式设置sched_runtime - CPU 密集批处理:EEVDF 在任务数少时与 CFS 行为接近,但任务数多时吞吐可能略有变化,因为合格性判定引入了额外的分支
三、调度类与优先级体系
3.1 五大调度类
Linux 采用「调度类链表」结构,pick_next_task() 从优先级最高的类开始遍历,第一个返回非空任务的类胜出:
| 调度类 | 策略常量 | 优先级范围 | 典型用途 |
|---|---|---|---|
stop_sched_class | 无(内核内部) | 最高 | CPU 热插拔、migration 线程 |
dl_sched_class | SCHED_DEADLINE | 按 deadline 排序 | 硬实时周期性任务 |
rt_sched_class | SCHED_FIFO / SCHED_RR | 1–99 | 软实时、音视频线程 |
fair_sched_class | SCHED_NORMAL / SCHED_BATCH / SCHED_IDLE | 100–139(nice -20–19) | 通用进程 |
idle_sched_class | SCHED_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 竞争时) |
|---|---|---|
| -20 | 88761 | 约 45.6% |
| -10 | 9548 | 约 15.0% |
| -5 | 3355 | 约 6.6% |
| 0 | 1024 | 约 2.4%(100 个任务时) |
| 5 | 335 | 约 0.8% |
| 10 | 110 | 约 0.3% |
| 19 | 15 | 约 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:唤醒时是否在该域寻找空闲 CPUSD_SHARE_PKG_RESOURCES:域内是否共享 LLC 等资源(影响 cache-hot 迁移决策)SD_NUMA:是否执行 NUMA 感知的迁移
4.2 负载均衡的三个触发点
内核在三种时机触发负载均衡,均由 fair_sched_class 实现:
- idle balance:当前 CPU 即将进入 idle 时,主动到更高层级的域「拉取」任务,避免 CPU 空转
- newidle balance:CPU 刚变为 idle(
schedule()选择了 idle 任务)时触发,比 idle balance 更轻量 - 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 |
| 关闭周期 tick | nohz_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 performance | scaling_governor |
| 禁用节能状态 | cpupower idle-set -D 0 | cpuidle 目录 |
注意:即使配置完整,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 调度器把系统卡死:
- 看门狗(watchdog):若某个任务在
scx_bpf_error()之前长时间未被调度,内核会打印警告并中止该调度器 - 超时回退:
/sys/kernel/sched_ext/下的enable_seq与state反映状态,出错时自动切回 CFS/EEVDF - 禁用开关:启动参数
sched_ext=disabled或sysctl kernel.sched_ext_enable=0可全局关闭 - 验证器约束: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 等性能观测工具的系统性使用指南
延伸阅读
- Linux Kernel Documentation:
Documentation/scheduler/sched-design-CFS.rst - Linux Kernel Documentation:
Documentation/scheduler/sched-eevdf.rst - Linux Kernel Documentation:
Documentation/scheduler/sched-stats.rst - LWN.net, “An EEVDF CPU scheduler for Linux”(Jonathan Corbet,2023)
- LWN.net, “The extensible scheduler class”(2023–2024 系列报道)
- Stoica I., Abdel-Wahab H., “Earliest Eligible Virtual Deadline First: A Flexible and Accurate Mechanism for Proportional Share Resource Allocation”(1995)
- 《Linux Kernel Development》(Robert Love 著)—— 调度器章节
- 《Understanding the Linux Kernel》(Bovet & Cesati 著)—— 进程调度与 SMP 均衡
- Linux 源码:
kernel/sched/fair.c、kernel/sched/core.c、kernel/sched/pelt.c、kernel/sched/ext.c - 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 "=== 完成 ==="
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。