sched/fair: Let sync wakeups target the waker's core

CFS 调度器 · 任务唤醒路径 · SMT 拓扑感知

💡 一句话总结

在多核(超线程)CPU 上,一个线程写完数据、通过同步唤醒叫醒另一个线程继续干活时,调度器现在会把这个"被唤醒的线程"放到那个"即将空闲、且缓存还是热的"核心上,让它直接复用数据而不用重新从内存加载。补丁作者自报在 POWER11 上 producer-consumer 握手(-l 5)的单次访问延迟降低约 11%(作者自报、未独立验证,且 review 质疑该收益在 x86 等特定 SMT 布局下可能不成立)。

📋 补丁基本信息

项目内容
状态In Review(首版提交,等待维护者反馈)
补丁类型优化(同步唤醒路径性能优化)
当前版本v1 · lore 链接
版本演进首版,暂无演进
作者机构Madadi Vineeth Reddy (IBM)
提交日期2026-08-01
改动范围kernel/sched/fair.c,+38/-11 行,1 文件
核心函数select_idle_core() / select_idle_sibling() / select_task_rq_fair()
原始链接lore Message-ID

📊 速览卡片

核心机制
sync 唤醒感知
优化目标
复用热 cache
适用场景
生产-消费握手
自报提升
producer +11%

注:+11% 为补丁作者在 commit message 中自报数据(POWER11 producer_consumer -l 5),未独立验证。

🎯 解决什么问题

背景 / 原始动机:同步唤醒本该用上"即将空闲"的核,为什么没做到
先讲问题:多核 CPU 上,一个线程(producer)写完数据后会用"同步唤醒"(sync wakeup,一种特殊唤醒方式:waker 唤醒 wakee 后自己即将睡眠)叫醒另一个线程(consumer)继续处理。理想情况下,consumer 应放在 producer 刚用过的那个核上——因为数据的热点还在那个核的缓存里,放一起能直接复用,不用重新从内存加载。

为什么没做到:调度器选核时只判断"这个核现在忙不忙"。producer 所在的核此刻还在运行 producer,被判为"忙"排除在外;可 producer 马上要睡了,这个核其实"即将空闲"且缓存是热的——本该是最佳落点却被略过。

谁在解决:上游已有两个方向在探索"同步唤醒应落到 waker 附近"——Shubhang Kaushik(Ampere)从 wake-affine 分支直接返回 waker CPU(v3 限定非 SMT,因 SMT 下会把两人堆到同一硬件线程);K Prateek Nayak(AMD)把决策移进 select_idle_sibling() 的 if (!has_idle_core) 分支。本补丁作者自述在 Shubhang v2 线程上提出过自己的思路,现在独立成补丁,覆盖两者都没覆盖的情形(LLC 里有空闲核时的同步唤醒)。
解读(AI 分析):三个补丁是同一方向的不同实现粒度,作者选择从 select_idle_core() 内部的 core 空闲性判断入手。
系统层面:同步唤醒信号在选核时被丢弃
机制背景:调度器每次把一个线程放上 CPU 前,要调用"选核"逻辑 select_idle_sibling(),在 LLC(最后一级缓存,多个 CPU 核共享的大缓存)里找一个空闲核放它。

问题链条:同步唤醒时,内核会给调度器打一个 WF_SYNC 标志,意思是"waker 即将睡眠"。选核逻辑的早期步骤 wake_affine_idle() 本来会因此返回 waker 所在的核——但下一步 select_idle_sibling() 又把这个决定丢了:因为它逐个检查核是否空闲时,用 available_idle_cpu() 判断,而 waker 还在这核上跑着,available_idle_cpu() 返回"不空闲",于是 waker 的核被略过,扫描继续到别处找。

