[PATCH 09/15] sched_ext: Fix a cpu idling after put_prev_task_scx() queues its task on the local DSQ

From: Tejun Heo

Date: Fri Oct 23 2026 - 01:31:57 EST


put_prev_task_scx() enqueues a still-runnable task when the pick has settled
on idle. The insert can land the task on the cpu's own local DSQ, and then
the cpu must not go idle over it. 7a9df7667963 ("sched_ext: Fix stall when a
task enqueued with SCX_ENQ_LAST lands back on its CPU's local DSQ")
reschedules the idle task in that case, but only after the SCX_ENQ_LAST
insert.

The plain insert, taken when the task can't stay on the cpu, can land on the
local DSQ too: rescue can admit a task there when its scheduler lacks
SCX_CAP_BASE on the cpu. Without the resched the cpu idles with the task
queued until an unrelated wakeup or the rescue timer's escalation.

Reschedule after either insert. The following patch makes the plain insert
the common path for bypassed tasks under rescue.

Fixes: 5fd501744b10 ("sched_ext: Add bandwidth-limited rescue execution for stranded tasks")
Signed-off-by: Tejun Heo <tj@xxxxxxxxxx>
---
kernel/sched/ext/ext.c | 31 ++++++++++++++++++-------------
1 file changed, 18 insertions(+), 13 deletions(-)

diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index 3ca150154a3e..2e8596a74188 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -3599,6 +3599,8 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
(rq->scx.flags & SCX_RQ_PROXY_PENDING);
bool proxy_block = rq->scx.flags & SCX_RQ_PROXY_BLOCKING;
bool rescue_keep = false;
+ bool going_idle = !proxy_put && next &&
+ sched_class_above(&ext_sched_class, next->sched_class);

rq->scx.flags &= ~SCX_RQ_PROXY_PENDING;

@@ -3704,25 +3706,28 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
* locally runnable and can legitimately go idle with @p still
* runnable (see do_pick_task_scx()).
*/
- if (!proxy_put && next &&
- sched_class_above(&ext_sched_class, next->sched_class) &&
- scx_task_can_stay_on_cpu(rq, p)) {
+ if (going_idle && scx_task_can_stay_on_cpu(rq, p)) {
WARN_ON_ONCE(!sched_core_enabled(rq) &&
!(sch->ops.flags & SCX_OPS_ENQ_LAST));
scx_do_enqueue_task(rq, p, SCX_ENQ_LAST, -1);
-
- /*
- * A task put on the local DSQ runs, as anywhere else.
- * Here the insert can't reschedule on its own: the pick
- * has settled on @next and @p is still curr, so
- * resched_curr() would flag @p and __schedule() clears
- * that right after the pick. Flag @next instead.
- */
- if (p->scx.dsq == &rq->scx.local_dsq)
- set_tsk_need_resched(next);
} else {
scx_do_enqueue_task(rq, p, 0, -1);
}
+
+ /*
+ * @p may be on the local DSQ now. With %SCX_ENQ_LAST the
+ * scheduler can put the last runnable task there. The plain
+ * insert runs when @p cannot stay on this cpu, and rescue can
+ * still admit it here.
+ *
+ * The cpu must not go idle with @p queued on its local DSQ. The
+ * usual resched doesn't work: the pick has settled on @next and
+ * @p is still curr, so resched_curr() would set need_resched on
+ * @p, which __schedule() clears right after the pick. Set it on
+ * @next instead.
+ */
+ if (going_idle && p->scx.dsq == &rq->scx.local_dsq)
+ set_tsk_need_resched(next);
}

switch_class:
--
2.56.0