sched discussion
[PATCH 0/2] sched/psi: Fix trigger time arithmetic
LLM 分析
PSI:修复 trigger 时间算术的整数宽度 bug
系列概况
- 标题: [PATCH 0/2] sched/psi: Fix trigger time arithmetic
- 作者: Guopeng Zhang zhangguopeng@kylinos.cn
- 版本: v1 (2/2 系列)
- 规模: 2 个 patch, 1 个文件 (
kernel/sched/psi.c), 6 行新增 / 4 行删除 - 修改文件:
kernel/sched/psi.c - 代码统计: Patch 1/2: 4+, 2-; Patch 2/2: 2+, 2-; 合计 6+, 4-
- Message-ID:
20260717102824.985950-1-guopeng.zhang@linux.dev - 完整性: cover letter + 2 个 patch + 4 个回复 (Suren 两条点评 + Tao Cui 两条 Acked-by)
补丁目的
PSI (Pressure Stall Information) 在创建 trigger 时把用户给的微秒值换算成纳秒, 并在窗口采样时按比例分摊 prev_growth。这两段算术在不同代码路径上分别踩了两个整数宽度坑:
window_update()里的remaining用了u32, 10 秒窗口在算剩余时间时被截断。- 即使把
remaining改成u64,prev_growth * remaining中间值仍可能溢出u64。 - 32 位架构上,
threshold_us * NSEC_PER_USEC先按 32 位算再赋值给u64, 导致窗口被错误缩小。
目标是让 PSI 在 32 位/64 位内核上都能正确处理"接近上限"的 trigger 配置。
旧流程的问题
trigger create: window_update():
u32 threshold_us | u32 remaining
u32 window_us | growth += prev_growth * remaining / size
--------- | ------------------------------
t->threshold = us * 1000L | remaining = win->size - elapsed
t->win.size = us * 1000L | (on 32-bit, the *1000L is 32-bit)
(truncate/wrap before u64) |
- 10s 窗口过去 2s 后, 剩余 8e9 ns 被
u32截成约 3.7e9。 prev_growth * remaining两个 10e9 量级的值相乘超过U64_MAX。- 32 位机上
6,000,000us * 1000绕回成约 1.7e9 ns, 导致win.size < threshold, trigger 失效。
新流程
trigger create: window_update():
u64 threshold_us (cast) | u64 remaining
u64 window_us (cast) | growth += mul_u64_u64_div_u64(
--------- | prev_growth, remaining, size)
t->threshold = (u64)us * NSEC | (uses __int128 internally, no overflow)
t->win.size = (u64)us * NSEC |
(full 64-bit arithmetic) |
/* Patch 1/2: window_update() */
- u32 remaining;
+ u64 remaining;
- growth += div64_u64(win->prev_growth * remaining, win->size);
+ growth += mul_u64_u64_div_u64(win->prev_growth, remaining, win->size);
/* Patch 2/2: psi_trigger_create() */
- t->threshold = threshold_us * NSEC_PER_USEC;
- t->win.size = window_us * NSEC_PER_USEC;
+ t->threshold = (u64)threshold_us * NSEC_PER_USEC;
+ t->win.size = (u64)window_us * NSEC_PER_USEC;
Patch 概览
- 1/2:
window_update()中remaining升u64, 并换用 64-bit 安全除法函数mul_u64_u64_div_u64(内部用__int128), 新增<linux/math64.h>头文件。 - 2/2:
psi_trigger_create()在乘NSEC_PER_USEC之前显式 cast 到u64, 避免 32 位机的隐式宽度截断。
关键实现
/* 修复后的 window_update 路径 */
if (elapsed > win->size)
window_reset(win, now, value, growth);
else {
remaining = win->size - elapsed; /* u64 now, no truncation */
growth += mul_u64_u64_div_u64(
win->prev_growth, /* u64 */
remaining, /* u64 */
win->size); /* u64 */
}
mul_u64_u64_div_u64 由 <linux/math64.h> 提供, 内部把两个 u64 提升到 __int128 再乘除, 避免在 U64_MAX 之前就溢出。
类比
把 PSI trigger 想成"水表": 用户在表盘上以微秒拨一个刻度, 但表内部用纳秒刻盘 (1:1000 换算)。问题在于:
- 32 位机的旧表盘只有 32 个槽位, 你拨 6,000,000 微秒时指针还没转到对应位置就被旁边的齿轮"咬回"到起点, 于是你以为窗口只有 1.7 秒。
- 10 秒窗口算"剩余时间"时, 旧码用了一个只能装 4,294,967,295 纳秒 (约 4.3 秒) 的小杯子去接 8 秒的水, 溢出的水被直接倒掉。
mul_u64_u64_div_u64就像先换一把 128 位的大壶把水存够, 再平均分到目标杯里, 根本不会溢出。
Highlight:风险与注意点
- 两次 cast 看似"零行为变化", 但 32 位机用户长期被静默错配 trigger; 回归测试覆盖 32 位配置才能复现。
prev_growth * remaining > U64_MAX需要窗口接近 10s 且prev_growth也接近上限才会触发, 常规压力场景难以命中, 需针对性 fuzz/单测。Fixes都指向同一个 commit0e94682b73bf, 说明 PSI monitor 引入时就埋了这两个 bug, 后续若有更多基于窗口的算术 (例如聚合统计) 应一并审计。- Suren 的两条回复被截断, 看不到是否真的提了问题; 需到 lore 原始页面确认是否还有未公开的 review 意见。
版本变化
仅 v1, 无 v1->v2 演进记录。
一句话总结
把 PSI trigger 创建与窗口插值两段算术从 u32/隐式宽度升级到显式 u64 + 128-bit 安全乘法, 避免长窗口在 32 位机上被截断或溢出, 让压力监测与用户配置真正一致。