为什么这是个缺陷:选核逻辑只懂"忙/闲"二值,表达不了第三种状态——"核正被即将睡眠的 waker 占用,马上会闲下来"。于是把本该最优的落点(即将空闲 + 缓存热)当成忙核排除。
场景层面:producer-consumer 握手把热数据放冷核
场景特征:严格两任务握手的 producer-consumer——producer 写完数据后 WF_SYNC 唤醒 consumer 并即将睡眠,二者交替执行、访问同一份数据。此时 waker core 的 L1/L2 缓存中已驻留这份热点数据。

为什么此场景遇到系统缺陷:waker core 当前被 waker 占用,available_idle_cpu() 判它为忙,于是选核扫描略过它,把 wakee 放到 LLC 里另一个空闲核——这个核没有热点数据,wakee 首次访问必须从内存/上级缓存重新加载,握手延迟被放大。而 waker core 其实"即将"空闲(waker 马上阻塞),它的缓存又是热的,本该是最佳落点却因"当前 busy"被排除。
受影响负载:严格两任务握手的 producer-consumer、管道通信、延迟敏感握手 · 为什么此特性解决此场景:把"即将空闲"的 waker core 视为候选,wakee 落在其 sibling 复用热 cache,避免冷加载
producer-consumer 同步唤醒握手时序对比
图 1:producer-consumer 同步唤醒握手——补丁前 wakee 落冷核(缓存未命中);补丁后 sync_cpu 标记 waker 即将空闲,wakee 落 sibling 复用热 cache(off-CPU 等待减少)
来源:基于 lore 真实补丁 diff + commit message 数据绘制

🧩 核心机制

把"即将阻塞的 waker CPU"(sync_cpu)一路传进 select_idle_core(),在检查核心是否空闲时把它当作空闲看待——于是这个核心保持"空闲核候选"资格,被唤醒的任务就落在它的兄弟线程上。

从系统层面看
模块:kernel/sched/fair.c 的任务唤醒路径——select_task_rq_fair() → select_idle_sibling() → select_idle_cpu() → select_idle_core()。

改了啥:给这条链路的三个函数新增 sync_cpu 参数;select_task_rq_fair() 在 want_affine && sync && new_cpu==cpu 且运行队列仅 1 个可运行任务时把当前 CPU 设为 sync_cpu;select_idle_core() 遍历核心的 SMT 兄弟线程时,把 cpu==sync_cpu 视为空闲(!available_idle_cpu(cpu) && !sync_waker),同时禁止它作为兜底目标(!sync_waker && ...)。

为什么要这么改 / 解决什么技术问题:选核逻辑原本只能区分"核忙/核闲"两种状态,无法表达第三种——"核正被即将睡眠的 waker 占用"。sync_cpu 用参数显式传递这一语义,让核心空闲性判断多出一个维度。技术上解决的是"扫描器对即将释放的资源视而不见"的问题:waker 所在核心即将空闲且缓存热,却被当作忙核排除。

为什么支撑场景提升:producer-consumer 握手里,wakee 落到 waker core 的兄弟线程(sibling,指同一物理核心下的兄弟硬件线程,如 Intel 超线程中的两个逻辑核;同一核心的 siblings 共享 L1/L2 缓存)后与 waker 共享 L1/L2,热点数据无需从内存重载,握手每轮省下一次缓存未命中的延迟——这正是基准里 producer_consumer -l 5(每轮工作最小、唤醒占主导)收益最大(+11%)的原因。
sync wakeup 目标选择流程对比
图 2:补丁前 wakee 放 cold CPU;补丁后 sync_cpu 标记为空闲,wakee 放 waker core 的 sibling 复用 cache
来源:基于 lore 真实补丁 diff 绘制
步骤操作目的
识别 syncwant_affine && sync && new_cpu == cpu 且 rq 仅 1 runnable确认 waker 即将阻塞、core 即将空闲
传参select_idle_sibling(p, prev, new_cpu, sync_cpu)把 waker CPU 传入下游扫描
视为空闲!available_idle_cpu(cpu) && !sync_wakerwaker core 保持 idle-core 候选
落核wakee 放在 waker core 的 sibling共享 cache,避免冷加载

