0/3 已展开

LLM 分析

sched/fair:修复 same-task repick 后 fair hrtick 漏启动的问题

系列概况

字段内容
标题[PATCH] sched/fair: Restart hrtick after same-task repicks
作者Shubhang Kaushik (Ampere) sh@gentwo.org
版本v1,单 patch,3 messages
规模3 files changed, 50 insertions(+), 2 deletions(-)
修改文件kernel/sched/core.c, kernel/sched/fair.c, kernel/sched/sched.h
代码统计core.c +2,fair.c +29 -1,sched.h +21 -1
Message-ID20260813-sched-fair-hrtick-restart-v1-1-4230d1e18fbb@gentwo.org
完整性完整,base-commit 3aa1dcaa4f6f5ae08936491e08bd456f331f2d40,change-id 20260813-sched-fair-hrtick-restart-ab9d3d47ef78

补丁目的

修复 fair hrtick 在 same-task repick 路径下漏重新启动导致的回归。Fair hrtick 是 one-shot 高精度定时器,用于在 task 接近用完时间片时触发精准 preemption。正常路径下,下一次 hrtick 由 set_next_task_fair() -> hrtick_start_fair() 启动。但当 hrtick 自己到期引发 reschedule、又重新选中同一 task 时(put_prev_set_next_task 命中 next == prev),set_next_task_fair() 会被跳过,从而没有任何新 fair hrtick 被 arm 上。

作者用 HRTICK 启用、base_slice_ns=3000000、两个 CPU-bound 任务固定到一颗 CPU 的 micro-benchmark 量化影响:nice-0 任务 runtime 超过 8ms 的区间在 10s perf sched 捕获中从 228 个降到 34-38。

旧流程的问题

当 hrtick 触发并把当前任务抢回自己时:

  1. task_tick_fair(rq, curr, queued=1) 走到 entity_tick()
  2. resched_curr() 申请调度;
  3. schedule()pick_task_fair() 选中当前任务(vruntime 尚未退化);
  4. 进入 put_prev_set_next_task(rq, next=curr, prev=curr)
  5. if (next == prev) 早 return,类回调全部跳过;
  6. set_next_task_fair()hrtick_start_fair() 从未被调用
  7. 直到下一次正常 set_next_task_fair() 之前,fair hrtick 永远不会再触发。

新流程

  1. task_tick_fair(queued=1) 处把"是否需要在 fast path 补一次 hrtick"的判断写到 rq 上的 hrtick_rearm_fair 标志;
  2. put_prev_set_next_task()next == prev 分支调用新 helper hrtick_rearm_fair(rq, next),由它消费 flag 并启动新的 fair hrtick;
  3. 收紧触发条件:仅当 hrtick_enabled_fair 开启、h_nr_runnable > 1h_nr_runnable == h_nr_queued 时记录。
  hrtick expires
        |
        v
  task_tick_fair(queued=1)
        |-- set rq->hrtick_rearm_fair = enabled && nr_runnable>1 && nr_runnable==nr_queued
        v
  entity_tick() -> resched_curr()
        |
        v
  schedule() -> pick_task_fair() selects curr
        |
        v
  put_prev_set_next_task(rq, curr, curr)
        +-------------------------------+
        |   next == prev ?  yes         |
        |   skip class callbacks        |
        |   call hrtick_rearm_fair(rq)  |
        +-------------------------------+
                    |
                    v
        __hrtick_rearm_fair()
            re-check: enabled, !active, fair class
                    |
                    v
        hrtick_start_fair()  --> arm new fair hrtick

两条状态变化对比:

       [OLD path]                    [NEW path]
hrtick -> queued tick            hrtick -> queued tick
   marked: (none)                   marked: rq->hrtick_rearm_fair = true (gated)
pick_task_fair = curr           pick_task_fair = curr
next == prev -> return          next == prev -> hrtick_rearm_fair() consumes
   result: no new hrtick            flag, __hrtick_rearm_fair() arms new
                                   hrtick_start_fair()

关键实现

