sched discussion
[PATCH v6 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work
LLM 分析
sched/cache:减少 task_cache_work 的扫描开销
系列概况
- 标题:
[PATCH v6 0/2] Cache aware scheduling: Reduce the overhead of task_cache_work - 作者: Luo Gengkun
<luogengkun2@huawei.com> - 版本: v6(已历经 v1 到 v6 多轮迭代;cache-aware scheduling 主体已合入主干,本系列属于后继优化补丁)
- 规模: 2 个 patch,主体 +35/-10 行,附加调试 patch +43/-5 行
- 修改文件:
include/linux/mm_types.hinclude/linux/sched.hkernel/sched/fair.cinclude/trace/events/sched.h(调试 patch)kernel/sched/features.h(调试 patch)
- 代码统计:主体补丁 35+/10-,调试补丁 43+/5-
- Message-ID:
20260717041231.3324851-1-luogengkun2@huawei.com - 完整性: 完整 series;含 cover letter、主体 patch、显式标注 DO-NOT-APPLY 的调试 patch,以及 maintainer/Reviewer 回复(Chen Yu)
补丁目的
cache-aware scheduling 在每个 mm 上记录 pcpu_sched->epoch,由 task_cache_work() 周期性地扫描 LLC 域内全部 CPU,按 fraction_mm_sched 选 pref_llc、更新 nr_running_avg。在多 NUMA、CPU 数量很大的系统里这种全量扫描非常昂贵:perf top -e cycles:k 显示 task_cache_work 占 0.81%;Redis 在 400000 RPS、NUMA balancing 关闭时,p99 latency 相对 baseline 退化 25.68%。
本系列要解决的问题:让 task_cache_work 只扫"最近真正跑过本 mm"的 CPU,把扫描集合从全 LLC 收窄到 llc_epoch_affinity_timeout 内的访问子集。
旧流程的问题
旧实现每次都遍历 sched_domain_span(sd) 内全部 CPU,绝大多数 CPU 上的 pcpu_sched 长期为 0,但仍要走 fraction_mm_sched 取锁、读 epoch、做统计;CPU 越多浪费越严重。
task_cache_work() {
sd = ...; // pick LLC sched domain
for_each_cpu(i, sched_domain_span(sd)) {
occ = fraction_mm_sched(cpu_rq(i), pcpu_sched_i);
// ... update pref_llc / nr_running_avg
}
}
新流程
新实现维护两个新成员:
mm->sc_stat.visited_cpus:位图,记录"近期跑过本 mm 的 CPU"mm->pcpu_sched->epoch_last_visit:per-CPU,记录"该 CPU 最近一次为该 mm 更新 epoch 的时刻"
account_mm_sched(): task_cache_work():
epoch_last_visit = epoch cpumask_and(cpus, sd_span,
if (!visited[cpu]) visited_cpus)
visited[cpu] = 1 for_each_cpu_and(i, sd, visited) {
nr_running++ if same mm
fraction_mm_sched(): occ = fraction_mm_sched(i)
if (epoch - last_visit if (occ == 0) continue;
> affinity_timeout) }
visited[cpu] = 0
简而言之:写入侧记录访问、读取侧只扫记录、超时自动回收。
Patch 概览
- Patch 1/2(主体,sched/cache):在
mm_struct内加cpumask_var_t visited_cpus与epoch_last_visit字段;改fraction_mm_sched接受(int cpu, struct mm_struct *mm),并对超时的 CPU 清位;改task_cache_work用for_each_cpu_and仅扫visited_cpus子集;v6 把cpumask_var_t改成动态分配,并直接把 LLC span 与visited_cpus求交以避免跨节点 LLC 拓扑下的漏扫。 - Patch 2/2(
-- DO NOT APPLY!!! --,sched/cache/debug):调试补丁,引入SC_VISITsched feature(默认 false)和sched_cache_scantracepoint,方便 A/B 对比扫描数量,不应合入。
关键实现
1) include/linux/mm_types.h:动态分配 visited_cpus
static inline int mm_alloc_sched_noprof(struct mm_struct *mm)
{
...
if (!zalloc_cpumask_var(&mm->sc_stat.visited_cpus, GFP_KERNEL)) {
free_percpu(pcpu_sched);
return -ENOMEM;
}
...
}
static inline void mm_destroy_sched(struct mm_struct *mm)
{
free_cpumask_var(mm->sc_stat.visited_cpus);
...
}
v6 用 cpumask_var_t 代替内嵌 cpumask_t,避免 NR_CPUS 很大时每个 mm 多占用 CPUMASK_BYTES 字节内存。Chen Yu 在回复中提示这里需要保证 zalloc_cpumask_var / free_cpumask_var 成对。
2) kernel/sched/fair.c:fraction_mm_sched 接收 (cpu, mm) 并加过期回收
static unsigned long fraction_mm_sched(int cpu, struct mm_struct *mm)
{
struct sched_cache_time *pcpu_sched =
per_cpu_ptr(mm->sc_stat.pcpu_sched, cpu);
struct rq *rq = cpu_rq(cpu);
__update_mm_sched(rq, pcpu_sched);
/* Skip the rq that has not been hit for a long time */
if ((rq->cpu_epoch - pcpu_sched->epoch_last_visit) >
llc_epoch_affinity_timeout) {
cpumask_clear_cpu(cpu, mm->sc_stat.visited_cpus);
return 0;
}
...
}
v6 change #4 把 __update_mm_sched 提前到 timeout 检查之前,确保即使 CPU 已过期仍然在 visited_cpus 里,回收仍能正确生效。
3) account_mm_sched 跟踪"最近访问过"
pcpu_sched->epoch_last_visit = epoch;
if (!cpumask_test_cpu(cpu_of(rq), mm->sc_stat.visited_cpus))
cpumask_set_cpu(cpu_of(rq), mm->sc_stat.visited_cpus);
每次任务在本 CPU 上累加运行时间时,把该 CPU 标记为"访问过",并打上 epoch 时间戳。
4) task_cache_work 只扫访问过的 CPU
cpumask_and(cpus, cpus, mm->sc_stat.visited_cpus);
...
for_each_cpu_and(i, sched_domain_span(sd), mm->sc_stat.visited_cpus) {
cur = rcu_dereference_all(cpu_rq(i)->curr);
if (cur && !(cur->flags & (PF_EXITING | PF_KTHREAD)) &&
cur->mm == mm)
nr_running++;
occ = fraction_mm_sched(i, mm);
if (occ == 0)
continue;
...
}
nr_running++ 提前到 fraction_mm_sched 之前,避免过期 CPU 影响运行队列计数。
5) 调试 patch 透出可观测性
// features.h
SCHED_FEAT(SC_VISIT, false)
// fair.c
if (sched_feat(SC_VISIT) &&
(rq->cpu_epoch - pcpu_sched->epoch_last_visit) >
llc_epoch_affinity_timeout) {
cpumask_clear_cpu(cpu, mm->sc_stat.visited_cpus);
return 0;
}
...
trace_sched_cache_scan(p, scanned);
通过 /sys/kernel/debug/sched/features 切换 SC_VISIT 即可在 trace 中直接对比"扫了多少 CPU"。
类比
把 cache-aware scheduling 想象成公司前台的总机接线员:旧方案是"每次客人来访,都把全公司 800 个工位的电话打一遍问最近有没有见过他",绝大多数工位其实根本没见过。新方案改成"先看门禁刷卡记录,只给今天进过这栋楼的几个人打电话"。门禁记录(visited_cpus)会自动做"过期清理",一个月没刷卡就从名单里划掉(cpumask_clear_cpu),名单不会无限增长。
Highlight:风险与注意点
- 跨节点 LLC 拓扑:
for_each_cpu_and直接用sched_domain_span(sd)做交集;如果宿主 LLC 跨 NUMA,务必保证sd取得正确(v6 change #2 强调"intersected with LLC scheduling domain span directly")。后续若引入sched_cluster_active路径,要重新审视等价检查。 - 配对与泄漏:
zalloc_cpumask_var失败时必须free_percpu(pcpu_sched)再返回-ENOMEM;mm_destroy_sched必须无条件free_cpumask_var(即使 alloc 失败也要保持对称)。Chen Yu 在回复中专门提醒这套配对。 - race 窗口:
__update_mm_sched(持有cpu_epoch_lock)与task_cache_work(无锁读 epoch)之间存在窗口,单纯用 epoch 做判据会被周期性刷新掩盖。v4 引入epoch_last_visit、v3 把比较基准挪进fraction_mm_sched都是为了消除这个 race。 - debug patch 真的不要合入:patch 2/2 显式
DO NOT APPLY!!!。如果合入,SC_VISIT=false默认会让原本的优化被绕过,使task_cache_work又退回到全量扫描;仅用于echo SC_VISIT > /sys/kernel/debug/sched/features做 A/B 测试。 - 结果解读:
perf上 0.81% -> 0.02% 是显著收益;但 hackbench 中threads 1 20、threads 4 10等少量组合仍报REGRESSED,schedcachevsschedcache_visit也有零星回退,PR 上游需要继续做更广基准。 - 统计口径差异:cover letter 的
p99 latency与cycles:k数据来自 AMD 服务器;不同 LLC 拓扑(特别是单 LLC 域覆盖全部 CPU 时)收益会显著缩小,需先看拓扑再外推。
版本变化
- v6:用
cpumask_var_t动态分配visited_cpus;for_each_cpu_and直接对 LLC span 与visited_cpus取交;epoch_timeout重命名为epoch_last_visit;__update_mm_sched前移到 timeout 检查之前。 - v5:恢复
get_scan_cpumasks()以满足 NUMA_BALANCING 约束;用for_each_cpu_and()在 LLC 域内过滤。 - v4:基于 master rebase;引入
epoch_last_visit替换 epoch;nr_running自增前移;删除尾部冗余work->next重置;新增 debug patch。 - v3:移除 static key 默认开启;复用
llc_epoch_affinity_timeout;epoch 比较移入fraction_mm_sched防 race;尾部重置work->next防同进程并发。
一句话总结
通过 visited_cpus 位图与 epoch_last_visit 的过期回收,把 task_cache_work 的扫描集合从全 LLC 收窄到最近真正跑过本 mm 的 CPU 子集:在 384 CPU 的多 NUMA 平台上把 task_cache_work 的 cycles 占比从 0.81% 降到 0.02%,Redis p99 退化从 25.68% 收敛到 1.14%。