🔬 关键代码

+		if (want_affine && sync && new_cpu == cpu) {
+			struct rq *rq = cpu_rq(cpu);
+
+			if ((rq->nr_running - cfs_h_nr_delayed(rq)) == 1)
+				sync_cpu = cpu;
+		}
+		return select_idle_sibling(p, prev_cpu, new_cpu, sync_cpu);

▲ 识别"waker 即将阻塞且无其他 runnable"的情形,把该 CPU 作为 sync_cpu 传入

+		bool sync_waker = (cpu == sync_cpu);
+		/* ...把即将阻塞的 waker 视为 idle,让 core 保持候选... */
+		if (!available_idle_cpu(cpu) && !sync_waker) {
 			idle = false;
 			...
 		}
-		if (*idle_cpu == -1 && cpumask_test_cpu(cpu, cpus))
+		if (!sync_waker && *idle_cpu == -1 && cpumask_test_cpu(cpu, cpus))
 			*idle_cpu = cpu;

▲ 核心:sync_cpu 视为空闲,但不作为"兜底目标"(它还没真正空闲,不能当 fallback)

📈 性能影响

数据出处:下表所有数字均来自补丁作者 Madadi Vineeth Reddy 在 commit message 中自报(POWER11,SMT8,160 CPUs / 20 cores,每组 40 任务,5 次运行取中位数,全部归一化到 baseline)。均为作者自报数据,尚未获独立第三方复现验证。

producer_consumer 性能对比柱状图
图 3:producer_consumer 性能对比(归一化,越低越好)——补丁收益集中在低负载(-l 5 +11%、-l 10 +7.7%),随每轮工作量增大收益消失(-l 20 持平)
来源:补丁作者 commit message 自报数据(未独立验证)
场景/用例运行环境度量基线补丁后
producer_consumer 严格两任务握手(-l 5,低负载)POWER11 SMT8 160CPU/20corestime/access(越低越好)1.000.89(+11%)
producer_consumer(-l 10)同上time/access1.000.92(+7.7%)
hackbench process-pipe 1-group同上完成时间(越低越好)1.000.94(+6.4%)
hackbench thread-pipe 1-group同上完成时间1.000.92(+7.8%)
hackbench thread-pipe 4-group(高负载)同上完成时间1.001.09(-8.8% 回退)

作者自述:随利用率升高(1-group 占 25% CPU → 8-group 达 200%)补丁收益缩水——它需要有 LLC 里的空闲核才有效;多数数字为正且在 run-to-run 波动内,仅 thread-pipe 4-group 有 -8.8% 回退。schbench p99 延迟/RPS 均在波动范围内无明显变化。

on-CPU vs off-CPU 视角(解读(AI 分析)):本补丁优化的是 off-CPU 等待环节——它减少的是 wakee 因"落冷核 + 缓存未命中"产生的等待/重载时间,而非减少 CPU 计算量(on-CPU)。producer-consumer 握手里每轮收益主要来自省下一次缓存未命中的延迟,而非省指令。补丁作者未提供 on/off-CPU 分项数据,此项为逻辑推演。

⚠️ 局限提示:以上数据仅覆盖 POWER11(SMT sibling 连续编号)。review 质疑该收益在 x86/arm64 等非连续 SMT 编号布局下可能不成立(见下文 💬 讨论焦点),目前缺少 x86/arm64 实测数据。

💬 讨论焦点