/* kernel/sched/fair.c */
void __hrtick_rearm_fair(struct rq *rq, struct task_struct *p)
{
    rq->hrtick_rearm_fair = false;

    if (!hrtick_enabled_fair(rq))
        return;
    if (hrtick_active(rq))
        return;
    if (p->sched_class != &fair_sched_class)
        return;

    hrtick_start_fair(rq, p);
}
/* kernel/sched/fair.c - task_tick_fair(..., queued) */
if (queued) {
    /*
     * Fair hrtick is one-shot. If this hrtick-triggered
     * reschedule picks the same task again, set_next_task_fair()
     * will be skipped. Mark that path for a possible restart, but
     * avoid delayed-dequeue cases where queued entities are not all
     * runnable.
     */
    rq->hrtick_rearm_fair = hrtick_enabled_fair(rq) &&
                            rq->cfs.h_nr_runnable > 1 &&
                            rq->cfs.h_nr_runnable == rq->cfs.h_nr_queued;
}
/* kernel/sched/sched.h - rq struct */
struct rq {
    ...
    bool hrtick_rearm_fair;
    ...
};
/* kernel/sched/sched.h - helper guarded by CONFIG_SCHED_HRTICK */
#ifdef CONFIG_SCHED_HRTICK
void __hrtick_rearm_fair(struct rq *rq, struct task_struct *p);

static inline void hrtick_rearm_fair(struct rq *rq, struct task_struct *p)
{
    if (rq->hrtick_rearm_fair)
        __hrtick_rearm_fair(rq, p);
}
#else
static inline void hrtick_rearm_fair(struct rq *rq,
                                     struct task_struct *p) { }
#endif
/* kernel/sched/sched.h - put_prev_set_next_task fast path */
if (next == prev) {
    /* Same-task repicks skip class callbacks. Restart fair hrtick
     * if the queued tick path marked it as needed. */
    hrtick_rearm_fair(rq, next);
}

hrtick_schedule_exit() 也在退出路径同步清 flag,避免跨进程残留。

类比

把 fair hrtick 想象成厨房里单次计时器:厨师做完一道菜前要按一次"开始"。第一次倒计时响起(hrtick 到期),厨师看了一眼:还是同一道菜——按原代码的逻辑,厨师直接继续,根本没重新上发条,下一次什么时候再响没人知道,于是这道菜可能整体超时。

这个补丁等于给厨师加了一张便签纸rq->hrtick_rearm_fair):

  • 当厨师因为"这道菜超时请检查"而被打断(queued tick)时,便签上写一句"要补一次计时";
  • 等回到这道菜(next == prev)时,看一眼便签,按需重新启动计时。

h_nr_runnable > 1 是"还有别的菜正排队,确实值得再起表";h_nr_runnable == h_nr_queued 则要求所有排队的菜都是真在排队、没有"假装排队其实还没准备好"(delayed-dequeue)的状态——免得在不该响的时候平白补一次计时。

Highlight:风险与注意点

  1. h_nr_runnable == h_nr_queued 与 DELAY_DEQUEUE 兼容性争议:Zhan Xusheng 指出 set_delayed() 只减 h_nr_runnable、不动 h_nr_queuedclear_delayed() 再加回,因此只要 rq 上有 delayed-dequeued 实体,等式就被打破;作者的 micro-bench 用两个 CPU-bound 任务从来没触发这种情况,可能让结论过于乐观。建议 Zhan:加入一个 sleep loop 的第三个任务再测一次,最坏情况需要回退到只用 h_nr_runnable > 1
  2. rq->hrtick_rearm_fair 这个 flag 是否必需:Zhan 进一步质疑——__hrtick_rearm_fair() 内部已经检查 hrtick_enabled_fair!hrtick_activefair_sched_class,并且只要发生 same-task repick 就"应该 arm",那么把这个意图塞进 rq flag 没有增量过滤,反而让"是否同任务抢回"被隐式编码。下一版可能改成在 put_prev_set_next_task 内直接调用 helper。
  3. rq 字段代价:每个 rq 多一个 bool,多核系统全局 × nr_cpu,可忽略但是 review 注意点。
  4. 历史背景:作者引用 20241111074841.8802-1-shijie@os.amperecomputing.com,是旧 pick_next_task_fair() 时代的同类问题,可参照对比新旧路径。
  5. 退出/类切换路径清理hrtick_schedule_exit() 重置 flag 是对的,但需要确认在调度类切换、set_next_task_* 选非 fair 任务等路径上,flag 不会被错误保留到下一次 round。

版本变化

v1 首发,无版本演化。

一句话总结

为 fair hrtick one-shot 在 next == prev 跳过 set_next_task_fair() 后漏 arm 的窗口补一次 hrtick_start_fair(),用 rq 上的 hrtick_rearm_fair 标志驱动;但 h_nr_runnable == h_nr_queuedDELAY_DEQUEUE 兼容性受到 reviewer 质疑,预期 v2 会放宽条件或重构为路径内调用。