sched/proxy:优化 try_to_wake_up() — 代理调度唤醒热路径瘦身

代理调度(sched/proxy)· 唤醒热路径去防御分支 · 建立 blocked_on 清理不变量 · Linux 7.2

💡 一句话总结

在启用代理调度(一种让"阻塞在互斥锁上的任务"把 CPU 执行权让给锁持有者代为运行的调度机制)的 Linux 7.2 内核上,最热的任务唤醒路径 try_to_wake_up() 每唤醒一个任务都要额外做一次「这个任务是不是还挂在某把锁上」的检查,命中时还要抢一把自旋锁去清掉遗留的挂锁记录。本补丁把清理动作前移到代理调度的决策路径 find_proxy_task(),在那里保证"被停用(deactivate)的任务挂锁记录必然已清空",从而把这段防御性代码从通用的唤醒热路径上整段删掉——从此每次唤醒少一次静态分支判断、少一次字段读取,代理阻塞任务被唤醒时更不再需要在热路径上抢锁。补丁未提供基准数据(解读(AI 分析))。

📋 补丁基本信息

项目内容
补丁类型优化(performance)· 热路径减指令/去锁(hot-path)
状态Merged(绿)· 合入版本:Linux 7.2(v7.2-rc1 已含)
当前版本主线合入版(本 commit 即最终版)· git.kernel.org 链接
版本演进本 commit 为合入主线的最终版,无独立 v1/v2 演进。所属 sched/proxy(代理调度)系列:父提交 1628b252 出自 John Stultz 2026-05-12 发布的系列(系列第 8 补丁);本补丁为 Peter Zijlstra 2026-05-26 的后续优化提交(Link 标签)。lore 索引不可用,无法回溯系列各版本的 v1→v2 差异。
讨论/演进信息来自本地内核 git 的 commit message(Link + Acked-by 链),lore 不可用已如实标注。
作者机构Peter Zijlstra (Intel);Acked-by: John Stultz (Google)
提交日期2026-05-26
改动范围kernel/sched/core.c,+4/-10 行,1 文件
核心函数try_to_wake_up() / find_proxy_task() / proxy_deactivate() / clear_task_blocked_on()
原始链接commit abc40cca0efd(Link 标签:patch.msgid.link)

📊 速览卡片

核心机制
清理前移
优化目标
减唤醒开销
适用场景
代理调度内核
特性等级
★★★
实测提升
未提供

特性等级依据:优化的是内核最热的唤醒路径,方向明确、落地零风险(纯删防御代码 + 加断言,无行为变化)、无需任何配置;但收益仅在 CONFIG_SCHED_PROXY_EXEC 编译开启且代理调度激活时才有意义(非 proxy 内核中该子句编译期即恒假、几乎免费),且补丁未提供任何量化数据、受益场景窄——综合 4 原则评 ★★★(解读(AI 分析))。

🎯 解决什么问题

