sched discussion
[PATCH v2] sched: Reject policy changes with SCHED_FLAG_KEEP_PARAMS
LLM 分析
sched:SCHED_FLAG_KEEP_PARAMS 必须拒绝 policy 变更
系列概况
- 标题:[PATCH v2] sched: Reject policy changes with SCHED_FLAG_KEEP_PARAMS
- 作者:Andrea Righi <arighi@nvidia.com>
- 版本:v2(v1 链接:
20260730055011.2267333-1-arighi@nvidia.com) - 规模:1 个文件,+9 / -2
- 修改文件:
kernel/sched/syscalls.c - 代码统计:只改
__sched_setscheduler(),新增 1 处拒绝判断 + 收紧 2 处已有条件,无新增数据结构或 API - Message-ID:
20260730135858.2460751-1-arighi@nvidia.com - 完整性:thread 共 3 封(1 patch + 2 回复);patch 含完整 diff、两条
Fixes:、Suggested-by: Peter Zijlstra、Signed-off-by、v2 changelog。评审串尚未出现 Reviewed-by/Acked-by,reset_on_fork的追问也还没有 Peter 的答复,因此线程处于未收敛状态。
补丁目的
SCHED_FLAG_KEEP_PARAMS 的语义是:调用方通过 sched_setattr() 想改的是旁路属性(典型是 uclamp 上下限、部分 flags),而不希望内核真的去改写 p->policy、优先级参数和 p->sched_class。
问题在于:sched_setattr() 的 ABI 仍然要求传一个 policy 字段。旧实现里,即使带了 KEEP_PARAMS(最终不会写任何参数),这个传进来的 policy 依然会被当真,从而:
- 进入 deadline 带宽核算
sched_dl_overflow(),可能真的扣/加 SCHED_DEADLINE 配额,或者返回-EBUSY; - 让
prev_class != next_class成立,置上DEQUEUE_CLASS,从而通过sched_changeguard 触发 class 切换回调(switching/switched_{from,to})。
结果就是**"账已经记了、事件已经发了,但任务状态其实一点没变"**。本补丁的目标是让 KEEP_PARAMS 真正成为"只改能改的那部分":显式拒绝 policy 变更,并把跟 policy/class 相关的副作用一并跳过。
旧流程的问题
OLD: __sched_setscheduler(p, attr, policy) with SCHED_FLAG_KEEP_PARAMS set
|
+-- [1] (dl_policy(policy) || dl_task(p)) ? --yes--> sched_dl_overflow()
| DL bandwidth accounted,
| or -EBUSY returned
|
+-- [2] prev_class != next_class ? --yes--> queue_flags |= DEQUEUE_CLASS
| (next_class derived from the *requested* policy)
| class callbacks fire
|
+-- [3] scoped_guard(sched_change, p, queue_flags):
| KEEP_PARAMS => params / policy / sched_class are NOT written back
|
'-- RESULT: bandwidth charged + class events emitted,
but p->policy / p->sched_class unchanged ==> INCONSISTENT
也就是说,KEEP_PARAMS 只压住了"最后写回"这一步,却没有压住写回之前的记账与回调。
新流程
NEW: __sched_setscheduler(p, attr, policy) [rq lock held here]
|
+-- KEEP_PARAMS && policy != p->policy ? --yes--> retval = -EINVAL; goto unlock
| (checked under the task's rq lock => no TOCTOU vs concurrent setscheduler)
|
+-- !KEEP_PARAMS && (dl_policy(policy) || dl_task(p))
| --> sched_dl_overflow() [entirely skipped when KEEP_PARAMS]
|
+-- !KEEP_PARAMS && prev_class != next_class
| --> queue_flags |= DEQUEUE_CLASS [never set when KEEP_PARAMS]
|
'-- scoped_guard(sched_change, p, queue_flags):
applies only what the guarded update is actually allowed to apply
三处闸门语义一致:要么 policy 相同(副作用天然为空),要么直接 -EINVAL 拒绝。
Patch 概览
| 位置 | 改动 | 修复的 Fixes: |
|---|---|---|
__sched_setscheduler() recheck 之后 | 新增 KEEP_PARAMS && policy != p->policy -> -EINVAL | — (总闸门) |
sched_dl_overflow() 调用点 | 前置 !KEEP_PARAMS 条件 | a509a7cd7974 (uclamp 扩展 sched_setattr()) |
DEQUEUE_CLASS 置位点 | 前置 !KEEP_PARAMS 条件 | 637b0682821b (把 switch{ing,ed}_{to,from} 折进 change pattern) |
关键实现
/* KEEP_PARAMS only makes sense if the scheduling policy is unchanged */
if ((attr->sched_flags & SCHED_FLAG_KEEP_PARAMS) && policy != p->policy) {
retval = -EINVAL;
goto unlock;
}
/*
* If setscheduling to SCHED_DEADLINE (or changing the parameters
* of a SCHED_DEADLINE task) we need to check if enough bandwidth
* is available.
*/
if (!(attr->sched_flags & SCHED_FLAG_KEEP_PARAMS) &&
(dl_policy(policy) || dl_task(p)) && sched_dl_overflow(p, policy, attr)) {
retval = -EBUSY;
goto unlock;
}
prev_class = p->sched_class;
next_class = __setscheduler_class(policy, newprio);
if (!(attr->sched_flags & SCHED_FLAG_KEEP_PARAMS) && prev_class != next_class)
queue_flags |= DEQUEUE_CLASS;
scoped_guard (sched_change, p, queue_flags) {
...
}
要点:
- 锁的位置是 v2 的核心:拒绝检查放在
goto recheck之后、unlock标签可达的区间内,即持有该 task 的 rq lock 时再读p->policy。若在锁外比较,就会出现"读到旧 policy 判定通过、随后并发sched_setscheduler()改掉 policy"的 TOCTOU。 sched_dl_overflow()整段跳过,而不是"算完再丢弃"。这一点很关键:该函数本身会做dl_b->total_bw的加减,不是纯查询函数,所以必须不进入,而不是忽略返回值。DEQUEUE_CLASS不再置位,于是sched_changeguard 不会走 class 迁移路径,p->sched_class保持不变时也就不会发出误导性的switched_from/switched_to回调。
类比
把 __sched_setscheduler() 想成银行柜台,业务单上有两栏:账户类型(policy)和备注(uclamp 等旁路属性)。
SCHED_FLAG_KEEP_PARAMS相当于客户在单据上勾了「只改备注,账户一律不动」。- 旧流程里,柜员一边看到勾选了"不动账户",一边又照着"账户类型"栏去调总行的额度台账(deadline 带宽),还顺手给风控发了一封"该客户已迁移账户类型"的通知(class 回调);最后才想起"哦,客户说不动账户",于是把账户信息原样放回。结果:台账动了、通知发了,账户其实没变。
- 补丁做的事就是柜员先做"单据自检":如果勾了"不动账户"却又在账户类型栏填了别的值,直接退单(
-EINVAL);两栏一致才办理,且全程不碰额度台账、不发迁移通知。 - 而"必须在锁下自检"就像柜员必须握着客户档案原件再比对,否则同事在旁边刚把账户类型改了,你手上的复印件已经过期。
Highlight:风险与注意点
reset_on_fork是同类隐患但未修:K Prateek Nayak 指出,reset_on_fork在KEEP_POLICY的 early-unlock 路径里是在 rq_lock 之外计算的;当 policy、参数、uclamp 都没变化而走 early unlock 时,p->reset_on_fork仍会被写成这个锁外算出的值。两个并发sched_setscheduler()下,先完成的那个可能被落后者用陈旧拷贝覆盖。Andrea 把问题转给了 Peter,本补丁不覆盖这一点,是明确的待跟进项。
reset_on_fork concern (raised by K Prateek Nayak) -- NOT fixed here:
CPU0: sched_setscheduler(A) CPU1: sched_setscheduler(B)
------------------------------ ------------------------------
compute reset_on_fork = X compute reset_on_fork = Y
(outside rq lock) (outside rq lock)
rq_lock; p->reset_on_fork = Y
early unlock (KEEP_POLICY,
nothing else changed)
rq_lock; p->reset_on_fork = X
^-- stale value wins, B's update lost
- ABI 行为变化:以前"带
KEEP_PARAMS又填了不同 policy"是被静默容忍的(虽然产生错账),现在会硬性返回-EINVAL。如果有用户态或库把attr.sched_policy随便填成 0 /SCHED_NORMAL而任务实际是 FIFO/DEADLINE,那么升级后这些调用会直接失败。这是最容易被忽视的兼容性风险,值得在 changelog 中点明。 -EBUSY语义收窄:KEEP_PARAMS 路径不再可能返回-EBUSY(带宽不足)。这本身是修复,但依赖该返回值做探测的测试可能需要调整。- 两条
Fixes:指向不同根因:一条是 uclamp 引入KEEP_PARAMS时就存在的带宽误记账,一条是 class 回调折叠进 change pattern 后新暴露的回调误触发。回移(backport)到稳定分支时需要注意目标分支是否已含637b0682821b,否则第三处 hunk 无对应上下文。 KEEP_PARAMSvsKEEP_POLICY容易混淆:讨论中同时出现两个 flag 名,评审里的reset_on_fork问题走的是KEEP_POLICY的 early-unlock 路径,而本补丁改的是KEEP_PARAMS(KEEP_ALL= 两者并集)。阅读时别把两条路径当成一条。
版本变化
- v1 -> v2(changelog 明确记录):
- 按 Peter Zijlstra 的建议,从"只旁路副作用"升级为显式拒绝 policy 变更(新增
-EINVAL分支),语义更清晰,也避免调用方误以为 policy 已被接受; - 把这个 policy 检查移到 rq 锁保护范围内执行,防止与并发
sched_setscheduler()在p->policy上竞态。
- 按 Peter Zijlstra 的建议,从"只旁路副作用"升级为显式拒绝 policy 变更(新增
一句话总结
这个 v2 补丁让 SCHED_FLAG_KEEP_PARAMS 名副其实:在 rq 锁下显式拒绝伴随 policy 变更的调用,并跳过 deadline 带宽记账与 class 切换回调,消除"副作用已发生、任务状态却没变"的不一致;评审中另外浮出的 reset_on_fork 锁外计算问题仍待 Peter 定夺。