sched discussion
[PATCH] sched/numa: Reset NUMA fault locality after scan period update
LLM 分析
sched/numa:重置 NUMA fault locality 状态
系列概况
- 标题:
[PATCH] sched/numa: Reset NUMA fault locality after scan period update - 作者:Eric Kim
<seohyun.kim@outlook.kr>;问题由 Binwon Song 报告 - 版本:v1 → v2(单 patch 系列)
- 规模:1 个 patch;v1 = +2 行;v2 = +5 / -2 行
- 修改文件:
kernel/sched/fair.c - 代码统计:v1
2 insertions(+),v25 insertions(+), 2 deletions(-) - Message-ID:
- v1:
OS7PR01MB13914DA08AC588DE5544B03E796D62@OS7PR01MB13914.jpnprd01.prod.outlook.com - 回复:
20260803071014.GW49951@noisy.programming.kicks-ass.net - v2:
OS7PR01MB139144C7E07C97B3F3B47016796D52@OS7PR01MB13914.jpnprd01.prod.outlook.com
- v1:
- 完整性:完整,含 patch + maintainer 评审 + 修订后 v2
补丁目的
update_task_scan_period() 在两种情况下会主动增大扫描周期:
- 没有发生任何 fault(说明访问模式不再本地)。
- 出现一次失败的 migration(说明把任务搬过去也没改善)。
此时应该“重新观察”——也就是在更大的时间窗口下重新统计本地度。但当前实现只更新了 numa_scan_period 和 numa_next_scan,没有清空 numa_faults_locality[]。这个数组记录了上一轮 NUMA 节点上的局部性统计,会被后续决策继续使用,造成 “用旧数据决定新周期” 的脏读。
旧流程的问题
在 update_task_scan_period 中“增大扫描周期”的早返回分支:
p->mm->numa_next_scan = jiffies + msecs_to_jiffies(p->numa_scan_period);
return; // ← 这里直接 return
而函数尾部本来已经有一段:
memset(p->numa_faults_locality, 0, sizeof(p->numa_faults_locality));
但因为上面的早 return 跳过了函数尾部,faults_locality 没被清零。结果就是:下一次扫描窗口开始时,旧 locality 数据仍在 p->numa_faults_locality 里,污染下一轮“是否要再放大周期 / 是否要再迁移”的判断。
新流程
v1:在早 return 之前先 memset(...) 清零,确保增大周期分支也清空旧 locality。
v2:把尾部的 memset 提到 out: 标签里,把早 return 改成 goto out,让两条路径共用同一份清零代码。
Patch 概览
v1(Eric Kim)
@@ update_task_scan_period(...)
p->mm->numa_next_scan = jiffies + msecs_to_jiffies(p->numa_scan_period);
+ memset(p->numa_faults_locality, 0,
+ sizeof(p->numa_faults_locality));
return;
}
v2(Eric Kim,根据 Peter Zijlstra 评审调整)
@@ update_task_scan_period(...)
- return;
+ goto out;
...
+out:
+ memset(p->numa_faults_locality, 0,
+ sizeof(p->numa_faults_locality));
关键实现
函数结构变成“单出口”:
static void update_task_scan_period(struct task_struct *p,
u64 now, bool find_idle)
{
...
if (increase_scan_period) {
p->numa_scan_period = ...;
goto out; /* 原本是 return */
}
/* ... 正常缩周期 / 计算新周期 ... */
p->mm->numa_next_scan = jiffies + msecs_to_jiffies(p->numa_scan_period);
out:
memset(p->numa_faults_locality, 0,
sizeof(p->numa_faults_locality));
}
要点:
memset不再出现在两个不同位置,只有一个out:出口统一收尾。- 行为不变:增大周期时 faults_locality 仍然被清零。
- 旧位置(函数末尾)的
memset被搬到out:下,但执行顺序上仍然在numa_next_scan之后,语义和原本相同。
类比
想象你每天记录自己走了多少步:
- “增大扫描周期”就像你换了一双新跑鞋,决定重新评估。
- 旧版的 bug 等于:你昨天走得多的腿(前一个鞋子)今天依旧被记录着,于是你以为是这条腿累,其实是数据串了。
- v2 的做法相当于:你每次开始新评估前都擦干净记步器,不管今天要测 5 分钟还是 5 小时,都从零开始数。
goto out的清理,相当于把记步器集中放在“出门前最后一步”——出门前不管走哪条路都必经同一个玄关。
状态/控制流图
update_task_scan_period()
|
v
+-------------------+
| compute new period |
+-------------------+
|
+---------+----------+
| increase? |
+---------+----------+
yes | | no
| v
| update p->numa_scan_period /
| schedule numa_next_scan
| |
v v
goto out goto out
\ /
\ /
v v
+-----------+
| out: |
| memset |
| faults_ |
| locality |
+-----------+
Highlight:风险与注意点
- v1 把
memset写在return;之前,但函数尾部本来就有一段memset,容易出现“双写”或后续维护者误删其中一处;v2 改成单出口彻底消除重复。 numa_faults_locality必须在 决定增大扫描周期 之后立刻清零;如果清得太晚,下一次task_tick_fair/numa_scan仍然读到旧数据。goto out是 Linux 内核常见风格(单出口清理),但要小心:未来如果有人往out:之后加新逻辑,可能改变放大分支的语义,需要仔细 review。- 没有看到性能数据;理论上多一次
memset数组(sizeof(p->numa_faults_locality),通常NR_NUMA_HINT_FAULT_TYPES * NR_NUMA_NODES),代价可忽略,但可以纳入后续 stress 跑分。 - 缺乏
Fixes:tag,建议上游加上对应 commit(应该是引入当前update_task_scan_period早 return 的 commit)。
版本变化
| 版本 | 关键改动 |
|---|---|
| v1 | 在早 return 之前加 memset(p->numa_faults_locality, 0, ...) |
| v2 | 把 return 改成 goto out;函数尾部的 memset 收拢到 out: 标签下,两条路径共用同一段清零 |
Peter Zijlstra 的评审只提了一个建议(share core with the tail),Eric 在 v2 中直接落实,系列最小化收口。
一句话总结
NUMA scan 周期被放大时必须重置 numa_faults_locality,v2 用 goto out 把清零路径统一为函数单出口,避免旧 locality 数据污染下一轮调度决策。