系列整体定位(本补丁所属 sched/proxy 代理调度系列)
sched/proxy(代理调度)系列整体解决的是调度器不感知"任务因互斥锁阻塞"的问题:传统上任务在互斥锁上阻塞,CPU 只能空转或换跑别的任务,锁持有者(往往是更高优先级的调度上下文)却可能被排在别处。代理调度让阻塞任务把自己的 CPU 执行权让渡(donate)给锁持有者,以阻塞任务的调度上下文代为运行,直到锁释放——这既避免 CPU 空转,也天然缓解优先级反转。系列由 Peter Zijlstra 长期开发、John Stultz(Google)主导上游化(与 ghOSt 的 mutex proxy execution 方向一致),2026-05-12 由 jstultz 在 LKML 发布系列,随后陆续合入 Linux 7.2。本补丁是系列落地后的收尾性热路径优化:主体机制(blocked_on 关联、find_proxy_task() 链式查找、proxy_deactivate() 停用)已就位,它回头把系列早期遗留在通用唤醒路径上的防御代码清掉。单看本补丁只是删了 10 行,放到系列里看,它标志着代理调度主体机制已经稳定到可以"收紧公共路径"的程度。
背景 / 原始动机
commit message 把动机讲得很直白(原文):
"The reason for the clause in try_to_wake_up() is, per its comment, that find_proxy_task()'s proxy_deactivate() is not always called with a cleared p->blocked_on. However, that seems silly and easily cured. Make sure to always call proxy_deactivate() with a cleared p->blocked_on such that we might remove this clause from the common wake-up path."
即:try_to_wake_up() 里那段防御代码存在的原因,是代理调度的 proxy_deactivate() 调用时没有保证 p->blocked_on(任务挂锁记录)已清空。Peter 认为这"很傻、且容易治好"——与其在每段唤醒都要走的公共路径上兜底,不如让清理动作只发生在代理调度自己的路径上,从而把兜底代码从公共路径删掉。这是对刚合入的代理调度系列的热路径清理优化,不是修 bug。
系统层面:最热唤醒路径背着一段只为代理调度服务的防御代码
try_to_wake_up() 是内核最热的函数之一——几乎所有唤醒(互斥锁解锁、条件变量、管道/套接字就绪、futex 等)最终都走它。其中有一段用 #ifndef CONFIG_PREEMPT_RT 包裹的公共代码: if (task_is_blocked(p)) clear_task_blocked_on(p, NULL);。它在每次唤醒时执行:task_is_blocked(p) 先查代理调度是否启用(一个静态分支,默认开),启用时再读 p->blocked_on 字段并分支;一旦命中(被唤醒的任务确实还挂在某把锁上),clear_task_blocked_on() 会拿 p->blocked_lock 自旋锁(raw_spinlock_irqsave,连中断一起关)去清记录。这套开销对所有唤醒都背着,只为极少数的代理调度场景兜底。
场景层面:启用代理调度、互斥锁密集的构建
  • 构建前提:内核编译时打开 CONFIG_SCHED_PROXY_EXEC,且启动时未用 sched_proxy_exec=0 关掉(static key 默认 true)。只有这种构建里 task_is_blocked() 才会真正读 p->blocked_on、这段代码才是有成本的。
  • 负载场景:代理调度面向"任务频繁阻塞在互斥锁上、且调度器代锁持有者执行"的用法——例如 Google 的 ghOSt 式用户态调度、实时/低延迟敏感负载。这类负载互斥锁解锁/唤醒频率极高,每个唤醒都在 try_to_wake_up() 里多付出一次检查;代理阻塞任务被唤醒时更是每次都要在热路径上抢 blocked_lock。
  • 触发条件:clear_task_blocked_on() 的锁开销只在"被唤醒任务仍挂着 blocked_on"时触发(代理调度被激活、任务被停用后未清链就又被唤醒),但 task_is_blocked() 的静态分支 + 字段读取是每个唤醒都在付的。
受影响负载:CONFIG_SCHED_PROXY_EXEC 构建上的互斥锁密集/高唤醒率负载 · 因果:唤醒路径每次都要为代理调度兜底做检查、命中还要抢锁 → 本补丁把清理移到代理调度路径、从公共唤醒路径删掉 → 每次唤醒少一段分支+读取,代理阻塞唤醒不再热路径抢锁

🧩 核心机制

核心一句话:把"停用任务前先清挂锁记录"变成代理调度路径上的强不变量,从而让通用唤醒路径不再需要兜底。思路与工程上"把特例从共享热路径挪到特例自己的路径"是同一件事——特例自己处理好自己,公共路径就干净了。

