0/3 已展开

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-ID20260730135858.2460751-1-arighi@nvidia.com
  • 完整性:thread 共 3 封(1 patch + 2 回复);patch 含完整 diff、两条 Fixes:Suggested-by: Peter ZijlstraSigned-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 依然会被当真,从而:

  1. 进入 deadline 带宽核算 sched_dl_overflow(),可能真的扣/加 SCHED_DEADLINE 配额,或者返回 -EBUSY
  2. prev_class != next_class 成立,置上 DEQUEUE_CLASS,从而通过 sched_change guard 触发 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_change guard 不会走 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_forkKEEP_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_PARAMS vs KEEP_POLICY 容易混淆:讨论中同时出现两个 flag 名,评审里的 reset_on_fork 问题走的是 KEEP_POLICY 的 early-unlock 路径,而本补丁改的是 KEEP_PARAMSKEEP_ALL = 两者并集)。阅读时别把两条路径当成一条。

版本变化

  • v1 -> v2(changelog 明确记录):
    • 按 Peter Zijlstra 的建议,从"只旁路副作用"升级为显式拒绝 policy 变更(新增 -EINVAL 分支),语义更清晰,也避免调用方误以为 policy 已被接受;
    • 把这个 policy 检查移到 rq 锁保护范围内执行,防止与并发 sched_setscheduler()p->policy 上竞态。

一句话总结

这个 v2 补丁让 SCHED_FLAG_KEEP_PARAMS 名副其实:在 rq 锁下显式拒绝伴随 policy 变更的调用,并跳过 deadline 带宽记账与 class 切换回调,消除"副作用已发生、任务状态却没变"的不一致;评审中另外浮出的 reset_on_fork 锁外计算问题仍待 Peter 定夺。