sched discussion
[PATCH] sched/core: Make fallback CPU selection NUMA-aware
LLM 分析
sched/core:让 fallback CPU 选择感知 NUMA 拓扑
系列概况
- 标题:[PATCH] sched/core: Make fallback CPU selection NUMA-aware
- 作者:Yury Norov ynorov@nvidia.com
- 版本:单 patch,未标注 v?
- 规模:1 个文件,25 行新增,22 行删除
- 修改文件:kernel/sched/core.c
- 代码统计:+25 / -22
- Message-ID:20260905033656.311477-1-ynorov@nvidia.com
- 完整性:邮件只包含一个 patch,diff 内嵌在正文里
补丁目的
select_fallback_rq() 在目标 CPU 不再可用时,要为当前任务重新挑一个 CPU 跑起来。理想情况下,应该选"最近的"——优先同节点、同 socket、同一 NUMA 拓扑圈。但旧实现只看一眼本地节点,剩下就退回到对 p->cpus_ptr 做按编号的线性扫描。在多 NUMA 节点系统上,这条线性路径很可能在拓扑距离上"跨过去"再"折回来",导致 fallback 选到一颗其实更远的 CPU。本 patch 改用调度器现成的 sched_numa_hop_mask 按拓扑距离逐跳扩搜,让 fallback 选择保持局部性。
旧流程的问题
nid 取自 cpu_to_node(cpu),如果有效就先在同节点 cpumask_of_node(nid) 里扫一遍;找不到就跳出去对 p->cpus_ptr 用 for_each_cpu 顺序扫描。在两节点系统中这没问题,因为"非本地"就只剩一颗可去的地方;但在 >=3 节点系统里,按编号扫描会撞上编号低但拓扑远的 CPU,破坏 locality,还会重复扫描同节点里已经找过的 CPU。
新流程
借助 for_each_numa_hop_mask(cpus, nid) 按 NUMA 距离一圈一圈往外扩,每跳都用 for_each_cpu_andnot(dest_cpu, cpus, prev) 只看新进入这一圈的 CPU,避免重复。prev 在每跳结束时累加当前 hop mask,所以下一跳天然不会重复扫老 CPU。最后一段 for_each_cpu_andnot(dest_cpu, p->cpus_ptr, prev) 处理拓扑正在重建时 hop mask 还没就绪的窗口,把 hop mask 没覆盖到的"残余"在线 CPU 也扫一遍。
关键实现
int nid = IS_ENABLED(CONFIG_NUMA) ? cpu_to_node(cpu) : NUMA_NO_NODE;
const struct cpumask *prev = cpu_none_mask, *cpus;
/* Look for the closest allowed, online CPU. */
rcu_read_lock();
for_each_numa_hop_mask(cpus, nid) {
for_each_cpu_andnot(dest_cpu, cpus, prev) {
if (is_cpu_allowed(p, dest_cpu)) {
rcu_read_unlock();
goto out;
}
}
prev = cpus;
}
/*
* NUMA masks may be unavailable or incomplete while the
* topology is being rebuilt. Search CPUs not covered by them
* before relaxing the task's affinity.
*/
for_each_cpu_andnot(dest_cpu, p->cpus_ptr, prev) {
if (is_cpu_allowed(p, dest_cpu)) {
rcu_read_unlock();
goto out;
}
}
IS_ENABLED(CONFIG_NUMA)让非 NUMA 构建也能用同一段代码:此时nid == NUMA_NO_NODE,hop mask 应退化为覆盖所有 CPU 的 mask。prev = cpu_none_mask是"空集"占位,使得第一跳就等于整张 hop mask。- RCU 读锁保护 hop mask 的并发访问(hop mask 可能在 topology rebuild 时被替换)。
+----------------------------+
| select_fallback_rq |
+-------------+--------------+
|
+----------v-----------+
| hop 0: local node | prev = {}
+----------+-----------+
|
+----------v-----------+
| hop 1: 1-hop nodes | prev = hop0
+----------+-----------+
|
+----------v-----------+
| hop 2: 2-hop nodes | prev = hop0|hop1
+----------+-----------+
|
+----------v-----------+
| residual: |
| cpus_ptr \ prev | prev = hop0|hop1|hop2
| (covers hotplug gap) |
+----------+-----------+
|
allowed CPU -> goto out
类比
想象你在一座陌生城市找人开会:
- 第一步:先敲自己办公室的门(同节点)。
- 第二步:走到隔壁楼层(1-hop)。
- 第三步:跑去对面那栋楼(2-hop)。
- 你随身带一个小本本 (
prev) 记下"已经敲过的门",每去一层新楼,只看本本里没记过的那些房间 (cpus & ~prev)。 - 走到最后发现 hop mask 这张"通讯录"还没把今天临时搭的活动板房记进去,于是最后再扫一遍整栋大楼剩下的房间 (
cpus_ptr \ prev),兜底以防漏人。
旧实现相当于:不记本本,每去新楼层都把整栋楼再敲一遍,并且越敲越远。
Highlight:风险与注意点
- rcu_read_lock 与 rcu_read_unlock 的配对:diff 里只看到两处
rcu_read_unlock(); goto out;,其它 fallback 路径(cpuset/possiblestate 分支)需对照未在 diff 中展示的下文,确认所有goto out之前都已解锁。 - 非 NUMA 配置 (
IS_ENABLED(CONFIG_NUMA)==0) 下nid = NUMA_NO_NODE,需要for_each_numa_hop_mask在 NUMA_NO_NODE 上仍能给出覆盖所有 CPU 的 mask,否则第一跳就空集,行为退化到只剩残余扫描——需结合sched_numa.h中 hop mask 的定义确认。 - 拓扑重建窗口:
prev累积依赖每一跳返回的 mask 本身正确;若第一跳 mask 在 hotplug 中刚被换为不完整版本,可能跳过本应被考虑的在线 CPU;补丁末尾的残余扫描正是为此兜底,但仍建议用 hotplug stress 验证。 nid == -1(节点已 offline)的语义:旧代码直接走p->cpus_ptr;新代码通过 hop mask 也能落到残余扫描路径,但要确认亲和性放宽前的 fallback 行为对 cpuset 隔离仍然等价。- 性能:现在每跳做一次
cpus & ~prev的 mask 运算,对于很小的 cpuset(<8 CPU)开销可忽略;对几百 CPU + 几十 NUMA 节点的大机器,循环体内仅一次is_cpu_allowed,命中即返回,最坏情况下仍 O(全部在线 CPU)。
一句话总结
把 fallback CPU 选择从"本地节点 + 线性 cpumask"改成"按 NUMA 距离逐跳扩搜 + 残余兜底",让多节点系统上的降级目标更近、且在拓扑重建期间仍然可靠。