sched discussion
[PATCH] sched/fair: Restart hrtick after same-task repicks
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-ID | 20260813-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 触发并把当前任务抢回自己时:
task_tick_fair(rq, curr, queued=1)走到entity_tick();resched_curr()申请调度;schedule()→pick_task_fair()选中当前任务(vruntime 尚未退化);- 进入
put_prev_set_next_task(rq, next=curr, prev=curr); if (next == prev)早 return,类回调全部跳过;set_next_task_fair()与hrtick_start_fair()从未被调用;- 直到下一次正常
set_next_task_fair()之前,fair hrtick 永远不会再触发。
新流程
- 在
task_tick_fair(queued=1)处把"是否需要在 fast path 补一次 hrtick"的判断写到 rq 上的hrtick_rearm_fair标志; - 在
put_prev_set_next_task()的next == prev分支调用新 helperhrtick_rearm_fair(rq, next),由它消费 flag 并启动新的 fair hrtick; - 收紧触发条件:仅当
hrtick_enabled_fair开启、h_nr_runnable > 1、h_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:风险与注意点
h_nr_runnable == h_nr_queued与 DELAY_DEQUEUE 兼容性争议:Zhan Xusheng 指出set_delayed()只减h_nr_runnable、不动h_nr_queued,clear_delayed()再加回,因此只要 rq 上有 delayed-dequeued 实体,等式就被打破;作者的 micro-bench 用两个 CPU-bound 任务从来没触发这种情况,可能让结论过于乐观。建议 Zhan:加入一个 sleep loop 的第三个任务再测一次,最坏情况需要回退到只用h_nr_runnable > 1。rq->hrtick_rearm_fair这个 flag 是否必需:Zhan 进一步质疑——__hrtick_rearm_fair()内部已经检查hrtick_enabled_fair、!hrtick_active、fair_sched_class,并且只要发生 same-task repick 就"应该 arm",那么把这个意图塞进 rq flag 没有增量过滤,反而让"是否同任务抢回"被隐式编码。下一版可能改成在put_prev_set_next_task内直接调用 helper。- rq 字段代价:每个 rq 多一个
bool,多核系统全局 × nr_cpu,可忽略但是 review 注意点。 - 历史背景:作者引用 20241111074841.8802-1-shijie@os.amperecomputing.com,是旧
pick_next_task_fair()时代的同类问题,可参照对比新旧路径。 - 退出/类切换路径清理:
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_queued 与 DELAY_DEQUEUE 兼容性受到 reviewer 质疑,预期 v2 会放宽条件或重构为路径内调用。