0/7 已展开

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。这两段算术在不同代码路径上分别踩了两个整数宽度坑:

  1. window_update() 里的 remaining 用了 u32, 10 秒窗口在算剩余时间时被截断。
  2. 即使把 remaining 改成 u64, prev_growth * remaining 中间值仍可能溢出 u64
  3. 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()remainingu64, 并换用 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 都指向同一个 commit 0e94682b73bf, 说明 PSI monitor 引入时就埋了这两个 bug, 后续若有更多基于窗口的算术 (例如聚合统计) 应一并审计。
  • Suren 的两条回复被截断, 看不到是否真的提了问题; 需到 lore 原始页面确认是否还有未公开的 review 意见。

版本变化

仅 v1, 无 v1->v2 演进记录。

一句话总结

把 PSI trigger 创建与窗口插值两段算术从 u32/隐式宽度升级到显式 u64 + 128-bit 安全乘法, 避免长窗口在 32 位机上被截断或溢出, 让压力监测与用户配置真正一致。