sched/proxy:优化 try_to_wake_up() — 代理调度唤醒热路径瘦身
💡 一句话总结
在启用代理调度(一种让"阻塞在互斥锁上的任务"把 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(代理调度)系列整体解决的是调度器不感知"任务因互斥锁阻塞"的问题:传统上任务在互斥锁上阻塞,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 行,放到系列里看,它标志着代理调度主体机制已经稳定到可以"收紧公共路径"的程度。
"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()的静态分支 + 字段读取是每个唤醒都在付的。
🧩 核心机制
核心一句话:把"停用任务前先清挂锁记录"变成代理调度路径上的强不变量,从而让通用唤醒路径不再需要兜底。思路与工程上"把特例从共享热路径挪到特例自己的路径"是同一件事——特例自己处理好自己,公共路径就干净了。
① 落实不变量(机制核心):
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 已能完整说明,删图读者理解不变差)。
📈 性能影响
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 分析)),无量化提升百分比。
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,已如实标注)。
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 变更,工具链与用户态不受影响。
- 架构无关:改动不涉及任何硬件特性,全架构适用。
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 分析))。
🔗 交叉引用
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 基于公开知识辅助生成,仅供学习参考,请勿直接引用作为依据。作者不对信息的准确性、完整性及适用性作保证,亦不对因使用本站内容产生的任何损失承担责任。