0/3 已展开

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(+),v2 5 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
  • 完整性:完整,含 patch + maintainer 评审 + 修订后 v2

补丁目的

update_task_scan_period() 在两种情况下会主动增大扫描周期:

  1. 没有发生任何 fault(说明访问模式不再本地)。
  2. 出现一次失败的 migration(说明把任务搬过去也没改善)。

此时应该“重新观察”——也就是在更大的时间窗口下重新统计本地度。但当前实现只更新了 numa_scan_periodnuma_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, ...)
v2return 改成 goto out;函数尾部的 memset 收拢到 out: 标签下,两条路径共用同一段清零

Peter Zijlstra 的评审只提了一个建议(share core with the tail),Eric 在 v2 中直接落实,系列最小化收口。

一句话总结

NUMA scan 周期被放大时必须重置 numa_faults_locality,v2 用 goto out 把清零路径统一为函数单出口,避免旧 locality 数据污染下一轮调度决策。