sched discussion
[PATCH] sched/psi: Skip CPUs with zero non-idle jiffies in per-cpu aggregation
LLM 分析
Linux PSI 聚合优化:跳过 zero non-idle delta 的 CPU
系列概况
- 标题:sched/psi: Skip CPUs with zero non-idle delta in per-CPU aggregation
- 作者:Zhan Xusheng zhanxusheng@xiaomi.com
- 版本:v1 → v6,时间跨度 2026-02-03 → 2026-08-04
- 规模:单 patch 系列,共 6 个版本
- 修改文件:kernel/sched/psi.c
- 代码统计:v1/v2/v3/v4 = +7 行;v5/v6 = +3 行(精简内联注释)
- Message-ID:20260203100007.22044-1-zhanxusheng@xiaomi.com(v1)→ 20260804092554.3854840-1-zhanxusheng@xiaomi.com(v6)
- 完整性:8 封邮件;含 Johannes Weiner 1 条 ack + 1 条 Peter Zijlstra 数据诉求
补丁目的
collect_percpu_times() 每个采样窗口遍历所有 possible CPU,对每个 CPU 算出 PSI_NONIDLE 时间(nsecs_to_jiffies)作为权重,再加权到 deltas[]。
当 PSI_NONIDLE delta 为 0 时,加权贡献本身就是 0,纯属空跑。
本 patch 利用 get_recent_times() 已经设好的 cpu_changed_states & (1 << PSI_NONIDLE) 位作为天然 zero-delta 标记,命中即 continue,省掉 nsecs_to_jiffies 与后续 u64 mul-add。
旧流程的问题
for each possible cpu:
get_recent_times(... &cpu_changed_states);
changed_states |= cpu_changed_states;
nonidle = nsecs_to_jiffies(times[PSI_NONIDLE]); /* 一定执行的除法 */
nonidle_total += nonidle;
for s in PSI_NONIDLE:
deltas[s] += times[s] * nonidle; /* 结果是 += 0 */
nsecs_to_jiffies 一定执行;容器/监控密集场景下,PSI trigger 触发频率(百 Hz 级)放大 O(nr_possible_cpus) 扫描的成本,CPU 越多浪费越明显。
新流程
for each possible cpu:
get_recent_times(... &cpu_changed_states);
if (!(cpu_changed_states & (1 << PSI_NONIDLE)))
continue;
changed_states |= cpu_changed_states;
nonidle = nsecs_to_jiffies(times[PSI_NONIDLE]);
nonidle_total += nonidle;
for s in PSI_NONIDLE:
deltas[s] += times[s] * nonidle;
changed_states |= cpu_changed_states 在循环里仍执行,聚合层 trigger reschedule 决策不受影响。
Patch 概览
- v1:
if (!nonidle) continue;—— 先算 nonidle 再判断,跳过一次 jiffies 转换。 - v2/v3:依 Johannes 建议改为
cpu_changed_states位判断,省掉nsecs_to_jiffies。 - v4:去掉误加的 Reviewed-by,改 Suggested-by;commit 措辞改为 "delta"。
- v5:精简注释(+3 行)+ i7-8700 的 4 组 A/B 数据。
- v6:补 -smp 2..32 的 A/B 数据;强调
psi_rtpoll_work()调用频率。
关键实现
最终 hunk(v5/v6)落在 collect_percpu_times() 内、get_recent_times() 之后:
get_recent_times(group, cpu, aggregator, times, &cpu_changed_states);
+ if (!(cpu_changed_states & (1 << PSI_NONIDLE)))
+ continue;
changed_states |= cpu_changed_states;
nonidle = nsecs_to_jiffies(times[PSI_NONIDLE]);
get_recent_times() 只在 PSI_NONIDLE delta 非零时置位,是天然的"非零标记",无需新状态变量。
类比
老办法像图书馆每周盘点:每类书不管这周有没有新书,都要把书架重新数一遍再算借出率;新办法先翻"本周新书登记本"(cpu_changed_states 位),没新增就直接跳过这一类(continue)。馆藏(CPU 数)越多,省得越多。
+--------------------------+
| for each possible cpu |
+--------------------------+
|
v
+--------------------------+
| get_recent_times(...) |
| sets PSI_NONIDLE bit |
+--------------------------+
|
v
+--------------------------+
| test PSI_NONIDLE bit |
+-----------+--------------+
bit clear | bit set
v v
+-----------+ +-----------------------+
| continue | | changed_states |= ... |
+-----------+ +-----------------------+
|
v
+-----------------------+
| nsecs_to_jiffies() |
+-----------------------+
|
v
+-----------------------+
| deltas[s] += t[s]*n |
+-----------------------+
Highlight:风险与注意点
- 假设
get_recent_times()的 PSI_NONIDLE 置位语义稳定;若日后调整置位条件,skip 可能漏掉真正需要累加的 CPU,回归旧 bug。 - v2/v3 把 Johannes 的 "Makes sense + 实现建议" 误标 Reviewed-by,v4 改 Suggested-by 才符合 review tag 的语义(v4 changelog 已明确说明)。
- Peter Zijlstra 在 v4 直接要求 benchmark 证据;前几版没数据会被打回,v5/v6 的 A/B 表是入主干的关键。
- skip 路径不能影响
changed_states最终的 OR 结果,否则psi_rtpoll_work()的 trigger 行为会变化;当前实现中changed_states |= cpu_changed_states仍在 skip 之后执行,行为正确。 - 用户态监控 agent 是最大受益者,但 -smp 超过物理核时基线本身有调度噪声,v6 数据展示的是"趋势"而非单点最优。
版本变化
- v1 → v2:判断条件由
!nonidle改为cpu_changed_states位检查,省掉nsecs_to_jiffies。 - v2 → v3:仅 rebase 重发,逻辑无变化。
- v3 → v4:删除 Reviewed-by 改 Suggested-by;commit message 措辞改为 "PSI_NONIDLE delta"。
- v4 → v5:精简掉内联注释块(-4 行)+ i7-8700 (6C/12T) 的 4 组 A/B 数据。
- v5 → v6:补 -smp 2/4/8/12/16/24/32 的 A/B 数据;明确
psi_rtpoll_work()调用频率量级(~160 次/s)。
一句话总结
借 get_recent_times() 已设的 PSI_NONIDLE 位作为 zero-delta 标记,让 collect_percpu_times() 直接跳过对加权贡献为 0 的 CPU,在 12 线程以上机器的 /proc/pressure/cpu 读延迟上省 4%-18%,随 CPU 数放大。