从系统层面看
三段改动共同建立并落实同一个不变量:
① 落实不变量(机制核心):find_proxy_task() 里有三个会跳向 deactivate:(最终调用 proxy_deactivate())的分支。改动把 clear_task_blocked_on(p, PROXY_WAKING) / __clear_task_blocked_on(p, NULL) 从"只有 task_current(rq, p) 时才执行"提前到无条件先执行,并给第三个分支补上了原本缺失的清理。这样无论走哪条路停用任务,到达 proxy_deactivate() 时 p->blocked_on 必为 NULL。
② 守卫不变量(防回归):proxy_deactivate() 里加 WARN_ON_ONCE(donor->blocked_on)——以后若再有路径没清链就停用任务,会立刻爆 WARN,把"不变量被破坏"暴露在开发/测试阶段。
③ 消费不变量(收益落点):try_to_wake_up() 删掉整段 if (task_is_blocked(p)) clear_task_blocked_on(p, NULL);——因为现在"任务被唤醒时仍挂着 blocked_on"这个状态不可能再从代理停用路径产生,唤醒路径无需再兜底。
为什么能支撑场景提升:收益不靠新增机制,而是靠把固定成本从"每唤醒一次就付一次"降为"代理停用一次才付一次"(且后者本来就在代理路径上、通常已持有 blocked_lock,成本更低)。互斥锁密集负载唤醒越频繁,删掉的检查与抢锁越多。
步骤操作目的
① 落实find_proxy_task() 三个 goto deactivate 前无条件先 clear_task_blocked_on(p, ...)保证 proxy_deactivate() 被调用时 p->blocked_on 必已清空(不变量成立)
② 守卫proxy_deactivate() 加 WARN_ON_ONCE(donor->blocked_on)调试期强制不变量,未来任何路径漏清会立刻暴露
③ 消费try_to_wake_up() 删除 if (task_is_blocked(p)) clear_task_blocked_on(p, NULL);通用唤醒热路径不再为代理调度兜底,每次唤醒少一段分支+读取
关键代码片段 ①:不变量落实 —— 清理动作无条件前移
@@ -6864,9 +6857,9 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
 	for (p = donor; (mutex = p->blocked_on); p = owner) {
 		/* if its PROXY_WAKING, do return migration or run if current */
 		if (mutex == PROXY_WAKING) {
+			clear_task_blocked_on(p, PROXY_WAKING);
 			if (task_current(rq, p)) {
 				p->is_blocked = 0;
-				clear_task_blocked_on(p, PROXY_WAKING);
 				return p;
 			}
 			goto deactivate;

▲ 这是机制成立的关键:原来 clear_task_blocked_on() 只在「p 恰好是当前任务、直接原地跑」的分支里执行;一旦走到 goto deactivate(停用后交给 proxy_deactivate()),挂锁记录就留着不清。改动把它提前到 if (task_current(...)) 判断之前无条件执行——停用路径也先清链,不变量才成立。

关键代码片段 ②:收益落点 —— 公共唤醒路径整段删除
@@ -4343,14 +4343,6 @@ int try_to_wake_up(struct task_struct *p, unsigned int state, int wake_flags)
 		 */
 		WRITE_ONCE(p->__state, TASK_WAKING);
 
-		/*
-		 * We never clear the blocked_on relation on proxy_deactivate.
-		 * If we don't clear it here, we have TASK_RUNNING + p->blocked_on
-		 * when waking up. Since this is a fully blocked, off CPU task
-		 * waking up, it should be safe to clear the blocked_on relation.
-		 */
-		if (task_is_blocked(p))
-			clear_task_blocked_on(p, NULL);
 		/*
 		 * If the owning (remote) CPU is still in the middle of schedule() with
 		 * this task as prev, considering queueing p on the remote CPUs wake_list

▲ 被删的注释原文恰好说明了旧设计的痛点:"We never clear the blocked_on relation on proxy_deactivate"——正因为停用路径不保证清链,唤醒路径才被迫兜底。不变量建立后,这段兜底连同它的理由一起消失,try_to_wake_up() 的热路径上少一个静态分支判断 + 少一次 p->blocked_on 字段读取。

本补丁为简单改动(+4/-10 行,纯代码迁移 + 断言),无图(决策依据:机制是"把清理动作从公共路径移到特例路径"的单点不变量,两段 diff 已能完整说明,删图读者理解不变差)。

📈 性能影响

提升角度(方法论分类)
主角度:on-CPU 计算效率(热路径减分支/减读取)。改动删的是 try_to_wake_up() 每次唤醒都执行的一段判断:task_is_blocked() 里的静态分支测试(static_branch_likely(&__sched_proxy_exec))加一次 p->blocked_on 读取加一个条件分支。这些都在唤醒线程自己的 CPU 上执行,属于典型的 on-CPU 指令开销。
次角度:off-CPU 锁等待(唤醒路径去锁)。当被唤醒任务确实挂着 blocked_on(代理调度激活时)旧代码会在热路径执行 raw_spinlock_irqsave(&p->blocked_lock)——一把关中断的自旋锁。现在这个锁获取只发生在代理停用路径(那里通常已持有 blocked_lock,无需重复抢锁),公共唤醒路径不再碰这把锁。Brendan Gregg 视角:优化的是唤醒方(waker)的墙钟时间里 on-CPU 计算段,以及代理阻塞唤醒时被锁等待占掉的一小段 off-CPU 时间。
受益场景
  • 启用代理调度的构建:CONFIG_SCHED_PROXY_EXEC=y 且未用启动参数关闭。此类内核中 task_is_blocked() 的静态分支默认开,每个唤醒都付检查成本,删掉即省。
  • 互斥锁密集、唤醒率高的负载:代理调度面向的用法(ghOSt 式用户态调度、锁频繁传递的并发负载)解锁/唤醒极频繁,是 try_to_wake_up() 的高频调用者;唤醒越密集,删掉的分支+读取累积越明显。
  • 代理阻塞唤醒:任务被停用后因锁释放被唤醒的场景,旧代码每次都要抢 blocked_lock,新代码不再需要——这是本补丁省掉的最"贵"的一部分。
场景/用例运行环境改进前改进后
通用任务唤醒路径CONFIG_SCHED_PROXY_EXEC=y 的内核每次唤醒:静态分支 + 读 p->blocked_on + 条件分支无(整段删除)
代理阻塞任务被唤醒同上,代理调度激活热路径 raw_spinlock_irqsave(&p->blocked_lock) 抢锁清理无(清理已前移到停用路径)

说明:补丁未提供基准数据——commit message 无任何 benchmark/数字。上表为"改了什么、省了什么"的定性对比,属逻辑分析(解读(AI 分析)),无量化提升百分比。

逻辑收益分析(解读(AI 分析))
从"改动路径 → 高频场景 → 成本项"反推:try_to_wake_up() 是每次唤醒必经之路(互斥锁/条件变量/管道/套接字),每多一段分支都是一次额外的指令流气泡与一次 task_struct 字段访问(该字段与热字段不在同一 cache line 时还可能多一次缓存行读取)。删除后:
① 每次唤醒省一次静态分支 + 一次字段读取——固定的小额 on-CPU 收益,唤醒频率越高累计越大(解读(AI 分析))。
② 代理阻塞唤醒省一把关中断的自旋锁——这是最大头的单点收益:raw_spinlock_irqsave 除锁本身的争用外还含关/开中断的开销;且旧代码在唤醒路径拿锁与代理路径清链可能形成锁的反复获取,新代码只在代理路径拿一次(解读(AI 分析))。
③ 对非 proxy 内核为 no-op——CONFIG_SCHED_PROXY_EXEC 未编译时 sched_proxy_exec() 编译期恒 false,task_is_blocked() 恒假,该子句本就被编译器折叠为死代码,删除与否无差别(事实,非推测)。

🔄 方案演进

本补丁是合入主线的最终版,无独立 v1/v2 演进。它属于 sched/proxy(代理调度)上游化系列,演进信息基于本地内核 git 的 commit message(lore 索引不可用,讨论来源为本地 git,已如实标注)。

系列脉络:代理调度上游化 → 热路径清理
代理调度是什么:当任务阻塞在互斥锁上时,不再让 CPU 空转,而是把执行权"代理"给锁持有者,让持有者以阻塞任务的调度上下文(nice 值、cgroup、时片)运行,直到锁可用。这是 Peter Zijlstra 长期开发的功能(与 Google ghOSt 的 mutex proxy execution 方向一致),在 Linux 7.x 周期推进上游化。
2026-05-12:John Stultz(Google)发布代理调度系列(系列第 8 补丁的 Link),引入 blocked_on / blocked_donor 关联、find_proxy_task() 链式查找等主体机制。
2026-05-26:Peter Zijlstra 提交本补丁——在系列主体机制落地后,回头清理 try_to_wake_up() 里为兼容"停用路径不清链"而遗留的防御代码。
合入:v7.2-rc1 已含本 commit(git describe --contains 确认为 Linux 7.2)。
设计权衡
本补丁的取舍是"把正确性负担从共享路径转移到特例路径":唤醒路径最热、调用面最广,不应为代理调度的小众状态付账;代理停用路径本来就要在 blocked_lock 保护下处理任务的挂锁关系,顺手清链成本更低。用 WARN_ON_ONCE(donor->blocked_on) 换掉 try_to_wake_up 的运行时检查,等于把"运行时每唤醒一次的防御"换成"调试期一次性的断言"——同一份安全保证,成本从 O(每次唤醒) 降到 O(每次代理停用)。lore 不可用,无法回溯邮件列表上是否有更早版本或 review 讨论(解读(AI 分析):该权衡依据 commit message 与代码结构推断)。

注:本 commit 自身为合入主线的最终版,无独立 v1/v2 版本史;以上为"系列整体"的合入节奏与本补丁在系列中的位置。

⚠️ 风险与局限

收益成立的前提
  • 编译前提:内核须 CONFIG_SCHED_PROXY_EXEC=y(且非 PREEMPT_RT——RT 下 clear_task_blocked_on 是空操作)。非 proxy 内核中 sched_proxy_exec() 编译期恒 false,被删子句本就是死代码,本补丁为 no-op。
  • 运行前提:即使编译开启,若启动用 sched_proxy_exec=0 关掉 static key,task_is_blocked() 也恒假,收益同样消失。
  • 负载前提:收益与唤醒频率成正比——互斥锁密集、高唤醒率负载收益明显;低频唤醒场景几乎无感。
生产落地影响
  • 无新配置/无 ABI 变化:纯删代码 + 加断言,升级无需任何操作,对现有依赖 try_to_wake_up 行为的组件无影响。
  • 新调试断言的意义:WARN_ON_ONCE(donor->blocked_on) 是给开发者看的护栏——若未来代理调度代码新增一条"不清链就停用"的路径,会在测试阶段爆 WARN。运维上若在 production 日志看到该 WARN,说明代理停用路径存在遗漏,应上报。
  • 行为完全一致:删除的是"保证正确性的防御代码",不变量建立后正确性由代理路径保证,唤醒路径删掉兜底不改变任何可见行为(事实,依据 commit message"easily cured"与不变量分析)。
生态/兼容性
  • 依赖系列前置:本补丁依赖代理调度主体机制(blocked_on、find_proxy_task()、proxy_deactivate())已合入,不能独立回移植到无代理调度的老内核。
  • 对用户态透明:无系统调用/API/sysctl 变更,工具链与用户态不受影响。
  • 架构无关:改动不涉及任何硬件特性,全架构适用。
review 质疑(若有)
lore 不可用:本次 lore MCP 查询全部超时,无法回溯邮件列表的具体 review 讨论(每条观点带 message-id 的要求无法满足)。可用的社区参与证据来自 commit message 的签名链:Acked-by: John Stultz <jstultz@google.com>——Google 侧(ghOSt 生态)对代理调度系列的主持者认可本优化;父提交链显示该系列由 Peter Zijlstra 与 Juri Lelli(Red Hat)、Valentin Schneider、Connor O'Brien / John Stultz(Google)等多方共同署名开发。未发现可呈现的公开 review 质疑(解读(AI 分析))。
严重度:MINOR(落地几乎零风险——纯删防御代码、加调试断言,行为不变;唯一"风险"是未来新增代理路径若漏清链会触发 WARN,属开发期护栏而非生产隐患)· 落地场景:最需要关注的是收益仅限 proxy 构建,非 proxy 内核升级无感

🔗 交叉引用

📌 关联工作 / 系列其他补丁
1628b252 sched: Add blocked_donor link to task for smarter mutex handoffs — 本补丁的父提交,代理调度系列成员:引入 blocked_donor 关联实现更智能的互斥锁交接(本补丁依赖的代理机制之一)
jstultz@google.com 代理调度系列第 8 补丁(2026-05-12) — 本补丁所在系列在邮件列表的入口(父提交所属)
本补丁的 Link 标签(20260526113322.244729903@infradead.org) — Peter 提交本补丁对应的邮件列表消息
abc40cca0efd sched/proxy: Optimize try_to_wake_up() — 本补丁本体(git.kernel.org 规范链接)
[日报] sched/cache:缓存感知负载均衡 — 同为 Linux 7.2 调度器热路径/性能优化系列,本站同日另一篇调度器日报(对照阅读)
⚠️ 免责声明

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