Review 意见:收益可能仅限"连续编号"的 SMT 布局
Zhan Xusheng(Xiaomi)在 review 中顺着 select_idle_cpu() 的实现逐层论证了收益的架构依赖:
  1. select_idle_cpu() 用 for_each_cpu_wrap(cpu, cpus, target + 1) 从 target+1 开始按 CPU 编号循环扫描
  2. POWER SMT8 的 sibling 连续编号,waker 的 sibling 就在 target + 1,最先被扫到 → 正好命中"落到 waker core"的意图
  3. 但 x86 常见布局(sibling 在 cpu + nr_cores 而非 cpu + 1)下,扫描会先访问其他 core;若其中任意一个完全空闲,select_idle_core() 就返回那个 cold core(没有热点数据的核)
  4. 结论:缓存共享收益可能 主要特定于连续编号的 SMT;x86/arm64 下收益能否实现存疑
明确请求:Zhan 请作者分享 x86 / arm64 SMT2 的测试数据,以确认 waker 的 sibling 在这些布局下是否真能被选中(截至 lore 检索时作者尚未回应)
作者的方案定位(commit message 自述)
作者在 commit message 中主动对比了两个已存在的关联补丁:Shubhang Kaushik 从 wake-affine 分支直接返回 waker CPU(v3 限定非 SMT),Prateek 把决策移进 select_idle_sibling()。本补丁定位为覆盖"LLC 中有空闲核"这一不相交情形,无空闲核时 no-op,且自述此思路曾在 Shubhang v2 线程讨论过。
解读(AI 分析):三个补丁是同一方向(sync 唤醒倾向 waker core)的不同实现粒度,后续可能需整合;但 Zhan 的架构依赖质疑目前无人回应,是待决问题。

🔗 交叉引用

📌 关联工作(同一方向的不同解法)
Shubhang Kaushik(Ampere)— Prefer waker CPU for non-SMT reciprocal sync wakeups (v3) — 从 wake-affine 分支直接返回 waker CPU;v3 限制为 !sched_smt_active(),因 SMT 下会把两人堆到一个硬件线程
K Prateek Nayak(AMD)— 同主题 (v3) — 把决策移进 select_idle_sibling() 的 if (!has_idle_core) 分支,优先 prev 的 idle sibling 再堆 waker
🔄 方案差异
本补丁覆盖不相交情形:sync 唤醒时 waker core 已持有数据,LLC 里有空闲核时把 wakee 导向 waker core 的 sibling 而非冷核;无空闲核时 no-op。作者说明此思路曾在 Shubhang v2 线程讨论过。

⚠️ 风险与局限

潜在回归 / 并发 / 边界
架构依赖(已由 review 指出):收益依赖 SMT sibling 连续编号——POWER SMT8 上 sibling 紧邻 target+1 先被扫到;x86/arm64 非连续编号布局下可能先命中其他空闲核,收益不成立,且无 x86/arm64 实测。

高负载回退:thread-pipe 4-group 有 -8.8% 回退(作者自报),随利用率升高(8-group 达 200% CPU)收益消失——它需要有 LLC 里的空闲核。

边界(no-op 情形,作者自述):waker core 无 idle sibling / waker rq 多个 runnable / prev_cpu 已有效 / 非 SMT 系统——这些情形补丁不改变行为。
严重度:MAJOR(高负载场景有可测回退)· review 质疑:Zhan Xusheng(Xiaomi)提出 x86 布局收益存疑,作者尚未回应

✅ 关键洞察

  • 发现:sync 唤醒时把"即将空闲的 waker core"视为 idle 候选,wakee 落在其 sibling 上复用热 cache(off-CPU 等待减少,非计算减少)
  • 证据:作者自报 POWER11 producer_consumer -l 5 提升 +11%、hackbench 多数场景 +3~8%(未独立验证)
  • 边界:4 个 no-op 条件(waker core 无 idle sibling / rq 多个 runnable / prev_cpu 已有效 / 非 SMT)
  • 风险 / 建议:收益可能仅限 SMT sibling 连续编号的架构(POWER);建议补测 x86/arm64 SMT2 数据验证 Zhan 的质疑;thread-pipe 4-group -8.8% 回退需关注
⚠️ 免责声明

本站内容均由 AI 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。