sched discussion
[PATCH] sched/numa: Fix time unit mismatch in scan staggering
LLM 分析
sched/numa:修复 scan staggering 的时间单位错误
系列概况
- 标题:[PATCH] sched/numa: Fix time unit mismatch in scan staggering
- 作者:Hui Su sh_def@163.com
- 版本:单封 patch(无 v2/v3 演进,无后续回复)
- 规模:1 file changed, 4 insertions(+), 5 deletions(-)
- 修改文件:
kernel/sched/fair.c(init_numa_balancing()) - 代码统计:净 -1 行,整体替换 9 行实现
- Message-ID:20260827095902.2645166-1-sh_def@163.com
- 完整性:单 patch 完整可独立 review;引用
Fixes: 137844759843指向引入 bug 的提交
补丁目的
init_numa_balancing() 负责为新 fork 出来的 task 初始化 node_stamp,
用于错开(stagger)共享同一 mm_struct 的多个线程触发 NUMA scan 的时间,
避免它们同时打爆某个 node 的 page fault 路径。但原实现把"毫秒"和"纳秒"
混在一起做 min_t() 比较,使得实际计算出的 stagger 几乎为 0,
多线程 NUMA 工作负载因此被错误地集中在最初几毫秒内完成扫描。
旧流程的问题
unsigned int delay;
delay = min_t(unsigned int, task_scan_max(current),
current->numa_scan_period * mm_users * NSEC_PER_MSEC);
delay += 2 * TICK_NSEC;
p->node_stamp = delay;
关键问题:
- 单位错配:
task_scan_max()返回毫秒(ms),
current->numa_scan_period * mm_users * NSEC_PER_MSEC却是纳秒(ns),
min_t()在两个完全不同的单位之间取最小值,逻辑上无意义。 - unsigned int 截断:所有运算都落在 32 bit,
当 ns 值超过UINT_MAX时直接被截断;32 位平台上乘法本身也会溢出。 - 实际效果:默认
scan_period_min = 1000 ms,2 个 mm user 时
min_t()选出2000(ms),但p->node_stamp期望 ns 量级;
结果node_stamp大约只有 2 ms,stagger 形同虚设。
新流程
u64 delay_ms;
delay_ms = (u64)current->numa_scan_period * mm_users;
delay_ms = min_t(u64, delay_ms, task_scan_max(current));
p->node_stamp = delay_ms * NSEC_PER_MSEC + 2 * TICK_NSEC;
思路调整:
- 先在 ms 域内比较,去掉
(...)*NSEC_PER_MSEC; - 全部用
u64,避免 32 位溢出与截断; min_t()之后才统一换算成 ns,最后再加 2 个 tick 的偏移。
Patch 概览
| 项 | 内容 |
|---|---|
| 修复 commit | 137844759843 ("sched/numa: Stagger NUMA balancing scan periods for new threads") |
| 影响函数 | init_numa_balancing() |
| 触发条件 | 新线程 fork,且共享 mm_struct(mm_users >= 1) |
| 受益场景 | 多线程 NUMA 工作负载(数据库、SPECjbb、JVM 等) |
关键实现
把整段 delay 替换为 delay_ms,并把 unsigned int 升到 u64:
u64 delay_ms;
delay_ms = (u64)current->numa_scan_period * mm_users;
delay_ms = min_t(u64, delay_ms, task_scan_max(current));
p->node_stamp = delay_ms * NSEC_PER_MSEC + 2 * TICK_NSEC;
注意 (u64) 强转放在乘法第一个操作数上——这是标准写法,
能强制编译器把整条乘法链提升到 64 bit,避免 unsigned int * int
先按 32 位算再提升的潜在溢出。
消费侧 task_tick_numa() 已经按 ns 解释 node_stamp,
所以最后乘回 NSEC_PER_MSEC 就能让 stagger 时间真正落在秒级/百毫秒级。
类比
把 node_stamp 想成"闹钟下次响的时刻":
- 旧实现:把"5 分钟"和"300 纳秒"放在一起选更小的那个,
闹钟设成了"再等 300 纳秒响",相当于立刻响。 - 新实现:先把分钟数算清楚(2000 分钟),再统一换算成纳秒,
闹钟被正确设到 2000 分钟之后,多个线程自然错开响铃。
也可以理解成"先比距离再换单位":你不会拿"3 公里"和"5 米"比谁更短,
而是先把两边都化成米再比。
Highlight:风险与注意点
- 这是回归修复:bug 由 commit
137844759843引入,
旧版本上 stagger 工作正常;stable backport 优先级较高。 Fixes:tag 引用的是 commit hash 短前缀:patch 引用是
137844759843,合并时需要核对完整 hash。- 单封 patch,无 review 反馈:作者还没有收到任何回复,
后续是否会有 Ingo/Peter 给出 ack 或要求 v2 仍待观察。 - 32 位平台上的额外风险:旧逻辑在 32 位 kernel 上
numa_scan_period * mm_users * NSEC_PER_MSEC早早溢出,
修复同时解决了这条隐藏路径。 - 验证点:可以用
tracepoint:sched_migrate_task或
/proc/<pid>/numa_scan_seq在多线程工作负载下观察
不同线程node_stamp是否真正错开。
task_scan_max() numa_scan_period * mm_users
| |
v v
(ms) (* NSEC_PER_MSEC) -> (ns)
\ /
\ min_t(unsigned int) / <-- unit mismatch
+--------------+------------+
|
v
unsigned int delay
|
v p->node_stamp = delay (expected ns, got ms)
|
v
task_tick_numa() fires almost immediately
task_scan_max() numa_scan_period * mm_users
| |
v v
(ms) (u64, ms)
\ /
\ min_t(u64) / <-- same-unit compare
+--------------+------------+
|
v
u64 delay_ms
|
v
delay_ms * NSEC_PER_MSEC + 2 * TICK_NSEC
|
v
p->node_stamp (ns, correct magnitude)
|
v
task_tick_numa() fires after the real stagger window
版本变化
当前只看到 v1,无 v2/v3。
与其他相关 patch 系列的关联
- 直接修复对象:
137844759843 ("sched/numa: Stagger NUMA balancing scan periods for new threads")。 - 相关背景:
sched/numa子系统中task_scan_max()、
numa_scan_period、NSEC_PER_MSEC等单位约定在 fair.c 多个函数中保持一致;
本 patch 仅修正init_numa_balancing()的局部计算错误,
未触及task_tick_numa()或task_scan_stat()的消费逻辑。
一句话总结
init_numa_balancing() 把毫秒和纳秒丢进同一个 min_t() 还用 32 bit 算,
导致多线程 NUMA stagger 几乎失效;本 patch 改成在毫秒域内比较、用 u64
算乘法、最后再统一换算成纳秒,让 node_stamp 真正落到应有的时间量级。