0/1 已展开

LLM 分析

sched/fair: 更新扫描周期后重置 NUMA fault locality

系列概况

  • 标题: [PATCH v3] sched/fair: Reset NUMA fault locality after scan period update
  • 作者: Eric Kim seohyun.kim@outlook.kr
  • 版本: v3,单 patch 独立成帖
  • 规模: 1 file changed, 5 insertions(+), 2 deletions(-)
  • 修改文件: kernel/sched/fair.c,只动 update_task_scan_period()
  • 代码统计: +5 / -2
  • Message-ID: OS7PR01MB13914CB69D682A599970F258C96A62@OS7PR01MB13914.jpnprd01.prod.outlook.com
  • 完整性: 完整(diff + commit message + v1/v2/v3 changelog)

补丁目的

修一个 control-flow 漏 cleanup 的 bug。

update_task_scan_period() 用来根据上一个扫描窗口的统计决定是否把
p->numa_scan_period 拉长或缩短。当 period == 0,即“上一窗口既没 fault
也没迁移失败”时,函数会把 numa_scan_period 翻倍(clamp 到
numa_scan_period_max),然后直接 return

问题在于函数尾部本来有一句
memset(p->numa_faults_locality, 0, sizeof(...)),目的是把“上一窗口的
locality 统计”清零,让下个窗口从干净状态重新统计。但 period == 0 的早返回
绕过了这段 cleanup,下一次进入函数时看到的 locality 仍是上一个窗口的旧值,
于是错误地继续翻倍周期,最终把 numa_scan_period 顶到 numa_scan_period_max
相当于把该 task 的 NUMA 扫描永久冻结在错误节点上。

旧流程的问题

旧控制流:

if (!period) {
    /* 拉长周期 */
    if (p->numa_scan_period < numa_scan_period_max)
        p->numa_scan_period = min(p->numa_scan_period * 2,
                                  numa_scan_period_max);
    return;          /* 早返回,跳过尾部 memset */
}
/* ... */
out:
    p->numa_scan_period = clamp(...);
    memset(p->numa_faults_locality, 0,
           sizeof(p->numa_faults_locality)); /* 永远走不到 */

后果:

  • numa_faults_locality 是判断“上一窗口是否 fault / 失败”的统计源。
  • period == 0 后 locality 没清零,下个窗口统计时仍看到非零值。
  • scheduler 误以为“仍在发生 fault”,于是周期反复翻倍。
  • 最终撞到 numa_scan_period_max,该 task NUMA 平衡停摆。

新流程

return 改为 goto out,强制所有退出路径经过同一个 cleanup 标签:

if (!period) {
    if (p->numa_scan_period < numa_scan_period_max)
        p->numa_scan_period = min(p->numa_scan_period * 2,
                                  numa_scan_period_max);
    goto out;        /* 走尾部 cleanup */
}
/* ... */
out:
    p->numa_scan_period = clamp(...);
    memset(p->numa_faults_locality, 0,
           sizeof(p->numa_faults_locality));

效果:任何路径退出都清空 locality,下个窗口从干净状态重新统计,
不会再被旧 locality 推着翻倍。

Patch 概览

唯一改动集中在 kernel/sched/fair.cupdate_task_scan_period() 函数:

  • hunk 1:return; -> goto out;(一行)
  • hunk 2:把原本紧贴函数结尾大括号的 memset(...) 移到新加的 out: 标签后
    (多一行 label,两行续行),让 cleanup 成为显式汇合点。

数据语义未变,只调整 control-flow;不引入新锁、新统计字段。

关键实现

@@ -3469,7 +3469,7 @@ static void update_task_scan_period(struct task_struct *p,
-       return;
+       goto out;
@@ -3513,7 +3513,10 @@ static void update_task_scan_period(struct task_struct *p,
-       memset(p->numa_faults_locality, 0, sizeof(p->numa_faults_locality));
+
+out:
+       memset(p->numa_faults_locality, 0,
+              sizeof(p->numa_faults_locality));

技术点:

  • Linux kernel 习惯的 goto out cleanup-label 模式,grep “goto out” 即可审查
    所有退出路径是否汇合,对 static analysis 友好。
  • 不动 numa_faults_locality 的角色定义,只修“清零时机”。

类比

把 NUMA 扫描想成体检安排:

  • numa_scan_period 是两次体检之间的间隔天数;
  • numa_faults_locality 是上一次体检填的指标表。

规则 1:上次体检全正常(无 fault / 无失败),就把下次间隔翻倍,最长一年(numa_scan_period_max)。
规则 2:在写新体检排期前,把上一份指标表清空。

旧代码:满足规则 1 后直接下班(return),忘清指标表。下次体检前的统计
仍看到旧指标,系统以为“上次有异常”,间隔再翻倍、再翻倍……最终“永远不复查”。

新代码:goto out 把清表动作强制执行,体检制度恢复。

ASCII 流程图

                 update_task_scan_period(p, period)
                                |
                       period == 0 ?
                       /                \
                     YES                NO
                      |                  |
        double p->numa_scan_period   update numa_next_scan
        (clamp to numa_scan_period_max)
                      |                  |
        old: return   /  new: goto out   |
                      \                /
                       +---->   out:   <----+
                               memset(p->numa_faults_locality, 0)
                               clamp(p->numa_scan_period)
                                      |
                                      v
                                  function exit
旧控制流(bug):
  no-fault -> double period -> return
                                    |
                                    v
                          locality stays dirty
                                    |
                                    v
                next window: sees non-zero -> double again
                                    |
                                    v
                  period hits numa_scan_period_max
                                    |
                                    v
            task frozen on wrong NUMA node (no scan)

新控制流(fix):
  no-fault -> double period -> goto out
                                    |
                                    v
                  memset(locality, 0) + clamp(period)
                                    |
                                    v
                  next window: clean statistics

Highlight:风险与注意点

  1. 早返回路径必须遵守 goto out 而不是 return,这是隐性约定;如果未来
    在该函数加新早返回,需要在 review 时重点扫一遍。
  2. bug 触发要“连续两个窗口都看到旧 locality 非零”,普通负载不易复现,
    需要构造跨 NUMA 节点、迁移代价高的多节点 workload 测试。
  3. 撞到 numa_scan_period_max 后只是“性能下降”而非 crash,线上可能长期
    没察觉,需要在 selftest 或 mmtests 里加回归用例。
  4. v2 起就按 Peter Zijlstra 建议共用 out 路径,v3 仅润色 commit message
    与 subsystem 前缀,逻辑稳定;后续不要再退回 return 写法。
  5. commit message 的 Closes: 字段留空,建议补上 Binwon Song 报告的
    lore 链接以便追溯。

版本变化

  • v1 -> v2:按 Peter Zijlstra 的 review,把 numa_faults_locality 清零移到
    函数尾部,与 out: 标签共享清理路径;不再在 period == 0 分支就地清零。
  • v2 -> v3:仅润色 commit message(更清晰地描述 bug 现象),并将 subsystem
    前缀规范化为 sched/fair;控制流与 v2 完全一致。

一句话总结

update_task_scan_period() 的早 return 改成 goto out,强制在
调整 NUMA 扫描周期时一并清空 numa_faults_locality,避免旧 locality
反复推动 numa_scan_period 顶到 numa_scan_period_max