* [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext
@ 2026-07-10 8:36 Andrea Righi
2026-07-10 8:36 ` [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling Andrea Righi
` (9 more replies)
0 siblings, 10 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
This series enables using proxy execution with sched_ext and is based on early
work by John Stultz [1].
Background
==========
Proxy execution (proxy-exec) lets a waiting task ("donor") donate its scheduling
context to a mutex owner, so the owner can run while the donor stays eligible on
the runqueue.
Currently, proxy-exec and sched_ext are mutually exclusive at build time: we
can't enable CONFIG_SCHED_PROXY_EXEC=y and CONFIG_SCHED_CLASS_EXT=y in the same
kernel.
This restriction can be problematic for Linux distributions and for anyone who
wants to ship one kernel and choose features at runtime.
Why are they mutually exclusive?
================================
sched_ext schedulers drive dispatch through their own interfaces. A proxy-exec
handoff can run a task that the BPF scheduler never dispatched through that
path. sched_ext callbacks then observe a "current" task that does not match what
the BPF side considers running, so kfuncs and helper state can see an
inconsistent view of the executing task.
sched_ext also tracks runnable work through Dispatch Queues (DSQs) and BPF
chosen dispatch rules, while the core scheduler still maintains classic per-CPU
runqueues and pick paths. A proxy handoff can therefore switch the CPU to a task
that the BPF scheduler never inserted or ordered through its DSQ interface.
DSQ state, vtime, and "who is running" bookkeeping inside the BPF program can
then disagree with what the core actually executes, so helpers and kfuncs that
assume their dispatched task is current may observe stale or inconsistent state.
Design: supporting proxy-exec with sched_ext
============================================
Provide proxy-exec in sched_ext as optional per-scheduler capability: a BPF
scheduler can set the ops flag SCX_OPS_ENQ_BLOCKED to keep mutex-blocked donors
runnable and receive them through ops.enqueue() with SCX_ENQ_BLOCKED set.
This flag requires an ops.enqueue() callback and gives BPF control over whether,
where, and in what order to dispatch each donor. The core then walks the
mutex-owner chain and, when needed, migrates the donor's scheduling context to
the owner's rq before executing the owner.
Without the SCX_OPS_ENQ_BLOCKED flag, mutex waiters block normally and do not
participate in proxy-exec while owned by that scheduler.
The donor-to-owner handoff is modeled like a "function call" from the
scheduler's perspective. The donor remains the running scheduling entity
selected by BPF: its scheduling context, runtime and slice are consumed while
the core temporarily invokes the mutex owner's code to make the critical section
progress. It is not a scheduler-visible switch to the owner.
Accordingly, the donor remains the scheduling context presented to sched_ext,
while the mutex owner is treated solely as the execution context selected
internally by the core scheduler. Scheduling state is accounted against
rq->donor where appropriate, while rq->curr identifies the execution context.
The internal owner substitution does not generate synthetic sched_ext callbacks
for a task that BPF did not dispatch.
The sched_ext callback bookkeeping is adjusted accordingly. Blocked proxy donors
do not generate spurious ops.running() callbacks and ops.stopping() is only
called after a real running transition. The core also drops mutex locks before
proxy rescheduling invokes scheduling-class callbacks, so callbacks can safely
inspect the proxy chain.
Scheduler ownership changes need special handling because a donor may already be
blocked when a root scheduler is enabled or when a task moves between root and
sub-schedulers. If the incoming scheduler does not set SCX_OPS_ENQ_BLOCKED, the
retained donor is removed from the runqueue and stays on the normal mutex wait
path. Otherwise it remains eligible for BPF admission.
The mutex owner's CPU can be queried from BPF using scx_bpf_task_proxy_cpu(p) or
scx_bpf_task_proxy_cid(p), with p being the mutex-blocked task. It's up to the
BPF scheduler to decide how to use this information. For example, it may inspect
the task currently running at that location, steer the donor there, or request
preemption to expedite the proxy-exec handoff. Schedulers that do not need this
information can keep the donor on its current CPU/cid and let the core perform
the handoff.
scx_qmap has been modified with a -B option to enable queueing mutex-blocked
tasks for proxy-exec.
A new kselftest (enq_blocked) is also introduced to validate proxy-exec with
sched_ext. The test creates a three-task priority inversion on one CPU: a
low-priority owner (nice +19) holds a kernel mutex, a high-priority donor
(nice -20) blocks on it and a nice 0 contender competes for the CPU. The test
runs with SCX_OPS_ENQ_BLOCKED first disabled and then enabled. In the enabled
run it checks that blocked donors reach ops.enqueue() with SCX_ENQ_BLOCKED and
that scx_bpf_task_proxy_cpu() reports the shared CPU. Access to the mutex is
provided by a loadable kernel module built via TEST_GEN_MODS_DIR, with the test
responsible for its lifecycle.
Example kselftest run:
$ sudo tools/testing/selftests/sched_ext/runner -t enq_blocked
===== START =====
TEST: enq_blocked
DESCRIPTION: Verify BPF-driven proxy donor admission
OUTPUT:
[SCX_OPS_ENQ_BLOCKED=disabled]
proxy_exec=enabled
owner_nice=19
donor_nice=-20
contender_nice=0
mutex_hold_avg_ns=254084719 (254.084 ms, samples=10)
mutex_wait_avg_ns=254095120 (254.095 ms, samples=10)
nr_blocked_enqueues=0
[SCX_OPS_ENQ_BLOCKED=enabled]
proxy_exec=enabled
owner_nice=19
donor_nice=-20
contender_nice=0
mutex_hold_avg_ns=228884734 (228.884 ms, samples=10)
mutex_wait_avg_ns=207903720 (207.903 ms, samples=10)
nr_blocked_enqueues=51
[delta: enabled - disabled]
mutex_hold_delta_ns=-25199985 (-9.92%)
mutex_wait_delta_ns=-46191400 (-18.18%)
ok 1 enq_blocked #
===== END =====
=============================
RESULTS:
PASSED: 1
SKIPPED: 0
FAILED: 0
References
==========
[1] https://lore.kernel.org/all/20251206001451.1418225-1-jstultz@google.com
Git tree: git://git.kernel.org/pub/scm/linux/kernel/git/arighi/linux.git scx-proxy-exec
Changes in v4:
- Harden scx_bpf_task_proxy_cpu() locking and blocked-state validation, return
-ENOENT for unrunnable owners and drop unnecessary donor-affinity checks
(K Prateek Nayak, sashiko)
- Reschedule remote CPUs after donor deactivation, handle sched_setscheduler()
admission and assert scheduler-change locking (sashiko)
- Avoid a potential KCSAN data-race report in the lockless blocked-donor
migration check by using rcu_access_pointer() for rq->donor (sashiko)
- Fix tick dependency updates for incoming EXT contexts and keep the tick
enabled for blocked donors (sashiko)
- Dump EXT donors in scx_dump_state() (sashiko)
- Reordered the preparatory sched_ext changes so real running-state tracking
is established before donor-based accounting, and split the proxy
destination query kfuncs into a separate patch
- Add compatibility wrappers for the proxy CPU/cid kfuncs and document their
results as scheduling hints
- Link to v3: https://lore.kernel.org/all/20260706070410.282826-1-arighi@nvidia.com/
Changes in v3:
- Dropped the core restrictions on proxy-migrating migration-disabled and
single-CPU donors: proxy execution moves the scheduling context, not the
task's execution context. (Peter Zijlstra, K Prateek Nayak)
- Dropped the sched_ext-specific put_prev_task()/set_next_task() exception and
fixed ops.running()/ops.stopping() pairing inside sched_ext instead; track a
real running transition even when either callback is absent. (Peter Zijlstra)
- Dropped the kf_tasks[] nesting and nested ops.runnable() patches from v2;
extensive proxy-exec testing did not reproduce task-op re-entry, so retain
the existing non-nesting invariant.
- Replaced scx_bpf_task_is_blocked() with SCX_ENQ_BLOCKED in ops.enqueue()
flags, identifying blocked-donor admission directly at enqueue time.
- Expanded the curr/donor description to cover mixed scheduling classes and
fixed local preemption to expire rq->donor's slice. (Aiqun Maria Yu)
- Allow inactive blocked donors to be placed on a remote local DSQ while
preventing normal migration of an active rq donor. (Aiqun Maria Yu)
- Deactivate retained donors when ownership changes to a root or sub-scheduler
without SCX_OPS_ENQ_BLOCKED, and extend the selftest to cover attaching a
scheduler after the donor blocks. (K Prateek Nayak)
- Harden remote DSQ consumption by rejecting active tasks and rechecking
migration eligibility after locking the source rq. Fall back to the global
DSQ without treating an eligibility change during the lock handoff as a BPF
scheduler error.
- scx_qmap has a command line option (-B) to enable blocked-donor queueing
- Link to v2: https://lore.kernel.org/all/20260702171909.1994478-1-arighi@nvidia.com/
Changes in v2:
- Rebased onto sched_ext/for-7.3 and adapted the series to the split
sched_ext implementation and cid-form scheduler interfaces.
- Replaced the global sched_proxy_exec_scx boot-time opt-in with the
per-scheduler SCX_OPS_ENQ_BLOCKED capability, allowing BPF to control donor
admission and ordering through ops.enqueue().
- Added scx_bpf_task_is_blocked(), scx_bpf_task_proxy_cpu(), and
scx_bpf_task_proxy_cid(); enforce CPU/cid API separation for cid-form
schedulers.
- Added proxy exec support to scx_qmap, including optional owner-cid steering,
affinity validation, and fallback to the donor's current cid.
- Added a kselftest with a kernel mutex test and a three-task priority
inversion workload, test is executed with blocked task admission disabled and
enabled, validates the behavior, and reports hold/wait-time deltas.
- Link to v1: https://lore.kernel.org/all/20260506174639.535232-1-arighi@nvidia.com/
Andrea Righi (9):
sched/core: Drop mutex locks before proxy rescheduling
sched_ext: Fix ops.running/stopping() pairing for proxy-exec donors
sched_ext: Split curr|donor references properly
sched_ext: Fix TOCTOU race in consume_remote_task()
sched_ext: Delegate proxy donor admission to BPF schedulers
sched_ext: Add proxy destination query kfuncs
sched_ext: Add selftest for blocked donor admission
sched_ext: scx_qmap: Add proxy execution support
sched: Allow enabling proxy exec with sched_ext
John Stultz (1):
sched_ext: Handle blocked donor migration with proxy execution
include/linux/sched/ext.h | 2 +
init/Kconfig | 2 -
kernel/sched/core.c | 81 ++-
kernel/sched/ext/ext.c | 279 +++++++-
kernel/sched/ext/ext.h | 6 +
kernel/sched/ext/internal.h | 25 +-
kernel/sched/ext/sub.c | 12 +-
kernel/sched/sched.h | 8 +
kernel/sched/syscalls.c | 3 +
tools/sched_ext/include/scx/common.bpf.h | 2 +
tools/sched_ext/include/scx/compat.bpf.h | 18 +
tools/sched_ext/include/scx/compat.h | 1 +
tools/sched_ext/include/scx/enum_defs.autogen.h | 1 +
tools/sched_ext/include/scx/enums.autogen.bpf.h | 3 +
tools/sched_ext/include/scx/enums.autogen.h | 1 +
tools/sched_ext/scx_qmap.bpf.c | 13 +
tools/sched_ext/scx_qmap.c | 9 +-
tools/testing/selftests/sched_ext/.gitignore | 4 +
tools/testing/selftests/sched_ext/Makefile | 2 +
tools/testing/selftests/sched_ext/config | 2 +
.../testing/selftests/sched_ext/enq_blocked.bpf.c | 109 +++
tools/testing/selftests/sched_ext/enq_blocked.c | 733 +++++++++++++++++++++
tools/testing/selftests/sched_ext/enq_blocked.h | 27 +
.../selftests/sched_ext/test_modules/Makefile | 13 +
.../sched_ext/test_modules/scx_enq_blocked_test.c | 193 ++++++
25 files changed, 1501 insertions(+), 48 deletions(-)
create mode 100644 tools/testing/selftests/sched_ext/enq_blocked.bpf.c
create mode 100644 tools/testing/selftests/sched_ext/enq_blocked.c
create mode 100644 tools/testing/selftests/sched_ext/enq_blocked.h
create mode 100644 tools/testing/selftests/sched_ext/test_modules/Makefile
create mode 100644 tools/testing/selftests/sched_ext/test_modules/scx_enq_blocked_test.c
^ permalink raw reply [flat|nested] 21+ messages in thread
* [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-10 18:56 ` John Stultz
2026-07-10 8:36 ` [PATCH 02/10] sched_ext: Fix ops.running/stopping() pairing for proxy-exec donors Andrea Righi
` (8 subsequent siblings)
9 siblings, 1 reply; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
find_proxy_task() can call proxy_resched_idle() while holding a mutex
wait_lock and the blocked task lock. Guard cleanup does not run until
after the return expression is evaluated.
proxy_resched_idle() invokes scheduling-class callbacks through
put_prev_set_next_task(). Calling those callbacks with the mutex locks
held prevents them from safely inspecting the proxy chain, as doing so
may require acquiring the same locks.
This is a preparatory change to support proxy execution in sched_ext. It
leaves the guard scope before calling proxy_resched_idle() so sched_ext
callbacks can safely inspect the proxy chain.
Reviewed-by: K Prateek Nayak <kprateek.nayak@amd.com>
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
kernel/sched/core.c | 10 ++++++----
1 file changed, 6 insertions(+), 4 deletions(-)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 3cc6fb1d20547..0139cd4a8be7e 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -6923,7 +6923,7 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
if (!READ_ONCE(owner->on_rq) || owner->se.sched_delayed) {
/* XXX Don't handle blocked owners/delayed dequeue yet */
if (curr_in_chain)
- return proxy_resched_idle(rq);
+ goto resched_idle;
__clear_task_blocked_on(p, NULL);
goto deactivate;
}
@@ -6935,7 +6935,7 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
* and leave that CPU to sort things out.
*/
if (curr_in_chain)
- return proxy_resched_idle(rq);
+ goto resched_idle;
goto migrate_task;
}
@@ -6948,7 +6948,7 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
* case we should end up back in find_proxy_task(), this time
* hopefully with all relevant tasks already enqueued.
*/
- return proxy_resched_idle(rq);
+ goto resched_idle;
}
/*
@@ -6985,7 +6985,7 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
* So schedule rq->idle so that ttwu_runnable() can get the rq
* lock and mark owner as running.
*/
- return proxy_resched_idle(rq);
+ goto resched_idle;
}
/*
* OK, now we're absolutely sure @owner is on this
@@ -6997,6 +6997,8 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
WARN_ON_ONCE(owner && !owner->on_rq);
return owner;
+resched_idle:
+ return proxy_resched_idle(rq);
deactivate:
proxy_deactivate(rq, p);
return NULL;
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 02/10] sched_ext: Fix ops.running/stopping() pairing for proxy-exec donors
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
2026-07-10 8:36 ` [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-10 21:33 ` John Stultz
2026-07-10 8:36 ` [PATCH 03/10] sched_ext: Split curr|donor references properly Andrea Righi
` (7 subsequent siblings)
9 siblings, 1 reply; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
With proxy-exec, pick_next_task() can return a task with blocked_on set
(a proxy donor). put_prev_set_next_task() then calls set_next_task_scx()
on this "ghost" task even though the task only provides scheduling
context and never actually runs.
Calling ops.running() for such a donor produces a spurious running
event. Simply suppressing ops.running() is not sufficient because the
following put_prev_task_scx() would still invoke ops.stopping(),
resulting in an unpaired stopping event.
Introduce SCX_TASK_IS_RUNNING to track whether a task entered a real
running transition. Set and clear the flag independently of
ops.running() and ops.stopping(), as the callbacks are independently
optional. Invoke ops.running() only for non-blocked tasks and invoke
ops.stopping() only after a real running transition. This keeps the
callbacks paired for proxy donors while preserving stopping
notifications for schedulers which only implement ops.stopping().
This is a preparatory change for enabling proxy execution together with
sched_ext. The explicit running-state tracking is also required by later
donor-based accounting: it prevents an EXT owner executing for a non-EXT
donor from being treated as the active EXT scheduling context when it is
dequeued.
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
include/linux/sched/ext.h | 2 ++
kernel/sched/ext/ext.c | 33 +++++++++++++++++++++++++--------
2 files changed, 27 insertions(+), 8 deletions(-)
diff --git a/include/linux/sched/ext.h b/include/linux/sched/ext.h
index 75cb8b119fb79..c6f58e1a66cdb 100644
--- a/include/linux/sched/ext.h
+++ b/include/linux/sched/ext.h
@@ -102,6 +102,8 @@ enum scx_ent_flags {
SCX_TASK_SUB_INIT = 1 << 4, /* task being initialized for a sub sched */
SCX_TASK_IMMED = 1 << 5, /* task is on local DSQ with %SCX_ENQ_IMMED */
+ SCX_TASK_IS_RUNNING = 1 << 6, /* entered a real running transition */
+
/*
* Bits 8 to 10 are used to carry task state:
*
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index 5241b55a58eca..6b7efb19d2843 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -1989,9 +1989,13 @@ static bool dequeue_task_scx(struct rq *rq, struct task_struct *p, int core_deq_
* information meaningful to the BPF scheduler and can be suppressed by
* skipping the callbacks if the task is !QUEUED.
*/
- if (SCX_HAS_OP(sch, stopping) && task_current(rq, p)) {
- update_curr_scx(rq);
- SCX_CALL_OP_TASK(sch, stopping, rq, p, false);
+ if (task_current(rq, p) &&
+ (p->scx.flags & SCX_TASK_IS_RUNNING)) {
+ if (SCX_HAS_OP(sch, stopping)) {
+ update_curr_scx(rq);
+ SCX_CALL_OP_TASK(sch, stopping, rq, p, false);
+ }
+ p->scx.flags &= ~SCX_TASK_IS_RUNNING;
}
if (SCX_HAS_OP(sch, quiescent) && !task_on_rq_migrating(p))
@@ -2686,9 +2690,17 @@ static void set_next_task_scx(struct rq *rq, struct task_struct *p, bool first)
p->se.exec_start = rq_clock_task(rq);
- /* see dequeue_task_scx() on why we skip when !QUEUED */
- if (SCX_HAS_OP(sch, running) && (p->scx.flags & SCX_TASK_QUEUED))
- SCX_CALL_OP_TASK(sch, running, rq, p);
+ /*
+ * See dequeue_task_scx() for why we skip when !QUEUED. A blocked proxy
+ * donor is also skipped because it provides scheduling context but never
+ * runs itself.
+ */
+ if ((p->scx.flags & SCX_TASK_QUEUED) && !task_is_blocked(p)) {
+ if (SCX_HAS_OP(sch, running))
+ SCX_CALL_OP_TASK(sch, running, rq, p);
+
+ p->scx.flags |= SCX_TASK_IS_RUNNING;
+ }
clr_task_runnable(p, true);
@@ -2791,8 +2803,13 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
update_curr_scx(rq);
/* see dequeue_task_scx() on why we skip when !QUEUED */
- if (SCX_HAS_OP(sch, stopping) && (p->scx.flags & SCX_TASK_QUEUED))
- SCX_CALL_OP_TASK(sch, stopping, rq, p, true);
+ if ((p->scx.flags & SCX_TASK_QUEUED) &&
+ (p->scx.flags & SCX_TASK_IS_RUNNING)) {
+ if (SCX_HAS_OP(sch, stopping))
+ SCX_CALL_OP_TASK(sch, stopping, rq, p, true);
+
+ p->scx.flags &= ~SCX_TASK_IS_RUNNING;
+ }
if (p->scx.flags & SCX_TASK_QUEUED) {
set_task_runnable(rq, p);
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 03/10] sched_ext: Split curr|donor references properly
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
2026-07-10 8:36 ` [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling Andrea Righi
2026-07-10 8:36 ` [PATCH 02/10] sched_ext: Fix ops.running/stopping() pairing for proxy-exec donors Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 04/10] sched_ext: Fix TOCTOU race in consume_remote_task() Andrea Righi
` (6 subsequent siblings)
9 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
With proxy execution, the task selected by the scheduler and the task
physically executing can differ. A blocked mutex waiter donates its
scheduling context to the lock owner:
D -----------------> M -------------> O ----------------> T
[donor] blocked on [mutex] owned by [owner] preempted by [task]
\_________________________________^
donates scheduling context
where:
D = blocked donor
M = mutex
O = mutex owner
T = competing runnable task
During a proxy execution switch, D supplies the scheduling class,
priority, and runtime budget, while O supplies the execution context: O
is the task whose code physically executes. T is a competing runnable
task which may preempt the D/O proxy execution.
Consider FAIR and EXT tasks with sched_ext running in partial mode. FAIR
can be replaced with a higher scheduling class such as RT or deadline
without changing the class interaction described here. The possible
combinations are:
1. D is EXT, O is EXT, T is EXT
D can interrupt T according to BPF scheduling policy. O executes
with D's EXT priority and runtime budget, while T waits in EXT.
2. D is EXT, O is EXT, T is FAIR
D is visible to the BPF scheduler, but cannot preempt T because
EXT is below FAIR. Once T stops, BPF can dispatch D and O executes
with D's EXT priority and runtime budget. If T becomes runnable
again, it preempts the D/O proxy execution.
3. D is EXT, O is FAIR, T is EXT
This cannot represent T preempting O because EXT is below FAIR.
4. D is EXT, O is FAIR, T is FAIR
D cannot boost O above T because EXT is below FAIR. O and T
continue competing under FAIR. Once O releases M, D wakes and
resumes normal EXT scheduling.
5. D is FAIR, O is EXT, T is EXT
D preempts T as the higher-class scheduling context. O executes
with D's FAIR priority and runtime budget, while T waits in EXT.
D is not visible to the BPF scheduler.
6. D is FAIR, O is EXT, T is FAIR
D competes with T according to its FAIR deadline. When D is
selected, O executes with D's FAIR priority and runtime budget.
D is not visible to the BPF scheduler.
7. D is FAIR, O is FAIR, T is EXT
This cannot represent T preempting O because EXT is below FAIR.
8. D is FAIR, O is FAIR, T is FAIR
O, T, and D all have FAIR scheduling contexts. D remains runnable
as a blocked proxy donor. When CFS selects D, O executes using D's
FAIR scheduling context. When CFS selects O, O executes using its
own FAIR context, and when CFS selects T, T executes normally. D
is not visible to the BPF scheduler.
Thus, sched_ext policy and accounting must generally use rq->donor, the
scheduler-selected task which supplies the scheduling context, rather
than rq->curr, the task whose code physically executes. Without proxy
execution they are the same task.
On nohz_full CPUs, a blocked proxy donor must retain the scheduler tick
even when it has an infinite slice. Otherwise, a full dynticks CPU could
stop the tick while rq->curr and rq->donor differ, violating assumptions
made by the remote NOHZ tick path.
This is a conservative compromise that keeps the change local to
sched_ext, at the cost of a periodic tick while a blocked proxy donor is
selected. Allowing blocked proxy donors to run tickless would require
making the core scheduler's remote tick handling aware that rq->curr and
rq->donor can differ.
Moreover, extend scx_dump_state() to report both contexts. Each CPU
record now includes a donor= line. If an EXT donor differs from
rq->curr, also emit its detailed task record. The existing '*' marker
continues to identify rq->curr, while the donor= line identifies the
otherwise unmarked donor record.
Note that at this point in the series, CONFIG_SCHED_PROXY_EXEC still
depends on !CONFIG_SCHED_CLASS_EXT, so proxy execution and sched_ext
cannot be enabled together. The scheduling changes are therefore
preparatory. A later patch removes this restriction.
Co-developed-by: John Stultz <jstultz@google.com>
Signed-off-by: John Stultz <jstultz@google.com>
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
kernel/sched/ext/ext.c | 53 ++++++++++++++++++++++++++++--------------
1 file changed, 36 insertions(+), 17 deletions(-)
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index 6b7efb19d2843..1946824f3b8a9 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -1149,17 +1149,24 @@ static void touch_core_sched_dispatch(struct rq *rq, struct task_struct *p)
static void update_curr_scx(struct rq *rq)
{
- struct task_struct *curr = rq->curr;
+ struct task_struct *donor;
s64 delta_exec;
+ /*
+ * update_curr_scx() is selected through rq->donor->sched_class, not
+ * rq->curr->sched_class, so @donor is always an EXT task here. If an EXT
+ * owner executes for a FAIR donor, FAIR's update_curr() runs instead.
+ */
+ donor = rq->donor;
+
delta_exec = update_curr_common(rq);
if (unlikely(delta_exec <= 0))
return;
- if (curr->scx.slice != SCX_SLICE_INF) {
- curr->scx.slice -= min_t(u64, curr->scx.slice, delta_exec);
- if (!curr->scx.slice)
- touch_core_sched(rq, curr);
+ if (donor->scx.slice != SCX_SLICE_INF) {
+ donor->scx.slice -= min_t(u64, donor->scx.slice, delta_exec);
+ if (!donor->scx.slice)
+ touch_core_sched(rq, donor);
}
dl_server_update(&rq->ext_server, delta_exec);
@@ -1320,9 +1327,9 @@ static void local_dsq_post_enq(struct scx_sched *sch, struct scx_dispatch_q *dsq
if (rq->scx.flags & SCX_RQ_IN_BALANCE)
return;
- if ((enq_flags & SCX_ENQ_PREEMPT) && p != rq->curr &&
- rq->curr->sched_class == &ext_sched_class) {
- rq->curr->scx.slice = 0;
+ if ((enq_flags & SCX_ENQ_PREEMPT) && p != rq->donor &&
+ rq->donor->sched_class == &ext_sched_class) {
+ rq->donor->scx.slice = 0;
resched_curr(rq);
}
}
@@ -2470,7 +2477,8 @@ static void dispatch_to_local_dsq(struct scx_sched *sch, struct rq *rq,
}
/* if the destination CPU is idle, wake it up */
- if (!fallback && sched_class_above(p->sched_class, dst_rq->curr->sched_class))
+ if (!fallback && sched_class_above(p->sched_class,
+ dst_rq->donor->sched_class))
resched_curr(dst_rq);
}
@@ -2678,6 +2686,7 @@ static int balance_one(struct rq *rq, struct task_struct *prev)
static void set_next_task_scx(struct rq *rq, struct task_struct *p, bool first)
{
struct scx_sched *sch = scx_task_sched(p);
+ bool can_stop_tick;
if (p->scx.flags & SCX_TASK_QUEUED) {
/*
@@ -2703,6 +2712,7 @@ static void set_next_task_scx(struct rq *rq, struct task_struct *p, bool first)
}
clr_task_runnable(p, true);
+ can_stop_tick = p->scx.slice == SCX_SLICE_INF && !task_is_blocked(p);
/*
* @p is getting newly scheduled or got kicked after someone updated its
@@ -2713,7 +2723,7 @@ static void set_next_task_scx(struct rq *rq, struct task_struct *p, bool first)
* nohz. In the future, we might want to add a mechanism to update
* load_avgs periodically on tick-stopped CPUs.
*/
- if (p->scx.slice == SCX_SLICE_INF) {
+ if (can_stop_tick) {
if (!(rq->scx.flags & SCX_RQ_CAN_STOP_TICK)) {
/*
* Bypass mode always assigns finite slices, so @p
@@ -2734,7 +2744,8 @@ static void set_next_task_scx(struct rq *rq, struct task_struct *p, bool first)
/*
* @rq still references the outgoing scheduling context. A finite
- * slice is sufficient by itself to require the tick.
+ * slice or a blocked proxy donor is sufficient by itself to require
+ * the tick.
*/
if (tick_nohz_full_cpu(cpu_of(rq)))
tick_nohz_dep_set_cpu(cpu_of(rq), TICK_DEP_BIT_SCHED);
@@ -2907,7 +2918,7 @@ static struct task_struct *first_local_task(struct rq *rq)
static struct task_struct *
do_pick_task_scx(struct rq *rq, struct rq_flags *rf, bool force_scx)
{
- struct task_struct *prev = rq->curr;
+ struct task_struct *prev = rq->donor;
bool keep_prev;
struct task_struct *p;
@@ -4060,14 +4071,14 @@ static void run_deferred(struct rq *rq)
#ifdef CONFIG_NO_HZ_FULL
bool scx_can_stop_tick(struct rq *rq)
{
- struct task_struct *p = rq->curr;
+ struct task_struct *p = rq->donor;
struct scx_sched *sch = scx_task_sched(p);
if (p->sched_class != &ext_sched_class)
return true;
/*
- * @rq->curr may still reference an outgoing EXT task after it has been
+ * @rq->donor may still reference an outgoing EXT task after it has been
* dequeued. If no EXT tasks are accounted on @rq, ignore its stale
* slice state. If another task is dispatched from a DSQ,
* set_next_task_scx() will update the dependency for the incoming task.
@@ -4081,7 +4092,8 @@ bool scx_can_stop_tick(struct rq *rq)
/*
* @rq can dispatch from different DSQs, so we can't tell whether it
* needs the tick or not by looking at nr_running. Allow stopping ticks
- * iff the BPF scheduler indicated so. See set_next_task_scx().
+ * iff set_next_task_scx() determined that the selected scheduling context
+ * can run tickless.
*/
return rq->scx.flags & SCX_RQ_CAN_STOP_TICK;
}
@@ -6047,6 +6059,9 @@ static void scx_dump_cpu(struct scx_sched *sch, struct seq_buf *s,
dump_line(&ns, " curr=%s[%d] class=%ps",
rq->curr->comm, rq->curr->pid,
rq->curr->sched_class);
+ dump_line(&ns, " donor=%s[%d] class=%ps",
+ rq->donor->comm, rq->donor->pid,
+ rq->donor->sched_class);
if (!cpumask_empty(rq->scx.cpus_to_kick))
dump_line(&ns, " cpus_to_kick : %*pb",
cpumask_pr_args(rq->scx.cpus_to_kick));
@@ -6090,6 +6105,10 @@ static void scx_dump_cpu(struct scx_sched *sch, struct seq_buf *s,
if (rq->curr->sched_class == &ext_sched_class &&
(dump_all_tasks || scx_task_on_sched(sch, rq->curr)))
scx_dump_task(sch, s, dctx, rq, rq->curr, '*');
+ if (rq->donor != rq->curr &&
+ rq->donor->sched_class == &ext_sched_class &&
+ (dump_all_tasks || scx_task_on_sched(sch, rq->donor)))
+ scx_dump_task(sch, s, dctx, rq, rq->donor, ' ');
list_for_each_entry(p, &rq->scx.runnable_list, scx.runnable_node)
if (dump_all_tasks || scx_task_on_sched(sch, p))
@@ -7518,7 +7537,7 @@ static bool kick_one_cpu(s32 cpu, struct rq *this_rq, unsigned long *ksyncs)
unsigned long flags;
raw_spin_rq_lock_irqsave(rq, flags);
- cur_class = rq->curr->sched_class;
+ cur_class = rq->donor->sched_class;
/*
* During CPU hotplug, a CPU may depend on kicking itself to make
@@ -7530,7 +7549,7 @@ static bool kick_one_cpu(s32 cpu, struct rq *this_rq, unsigned long *ksyncs)
!sched_class_above(cur_class, &ext_sched_class)) {
if (cpumask_test_cpu(cpu, this_scx->cpus_to_preempt)) {
if (cur_class == &ext_sched_class)
- rq->curr->scx.slice = 0;
+ rq->donor->scx.slice = 0;
cpumask_clear_cpu(cpu, this_scx->cpus_to_preempt);
}
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 04/10] sched_ext: Fix TOCTOU race in consume_remote_task()
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
` (2 preceding siblings ...)
2026-07-10 8:36 ` [PATCH 03/10] sched_ext: Split curr|donor references properly Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 05/10] sched_ext: Handle blocked donor migration with proxy execution Andrea Righi
` (5 subsequent siblings)
9 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
When pulling a task from a non-local DSQ, scx_consume_dispatch_q()
checks whether the task can run on the destination rq via
task_can_run_on_remote_rq(). However, it then drops the destination rq
lock and locks the source rq in consume_remote_task() ->
unlink_dsq_and_lock_src_rq(). During this window, the task might become
migration disabled, making it invalid to migrate to the destination rq.
With proxy execution enabled, a mutex-intensive workload such as
stress-ng --pipeherd 0 can trigger this race when a donor becomes active
on its source rq during the unlocked window. Migrating the active
execution context can make it resume with IRQ and preemption state from
the wrong scheduling path, triggering sleeping-while-atomic warnings and
subsequent lockdep corruption.
Re-evaluate task_can_run_on_remote_rq() after locking the source rq. Use
a non-enforcing check because eligibility may have changed during the
lock handoff independently of BPF policy; reporting that transient race
through scx_error() would unnecessarily abort the scheduler. If the task
can no longer migrate, clear its DSQ association, reset the holding CPU,
and enqueue it to the global DSQ instead. Normal consumption filtering
then skips it on ineligible CPUs while allowing an eligible CPU to pick
it without forcing it onto the source local DSQ.
Also reject tasks which are currently executing. Although an on-CPU task
should normally not be available for remote DSQ consumption, checking it
explicitly prevents a stale or racy candidate from reaching
deactivate_task() and set_task_cpu().
While the destination rq lock is dropped, clear the tracked rq state and
restore it after reacquiring the lock. Otherwise, a nested ops.dequeue()
callback can attempt to restore an rq which is no longer locked.
Closing the race and preserving rq tracking across the lock handoff are
prerequisites for correctly supporting proxy execution with sched_ext.
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
kernel/sched/ext/ext.c | 35 +++++++++++++++++++++++++++++++----
1 file changed, 31 insertions(+), 4 deletions(-)
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index 1946824f3b8a9..de1eb6f193d9a 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -2145,8 +2145,10 @@ static void move_remote_task_to_local_dsq(struct task_struct *p, u64 enq_flags,
* - The BPF scheduler is bypassed while the rq is offline and we can always say
* no to the BPF scheduler initiated migrations while offline.
*
- * The caller must ensure that @p and @rq are on different CPUs.
- * If enforce == true, caller must hold @p's rq lock.
+ * The caller must ensure that @p and @rq are on different CPUs. If @enforce is
+ * true, report violations attributable to BPF-directed migrations. The caller
+ * must hold @p's rq lock to avoid reporting a transient race as a scheduler
+ * error.
*/
static bool task_can_run_on_remote_rq(struct scx_sched *sch,
struct task_struct *p, struct rq *rq,
@@ -2164,6 +2166,10 @@ static bool task_can_run_on_remote_rq(struct scx_sched *sch,
WARN_ON_ONCE(task_cpu(p) == cpu);
+ /* Don't migrate a task which is running on a CPU. */
+ if (task_on_cpu(task_rq(p), p))
+ return false;
+
/*
* If @p has migration disabled, @p->cpus_ptr is updated to contain only
* the pinned CPU in migrate_disable_switch() while @p is being switched
@@ -2257,11 +2263,32 @@ static bool unlink_dsq_and_switch_rq_lock(struct task_struct *p,
!WARN_ON_ONCE(src_rq != task_rq(p));
}
-static bool consume_remote_task(struct rq *this_rq,
+static bool consume_remote_task(struct scx_sched *sch, struct rq *this_rq,
struct task_struct *p, u64 enq_flags,
struct scx_dispatch_q *dsq, struct rq *src_rq)
{
if (unlink_dsq_and_switch_rq_lock(p, dsq, this_rq, src_rq)) {
+ /*
+ * Eligibility may have changed while switching rq locks. This is a
+ * kernel-side race, not an invalid BPF placement request, so don't
+ * abort the scheduler on failure. Fall back to the global DSQ, where
+ * normal consumption filters can select an eligible CPU without
+ * forcing the task onto the source local DSQ.
+ */
+ if (unlikely(!task_can_run_on_remote_rq(sch, p, this_rq, false))) {
+ p->scx.dsq = NULL;
+ p->scx.holding_cpu = -1;
+ scx_dispatch_enqueue(sch, src_rq,
+ find_global_dsq(sch, task_cpu(p)), p,
+ enq_flags | SCX_ENQ_CLEAR_OPSS |
+ SCX_ENQ_GDSQ_FALLBACK);
+ if (sched_class_above(p->sched_class,
+ src_rq->donor->sched_class))
+ resched_curr(src_rq);
+ switch_rq_lock(src_rq, this_rq);
+ return false;
+ }
+
move_remote_task_to_local_dsq(p, enq_flags, src_rq, this_rq);
return true;
} else {
@@ -2377,7 +2404,7 @@ bool scx_consume_dispatch_q(struct scx_sched *sch, struct rq *rq,
}
if (task_can_run_on_remote_rq(sch, p, rq, false)) {
- if (likely(consume_remote_task(rq, p, enq_flags, dsq, task_rq)))
+ if (likely(consume_remote_task(sch, rq, p, enq_flags, dsq, task_rq)))
return true;
goto retry;
}
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 05/10] sched_ext: Handle blocked donor migration with proxy execution
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
` (3 preceding siblings ...)
2026-07-10 8:36 ` [PATCH 04/10] sched_ext: Fix TOCTOU race in consume_remote_task() Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 06/10] sched_ext: Delegate proxy donor admission to BPF schedulers Andrea Righi
` (4 subsequent siblings)
9 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
From: John Stultz <jstultz@google.com>
With proxy execution enabled, mutex-blocked donors stay runnable so
their scheduling context can execute the lock owner.
sched_ext may normally relocate a queued blocked donor. set_task_cpu()
updates both task_cpu() and wake_cpu, making the destination rq the
donor's new callback home. This remains valid if the donor was
previously proxy-migrated: the BPF scheduler is intentionally selecting
a new return destination, which the next proxy migration will preserve.
The usual migration-disabled, affinity, and rq-online checks apply.
An active donor cannot be moved normally because the source rq still
references it through rq->donor for scheduling-class operations and
runtime accounting. Moving it would associate the task with a different
rq while leaving those source-rq references in place. The core proxy
migration path avoids this by switching the rq donor to idle before
moving the scheduling context.
Allow normal sched_ext migration of blocked donors unless the donor is
active on its rq. In that case, defer migration until it is switched out
or leave it to the proxy machinery.
Keep blocked donors on the local DSQ when they are put so they remain
visible to the proxy pick path.
Signed-off-by: John Stultz <jstultz@google.com>
Co-developed-by: Andrea Righi <arighi@nvidia.com>
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
kernel/sched/ext/ext.c | 29 +++++++++++++++++++++++++++++
1 file changed, 29 insertions(+)
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index de1eb6f193d9a..c755cbefe2f2f 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -2170,6 +2170,19 @@ static bool task_can_run_on_remote_rq(struct scx_sched *sch,
if (task_on_cpu(task_rq(p), p))
return false;
+ /*
+ * A blocked donor may be moved normally to select a new callback rq.
+ * set_task_cpu() updates wake_cpu and makes the destination rq its new
+ * callback home, even if the donor was previously proxy-migrated.
+ *
+ * Don't move an active donor while the source rq still references it for
+ * scheduling and accounting. The migration can be retried after the donor
+ * is switched out.
+ */
+ if (task_is_blocked(p) &&
+ rcu_access_pointer(task_rq(p)->donor) == p)
+ return false;
+
/*
* If @p has migration disabled, @p->cpus_ptr is updated to contain only
* the pinned CPU in migrate_disable_switch() while @p is being switched
@@ -2852,6 +2865,22 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
if (p->scx.flags & SCX_TASK_QUEUED) {
set_task_runnable(rq, p);
+ /*
+ * Mutex-blocked donors stay queued on the runqueue under proxy
+ * execution, but the donor never runs as itself, proxy-exec
+ * walks the blocked_on chain on the next __schedule() and runs
+ * the lock owner in its place.
+ *
+ * Put the donor on the local DSQ directly so pick_next_task()
+ * can still see it. find_proxy_task() will either run the chain
+ * owner or deactivate the donor so the wakeup path can return it
+ * and let BPF make a new dispatch decision once it is unblocked.
+ */
+ if (task_is_blocked(p)) {
+ scx_dispatch_enqueue(sch, rq, &rq->scx.local_dsq, p, 0);
+ goto switch_class;
+ }
+
/*
* If @p has slice left and is being put, @p is getting
* preempted by a higher priority scheduler class or core-sched
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 06/10] sched_ext: Delegate proxy donor admission to BPF schedulers
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
` (4 preceding siblings ...)
2026-07-10 8:36 ` [PATCH 05/10] sched_ext: Handle blocked donor migration with proxy execution Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-11 1:43 ` John Stultz
2026-07-10 8:36 ` [PATCH 07/10] sched_ext: Add proxy destination query kfuncs Andrea Righi
` (3 subsequent siblings)
9 siblings, 1 reply; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
Proxy execution keeps a blocked donor runnable so its scheduling context
can execute the mutex owner. Dispatching sched_ext donors on a local
DSQ bypasses the BPF scheduler ordering policy and can give donors
more CPU priority than intended to perform the proxy execution handoff.
Add SCX_OPS_ENQ_BLOCKED as an explicit proxy execution capability. Tasks
owned by schedulers without the flag block normally. Schedulers with the
flag receive blocked donors through ops.enqueue() with SCX_ENQ_BLOCKED
set in enq_flags and can apply their own admission policy.
From a high-level perspective, the resulting flow for a BPF scheduler
with SCX_OPS_ENQ_BLOCKED is:
D ------ blocked on -----> M ------ owned by -----> O
[donor] [mutex] [owner]
|
| ops.enqueue(D, SCX_ENQ_BLOCKED)
| BPF dispatches D to CPUi
v
+-----------------+
| CPUi local DSQ |
+-------+---------+
|
| pick_next_task() selects D
v
+-----------------+
| proxy exec | move D to O's rq
+-------+---------+
|
| run O using D's scheduling context
v
rq->curr = O
rq->donor = D
|
| O releases M
v
+-----------------+
| proxy exec | return D to CPUi via wakeup
+-----------------+
Scheduler ownership can change after a donor has already blocked. Since
sched_change preserves queued state, handle both iterator-driven moves
to root or sub-schedulers and sched_setscheduler() transitions into EXT.
Before entering a scheduler without the flag, deactivate the retained
donor. It remains on the mutex wait path and wakes normally when the
mutex becomes available.
The global sched_ext enable state can change while __schedule() holds
the runqueue lock. Once an EXT task has an assigned scheduler, consult
it directly so the task cannot retain a donor during the transition
unless the scheduler opted in. Tasks without an assigned scheduler keep
the generic proxy execution behavior.
The preparation helpers require and assert that p->pi_lock and p's rq
lock are held before updating rq state.
The donor starts associated with its original CPU. A BPF scheduler may
dispatch it directly into that CPU local DSQ to let the core resolve the
mutex owner and execute it with the donor scheduling context.
Reschedule a retained donor when its mutex wakes it so ops.dispatch()
can reconsider the now-unblocked task. Make SCX_OPS_ENQ_BLOCKED
override the exiting and migration-disabled enqueue fallbacks so
opted-in schedulers receive all eligible donor requests.
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
kernel/sched/core.c | 43 ++++++-
kernel/sched/ext/ext.c | 107 +++++++++++++++---
kernel/sched/ext/ext.h | 6 +
kernel/sched/ext/internal.h | 23 +++-
kernel/sched/ext/sub.c | 12 +-
kernel/sched/sched.h | 6 +
kernel/sched/syscalls.c | 3 +
tools/sched_ext/include/scx/compat.h | 1 +
.../sched_ext/include/scx/enum_defs.autogen.h | 1 +
.../sched_ext/include/scx/enums.autogen.bpf.h | 3 +
tools/sched_ext/include/scx/enums.autogen.h | 1 +
11 files changed, 185 insertions(+), 21 deletions(-)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 0139cd4a8be7e..3d72f64ffe627 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -6764,6 +6764,45 @@ static void proxy_deactivate(struct rq *rq, struct task_struct *donor)
block_task(rq, donor, state);
}
+/*
+ * Remove a retained proxy donor before changing its scheduler ownership.
+ * The caller holds p->pi_lock, so p cannot wake and migrate after block_task()
+ * drops it from the runqueue.
+ *
+ * Unlike the regular schedule() path, this must leave @p fully dequeued.
+ * DELAY_DEQUEUE may keep a blocked fair task queued with sched_delayed set,
+ * which would let the following sched_change preserve and re-enqueue it under
+ * the new scheduler. Complete an existing or newly-created delayed dequeue
+ * before returning.
+ */
+void sched_proxy_block_task(struct rq *rq, struct task_struct *p)
+{
+ unsigned long state = READ_ONCE(p->__state);
+
+ lockdep_assert_held(&p->pi_lock);
+ lockdep_assert_rq_held(rq);
+
+ if (!p->is_blocked || !task_on_rq_queued(p))
+ return;
+ if (WARN_ON_ONCE(state == TASK_RUNNING))
+ return;
+
+ if (task_current_donor(rq, p)) {
+ proxy_resched_idle(rq);
+ /* Kick the execution context if @rq is remote. */
+ resched_curr(rq);
+ }
+
+ if (!p->se.sched_delayed)
+ block_task(rq, p, state);
+ if (p->se.sched_delayed)
+ dequeue_task(rq, p, DEQUEUE_SLEEP | DEQUEUE_DELAYED |
+ DEQUEUE_NOCLOCK);
+
+ WARN_ON_ONCE(task_on_rq_queued(p));
+ WARN_ON_ONCE(p->se.sched_delayed);
+}
+
static inline void proxy_release_rq_lock(struct rq *rq, struct rq_flags *rf)
__releases(__rq_lockp(rq))
{
@@ -7006,6 +7045,7 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
proxy_migrate_task(rq, rf, p, owner_cpu);
return NULL;
}
+
#else /* SCHED_PROXY_EXEC */
static struct task_struct *
find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
@@ -7136,7 +7176,8 @@ static void __sched notrace __schedule(int sched_mode)
* task_is_blocked() will always be false).
*/
try_to_block_task(rq, prev, &prev_state,
- !task_is_blocked(prev));
+ !task_is_blocked(prev) ||
+ !scx_allow_proxy_exec(prev));
switch_count = &prev->nvcsw;
}
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index c755cbefe2f2f..60154b25ce975 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -23,6 +23,63 @@
DEFINE_RAW_SPINLOCK(scx_sched_lock);
+bool scx_allow_proxy_exec(const struct task_struct *p)
+{
+ struct scx_sched *sch;
+
+ if (p->sched_class != &ext_sched_class)
+ return true;
+
+ /*
+ * scx_enabled() may change while __schedule() holds only @p's rq lock.
+ * Once @p is associated with a scheduler, use that scheduler's policy
+ * even while the global enable state is transitioning.
+ */
+ sch = scx_task_sched(p);
+ return !sch || (sch->ops.flags & SCX_OPS_ENQ_BLOCKED);
+}
+
+/*
+ * Called after sched_setscheduler() validation and immediately before
+ * sched_change_begin(), with @p's pi and rq locks held.
+ */
+void scx_prepare_setscheduler(struct task_struct *p,
+ const struct sched_class *next_class)
+{
+ struct scx_sched *sch;
+
+ lockdep_assert_held(&p->pi_lock);
+ lockdep_assert_rq_held(task_rq(p));
+
+ if (p->sched_class == next_class || next_class != &ext_sched_class)
+ return;
+
+ sch = scx_task_sched(p);
+ if (WARN_ON_ONCE(!sch))
+ return;
+
+ /* Block retained donors that the incoming scheduler cannot manage. */
+ if (!(sch->ops.flags & SCX_OPS_ENQ_BLOCKED))
+ sched_proxy_block_task(task_rq(p), p);
+}
+
+/*
+ * Called with @p's pi and rq locks held immediately before
+ * sched_change_begin(). The caller must pass DEQUEUE_NOCLOCK so the rq clock
+ * is updated only once.
+ */
+void scx_prepare_task_sched_change(struct task_struct *p, struct scx_sched *sch)
+{
+ lockdep_assert_held(&p->pi_lock);
+ lockdep_assert_rq_held(task_rq(p));
+
+ update_rq_clock(task_rq(p));
+
+ /* Block retained donors that the incoming scheduler cannot manage. */
+ if (!(sch->ops.flags & SCX_OPS_ENQ_BLOCKED))
+ sched_proxy_block_task(task_rq(p), p);
+}
+
/*
* NOTE: sched_ext is in the process of growing multiple scheduler support and
* scx_root usage is in a transitional state. Naked dereferences are safe if the
@@ -1705,6 +1762,7 @@ static void scx_do_enqueue_task(struct rq *rq, struct task_struct *p, u64 enq_fl
struct scx_sched *sch = scx_task_sched(p);
struct task_struct **ddsp_taskp;
struct scx_dispatch_q *dsq;
+ bool enq_blocked;
unsigned long qseq;
WARN_ON_ONCE(!(p->scx.flags & SCX_TASK_QUEUED));
@@ -1737,15 +1795,22 @@ static void scx_do_enqueue_task(struct rq *rq, struct task_struct *p, u64 enq_fl
if (p->scx.ddsp_dsq_id != SCX_DSQ_INVALID)
goto direct;
+ /* %SCX_OPS_ENQ_BLOCKED takes precedence over the fallbacks below. */
+ enq_blocked = (sch->ops.flags & SCX_OPS_ENQ_BLOCKED) &&
+ task_is_blocked(p);
+ if (enq_blocked)
+ enq_flags |= SCX_ENQ_BLOCKED;
+
/* see %SCX_OPS_ENQ_EXITING */
- if (!(sch->ops.flags & SCX_OPS_ENQ_EXITING) &&
+ if (!enq_blocked && !(sch->ops.flags & SCX_OPS_ENQ_EXITING) &&
unlikely(p->flags & PF_EXITING)) {
__scx_add_event(sch, SCX_EV_ENQ_SKIP_EXITING, 1);
goto local;
}
/* see %SCX_OPS_ENQ_MIGRATION_DISABLED */
- if (!(sch->ops.flags & SCX_OPS_ENQ_MIGRATION_DISABLED) &&
+ if (!enq_blocked &&
+ !(sch->ops.flags & SCX_OPS_ENQ_MIGRATION_DISABLED) &&
is_migration_disabled(p)) {
__scx_add_event(sch, SCX_EV_ENQ_SKIP_MIGRATION_DISABLED, 1);
goto local;
@@ -2048,11 +2113,18 @@ static void wakeup_preempt_scx(struct rq *rq, struct task_struct *p, int wake_fl
{
/*
* Preemption between SCX tasks is implemented by resetting the victim
- * task's slice to 0 and triggering reschedule on the target CPU.
- * Nothing to do.
- */
- if (p->sched_class == &ext_sched_class)
+ * task's slice to 0 and triggering reschedule on the target CPU. A
+ * mutex-blocked task is kept queued for proxy execution, so its wakeup
+ * doesn't go through enqueue_task_scx(). If the BPF scheduler manages
+ * blocked donors, reschedule explicitly so that it can reconsider a
+ * donor it declined to dispatch while blocked.
+ */
+ if (p->sched_class == &ext_sched_class) {
+ if (p->is_blocked &&
+ (scx_task_sched(p)->ops.flags & SCX_OPS_ENQ_BLOCKED))
+ resched_curr(rq);
return;
+ }
/*
* Getting preempted by a higher-priority class. Reenqueue IMMED tasks.
@@ -2866,18 +2938,12 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
set_task_runnable(rq, p);
/*
- * Mutex-blocked donors stay queued on the runqueue under proxy
- * execution, but the donor never runs as itself, proxy-exec
- * walks the blocked_on chain on the next __schedule() and runs
- * the lock owner in its place.
- *
- * Put the donor on the local DSQ directly so pick_next_task()
- * can still see it. find_proxy_task() will either run the chain
- * owner or deactivate the donor so the wakeup path can return it
- * and let BPF make a new dispatch decision once it is unblocked.
+ * Mutex-blocked donors only stay queued when their BPF scheduler
+ * enables %SCX_OPS_ENQ_BLOCKED, so always delegate their admission.
*/
if (task_is_blocked(p)) {
- scx_dispatch_enqueue(sch, rq, &rq->scx.local_dsq, p, 0);
+ WARN_ON_ONCE(!(sch->ops.flags & SCX_OPS_ENQ_BLOCKED));
+ scx_do_enqueue_task(rq, p, 0, -1);
goto switch_class;
}
@@ -6629,6 +6695,11 @@ int scx_validate_ops(struct scx_sched *sch, const struct sched_ext_ops *ops)
return -EINVAL;
}
+ if ((ops->flags & SCX_OPS_ENQ_BLOCKED) && !ops->enqueue) {
+ scx_error(sch, "SCX_OPS_ENQ_BLOCKED requires ops.enqueue() to be implemented");
+ return -EINVAL;
+ }
+
/*
* SCX_OPS_TID_TO_TASK is enabled by the root scheduler. A sub-sched
* may set it to declare a dependency; reject if the root hasn't
@@ -6978,6 +7049,10 @@ static void scx_root_enable_workfn(struct kthread_work *work)
if (old_class != new_class)
queue_flags |= DEQUEUE_CLASS;
+ if (new_class == &ext_sched_class) {
+ scx_prepare_task_sched_change(p, sch);
+ queue_flags |= DEQUEUE_NOCLOCK;
+ }
scoped_guard (sched_change, p, queue_flags) {
p->scx.slice = READ_ONCE(sch->slice_dfl);
diff --git a/kernel/sched/ext/ext.h b/kernel/sched/ext/ext.h
index 0b7fc46aee08c..d708abf2c3bb8 100644
--- a/kernel/sched/ext/ext.h
+++ b/kernel/sched/ext/ext.h
@@ -18,8 +18,11 @@ bool scx_can_stop_tick(struct rq *rq);
void scx_rq_activate(struct rq *rq);
void scx_rq_deactivate(struct rq *rq);
int scx_check_setscheduler(struct task_struct *p, int policy);
+void scx_prepare_setscheduler(struct task_struct *p,
+ const struct sched_class *next_class);
bool task_should_scx(int policy);
bool scx_allow_ttwu_queue(const struct task_struct *p);
+bool scx_allow_proxy_exec(const struct task_struct *p);
void init_sched_ext_class(void);
static inline u32 scx_cpuperf_target(s32 cpu)
@@ -52,8 +55,11 @@ static inline bool scx_can_stop_tick(struct rq *rq) { return true; }
static inline void scx_rq_activate(struct rq *rq) {}
static inline void scx_rq_deactivate(struct rq *rq) {}
static inline int scx_check_setscheduler(struct task_struct *p, int policy) { return 0; }
+static inline void scx_prepare_setscheduler(struct task_struct *p,
+ const struct sched_class *next_class) {}
static inline bool task_on_scx(const struct task_struct *p) { return false; }
static inline bool scx_allow_ttwu_queue(const struct task_struct *p) { return true; }
+static inline bool scx_allow_proxy_exec(const struct task_struct *p) { return true; }
static inline void init_sched_ext_class(void) {}
#endif /* CONFIG_SCHED_CLASS_EXT */
diff --git a/kernel/sched/ext/internal.h b/kernel/sched/ext/internal.h
index 5ca44ad887864..8ac195934f63b 100644
--- a/kernel/sched/ext/internal.h
+++ b/kernel/sched/ext/internal.h
@@ -212,6 +212,19 @@ enum scx_ops_flags {
*/
SCX_OPS_TID_TO_TASK = 1LLU << 8,
+ /*
+ * If set, mutex-blocked tasks remain runnable as proxy donors and are
+ * passed to ops.enqueue() with %SCX_ENQ_BLOCKED. The BPF scheduler controls
+ * when donors are dispatched and whether they should preempt other work.
+ *
+ * If clear, mutex-blocked tasks are removed from the runqueue normally
+ * and cannot donate their scheduling context through proxy execution.
+ *
+ * For blocked donors, this flag takes precedence over
+ * %SCX_OPS_ENQ_EXITING and %SCX_OPS_ENQ_MIGRATION_DISABLED.
+ */
+ SCX_OPS_ENQ_BLOCKED = 1LLU << 9,
+
SCX_OPS_ALL_FLAGS = SCX_OPS_KEEP_BUILTIN_IDLE |
SCX_OPS_ENQ_LAST |
SCX_OPS_ENQ_EXITING |
@@ -220,7 +233,8 @@ enum scx_ops_flags {
SCX_OPS_SWITCH_PARTIAL |
SCX_OPS_BUILTIN_IDLE_PER_NODE |
SCX_OPS_ALWAYS_ENQ_IMMED |
- SCX_OPS_TID_TO_TASK,
+ SCX_OPS_TID_TO_TASK |
+ SCX_OPS_ENQ_BLOCKED,
/* high 8 bits are internal, don't include in SCX_OPS_ALL_FLAGS */
__SCX_OPS_INTERNAL_MASK = 0xffLLU << 56,
@@ -1339,6 +1353,12 @@ enum scx_enq_flags {
*/
SCX_ENQ_LAST = 1LLU << 41,
+ /*
+ * The task is blocked on a mutex and is being kept runnable as a proxy
+ * donor. Only passed to ops.enqueue() when %SCX_OPS_ENQ_BLOCKED is set.
+ */
+ SCX_ENQ_BLOCKED = 1LLU << 42,
+
/* high 8 bits are internal */
__SCX_ENQ_INTERNAL_MASK = 0xffLLU << 56,
@@ -1624,6 +1644,7 @@ void scx_task_iter_start(struct scx_task_iter *iter, struct cgroup *cgrp);
void scx_task_iter_unlock(struct scx_task_iter *iter);
void scx_task_iter_stop(struct scx_task_iter *iter);
struct task_struct *scx_task_iter_next_locked(struct scx_task_iter *iter);
+void scx_prepare_task_sched_change(struct task_struct *p, struct scx_sched *sch);
bool scx_consume_dispatch_q(struct scx_sched *sch, struct rq *rq,
struct scx_dispatch_q *dsq, u64 enq_flags);
bool scx_consume_global_dsq(struct scx_sched *sch, struct rq *rq);
diff --git a/kernel/sched/ext/sub.c b/kernel/sched/ext/sub.c
index ce76ae141e0a9..954aef5950e95 100644
--- a/kernel/sched/ext/sub.c
+++ b/kernel/sched/ext/sub.c
@@ -117,7 +117,9 @@ static void scx_fail_parent(struct scx_sched *sch,
if (scx_task_on_sched(parent, p))
continue;
- scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE) {
+ scx_prepare_task_sched_change(p, parent);
+ scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE |
+ DEQUEUE_NOCLOCK) {
scx_disable_and_exit_task(sch, p);
scx_set_task_sched(p, parent);
}
@@ -209,7 +211,9 @@ void scx_sub_disable(struct scx_sched *sch)
continue;
}
- scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE) {
+ scx_prepare_task_sched_change(p, parent);
+ scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE |
+ DEQUEUE_NOCLOCK) {
/*
* $p is initialized for $parent and still attached to
* @sch. Disable and exit for @sch, switch over to
@@ -503,7 +507,9 @@ void scx_sub_enable_workfn(struct kthread_work *work)
if (!(p->scx.flags & SCX_TASK_SUB_INIT))
continue;
- scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE) {
+ scx_prepare_task_sched_change(p, sch);
+ scoped_guard (sched_change, p, DEQUEUE_SAVE | DEQUEUE_MOVE |
+ DEQUEUE_NOCLOCK) {
/*
* $p must be either READY or ENABLED. If ENABLED,
* __scx_disabled_and_exit_task() first disables and
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 80b72d934ff37..dfa0cb722c00c 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -2471,6 +2471,12 @@ static inline bool task_is_blocked(struct task_struct *p)
return !!p->blocked_on;
}
+#ifdef CONFIG_SCHED_PROXY_EXEC
+void sched_proxy_block_task(struct rq *rq, struct task_struct *p);
+#else
+static inline void sched_proxy_block_task(struct rq *rq, struct task_struct *p) {}
+#endif
+
static inline int task_on_cpu(struct rq *rq, struct task_struct *p)
{
return p->on_cpu;
diff --git a/kernel/sched/syscalls.c b/kernel/sched/syscalls.c
index b215b0ead9a60..2bbba3dc8c890 100644
--- a/kernel/sched/syscalls.c
+++ b/kernel/sched/syscalls.c
@@ -678,6 +678,9 @@ int __sched_setscheduler(struct task_struct *p,
if (prev_class != next_class)
queue_flags |= DEQUEUE_CLASS;
+ if (!(attr->sched_flags & SCHED_FLAG_KEEP_PARAMS))
+ scx_prepare_setscheduler(p, next_class);
+
scoped_guard (sched_change, p, queue_flags) {
if (!(attr->sched_flags & SCHED_FLAG_KEEP_PARAMS)) {
diff --git a/tools/sched_ext/include/scx/compat.h b/tools/sched_ext/include/scx/compat.h
index 23d9ef3e4c9d2..8d5606e465080 100644
--- a/tools/sched_ext/include/scx/compat.h
+++ b/tools/sched_ext/include/scx/compat.h
@@ -117,6 +117,7 @@ static inline bool __COMPAT_struct_has_field(const char *type, const char *field
#define SCX_OPS_ALLOW_QUEUED_WAKEUP SCX_OPS_FLAG(SCX_OPS_ALLOW_QUEUED_WAKEUP)
#define SCX_OPS_BUILTIN_IDLE_PER_NODE SCX_OPS_FLAG(SCX_OPS_BUILTIN_IDLE_PER_NODE)
#define SCX_OPS_ALWAYS_ENQ_IMMED SCX_OPS_FLAG(SCX_OPS_ALWAYS_ENQ_IMMED)
+#define SCX_OPS_ENQ_BLOCKED SCX_OPS_FLAG(SCX_OPS_ENQ_BLOCKED)
#define SCX_PICK_IDLE_FLAG(name) __COMPAT_ENUM_OR_ZERO("scx_pick_idle_cpu_flags", #name)
diff --git a/tools/sched_ext/include/scx/enum_defs.autogen.h b/tools/sched_ext/include/scx/enum_defs.autogen.h
index da4b459820fdd..79b31eb7db7cb 100644
--- a/tools/sched_ext/include/scx/enum_defs.autogen.h
+++ b/tools/sched_ext/include/scx/enum_defs.autogen.h
@@ -55,6 +55,7 @@
#define HAVE_SCX_ENQ_IMMED
#define HAVE_SCX_ENQ_REENQ
#define HAVE_SCX_ENQ_LAST
+#define HAVE_SCX_ENQ_BLOCKED
#define HAVE___SCX_ENQ_INTERNAL_MASK
#define HAVE_SCX_ENQ_CLEAR_OPSS
#define HAVE_SCX_ENQ_DSQ_PRIQ
diff --git a/tools/sched_ext/include/scx/enums.autogen.bpf.h b/tools/sched_ext/include/scx/enums.autogen.bpf.h
index dafccbb6b69d2..7efe7b9346b49 100644
--- a/tools/sched_ext/include/scx/enums.autogen.bpf.h
+++ b/tools/sched_ext/include/scx/enums.autogen.bpf.h
@@ -130,6 +130,9 @@ const volatile u64 __SCX_ENQ_REENQ __weak;
const volatile u64 __SCX_ENQ_LAST __weak;
#define SCX_ENQ_LAST __SCX_ENQ_LAST
+const volatile u64 __SCX_ENQ_BLOCKED __weak;
+#define SCX_ENQ_BLOCKED __SCX_ENQ_BLOCKED
+
const volatile u64 __SCX_ENQ_CLEAR_OPSS __weak;
#define SCX_ENQ_CLEAR_OPSS __SCX_ENQ_CLEAR_OPSS
diff --git a/tools/sched_ext/include/scx/enums.autogen.h b/tools/sched_ext/include/scx/enums.autogen.h
index bbd4901f4fce3..f8fbeb7fbf95b 100644
--- a/tools/sched_ext/include/scx/enums.autogen.h
+++ b/tools/sched_ext/include/scx/enums.autogen.h
@@ -47,6 +47,7 @@
SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_IMMED); \
SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_REENQ); \
SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_LAST); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_BLOCKED); \
SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_CLEAR_OPSS); \
SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_DSQ_PRIQ); \
SCX_ENUM_SET(skel, scx_deq_flags, SCX_DEQ_SCHED_CHANGE); \
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 07/10] sched_ext: Add proxy destination query kfuncs
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
` (5 preceding siblings ...)
2026-07-10 8:36 ` [PATCH 06/10] sched_ext: Delegate proxy donor admission to BPF schedulers Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-10 21:54 ` John Stultz
2026-07-10 8:36 ` [PATCH 08/10] sched_ext: Add selftest for blocked donor admission Andrea Righi
` (2 subsequent siblings)
9 siblings, 1 reply; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
BPF schedulers admitting blocked proxy donors may want to know the CPU
or cid where the mutex owner will execute.
Introduce scx_bpf_task_proxy_cpu() and scx_bpf_task_proxy_cid() to
return the CPU or cid of the next mutex owner in the proxy chain.
The owner relationship may change immediately after the query, so expose
the result only as a scheduling hint. Return a negative errno when no
valid proxy destination or cid mapping is available.
Provide compatibility wrappers that return -EOPNOTSUPP when the kfuncs
are unavailable.
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
kernel/sched/core.c | 28 ++++++++++++++++
kernel/sched/ext/ext.c | 42 ++++++++++++++++++++++++
kernel/sched/ext/internal.h | 6 ++--
kernel/sched/sched.h | 2 ++
tools/sched_ext/include/scx/common.bpf.h | 2 ++
tools/sched_ext/include/scx/compat.bpf.h | 18 ++++++++++
6 files changed, 96 insertions(+), 2 deletions(-)
diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 3d72f64ffe627..39e2689ea6c3b 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -7046,6 +7046,34 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
return NULL;
}
+int task_proxy_cpu(struct task_struct *p)
+{
+ struct task_struct *owner;
+ struct mutex *mutex;
+
+ if (!sched_proxy_exec() || !READ_ONCE(p->is_blocked))
+ return -ENOENT;
+
+ guard(raw_spinlock_irqsave)(&p->blocked_lock);
+
+ mutex = __get_task_blocked_on(p);
+ if (!mutex)
+ return -ENOENT;
+
+ /*
+ * @blocked_lock stabilizes @blocked_on and thus the mutex lifetime.
+ * The owner is an atomic snapshot used only as a scheduling hint and
+ * may change as soon as this function returns, so wait_lock is not
+ * needed here.
+ */
+ owner = __mutex_owner(mutex);
+ if (!owner)
+ return -ENOENT;
+ if (!READ_ONCE(owner->on_rq) || owner->se.sched_delayed)
+ return -ENOENT;
+
+ return task_cpu(owner);
+}
#else /* SCHED_PROXY_EXEC */
static struct task_struct *
find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
diff --git a/kernel/sched/ext/ext.c b/kernel/sched/ext/ext.c
index 60154b25ce975..5d755d586e1cc 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -9432,6 +9432,45 @@ __bpf_kfunc bool scx_bpf_task_running(const struct task_struct *p)
return task_rq(p)->curr == p;
}
+/**
+ * scx_bpf_task_proxy_cpu - Return the next proxy execution CPU
+ * @p: task of interest
+ *
+ * Return the CPU of the mutex owner toward which @p's scheduling context
+ * would next be migrated for proxy execution. The owner relationship can
+ * change after this function returns, so the result is only a scheduling
+ * hint and the returned CPU may not be an allowed BPF dispatch destination
+ * for @p. Returns a negative errno if no valid proxy destination is available.
+ */
+__bpf_kfunc s32 scx_bpf_task_proxy_cpu(struct task_struct *p)
+{
+ return task_proxy_cpu(p);
+}
+
+/**
+ * scx_bpf_task_proxy_cid - Return the next proxy execution cid
+ * @p: task of interest
+ *
+ * cid-addressed equivalent of scx_bpf_task_proxy_cpu(). Return the cid of the
+ * mutex owner toward which @p's scheduling context would next be migrated for
+ * proxy execution. The owner relationship can change after this function
+ * returns, so the result is only a scheduling hint. Returns a negative errno
+ * if no valid proxy destination or cid mapping is available.
+ */
+__bpf_kfunc s32 scx_bpf_task_proxy_cid(struct task_struct *p)
+{
+ s16 *tbl = READ_ONCE(scx_cpu_to_cid_tbl);
+ s32 cpu;
+
+ cpu = task_proxy_cpu(p);
+ if (cpu < 0)
+ return cpu;
+ if (!tbl)
+ return -EINVAL;
+
+ return READ_ONCE(tbl[cpu]);
+}
+
/**
* scx_bpf_task_cpu - CPU a task is currently associated with
* @p: task of interest
@@ -9735,6 +9774,8 @@ BTF_ID_FLAGS(func, scx_bpf_get_possible_cpumask, KF_ACQUIRE)
BTF_ID_FLAGS(func, scx_bpf_get_online_cpumask, KF_ACQUIRE)
BTF_ID_FLAGS(func, scx_bpf_put_cpumask, KF_RELEASE)
BTF_ID_FLAGS(func, scx_bpf_task_running, KF_RCU)
+BTF_ID_FLAGS(func, scx_bpf_task_proxy_cpu, KF_RCU)
+BTF_ID_FLAGS(func, scx_bpf_task_proxy_cid, KF_RCU)
BTF_ID_FLAGS(func, scx_bpf_task_cpu, KF_RCU)
BTF_ID_FLAGS(func, scx_bpf_task_cid, KF_RCU)
BTF_ID_FLAGS(func, scx_bpf_locked_rq, KF_IMPLICIT_ARGS | KF_RET_NULL)
@@ -9770,6 +9811,7 @@ static const struct btf_kfunc_id_set scx_kfunc_set_any = {
*/
BTF_KFUNCS_START(scx_kfunc_ids_cpu_only)
BTF_ID_FLAGS(func, scx_bpf_kick_cpu, KF_IMPLICIT_ARGS)
+BTF_ID_FLAGS(func, scx_bpf_task_proxy_cpu, KF_RCU)
BTF_ID_FLAGS(func, scx_bpf_task_cpu, KF_RCU)
BTF_ID_FLAGS(func, scx_bpf_cpu_curr, KF_IMPLICIT_ARGS | KF_RET_NULL | KF_RCU_PROTECTED)
BTF_ID_FLAGS(func, scx_bpf_cpu_node, KF_IMPLICIT_ARGS)
diff --git a/kernel/sched/ext/internal.h b/kernel/sched/ext/internal.h
index 8ac195934f63b..df7420b816a04 100644
--- a/kernel/sched/ext/internal.h
+++ b/kernel/sched/ext/internal.h
@@ -214,8 +214,10 @@ enum scx_ops_flags {
/*
* If set, mutex-blocked tasks remain runnable as proxy donors and are
- * passed to ops.enqueue() with %SCX_ENQ_BLOCKED. The BPF scheduler controls
- * when donors are dispatched and whether they should preempt other work.
+ * passed to ops.enqueue() with %SCX_ENQ_BLOCKED. The BPF scheduler can
+ * query the next mutex owner's CPU or cid with scx_bpf_task_proxy_cpu()
+ * or scx_bpf_task_proxy_cid(). It controls when donors are dispatched and
+ * whether they should preempt work on the owner's CPU.
*
* If clear, mutex-blocked tasks are removed from the runqueue normally
* and cannot donate their scheduling context through proxy execution.
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index dfa0cb722c00c..8e55c922e16ca 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -2472,8 +2472,10 @@ static inline bool task_is_blocked(struct task_struct *p)
}
#ifdef CONFIG_SCHED_PROXY_EXEC
+int task_proxy_cpu(struct task_struct *p);
void sched_proxy_block_task(struct rq *rq, struct task_struct *p);
#else
+static inline int task_proxy_cpu(struct task_struct *p) { return -EOPNOTSUPP; }
static inline void sched_proxy_block_task(struct rq *rq, struct task_struct *p) {}
#endif
diff --git a/tools/sched_ext/include/scx/common.bpf.h b/tools/sched_ext/include/scx/common.bpf.h
index e7b3ba491c5e8..98a69be2dd502 100644
--- a/tools/sched_ext/include/scx/common.bpf.h
+++ b/tools/sched_ext/include/scx/common.bpf.h
@@ -95,6 +95,8 @@ s32 scx_bpf_pick_idle_cpu(const cpumask_t *cpus_allowed, u64 flags) __ksym;
s32 scx_bpf_pick_any_cpu_node(const cpumask_t *cpus_allowed, int node, u64 flags) __ksym __weak;
s32 scx_bpf_pick_any_cpu(const cpumask_t *cpus_allowed, u64 flags) __ksym;
bool scx_bpf_task_running(const struct task_struct *p) __ksym;
+s32 scx_bpf_task_proxy_cpu(struct task_struct *p) __ksym __weak;
+s32 scx_bpf_task_proxy_cid(struct task_struct *p) __ksym __weak;
s32 scx_bpf_task_cpu(const struct task_struct *p) __ksym;
struct rq *scx_bpf_locked_rq(void) __ksym;
struct task_struct *scx_bpf_cpu_curr(s32 cpu) __ksym __weak;
diff --git a/tools/sched_ext/include/scx/compat.bpf.h b/tools/sched_ext/include/scx/compat.bpf.h
index 133058578668d..d25996163206a 100644
--- a/tools/sched_ext/include/scx/compat.bpf.h
+++ b/tools/sched_ext/include/scx/compat.bpf.h
@@ -133,6 +133,24 @@ static inline void scx_bpf_cid_override(const s32 *cpu_to_cid, u32 cpu_to_cid__s
return scx_bpf_cid_override___compat(cpu_to_cid, cpu_to_cid__sz);
}
+/*
+ * v7.3: scx_bpf_task_proxy_cpu() and scx_bpf_task_proxy_cid() for querying
+ * the next proxy execution destination. Return -EOPNOTSUPP if unavailable.
+ */
+static inline s32 __COMPAT_scx_bpf_task_proxy_cpu(struct task_struct *p)
+{
+ if (bpf_ksym_exists(scx_bpf_task_proxy_cpu))
+ return scx_bpf_task_proxy_cpu(p);
+ return -EOPNOTSUPP;
+}
+
+static inline s32 __COMPAT_scx_bpf_task_proxy_cid(struct task_struct *p)
+{
+ if (bpf_ksym_exists(scx_bpf_task_proxy_cid))
+ return scx_bpf_task_proxy_cid(p);
+ return -EOPNOTSUPP;
+}
+
/**
* __COMPAT_is_enq_cpu_selected - Test if SCX_ENQ_CPU_SELECTED is on
* in a compatible way. We will preserve this __COMPAT helper until v6.16.
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 08/10] sched_ext: Add selftest for blocked donor admission
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
` (6 preceding siblings ...)
2026-07-10 8:36 ` [PATCH 07/10] sched_ext: Add proxy destination query kfuncs Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 09/10] sched_ext: scx_qmap: Add proxy execution support Andrea Righi
2026-07-10 8:36 ` [PATCH 10/10] sched: Allow enabling proxy exec with sched_ext Andrea Righi
9 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
SCX_OPS_ENQ_BLOCKED allows BPF schedulers to receive blocked proxy
donors through ops.enqueue(). SCX_ENQ_BLOCKED identifies donor admission
requests in enq_flags, while __COMPAT_scx_bpf_task_proxy_cpu() returns the
CPU of the mutex owner or a fallback error when the kfunc is unavailable.
Add test coverage for this interface and owner CPU.
Exercise a three-task priority inversion with all tasks pinned to the same
CPU using a weighted vruntime BPF scheduler. A nice +19 owner holds a
shared mutex, a nice -20 donor blocks on it and a nice 0 CPU contender
delays the owner.
Run the workload with SCX_OPS_ENQ_BLOCKED first disabled and then
enabled. In the enabled run, count blocked donor enqueues, validate that
the proxy CPU matches the shared CPU and enqueue the donor on its
current CPU's local DSQ to trigger proxy execution. Report average mutex
hold and wait times without enforcing performance results.
Proxy execution coverage requires CONFIG_SCHED_PROXY_EXEC=y, which the
selftest config selects. Access to the kernel mutex is provided via a
loadable kernel module, built through TEST_GEN_MODS_DIR and managed by
the test.
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
tools/testing/selftests/sched_ext/.gitignore | 4 +
tools/testing/selftests/sched_ext/Makefile | 2 +
tools/testing/selftests/sched_ext/config | 2 +
.../selftests/sched_ext/enq_blocked.bpf.c | 109 +++
.../testing/selftests/sched_ext/enq_blocked.c | 733 ++++++++++++++++++
.../testing/selftests/sched_ext/enq_blocked.h | 27 +
.../selftests/sched_ext/test_modules/Makefile | 13 +
.../test_modules/scx_enq_blocked_test.c | 193 +++++
8 files changed, 1083 insertions(+)
create mode 100644 tools/testing/selftests/sched_ext/enq_blocked.bpf.c
create mode 100644 tools/testing/selftests/sched_ext/enq_blocked.c
create mode 100644 tools/testing/selftests/sched_ext/enq_blocked.h
create mode 100644 tools/testing/selftests/sched_ext/test_modules/Makefile
create mode 100644 tools/testing/selftests/sched_ext/test_modules/scx_enq_blocked_test.c
diff --git a/tools/testing/selftests/sched_ext/.gitignore b/tools/testing/selftests/sched_ext/.gitignore
index ae5491a114c09..54a1fd2af713d 100644
--- a/tools/testing/selftests/sched_ext/.gitignore
+++ b/tools/testing/selftests/sched_ext/.gitignore
@@ -4,3 +4,7 @@
!Makefile
!.gitignore
!config
+!test_modules/
+!test_modules/scx_enq_blocked_test.c
+!test_modules/Makefile
+test_modules/*.mod.c
diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile
index 3cfe90e0f34fa..51a16b3d32d9b 100644
--- a/tools/testing/selftests/sched_ext/Makefile
+++ b/tools/testing/selftests/sched_ext/Makefile
@@ -5,6 +5,7 @@ include ../../../scripts/Makefile.arch
include ../../../scripts/Makefile.include
TEST_GEN_PROGS := runner
+TEST_GEN_MODS_DIR := test_modules
# override lib.mk's default rules
OVERRIDE_TARGETS := 1
@@ -164,6 +165,7 @@ all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubs
auto-test-targets := \
create_dsq \
dequeue \
+ enq_blocked \
enq_last_no_enq_fails \
ddsp_bogus_dsq_fail \
ddsp_vtimelocal_fail \
diff --git a/tools/testing/selftests/sched_ext/config b/tools/testing/selftests/sched_ext/config
index aa901b05c8ad6..affa3cf33470a 100644
--- a/tools/testing/selftests/sched_ext/config
+++ b/tools/testing/selftests/sched_ext/config
@@ -6,3 +6,5 @@ CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_BTF=y
+CONFIG_EXPERT=y
+CONFIG_SCHED_PROXY_EXEC=y
diff --git a/tools/testing/selftests/sched_ext/enq_blocked.bpf.c b/tools/testing/selftests/sched_ext/enq_blocked.bpf.c
new file mode 100644
index 0000000000000..21fd472bbf89e
--- /dev/null
+++ b/tools/testing/selftests/sched_ext/enq_blocked.bpf.c
@@ -0,0 +1,109 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
+ *
+ * Verify that SCX_OPS_ENQ_BLOCKED passes blocked proxy donors through
+ * ops.enqueue().
+ */
+
+#include <scx/common.bpf.h>
+
+#define SHARED_DSQ 0
+
+char _license[] SEC("license") = "GPL";
+
+s32 donor_pid;
+s32 expected_proxy_cpu;
+u64 nr_blocked_enqueues;
+u64 nr_bad_proxy_cpus;
+static u64 vtime_now;
+
+UEI_DEFINE(uei);
+
+s32 BPF_STRUCT_OPS(enq_blocked_select_cpu,
+ struct task_struct *p, s32 prev_cpu, u64 wake_flags)
+{
+ return prev_cpu;
+}
+
+void BPF_STRUCT_OPS(enq_blocked_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ if (enq_flags & SCX_ENQ_BLOCKED) {
+ int proxy_cpu = __COMPAT_scx_bpf_task_proxy_cpu(p);
+ int cpu = scx_bpf_task_cpu(p);
+
+ if (p->pid == donor_pid) {
+ __sync_fetch_and_add(&nr_blocked_enqueues, 1);
+ if (expected_proxy_cpu != proxy_cpu)
+ __sync_fetch_and_add(&nr_bad_proxy_cpus, 1);
+ }
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cpu,
+ SCX_SLICE_DFL, enq_flags);
+ return;
+ }
+
+ /* Limit the amount of budget an idling task can accumulate. */
+ u64 vtime = p->scx.dsq_vtime;
+
+ if (time_before(vtime, vtime_now - SCX_SLICE_DFL))
+ vtime = vtime_now - SCX_SLICE_DFL;
+
+ scx_bpf_dsq_insert_vtime(p, SHARED_DSQ, SCX_SLICE_DFL, vtime,
+ enq_flags);
+ scx_bpf_kick_cpu(scx_bpf_task_cpu(p), SCX_KICK_IDLE);
+}
+
+void BPF_STRUCT_OPS(enq_blocked_dispatch, s32 cpu, struct task_struct *prev)
+{
+ scx_bpf_dsq_move_to_local(SHARED_DSQ, 0);
+}
+
+void BPF_STRUCT_OPS(enq_blocked_running, struct task_struct *p)
+{
+ if (time_before(vtime_now, p->scx.dsq_vtime))
+ vtime_now = p->scx.dsq_vtime;
+}
+
+void BPF_STRUCT_OPS(enq_blocked_stopping, struct task_struct *p, bool runnable)
+{
+ u64 delta = scale_by_task_weight_inverse(p,
+ SCX_SLICE_DFL - p->scx.slice);
+
+ scx_bpf_task_set_dsq_vtime(p, p->scx.dsq_vtime + delta);
+}
+
+void BPF_STRUCT_OPS(enq_blocked_enable, struct task_struct *p)
+{
+ scx_bpf_task_set_dsq_vtime(p, vtime_now);
+}
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(enq_blocked_init)
+{
+ int ret;
+
+ ret = scx_bpf_create_dsq(SHARED_DSQ, -1);
+ if (ret) {
+ scx_bpf_error("failed to create DSQ %d (%d)", SHARED_DSQ, ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(enq_blocked_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+SEC(".struct_ops.link")
+struct sched_ext_ops enq_blocked_ops = {
+ .select_cpu = (void *)enq_blocked_select_cpu,
+ .enqueue = (void *)enq_blocked_enqueue,
+ .dispatch = (void *)enq_blocked_dispatch,
+ .running = (void *)enq_blocked_running,
+ .stopping = (void *)enq_blocked_stopping,
+ .enable = (void *)enq_blocked_enable,
+ .init = (void *)enq_blocked_init,
+ .exit = (void *)enq_blocked_exit,
+ .name = "enq_blocked",
+};
diff --git a/tools/testing/selftests/sched_ext/enq_blocked.c b/tools/testing/selftests/sched_ext/enq_blocked.c
new file mode 100644
index 0000000000000..ead0d0d79c85b
--- /dev/null
+++ b/tools/testing/selftests/sched_ext/enq_blocked.c
@@ -0,0 +1,733 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
+ *
+ * Exercise a three-task priority inversion with the owner, donor and contender
+ * all pinned to the same CPU. A high-priority donor blocks on a mutex held by a
+ * low-priority owner while a medium-priority contender consumes CPU. A
+ * weighted-vruntime BPF scheduler runs the workload with SCX_OPS_ENQ_BLOCKED
+ * first disabled and then enabled. When enabled, the scheduler observes the
+ * blocked donor in ops.enqueue(), validates the proxy CPU, and enqueues the
+ * donor on its current CPU's local DSQ to facilitate proxy execution.
+ *
+ * Report the average mutex hold and wait times for both runs and their deltas.
+ * The timing data is informational; only blocked-donor admission and proxy CPU
+ * selection are validated. CONFIG_SCHED_PROXY_EXEC=y is required to exercise
+ * the proxy-execution paths.
+ */
+#define _GNU_SOURCE
+
+#include <bpf/bpf.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <pthread.h>
+#include <sched.h>
+#include <scx/common.h>
+#include <stdatomic.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/resource.h>
+#include <sys/syscall.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "enq_blocked.bpf.skel.h"
+#include "enq_blocked.h"
+#include "scx_test.h"
+
+#define MODULE_NAME "scx_enq_blocked_test"
+#define MODULE_FILE "test_modules/" MODULE_NAME ".ko"
+#define DEVICE_PATH "/dev/scx_enq_blocked"
+#define WAIT_STEP_US 1000
+#define WAIT_TIMEOUT_MS 2000
+#define NR_TRIALS 10
+#define JOIN_TIMEOUT_MS ((NR_TRIALS + 1) * WAIT_TIMEOUT_MS)
+#define OWNER_NICE 19
+#define DONOR_NICE -20
+#define CONTENDER_NICE 0
+
+struct thread_ctx {
+ atomic_bool start_donor;
+ atomic_bool abort;
+ atomic_bool stop_contender;
+ atomic_int contender_status;
+ atomic_int donor_pid;
+ atomic_int donor_completed;
+ int fd;
+ int cpu;
+};
+
+static bool parse_bool(const char *value, bool *result)
+{
+ if (!strcasecmp(value, "1") || !strcasecmp(value, "y") ||
+ !strcasecmp(value, "yes") || !strcasecmp(value, "on") ||
+ !strcasecmp(value, "true")) {
+ *result = true;
+ return true;
+ }
+
+ if (!strcasecmp(value, "0") || !strcasecmp(value, "n") ||
+ !strcasecmp(value, "no") || !strcasecmp(value, "off") ||
+ !strcasecmp(value, "false")) {
+ *result = false;
+ return true;
+ }
+
+ return false;
+}
+
+static bool cmdline_bool(const char *name, bool default_value)
+{
+ char cmdline[4096], *newline, *saveptr = NULL, *token;
+ size_t name_len = strlen(name);
+ bool value = default_value;
+ FILE *file;
+
+ file = fopen("/proc/cmdline", "r");
+ if (!file)
+ return default_value;
+
+ if (!fgets(cmdline, sizeof(cmdline), file)) {
+ fclose(file);
+ return default_value;
+ }
+ fclose(file);
+ newline = strchr(cmdline, '\n');
+ if (newline)
+ *newline = '\0';
+
+ for (token = strtok_r(cmdline, " ", &saveptr); token;
+ token = strtok_r(NULL, " ", &saveptr)) {
+ bool parsed;
+
+ if (strncmp(token, name, name_len) || token[name_len] != '=')
+ continue;
+ if (parse_bool(token + name_len + 1, &parsed))
+ value = parsed;
+ }
+
+ return value;
+}
+
+static int module_path(char *path, size_t size)
+{
+ ssize_t len;
+ char *slash;
+
+ len = readlink("/proc/self/exe", path, size - 1);
+ if (len < 0)
+ return -errno;
+ path[len] = '\0';
+
+ slash = strrchr(path, '/');
+ if (!slash)
+ return -EINVAL;
+ *slash = '\0';
+
+ if (snprintf(slash, size - (slash - path), "/%s", MODULE_FILE) >=
+ size - (slash - path))
+ return -ENAMETOOLONG;
+
+ return 0;
+}
+
+static int load_test_module(bool *loaded_here)
+{
+ char path[PATH_MAX];
+ int fd, err;
+
+ err = module_path(path, sizeof(path));
+ if (err)
+ return err;
+
+ fd = open(path, O_RDONLY | O_CLOEXEC);
+ if (fd < 0)
+ return -errno;
+
+ if (syscall(SYS_finit_module, fd, "", 0)) {
+ err = errno;
+ close(fd);
+ if (err == EEXIST)
+ return 0;
+ return -err;
+ }
+
+ close(fd);
+ *loaded_here = true;
+ return 0;
+}
+
+static void unload_test_module(bool loaded_here)
+{
+ if (loaded_here && syscall(SYS_delete_module, MODULE_NAME, O_NONBLOCK))
+ SCX_ERR("Failed to unload %s (%d)", MODULE_NAME, errno);
+}
+
+static int pin_to_cpu(int cpu)
+{
+ cpu_set_t mask;
+
+ CPU_ZERO(&mask);
+ CPU_SET(cpu, &mask);
+ return sched_setaffinity(0, sizeof(mask), &mask) ? errno : 0;
+}
+
+static int set_nice(int nice)
+{
+ return setpriority(PRIO_PROCESS, 0, nice) ? errno : 0;
+}
+
+static bool wait_for_pid(atomic_int *pid)
+{
+ int waited_ms;
+
+ for (waited_ms = 0; waited_ms < WAIT_TIMEOUT_MS; waited_ms++) {
+ if (atomic_load_explicit(pid, memory_order_acquire) > 0)
+ return true;
+ usleep(WAIT_STEP_US);
+ }
+
+ return false;
+}
+
+static int wait_for_contender(struct thread_ctx *ctx)
+{
+ int status, waited_ms;
+
+ for (waited_ms = 0; waited_ms < WAIT_TIMEOUT_MS; waited_ms++) {
+ status = atomic_load_explicit(&ctx->contender_status,
+ memory_order_acquire);
+ if (status)
+ return status;
+ usleep(WAIT_STEP_US);
+ }
+
+ return -ETIMEDOUT;
+}
+
+static int wait_for_donor_state(struct thread_ctx *ctx, int expected)
+{
+ int state, waited_ms;
+
+ for (waited_ms = 0; waited_ms < WAIT_TIMEOUT_MS; waited_ms++) {
+ state = ioctl(ctx->fd, ENQ_BLOCKED_IOCTL_DONOR_STATE);
+ if (state == expected)
+ return state;
+ if (state < 0 && errno != ENOENT)
+ return -errno;
+ usleep(WAIT_STEP_US);
+ }
+
+ return -ETIMEDOUT;
+}
+
+static bool wait_for_donor(struct thread_ctx *ctx, int trial)
+{
+ int waited_ms;
+
+ for (waited_ms = 0; waited_ms < WAIT_TIMEOUT_MS; waited_ms++) {
+ if (atomic_load_explicit(&ctx->donor_completed,
+ memory_order_acquire) >= trial)
+ return true;
+ if (atomic_load_explicit(&ctx->abort, memory_order_relaxed))
+ return false;
+ usleep(WAIT_STEP_US);
+ }
+
+ return false;
+}
+
+static void *contender_fn(void *arg)
+{
+ struct thread_ctx *ctx = arg;
+ int err;
+
+ err = pin_to_cpu(ctx->cpu);
+ if (!err)
+ err = set_nice(CONTENDER_NICE);
+ atomic_store_explicit(&ctx->contender_status, err ? -err : 1,
+ memory_order_release);
+ if (err)
+ return (void *)(uintptr_t)err;
+
+ while (!atomic_load_explicit(&ctx->stop_contender,
+ memory_order_relaxed))
+ ;
+
+ return NULL;
+}
+
+static void *owner_fn(void *arg)
+{
+ struct thread_ctx *ctx = arg;
+ int err, i;
+
+ err = pin_to_cpu(ctx->cpu);
+ if (err)
+ return (void *)(uintptr_t)err;
+ err = set_nice(OWNER_NICE);
+ if (err)
+ return (void *)(uintptr_t)err;
+
+ for (i = 0; i < NR_TRIALS; i++) {
+ if (ioctl(ctx->fd, ENQ_BLOCKED_IOCTL_OWNER))
+ return (void *)(uintptr_t)errno;
+ if (!wait_for_donor(ctx, i + 1))
+ return (void *)(uintptr_t)ETIMEDOUT;
+ }
+
+ return NULL;
+}
+
+static int run_donor_trial(struct thread_ctx *ctx)
+{
+ int waited_ms;
+
+ for (waited_ms = 0; waited_ms < WAIT_TIMEOUT_MS; waited_ms++) {
+ if (!ioctl(ctx->fd, ENQ_BLOCKED_IOCTL_DONOR))
+ return 0;
+ if (errno != EAGAIN)
+ return -errno;
+ usleep(WAIT_STEP_US);
+ }
+
+ return -ETIMEDOUT;
+}
+
+static void *donor_fn(void *arg)
+{
+ struct thread_ctx *ctx = arg;
+ int err, i;
+
+ err = pin_to_cpu(ctx->cpu);
+ if (err)
+ return (void *)(uintptr_t)err;
+ err = set_nice(DONOR_NICE);
+ if (err)
+ return (void *)(uintptr_t)err;
+
+ atomic_store_explicit(&ctx->donor_pid, syscall(SYS_gettid),
+ memory_order_release);
+ while (!atomic_load_explicit(&ctx->start_donor, memory_order_acquire) &&
+ !atomic_load_explicit(&ctx->abort, memory_order_relaxed))
+ sched_yield();
+
+ if (atomic_load_explicit(&ctx->abort, memory_order_relaxed))
+ return NULL;
+
+ for (i = 0; i < NR_TRIALS; i++) {
+ err = run_donor_trial(ctx);
+ if (err)
+ return (void *)(uintptr_t)-err;
+ atomic_store_explicit(&ctx->donor_completed, i + 1,
+ memory_order_release);
+ }
+
+ return NULL;
+}
+
+static void print_avg_time(const char *name, u64 total_ns, u64 samples)
+{
+ u64 avg_ns = samples ? total_ns / samples : 0;
+
+ printf(" %s_avg_ns=%llu (%llu.%03llu ms, samples=%llu)\n", name,
+ (unsigned long long)avg_ns,
+ (unsigned long long)(avg_ns / 1000000),
+ (unsigned long long)((avg_ns / 1000) % 1000),
+ (unsigned long long)samples);
+}
+
+static void print_avg_delta(const char *name, u64 disabled_total,
+ u64 disabled_samples, u64 enabled_total,
+ u64 enabled_samples)
+{
+ u64 disabled_avg, enabled_avg;
+ s64 delta_ns;
+ double delta_pct;
+
+ if (!disabled_samples || !enabled_samples)
+ return;
+
+ disabled_avg = disabled_total / disabled_samples;
+ enabled_avg = enabled_total / enabled_samples;
+ delta_ns = (s64)enabled_avg - (s64)disabled_avg;
+ delta_pct = disabled_avg ? 100.0 * delta_ns / disabled_avg : 0.0;
+
+ printf(" %s_delta_ns=%+lld (%+.2f%%)\n", name,
+ (long long)delta_ns, delta_pct);
+}
+
+static int join_thread(pthread_t thread, const struct timespec *deadline,
+ int *thread_err)
+{
+ void *result;
+ int err;
+
+ err = pthread_timedjoin_np(thread, &result, deadline);
+ if (err)
+ return err;
+
+ *thread_err = (int)(uintptr_t)result;
+ return 0;
+}
+
+static void set_join_deadline(struct timespec *deadline)
+{
+ clock_gettime(CLOCK_REALTIME, deadline);
+ deadline->tv_sec += JOIN_TIMEOUT_MS / 1000;
+ deadline->tv_nsec += (JOIN_TIMEOUT_MS % 1000) * 1000000;
+ if (deadline->tv_nsec >= 1000000000) {
+ deadline->tv_sec++;
+ deadline->tv_nsec -= 1000000000;
+ }
+}
+
+static enum scx_test_status setup(void **ctx)
+{
+ struct enq_blocked *skel;
+ u64 flag;
+
+ skel = enq_blocked__open();
+ SCX_FAIL_IF(!skel, "Failed to open skel");
+ SCX_ENUM_INIT(skel);
+
+ flag = SCX_OPS_ENQ_BLOCKED;
+ if (!flag) {
+ enq_blocked__destroy(skel);
+ fprintf(stderr, "SKIP: SCX_OPS_ENQ_BLOCKED is unavailable\n");
+ return SCX_TEST_SKIP;
+ }
+
+ enq_blocked__destroy(skel);
+ *ctx = NULL;
+ return SCX_TEST_PASS;
+}
+
+static enum scx_test_status run_one(bool enq_blocked,
+ struct enq_blocked_stats *result)
+{
+ struct enq_blocked *skel;
+ struct thread_ctx thread_ctx = {};
+ struct bpf_link *link = NULL;
+ pthread_t owner, donor, contender;
+ struct timespec join_deadline;
+ cpu_set_t mask;
+ bool module_loaded = false;
+ bool owner_started = false, donor_started = false;
+ bool contender_started = false;
+ bool join_timed_out = false;
+ bool proxy_enabled;
+ enum scx_test_status status = SCX_TEST_PASS;
+ int cpu, donor_pid, donor_state, err, thread_err;
+ u64 nr_blocked;
+ struct enq_blocked_stats stats;
+
+ skel = enq_blocked__open();
+ if (!skel) {
+ SCX_ERR("Failed to open skel");
+ return SCX_TEST_FAIL;
+ }
+ SCX_ENUM_INIT(skel);
+ skel->struct_ops.enq_blocked_ops->flags =
+ SCX_OPS_ENQ_LAST |
+ (enq_blocked ? SCX_OPS_ENQ_BLOCKED : 0);
+ if (enq_blocked__load(skel)) {
+ SCX_ERR("Failed to load skel");
+ status = SCX_TEST_FAIL;
+ goto out_skel;
+ }
+
+ proxy_enabled = cmdline_bool("sched_proxy_exec", true);
+
+ err = load_test_module(&module_loaded);
+ if (err == -EPERM || err == -ENOENT) {
+ fprintf(stderr, "SKIP: cannot load mutex fixture (%d)\n", -err);
+ status = SCX_TEST_SKIP;
+ goto out_skel;
+ }
+ if (err) {
+ SCX_ERR("Failed to load mutex fixture (%d)", -err);
+ status = SCX_TEST_FAIL;
+ goto out_skel;
+ }
+
+ thread_ctx.fd = open(DEVICE_PATH, O_RDONLY | O_CLOEXEC);
+ if (thread_ctx.fd < 0) {
+ SCX_ERR("Failed to open %s (%d)", DEVICE_PATH, errno);
+ status = SCX_TEST_FAIL;
+ goto out_module;
+ }
+ if (ioctl(thread_ctx.fd, ENQ_BLOCKED_IOCTL_RESET_STATS)) {
+ SCX_ERR("Failed to reset mutex statistics (%d)", errno);
+ status = SCX_TEST_FAIL;
+ goto out_fd;
+ }
+
+ if (sched_getaffinity(0, sizeof(mask), &mask)) {
+ SCX_ERR("Failed to get CPU affinity (%d)", errno);
+ status = SCX_TEST_FAIL;
+ goto out_fd;
+ }
+ for (cpu = 0; cpu < CPU_SETSIZE; cpu++) {
+ if (CPU_ISSET(cpu, &mask))
+ break;
+ }
+ if (cpu == CPU_SETSIZE) {
+ status = SCX_TEST_SKIP;
+ goto out_fd;
+ }
+ thread_ctx.cpu = cpu;
+ skel->bss->expected_proxy_cpu = cpu;
+
+ if (ioctl(thread_ctx.fd, ENQ_BLOCKED_IOCTL_PREP_ATTACH)) {
+ SCX_ERR("Failed to prepare scheduler attachment (%d)", errno);
+ status = SCX_TEST_FAIL;
+ goto out_fd;
+ }
+
+ err = pthread_create(&owner, NULL, owner_fn, &thread_ctx);
+ if (err) {
+ SCX_ERR("Failed to create owner thread (%d)", err);
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+ owner_started = true;
+
+ err = pthread_create(&donor, NULL, donor_fn, &thread_ctx);
+ if (err) {
+ SCX_ERR("Failed to create donor thread (%d)", err);
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+ donor_started = true;
+
+ if (!wait_for_pid(&thread_ctx.donor_pid)) {
+ SCX_ERR("Timed out waiting for donor thread");
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+
+ donor_pid = atomic_load_explicit(&thread_ctx.donor_pid,
+ memory_order_acquire);
+ skel->bss->donor_pid = donor_pid;
+ atomic_store_explicit(&thread_ctx.start_donor, true,
+ memory_order_release);
+
+ donor_state = ENQ_BLOCKED_DONOR_SLEEPING;
+ if (proxy_enabled)
+ donor_state |= ENQ_BLOCKED_DONOR_ON_RQ;
+ err = wait_for_donor_state(&thread_ctx, donor_state);
+ if (err < 0) {
+ SCX_ERR("Donor did not block before scheduler attachment (%d)", -err);
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+
+ link = bpf_map__attach_struct_ops(skel->maps.enq_blocked_ops);
+ if (!link) {
+ SCX_ERR("Failed to attach scheduler");
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+
+ donor_state = ENQ_BLOCKED_DONOR_SLEEPING;
+ if (proxy_enabled && enq_blocked)
+ donor_state |= ENQ_BLOCKED_DONOR_ON_RQ;
+ err = wait_for_donor_state(&thread_ctx, donor_state);
+ if (err < 0) {
+ SCX_ERR("Unexpected donor state after scheduler attachment (%d)",
+ -err);
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+
+ if (ioctl(thread_ctx.fd, ENQ_BLOCKED_IOCTL_ATTACH_DONE)) {
+ SCX_ERR("Failed to complete scheduler attachment (%d)", errno);
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+
+ err = pthread_create(&contender, NULL, contender_fn, &thread_ctx);
+ if (err) {
+ SCX_ERR("Failed to create contender thread (%d)", err);
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+ contender_started = true;
+
+ err = wait_for_contender(&thread_ctx);
+ if (err != 1) {
+ SCX_ERR("Contender thread failed (%d)", -err);
+ status = SCX_TEST_FAIL;
+ goto out;
+ }
+
+out:
+ ioctl(thread_ctx.fd, ENQ_BLOCKED_IOCTL_ATTACH_DONE);
+ if (status != SCX_TEST_PASS) {
+ atomic_store_explicit(&thread_ctx.abort, true, memory_order_release);
+ atomic_store_explicit(&thread_ctx.start_donor, true,
+ memory_order_release);
+ }
+
+ set_join_deadline(&join_deadline);
+ if (donor_started) {
+ err = join_thread(donor, &join_deadline, &thread_err);
+ if (err == ETIMEDOUT) {
+ SCX_ERR("Timed out waiting for donor thread");
+ join_timed_out = true;
+ status = SCX_TEST_FAIL;
+ } else if (err) {
+ SCX_ERR("Failed to join donor thread (%d)", err);
+ status = SCX_TEST_FAIL;
+ } else {
+ donor_started = false;
+ if (thread_err) {
+ SCX_ERR("Donor thread failed (%d)", thread_err);
+ status = SCX_TEST_FAIL;
+ }
+ }
+ }
+ if (!join_timed_out && owner_started) {
+ err = join_thread(owner, &join_deadline, &thread_err);
+ if (err == ETIMEDOUT) {
+ SCX_ERR("Timed out waiting for owner thread");
+ join_timed_out = true;
+ status = SCX_TEST_FAIL;
+ } else if (err) {
+ SCX_ERR("Failed to join owner thread (%d)", err);
+ status = SCX_TEST_FAIL;
+ } else {
+ owner_started = false;
+ if (thread_err) {
+ SCX_ERR("Owner thread failed (%d)", thread_err);
+ status = SCX_TEST_FAIL;
+ }
+ }
+ }
+ atomic_store_explicit(&thread_ctx.stop_contender, true,
+ memory_order_release);
+ if (!join_timed_out && contender_started) {
+ err = join_thread(contender, &join_deadline, &thread_err);
+ if (err == ETIMEDOUT) {
+ SCX_ERR("Timed out waiting for contender thread");
+ join_timed_out = true;
+ status = SCX_TEST_FAIL;
+ } else if (err) {
+ SCX_ERR("Failed to join contender thread (%d)", err);
+ status = SCX_TEST_FAIL;
+ } else {
+ contender_started = false;
+ if (thread_err) {
+ SCX_ERR("Contender thread failed (%d)", thread_err);
+ status = SCX_TEST_FAIL;
+ }
+ }
+ }
+
+ /* Restore the fair scheduler before waiting for any stranded thread. */
+ if (join_timed_out) {
+ atomic_store_explicit(&thread_ctx.abort, true,
+ memory_order_release);
+ if (link) {
+ bpf_link__destroy(link);
+ link = NULL;
+ }
+ if (donor_started)
+ pthread_join(donor, NULL);
+ if (owner_started)
+ pthread_join(owner, NULL);
+ if (contender_started)
+ pthread_join(contender, NULL);
+ }
+
+ if (ioctl(thread_ctx.fd, ENQ_BLOCKED_IOCTL_GET_STATS, &stats)) {
+ SCX_ERR("Failed to read mutex statistics (%d)", errno);
+ status = SCX_TEST_FAIL;
+ } else {
+ *result = stats;
+ printf("\n[SCX_OPS_ENQ_BLOCKED=%s]\n",
+ enq_blocked ? "enabled" : "disabled");
+ printf(" proxy_exec=%s\n",
+ proxy_enabled ? "enabled" : "disabled");
+ printf(" owner_nice=%d\n", OWNER_NICE);
+ printf(" donor_nice=%d\n", DONOR_NICE);
+ printf(" contender_nice=%d\n", CONTENDER_NICE);
+ print_avg_time("mutex_hold", stats.hold_time_ns, stats.nr_holds);
+ print_avg_time("mutex_wait", stats.wait_time_ns, stats.nr_waits);
+ }
+
+ nr_blocked = skel->bss->nr_blocked_enqueues;
+ printf(" nr_blocked_enqueues=%llu\n",
+ (unsigned long long)nr_blocked);
+ if (status == SCX_TEST_PASS) {
+ if (enq_blocked && proxy_enabled && !nr_blocked) {
+ SCX_ERR("ops.enqueue() did not receive the blocked donor");
+ status = SCX_TEST_FAIL;
+ } else if ((!enq_blocked || !proxy_enabled) && nr_blocked) {
+ SCX_ERR("ops.enqueue() unexpectedly received %llu blocked donors",
+ (unsigned long long)nr_blocked);
+ status = SCX_TEST_FAIL;
+ } else if (skel->bss->nr_bad_proxy_cpus) {
+ SCX_ERR("scx_bpf_task_proxy_cpu() returned an unexpected CPU %llu times",
+ (unsigned long long)skel->bss->nr_bad_proxy_cpus);
+ status = SCX_TEST_FAIL;
+ }
+ }
+
+ if (skel->data->uei.kind != EXIT_KIND(SCX_EXIT_NONE)) {
+ SCX_ERR("Scheduler exited unexpectedly (kind=%llu code=%lld)",
+ (unsigned long long)skel->data->uei.kind,
+ (long long)skel->data->uei.exit_code);
+ status = SCX_TEST_FAIL;
+ }
+
+ if (link)
+ bpf_link__destroy(link);
+out_fd:
+ close(thread_ctx.fd);
+out_module:
+ unload_test_module(module_loaded);
+out_skel:
+ enq_blocked__destroy(skel);
+ return status;
+}
+
+static enum scx_test_status run(void *ctx)
+{
+ struct enq_blocked_stats disabled = {}, enabled = {};
+ enum scx_test_status status;
+
+ (void)ctx;
+
+ status = run_one(false, &disabled);
+ if (status != SCX_TEST_PASS)
+ return status;
+
+ status = run_one(true, &enabled);
+ if (status != SCX_TEST_PASS)
+ return status;
+
+ printf("\n[delta: enabled - disabled]\n");
+ print_avg_delta("mutex_hold", disabled.hold_time_ns,
+ disabled.nr_holds, enabled.hold_time_ns,
+ enabled.nr_holds);
+ print_avg_delta("mutex_wait", disabled.wait_time_ns,
+ disabled.nr_waits, enabled.wait_time_ns,
+ enabled.nr_waits);
+
+ return SCX_TEST_PASS;
+}
+
+struct scx_test enq_blocked = {
+ .name = "enq_blocked",
+ .description = "Verify BPF-driven proxy donor admission",
+ .setup = setup,
+ .run = run,
+};
+
+REGISTER_SCX_TEST(&enq_blocked)
diff --git a/tools/testing/selftests/sched_ext/enq_blocked.h b/tools/testing/selftests/sched_ext/enq_blocked.h
new file mode 100644
index 0000000000000..68094fb06c875
--- /dev/null
+++ b/tools/testing/selftests/sched_ext/enq_blocked.h
@@ -0,0 +1,27 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/* Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES */
+#ifndef __ENQ_BLOCKED_H
+#define __ENQ_BLOCKED_H
+
+#include <linux/ioctl.h>
+#include <linux/types.h>
+
+struct enq_blocked_stats {
+ __u64 hold_time_ns;
+ __u64 wait_time_ns;
+ __u64 nr_holds;
+ __u64 nr_waits;
+};
+
+#define ENQ_BLOCKED_IOCTL_OWNER _IO('s', 1)
+#define ENQ_BLOCKED_IOCTL_DONOR _IO('s', 2)
+#define ENQ_BLOCKED_IOCTL_RESET_STATS _IO('s', 3)
+#define ENQ_BLOCKED_IOCTL_GET_STATS _IOR('s', 4, struct enq_blocked_stats)
+#define ENQ_BLOCKED_IOCTL_PREP_ATTACH _IO('s', 5)
+#define ENQ_BLOCKED_IOCTL_ATTACH_DONE _IO('s', 6)
+#define ENQ_BLOCKED_IOCTL_DONOR_STATE _IO('s', 7)
+
+#define ENQ_BLOCKED_DONOR_SLEEPING (1U << 0)
+#define ENQ_BLOCKED_DONOR_ON_RQ (1U << 1)
+
+#endif /* __ENQ_BLOCKED_H */
diff --git a/tools/testing/selftests/sched_ext/test_modules/Makefile b/tools/testing/selftests/sched_ext/test_modules/Makefile
new file mode 100644
index 0000000000000..a0e9e9401ead6
--- /dev/null
+++ b/tools/testing/selftests/sched_ext/test_modules/Makefile
@@ -0,0 +1,13 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
+
+TESTMODS_DIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
+KDIR ?= $(if $(O),$(O),$(realpath ../../../../..))
+
+obj-m += scx_enq_blocked_test.o
+
+all:
+ +$(Q)$(MAKE) -C $(KDIR) M=$(TESTMODS_DIR) modules
+
+clean:
+ +$(Q)$(MAKE) -C $(KDIR) M=$(TESTMODS_DIR) clean
diff --git a/tools/testing/selftests/sched_ext/test_modules/scx_enq_blocked_test.c b/tools/testing/selftests/sched_ext/test_modules/scx_enq_blocked_test.c
new file mode 100644
index 0000000000000..e352301d9e274
--- /dev/null
+++ b/tools/testing/selftests/sched_ext/test_modules/scx_enq_blocked_test.c
@@ -0,0 +1,193 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
+ *
+ * Kernel mutex fixture for the sched_ext SCX_OPS_ENQ_BLOCKED selftest.
+ */
+
+#include <linux/atomic.h>
+#include <linux/fs.h>
+#include <linux/jiffies.h>
+#include <linux/ktime.h>
+#include <linux/miscdevice.h>
+#include <linux/module.h>
+#include <linux/mutex.h>
+#include <linux/sched.h>
+#include <linux/uaccess.h>
+
+#include "../enq_blocked.h"
+
+#define DONOR_WAIT_TIMEOUT msecs_to_jiffies(2000)
+#define ATTACH_WAIT_TIMEOUT msecs_to_jiffies(10000)
+#define MUTEX_HOLD_TIME msecs_to_jiffies(200)
+
+static DEFINE_MUTEX(test_mutex);
+static DEFINE_SPINLOCK(donor_lock);
+static struct task_struct *donor_task;
+static atomic_t owner_ready = ATOMIC_INIT(0);
+static atomic_t donor_started = ATOMIC_INIT(0);
+static atomic_t attach_pending = ATOMIC_INIT(0);
+static atomic_t attach_done = ATOMIC_INIT(0);
+static atomic64_t hold_time_ns = ATOMIC64_INIT(0);
+static atomic64_t wait_time_ns = ATOMIC64_INIT(0);
+static atomic64_t nr_holds = ATOMIC64_INIT(0);
+static atomic64_t nr_waits = ATOMIC64_INIT(0);
+
+static long run_owner(void)
+{
+ unsigned long timeout;
+ u64 start_ns;
+ long ret = 0;
+
+ atomic_set(&donor_started, 0);
+ mutex_lock(&test_mutex);
+ start_ns = ktime_get_ns();
+ atomic_set(&owner_ready, 1);
+
+ timeout = jiffies + DONOR_WAIT_TIMEOUT;
+ while (!atomic_read(&donor_started)) {
+ if (time_after(jiffies, timeout)) {
+ ret = -ETIMEDOUT;
+ goto out;
+ }
+ cond_resched();
+ }
+ if (atomic_xchg(&attach_pending, 0)) {
+ timeout = jiffies + ATTACH_WAIT_TIMEOUT;
+ while (!atomic_read(&attach_done)) {
+ if (time_after(jiffies, timeout)) {
+ ret = -ETIMEDOUT;
+ goto out;
+ }
+ cond_resched();
+ }
+ }
+
+ /* Keep yielding while the donor blocks on test_mutex. */
+ timeout = jiffies + MUTEX_HOLD_TIME;
+ while (time_before(jiffies, timeout))
+ cond_resched();
+
+out:
+ atomic_set(&owner_ready, 0);
+ atomic64_add(ktime_get_ns() - start_ns, &hold_time_ns);
+ atomic64_inc(&nr_holds);
+ mutex_unlock(&test_mutex);
+ return ret;
+}
+
+static long run_donor(void)
+{
+ unsigned long flags;
+ u64 start_ns;
+
+ if (!atomic_read(&owner_ready))
+ return -EAGAIN;
+
+ get_task_struct(current);
+ spin_lock_irqsave(&donor_lock, flags);
+ WARN_ON_ONCE(donor_task);
+ donor_task = current;
+ spin_unlock_irqrestore(&donor_lock, flags);
+
+ atomic_set(&donor_started, 1);
+ start_ns = ktime_get_ns();
+ mutex_lock(&test_mutex);
+
+ spin_lock_irqsave(&donor_lock, flags);
+ donor_task = NULL;
+ spin_unlock_irqrestore(&donor_lock, flags);
+ put_task_struct(current);
+
+ atomic64_add(ktime_get_ns() - start_ns, &wait_time_ns);
+ atomic64_inc(&nr_waits);
+ mutex_unlock(&test_mutex);
+ return 0;
+}
+
+static long get_donor_state(void)
+{
+ struct task_struct *task;
+ unsigned long flags;
+ long state = 0;
+
+ spin_lock_irqsave(&donor_lock, flags);
+ task = donor_task;
+ if (task)
+ get_task_struct(task);
+ spin_unlock_irqrestore(&donor_lock, flags);
+ if (!task)
+ return -ENOENT;
+
+ if (READ_ONCE(task->__state) != TASK_RUNNING)
+ state |= ENQ_BLOCKED_DONOR_SLEEPING;
+ if (READ_ONCE(task->on_rq))
+ state |= ENQ_BLOCKED_DONOR_ON_RQ;
+ put_task_struct(task);
+ return state;
+}
+
+static void reset_stats(void)
+{
+ atomic64_set(&hold_time_ns, 0);
+ atomic64_set(&wait_time_ns, 0);
+ atomic64_set(&nr_holds, 0);
+ atomic64_set(&nr_waits, 0);
+}
+
+static long get_stats(unsigned long arg)
+{
+ struct enq_blocked_stats stats = {
+ .hold_time_ns = atomic64_read(&hold_time_ns),
+ .wait_time_ns = atomic64_read(&wait_time_ns),
+ .nr_holds = atomic64_read(&nr_holds),
+ .nr_waits = atomic64_read(&nr_waits),
+ };
+
+ return copy_to_user((void __user *)arg, &stats, sizeof(stats)) ?
+ -EFAULT : 0;
+}
+
+static long enq_blocked_ioctl(struct file *file, unsigned int cmd,
+ unsigned long arg)
+{
+ switch (cmd) {
+ case ENQ_BLOCKED_IOCTL_OWNER:
+ return run_owner();
+ case ENQ_BLOCKED_IOCTL_DONOR:
+ return run_donor();
+ case ENQ_BLOCKED_IOCTL_RESET_STATS:
+ reset_stats();
+ return 0;
+ case ENQ_BLOCKED_IOCTL_GET_STATS:
+ return get_stats(arg);
+ case ENQ_BLOCKED_IOCTL_PREP_ATTACH:
+ atomic_set(&attach_done, 0);
+ atomic_set(&attach_pending, 1);
+ return 0;
+ case ENQ_BLOCKED_IOCTL_ATTACH_DONE:
+ atomic_set(&attach_done, 1);
+ return 0;
+ case ENQ_BLOCKED_IOCTL_DONOR_STATE:
+ return get_donor_state();
+ default:
+ return -EINVAL;
+ }
+}
+
+static const struct file_operations enq_blocked_fops = {
+ .owner = THIS_MODULE,
+ .unlocked_ioctl = enq_blocked_ioctl,
+};
+
+static struct miscdevice enq_blocked_device = {
+ .minor = MISC_DYNAMIC_MINOR,
+ .name = "scx_enq_blocked",
+ .fops = &enq_blocked_fops,
+ .mode = 0600,
+};
+
+module_misc_device(enq_blocked_device);
+MODULE_AUTHOR("Andrea Righi <arighi@nvidia.com>");
+MODULE_LICENSE("GPL");
+MODULE_DESCRIPTION("sched_ext blocked donor test module");
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 09/10] sched_ext: scx_qmap: Add proxy execution support
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
` (7 preceding siblings ...)
2026-07-10 8:36 ` [PATCH 08/10] sched_ext: Add selftest for blocked donor admission Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 10/10] sched: Allow enabling proxy exec with sched_ext Andrea Righi
9 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
Add a -B option to opt scx_qmap into queueing mutex-blocked tasks for
proxy execution. Without the option, SCX_OPS_ENQ_BLOCKED remains clear
and mutex waiters block normally. With -B, set SCX_OPS_ENQ_BLOCKED and
enable the corresponding BPF-side blocked-donor admission policy.
When qmap_enqueue() receives SCX_ENQ_BLOCKED for a donor with runtime
budget remaining, insert it directly into the local DSQ of its current
cid without refilling its slice. This lets proxy execution continue
using the donor's existing budget.
Once the donor exhausts its slice, let it proceed through qmap's normal
enqueue path. This applies the same placement and slice refill decisions
as other enqueues instead of imposing a proxy-specific policy.
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
tools/sched_ext/scx_qmap.bpf.c | 13 +++++++++++++
tools/sched_ext/scx_qmap.c | 9 +++++++--
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/tools/sched_ext/scx_qmap.bpf.c b/tools/sched_ext/scx_qmap.bpf.c
index fd9a82a676278..2ba3e2a4f625f 100644
--- a/tools/sched_ext/scx_qmap.bpf.c
+++ b/tools/sched_ext/scx_qmap.bpf.c
@@ -49,6 +49,7 @@ const volatile u64 sub_cgroup_id;
const volatile s32 disallow_tgid;
const volatile bool suppress_dump;
const volatile bool always_enq_immed;
+const volatile bool enq_blocked;
const volatile u32 immed_stress_nth;
const volatile u32 max_tasks;
@@ -396,6 +397,18 @@ void BPF_STRUCT_OPS(qmap_enqueue, struct task_struct *p, u64 enq_flags)
*/
taskc->core_sched_seq = qa.core_sched_tail_seqs[idx]++;
+ /*
+ * Admit a blocked donor with runtime budget left by inserting it into
+ * its current cid's local DSQ, reusing its remaining slice. If the
+ * slice is exhausted, fall through the normal enqueue path.
+ */
+ if (enq_blocked &&
+ (enq_flags & SCX_ENQ_BLOCKED) && p->scx.slice) {
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | scx_bpf_task_cid(p),
+ 0, enq_flags);
+ return;
+ }
+
/*
* IMMED stress testing: Every immed_stress_nth'th enqueue, dispatch
* directly to prev_cpu's local DSQ even when busy to force dsq->nr > 1
diff --git a/tools/sched_ext/scx_qmap.c b/tools/sched_ext/scx_qmap.c
index f1eaebcab5dc1..2b099167694f0 100644
--- a/tools/sched_ext/scx_qmap.c
+++ b/tools/sched_ext/scx_qmap.c
@@ -23,7 +23,7 @@ const char help_fmt[] =
"See the top-level comment in .bpf.c for more details.\n"
"\n"
"Usage: %s [-s SLICE_US] [-e COUNT] [-t COUNT] [-T COUNT] [-l COUNT] [-b COUNT]\n"
-" [-N COUNT] [-P] [-M] [-H] [-c CG_PATH] [-d PID] [-D LEN] [-S] [-p] [-I]\n"
+" [-N COUNT] [-P] [-M] [-H] [-c CG_PATH] [-d PID] [-D LEN] [-S] [-p] [-I] [-B]\n"
" [-F COUNT] [-v]\n"
"\n"
" -s SLICE_US Override slice duration\n"
@@ -42,6 +42,7 @@ const char help_fmt[] =
" -S Suppress qmap-specific debug dump\n"
" -p Switch only tasks on SCHED_EXT policy instead of all\n"
" -I Turn on SCX_OPS_ALWAYS_ENQ_IMMED\n"
+" -B Turn on SCX_OPS_ENQ_BLOCKED\n"
" -F COUNT IMMED stress: force every COUNT'th enqueue to a busy local DSQ (use with -I)\n"
" -C MODE cid-override test (shuffle|bad-dup|bad-range)\n"
" -v Print libbpf debug messages\n"
@@ -89,7 +90,7 @@ int main(int argc, char **argv)
skel->rodata->slice_ns = __COMPAT_ENUM_OR_ZERO("scx_public_consts", "SCX_SLICE_DFL");
skel->rodata->max_tasks = 16384;
- while ((opt = getopt(argc, argv, "s:e:t:T:l:b:N:PMHc:d:D:SpIF:C:vh")) != -1) {
+ while ((opt = getopt(argc, argv, "s:e:t:T:l:b:N:PMHc:d:D:SpIBF:C:vh")) != -1) {
switch (opt) {
case 's':
skel->rodata->slice_ns = strtoull(optarg, NULL, 0) * 1000;
@@ -149,6 +150,10 @@ int main(int argc, char **argv)
skel->rodata->always_enq_immed = true;
skel->struct_ops.qmap_ops->flags |= SCX_OPS_ALWAYS_ENQ_IMMED;
break;
+ case 'B':
+ skel->rodata->enq_blocked = true;
+ skel->struct_ops.qmap_ops->flags |= SCX_OPS_ENQ_BLOCKED;
+ break;
case 'F':
skel->rodata->immed_stress_nth = strtoul(optarg, NULL, 0);
break;
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* [PATCH 10/10] sched: Allow enabling proxy exec with sched_ext
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
` (8 preceding siblings ...)
2026-07-10 8:36 ` [PATCH 09/10] sched_ext: scx_qmap: Add proxy execution support Andrea Righi
@ 2026-07-10 8:36 ` Andrea Righi
9 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 8:36 UTC (permalink / raw)
To: Tejun Heo, David Vernet, Changwoo Min, John Stultz
Cc: Ingo Molnar, Peter Zijlstra, Juri Lelli, Vincent Guittot,
Dietmar Eggemann, Steven Rostedt, Ben Segall, Mel Gorman,
Valentin Schneider, K Prateek Nayak, Christian Loehle, David Dai,
Koba Ko, Aiqun Yu, Shuah Khan, sched-ext, linux-kernel
Now that sched_ext can handle proxy donors and BPF schedulers can opt in
to blocked-donor enqueueing with SCX_OPS_ENQ_BLOCKED, remove the
!SCHED_CLASS_EXT dependency from SCHED_PROXY_EXEC and allow both options
to be enabled together.
Signed-off-by: Andrea Righi <arighi@nvidia.com>
---
init/Kconfig | 2 --
1 file changed, 2 deletions(-)
diff --git a/init/Kconfig b/init/Kconfig
index 5230d4879b1c8..0817e62266e03 100644
--- a/init/Kconfig
+++ b/init/Kconfig
@@ -934,8 +934,6 @@ config SCHED_PROXY_EXEC
bool "Proxy Execution"
# Avoid some build failures w/ PREEMPT_RT until it can be fixed
depends on !PREEMPT_RT
- # Need to investigate how to inform sched_ext of split contexts
- depends on !SCHED_CLASS_EXT
# Not particularly useful until we get to multi-rq proxying
depends on EXPERT
help
--
2.55.0
^ permalink raw reply related [flat|nested] 21+ messages in thread
* Re: [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling
2026-07-10 8:36 ` [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling Andrea Righi
@ 2026-07-10 18:56 ` John Stultz
2026-07-10 20:47 ` Andrea Righi
0 siblings, 1 reply; 21+ messages in thread
From: John Stultz @ 2026-07-10 18:56 UTC (permalink / raw)
To: Andrea Righi
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
On Fri, Jul 10, 2026 at 1:39 AM Andrea Righi <arighi@nvidia.com> wrote:
>
> find_proxy_task() can call proxy_resched_idle() while holding a mutex
> wait_lock and the blocked task lock. Guard cleanup does not run until
> after the return expression is evaluated.
>
> proxy_resched_idle() invokes scheduling-class callbacks through
> put_prev_set_next_task(). Calling those callbacks with the mutex locks
> held prevents them from safely inspecting the proxy chain, as doing so
> may require acquiring the same locks.
>
> This is a preparatory change to support proxy execution in sched_ext. It
> leaves the guard scope before calling proxy_resched_idle() so sched_ext
> callbacks can safely inspect the proxy chain.
Thanks again for sending these out!
So, I wanted to understand more the importance of the sched_ext logic
being able to inspect the proxy chain?
I sort of roughly understand the argument that some sched_ext
schedulers may not want to keep blocked tasks on the rq (effectively
what the "Delegate proxy donor admission to BPF schedulers" patch
does), but that doesn't require deep inspection.
This change seems to be needed for "Add proxy destination query
kfuncs", where the commit message sounds reasonable, but as I think
more on it, it doesn't look correct (I'll comment more on the patch),
and I'm still confused a bit as to the purpose.
So can you describe the premise and a bit more detail as to why this
is important?
thanks
-john
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling
2026-07-10 18:56 ` John Stultz
@ 2026-07-10 20:47 ` Andrea Righi
2026-07-11 0:21 ` John Stultz
0 siblings, 1 reply; 21+ messages in thread
From: Andrea Righi @ 2026-07-10 20:47 UTC (permalink / raw)
To: John Stultz
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
Hi John,
On Fri, Jul 10, 2026 at 11:56:04AM -0700, John Stultz wrote:
> On Fri, Jul 10, 2026 at 1:39 AM Andrea Righi <arighi@nvidia.com> wrote:
> >
> > find_proxy_task() can call proxy_resched_idle() while holding a mutex
> > wait_lock and the blocked task lock. Guard cleanup does not run until
> > after the return expression is evaluated.
> >
> > proxy_resched_idle() invokes scheduling-class callbacks through
> > put_prev_set_next_task(). Calling those callbacks with the mutex locks
> > held prevents them from safely inspecting the proxy chain, as doing so
> > may require acquiring the same locks.
> >
> > This is a preparatory change to support proxy execution in sched_ext. It
> > leaves the guard scope before calling proxy_resched_idle() so sched_ext
> > callbacks can safely inspect the proxy chain.
>
> Thanks again for sending these out!
>
> So, I wanted to understand more the importance of the sched_ext logic
> being able to inspect the proxy chain?
>
> I sort of roughly understand the argument that some sched_ext
> schedulers may not want to keep blocked tasks on the rq (effectively
> what the "Delegate proxy donor admission to BPF schedulers" patch
> does), but that doesn't require deep inspection.
>
> This change seems to be needed for "Add proxy destination query
> kfuncs", where the commit message sounds reasonable, but as I think
> more on it, it doesn't look correct (I'll comment more on the patch),
> and I'm still confused a bit as to the purpose.
>
> So can you describe the premise and a bit more detail as to why this
> is important?
Inspecting the proxy chain is not strictly required. A BPF scheduler can just
accept a blocked donor, enqueue it on its current CPU and let the core
proxy-exec path resolve and "virtually" migrate it as needed to the owner's CPU.
The motivation for providing a scx_bpf_task_proxy_cpu() kfunc is purely for
optimization reasons. By knowing the owner's CPU, a scheduler could make more
informed admission decisions. For example, it can inspect the task currently
running on the target CPU, determine whether the donor should take precedence
over the running task and kick that CPU to trigger a preemption. Without this
information, the scheduler can only enqueue the donor and wait until the task
running on the owner's CPU releases it (potentially adding up to one time slice
of latency).
That said, I don't see a correctness dependency on exposing this information.
If the latency-policy use case is not strong enough to justify the API, I'm
happy to drop the query kfuncs and this preparatory lock-scope change for now,
the blocked-donor admission support can stand on its own.
Thanks,
-Andrea
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 02/10] sched_ext: Fix ops.running/stopping() pairing for proxy-exec donors
2026-07-10 8:36 ` [PATCH 02/10] sched_ext: Fix ops.running/stopping() pairing for proxy-exec donors Andrea Righi
@ 2026-07-10 21:33 ` John Stultz
2026-07-11 9:37 ` Andrea Righi
0 siblings, 1 reply; 21+ messages in thread
From: John Stultz @ 2026-07-10 21:33 UTC (permalink / raw)
To: Andrea Righi
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
On Fri, Jul 10, 2026 at 1:39 AM Andrea Righi <arighi@nvidia.com> wrote:
>
> With proxy-exec, pick_next_task() can return a task with blocked_on set
> (a proxy donor). put_prev_set_next_task() then calls set_next_task_scx()
> on this "ghost" task even though the task only provides scheduling
> context and never actually runs.
>
> Calling ops.running() for such a donor produces a spurious running
> event. Simply suppressing ops.running() is not sufficient because the
> following put_prev_task_scx() would still invoke ops.stopping(),
> resulting in an unpaired stopping event.
>
> Introduce SCX_TASK_IS_RUNNING to track whether a task entered a real
> running transition. Set and clear the flag independently of
> ops.running() and ops.stopping(), as the callbacks are independently
> optional. Invoke ops.running() only for non-blocked tasks and invoke
> ops.stopping() only after a real running transition. This keeps the
> callbacks paired for proxy donors while preserving stopping
> notifications for schedulers which only implement ops.stopping().
It took me a while to understand this.
It seems you're wanting to distinguish normal task selection and
execution (without proxy) from just task selection for proxy-donation
(where it doesn't run).
I think what makes it confusing is that TASK_IS_RUNNING is not set for
the case when the task is running (rq->curr) as a lock-owning proxy
for a waiting donor.
Would it maybe make it easier to follow if the flag was
TASK_BLOCKED_DONOR? And the logic was flipped a bit?
That might more clearly cover the case you intend here without extra
edge cases that you'll have to explain (well, you're running but
you're not the donor and running... ).
thanks
-john
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 07/10] sched_ext: Add proxy destination query kfuncs
2026-07-10 8:36 ` [PATCH 07/10] sched_ext: Add proxy destination query kfuncs Andrea Righi
@ 2026-07-10 21:54 ` John Stultz
2026-07-11 9:07 ` Andrea Righi
0 siblings, 1 reply; 21+ messages in thread
From: John Stultz @ 2026-07-10 21:54 UTC (permalink / raw)
To: Andrea Righi
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
On Fri, Jul 10, 2026 at 1:40 AM Andrea Righi <arighi@nvidia.com> wrote:
>
> BPF schedulers admitting blocked proxy donors may want to know the CPU
> or cid where the mutex owner will execute.
>
As I mentioned before, this probably needs some more context as to why
this is useful/important to the bpf scheduler.
> Introduce scx_bpf_task_proxy_cpu() and scx_bpf_task_proxy_cid() to
> return the CPU or cid of the next mutex owner in the proxy chain.
>
> The owner relationship may change immediately after the query, so expose
> the result only as a scheduling hint. Return a negative errno when no
> valid proxy destination or cid mapping is available.
>
> Provide compatibility wrappers that return -EOPNOTSUPP when the kfuncs
> are unavailable.
>
> Signed-off-by: Andrea Righi <arighi@nvidia.com>
> ---
> kernel/sched/core.c | 28 ++++++++++++++++
> kernel/sched/ext/ext.c | 42 ++++++++++++++++++++++++
> kernel/sched/ext/internal.h | 6 ++--
> kernel/sched/sched.h | 2 ++
> tools/sched_ext/include/scx/common.bpf.h | 2 ++
> tools/sched_ext/include/scx/compat.bpf.h | 18 ++++++++++
> 6 files changed, 96 insertions(+), 2 deletions(-)
>
> diff --git a/kernel/sched/core.c b/kernel/sched/core.c
> index 3d72f64ffe627..39e2689ea6c3b 100644
> --- a/kernel/sched/core.c
> +++ b/kernel/sched/core.c
> @@ -7046,6 +7046,34 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> return NULL;
> }
>
> +int task_proxy_cpu(struct task_struct *p)
> +{
> + struct task_struct *owner;
> + struct mutex *mutex;
> +
> + if (!sched_proxy_exec() || !READ_ONCE(p->is_blocked))
> + return -ENOENT;
> +
> + guard(raw_spinlock_irqsave)(&p->blocked_lock);
> +
> + mutex = __get_task_blocked_on(p);
> + if (!mutex)
> + return -ENOENT;
> +
> + /*
> + * @blocked_lock stabilizes @blocked_on and thus the mutex lifetime.
> + * The owner is an atomic snapshot used only as a scheduling hint and
> + * may change as soon as this function returns, so wait_lock is not
> + * needed here.
> + */
> + owner = __mutex_owner(mutex);
> + if (!owner)
> + return -ENOENT;
> + if (!READ_ONCE(owner->on_rq) || owner->se.sched_delayed)
> + return -ENOENT;
> +
> + return task_cpu(owner);
> +}
So, I'm somewhat skeptical of this. You do disclaim in the commit log
above that this is only a hint and it might change, but I fret it
might be to a point it's not worth much as a hint.
First: you're only looking at the immediate mutex owner, not the
actual runnable owner of the full chain. So current could be on cpu1,
the next owner on cpu2, but that task also just blocked on a mutex
owned on cpu3. And the task on cpu2 may be about to migrate to cpu3
(where current should ideally also proxy migrate to). So I'm not sure
what such an ephemeral value might be worth. Again, maybe the
optimization you have in mind is worth it, but it probably just needs
some additional explanation in the commit message.
Second: The blocked_lock does stabilize the mutex lifetime, but not
the owner's. Once you've read the __mutex_owner(), without holding the
mutex wait_lock, the mutex owner can be releasing the lock and then
immediately exit, causing the owner dereferences that follow here to
cause a UAF.
thanks
-john
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling
2026-07-10 20:47 ` Andrea Righi
@ 2026-07-11 0:21 ` John Stultz
2026-07-11 9:04 ` Andrea Righi
0 siblings, 1 reply; 21+ messages in thread
From: John Stultz @ 2026-07-11 0:21 UTC (permalink / raw)
To: Andrea Righi
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
On Fri, Jul 10, 2026 at 1:47 PM Andrea Righi <arighi@nvidia.com> wrote:
> Inspecting the proxy chain is not strictly required. A BPF scheduler can just
> accept a blocked donor, enqueue it on its current CPU and let the core
> proxy-exec path resolve and "virtually" migrate it as needed to the owner's CPU.
>
> The motivation for providing a scx_bpf_task_proxy_cpu() kfunc is purely for
> optimization reasons. By knowing the owner's CPU, a scheduler could make more
> informed admission decisions. For example, it can inspect the task currently
> running on the target CPU, determine whether the donor should take precedence
> over the running task and kick that CPU to trigger a preemption. Without this
> information, the scheduler can only enqueue the donor and wait until the task
> running on the owner's CPU releases it (potentially adding up to one time slice
> of latency).
So... I suspect I'm missing a subtlety of scx, but in the non-scx
case, if a blocked task is important enough to be selected on the
current cpu, the find_proxy_task() logic will walk the chain and do
the proxy-migration if needed and mark resched, so then that cpu can
evaluate which of the currently running task or the migrated donor is
important enough to run.
I worry the difficulty (with my poor understanding of the
optimization) is that it seems like its evaluting the preemption on
admission (I assume this means the point when we keep the blocked_on
task on the rq? I may be totally off base here), is that if a very
important task was breifly running at that moment, you might not allow
the blocked_on task to be enqueued. And at that point the donor is
effectively sleeping and no proxying can happen until it wakes (which
may be only when the lock owner eventually runs and releases the
lock). In this way the optimization might make a call in that instant
that results in *many* time slices of latency (particularly if there
are lots other unimportant tasks on that cpu).
By letting the proxy logic handle the proxy migration and
rescheduling, the blocked_on tasks are just in the same pool of
selectable tasks on that cpu and the scheduler on that cpu gets to
decide what is the most important thing to run next.
Now, I can see the benefit of potentially saving the resched kick on
the target cpu when we do a proxy-migration - it is an interesting
idea I should think more on to see how we might do that better in the
core logic.
> That said, I don't see a correctness dependency on exposing this information.
> If the latency-policy use case is not strong enough to justify the API, I'm
> happy to drop the query kfuncs and this preparatory lock-scope change for now,
> the blocked-donor admission support can stand on its own.
Yeah. If that's the case I wonder if it might be worth waiting a bit
on these optimizations until after the rest of the core proxy logic
makes it upstream and settles a bit?
It might also help make sure the demand for this
optimization/interface is strong before we start adding interfaces
prematurely.
Really, I'd like it to all be transparent to the class schedulers, but
given the complexities, just having the ability for the scx bpf
schedulers to disable keeping blocked_on tasks on the rq (effectively
disabling scx proxy donation) seems like a good initial step.
Again, I am really excited about your work here! I've sadly really not
had any time since Dec to look into sched_ext details, and I've had to
tell a few folks who are interested in both sched_ext and proxy_exec
that they should turn proxy_exec off if they want to use sched_ext.
So I'm very eager to have a better answer there!
Thanks for all your great work here!
-john
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 06/10] sched_ext: Delegate proxy donor admission to BPF schedulers
2026-07-10 8:36 ` [PATCH 06/10] sched_ext: Delegate proxy donor admission to BPF schedulers Andrea Righi
@ 2026-07-11 1:43 ` John Stultz
2026-07-11 8:24 ` Andrea Righi
0 siblings, 1 reply; 21+ messages in thread
From: John Stultz @ 2026-07-11 1:43 UTC (permalink / raw)
To: Andrea Righi
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
On Fri, Jul 10, 2026 at 1:40 AM Andrea Righi <arighi@nvidia.com> wrote:
> @@ -2866,18 +2938,12 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
> set_task_runnable(rq, p);
>
> /*
> - * Mutex-blocked donors stay queued on the runqueue under proxy
> - * execution, but the donor never runs as itself, proxy-exec
> - * walks the blocked_on chain on the next __schedule() and runs
> - * the lock owner in its place.
> - *
> - * Put the donor on the local DSQ directly so pick_next_task()
> - * can still see it. find_proxy_task() will either run the chain
> - * owner or deactivate the donor so the wakeup path can return it
> - * and let BPF make a new dispatch decision once it is unblocked.
> + * Mutex-blocked donors only stay queued when their BPF scheduler
> + * enables %SCX_OPS_ENQ_BLOCKED, so always delegate their admission.
> */
> if (task_is_blocked(p)) {
> - scx_dispatch_enqueue(sch, rq, &rq->scx.local_dsq, p, 0);
> + WARN_ON_ONCE(!(sch->ops.flags & SCX_OPS_ENQ_BLOCKED));
> + scx_do_enqueue_task(rq, p, 0, -1);
> goto switch_class;
> }
>
Just FYI: So in doing some testing running my priority-inversion-demo
test in a loop with this series (rebased on 7.2-rc2), goining through
the example schedulers in tools/sched_ext/ I hit this WARN_ON after
running awhile with the scx_pair scheduler.
Haven't done much debugging on it yet, may have to wait until monday,
but the splat is below.
thanks
-john
[25733.271828] WARNING: kernel/sched/ext/ext.c:2945 at
put_prev_task_scx+0x449/0x460, CPU#0: foreground/25080
[25733.277072] CPU: 0 UID: 0 PID: 25080 Comm: foreground Not tainted
7.2.0-rc2-00043-g9699f4add4b0 #39 PREEMPT(full)
[25733.286458] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996),
BIOS 1.17.0-debian-1.17.0-1 04/01/2014
[25733.295933] Sched_ext: pair (enabled+all), task: runnable_at=-798ms
[25733.295937] RIP: 0010:put_prev_task_scx+0x449/0x460
[25733.306251] Code: 48 8b 83 68 11 00 00 48 8b b5 10 04 00 00 48 39
b0 98 11 00 00 0f 85 79 ff ff ff e9 65 ff ff ff 90 0f 0b 90 e9 46 fe
ff ff 90 <0f> 0b 90 e9 b5 fd ff ff b8 02 00 00 00 e9 94 fc ff ff e8 90
bb 2d
[25733.322497] RSP: 0018:ffffc90011f7faa8 EFLAGS: 00010046
[25733.325750] RAX: 0000000000000001 RBX: ffff8881b8e2db80 RCX: ffff8880bd482650
[25733.333460] RDX: ffff8881b8e2e618 RSI: ffff8880bd482650 RDI: ffff8881073b2650
[25733.338640] RBP: ffff8881073b22c0 R08: 0000000000000000 R09: 0000000000000000
[25733.344710] R10: 0000000000000000 R11: 0000000000000000 R12: ffff88800ba23000
[25733.351179] R13: ffff8880bd4822c0 R14: ffffffff82f6a518 R15: ffffffff82f6a040
[25733.356585] FS: 00007f9badd90780(0000) GS:ffff888234841000(0000)
knlGS:0000000000000000
[25733.362523] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[25733.365426] CR2: 00007f91f1e48110 CR3: 000000001113c006 CR4: 0000000000370ef0
[25733.372285] Call Trace:
[25733.373817] <TASK>
[25733.375178] __pick_next_task+0xa8/0x180
[25733.378968] __schedule+0x268/0x2600
[25733.381092] ? mark_held_locks+0x40/0x70
[25733.383556] preempt_schedule_notrace+0x43/0xa0
[25733.386311] ? preempt_schedule_notrace_thunk+0x16/0x30
[25733.390954] preempt_schedule_notrace_thunk+0x16/0x30
[25733.395476] rcu_is_watching+0x3e/0x50
[25733.397722] lock_acquire+0x2c0/0x310
[25733.401168] ? _raw_spin_unlock_irqrestore+0x50/0x60
[25733.404224] schedule+0x9d/0x100
[25733.406428] ? schedule+0x7e/0x100
[25733.408130] schedule_preempt_disabled+0x18/0x30
[25733.411008] __mutex_lock+0x800/0x1100
[25733.415788] ? __start_renaming+0x5d/0x190
[25733.419011] ? lock_acquire+0xd9/0x310
[25733.421518] ? __start_renaming+0x5d/0x190
[25733.424188] __start_renaming+0x5d/0x190
[25733.426676] filename_renameat2+0x318/0x420
[25733.429258] ? find_held_lock+0x2b/0x80
[25733.432581] __x64_sys_rename+0x48/0x70
[25733.435240] do_syscall_64+0xf3/0x6c0
[25733.437675] entry_SYSCALL_64_after_hwframe+0x77/0x7f
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 06/10] sched_ext: Delegate proxy donor admission to BPF schedulers
2026-07-11 1:43 ` John Stultz
@ 2026-07-11 8:24 ` Andrea Righi
0 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-11 8:24 UTC (permalink / raw)
To: John Stultz
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
Hi John,
On Fri, Jul 10, 2026 at 06:43:44PM -0700, John Stultz wrote:
> On Fri, Jul 10, 2026 at 1:40 AM Andrea Righi <arighi@nvidia.com> wrote:
> > @@ -2866,18 +2938,12 @@ static void put_prev_task_scx(struct rq *rq, struct task_struct *p,
> > set_task_runnable(rq, p);
> >
> > /*
> > - * Mutex-blocked donors stay queued on the runqueue under proxy
> > - * execution, but the donor never runs as itself, proxy-exec
> > - * walks the blocked_on chain on the next __schedule() and runs
> > - * the lock owner in its place.
> > - *
> > - * Put the donor on the local DSQ directly so pick_next_task()
> > - * can still see it. find_proxy_task() will either run the chain
> > - * owner or deactivate the donor so the wakeup path can return it
> > - * and let BPF make a new dispatch decision once it is unblocked.
> > + * Mutex-blocked donors only stay queued when their BPF scheduler
> > + * enables %SCX_OPS_ENQ_BLOCKED, so always delegate their admission.
> > */
> > if (task_is_blocked(p)) {
> > - scx_dispatch_enqueue(sch, rq, &rq->scx.local_dsq, p, 0);
> > + WARN_ON_ONCE(!(sch->ops.flags & SCX_OPS_ENQ_BLOCKED));
> > + scx_do_enqueue_task(rq, p, 0, -1);
> > goto switch_class;
> > }
> >
>
> Just FYI: So in doing some testing running my priority-inversion-demo
> test in a loop with this series (rebased on 7.2-rc2), goining through
> the example schedulers in tools/sched_ext/ I hit this WARN_ON after
> running awhile with the scx_pair scheduler.
>
> Haven't done much debugging on it yet, may have to wait until monday,
> but the splat is below.
Yeah, this is because put_prev_task_scx() uses task_is_blocked(p), and we should
use p->is_blocked instead, since task_is_blocked() can become true before the
task has actually been processed as blocked by the scheduler.
I've fixed this in my local tree, this should disappear in the next version.
Thanks!
-Andrea
>
> thanks
> -john
>
> [25733.271828] WARNING: kernel/sched/ext/ext.c:2945 at
> put_prev_task_scx+0x449/0x460, CPU#0: foreground/25080
> [25733.277072] CPU: 0 UID: 0 PID: 25080 Comm: foreground Not tainted
> 7.2.0-rc2-00043-g9699f4add4b0 #39 PREEMPT(full)
> [25733.286458] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996),
> BIOS 1.17.0-debian-1.17.0-1 04/01/2014
> [25733.295933] Sched_ext: pair (enabled+all), task: runnable_at=-798ms
> [25733.295937] RIP: 0010:put_prev_task_scx+0x449/0x460
> [25733.306251] Code: 48 8b 83 68 11 00 00 48 8b b5 10 04 00 00 48 39
> b0 98 11 00 00 0f 85 79 ff ff ff e9 65 ff ff ff 90 0f 0b 90 e9 46 fe
> ff ff 90 <0f> 0b 90 e9 b5 fd ff ff b8 02 00 00 00 e9 94 fc ff ff e8 90
> bb 2d
> [25733.322497] RSP: 0018:ffffc90011f7faa8 EFLAGS: 00010046
> [25733.325750] RAX: 0000000000000001 RBX: ffff8881b8e2db80 RCX: ffff8880bd482650
> [25733.333460] RDX: ffff8881b8e2e618 RSI: ffff8880bd482650 RDI: ffff8881073b2650
> [25733.338640] RBP: ffff8881073b22c0 R08: 0000000000000000 R09: 0000000000000000
> [25733.344710] R10: 0000000000000000 R11: 0000000000000000 R12: ffff88800ba23000
> [25733.351179] R13: ffff8880bd4822c0 R14: ffffffff82f6a518 R15: ffffffff82f6a040
> [25733.356585] FS: 00007f9badd90780(0000) GS:ffff888234841000(0000)
> knlGS:0000000000000000
> [25733.362523] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> [25733.365426] CR2: 00007f91f1e48110 CR3: 000000001113c006 CR4: 0000000000370ef0
> [25733.372285] Call Trace:
> [25733.373817] <TASK>
> [25733.375178] __pick_next_task+0xa8/0x180
> [25733.378968] __schedule+0x268/0x2600
> [25733.381092] ? mark_held_locks+0x40/0x70
> [25733.383556] preempt_schedule_notrace+0x43/0xa0
> [25733.386311] ? preempt_schedule_notrace_thunk+0x16/0x30
> [25733.390954] preempt_schedule_notrace_thunk+0x16/0x30
> [25733.395476] rcu_is_watching+0x3e/0x50
> [25733.397722] lock_acquire+0x2c0/0x310
> [25733.401168] ? _raw_spin_unlock_irqrestore+0x50/0x60
> [25733.404224] schedule+0x9d/0x100
> [25733.406428] ? schedule+0x7e/0x100
> [25733.408130] schedule_preempt_disabled+0x18/0x30
> [25733.411008] __mutex_lock+0x800/0x1100
> [25733.415788] ? __start_renaming+0x5d/0x190
> [25733.419011] ? lock_acquire+0xd9/0x310
> [25733.421518] ? __start_renaming+0x5d/0x190
> [25733.424188] __start_renaming+0x5d/0x190
> [25733.426676] filename_renameat2+0x318/0x420
> [25733.429258] ? find_held_lock+0x2b/0x80
> [25733.432581] __x64_sys_rename+0x48/0x70
> [25733.435240] do_syscall_64+0xf3/0x6c0
> [25733.437675] entry_SYSCALL_64_after_hwframe+0x77/0x7f
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling
2026-07-11 0:21 ` John Stultz
@ 2026-07-11 9:04 ` Andrea Righi
0 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-11 9:04 UTC (permalink / raw)
To: John Stultz
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
On Fri, Jul 10, 2026 at 05:21:45PM -0700, John Stultz wrote:
> On Fri, Jul 10, 2026 at 1:47 PM Andrea Righi <arighi@nvidia.com> wrote:
> > Inspecting the proxy chain is not strictly required. A BPF scheduler can just
> > accept a blocked donor, enqueue it on its current CPU and let the core
> > proxy-exec path resolve and "virtually" migrate it as needed to the owner's CPU.
> >
> > The motivation for providing a scx_bpf_task_proxy_cpu() kfunc is purely for
> > optimization reasons. By knowing the owner's CPU, a scheduler could make more
> > informed admission decisions. For example, it can inspect the task currently
> > running on the target CPU, determine whether the donor should take precedence
> > over the running task and kick that CPU to trigger a preemption. Without this
> > information, the scheduler can only enqueue the donor and wait until the task
> > running on the owner's CPU releases it (potentially adding up to one time slice
> > of latency).
>
> So... I suspect I'm missing a subtlety of scx, but in the non-scx
> case, if a blocked task is important enough to be selected on the
> current cpu, the find_proxy_task() logic will walk the chain and do
> the proxy-migration if needed and mark resched, so then that cpu can
> evaluate which of the currently running task or the migrated donor is
> important enough to run.
With scx if a resched is triggered balance_one() does this:
if (prev is runnable and prev->scx.slice > 0)
keep running prev;
We need to set the prev->scx.slice to 0 to select the new task. That means, if
the donor is "more important" than the task running in the owner's CPU we need
to zero it's time slice. And This can be done via
scx_bpf_kick_cpu(cpu, SCX_KICK_PREEMPT).
That's the reason why I was considering introducing the
scx_bpf_task_proxy_cpu() kfunc.
However, I agree that this is adding too much complexity and we can ignore this
for now.
Moreover, let's also consider the following:
- if donor and owner are on the same CPU everything is already working as
intended without scx_bpf_task_proxy_cpu()
- if donor and owner are on different CPUs, something like this should happen
(D = donor, O = owner, C = contending task):
CPU0 CPU1
---- ----
D blocks C runs
ops.enqueue(D, SCX_ENQ_BLOCKED)
[D inserted into CPU0 local DSQ]
D briefly yields to idle
D selected from local DSQ
D migrates ---------------------> ops.enqueue(D, SCX_ENQ_BLOCKED)
[D enters CPU1 local DSQ]
resched, but C retains its slice
C continues
C's slice expires
ops.enqueue(C, 0)
[C is returned to the BPF policy]
D selected as donor
O proxy-runs using D's slice
O releases mutex
If we want to handle the preemption when the donor's CPU != owner's CPU we can
just use SCX_ENQ_PREEMPT when scx_bpf_cpu_curr(scx_bpf_task_cpu(p)) is an SCX
task.
So the BPF scheduler could do something like this:
ops.enqueue(p, enq_flags) {
if (enq_flags & SCX_ENQ_BLOCKED) { // p is a donor
struct task_struct *curr = scx_bpf_cpu_curr(cpu);
if ((curr && task_is_scx(curr) && is_more_important(p, curr))
enq_flags |= SCX_ENQ_PREEMPT; // curr->scx.slice to 0 + resched
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cpu, 0, flags);
}
Therefore we don't really need scx_bpf_task_proxy_cpu().
>
> I worry the difficulty (with my poor understanding of the
> optimization) is that it seems like its evaluting the preemption on
> admission (I assume this means the point when we keep the blocked_on
> task on the rq? I may be totally off base here), is that if a very
> important task was breifly running at that moment, you might not allow
> the blocked_on task to be enqueued. And at that point the donor is
> effectively sleeping and no proxying can happen until it wakes (which
> may be only when the lock owner eventually runs and releases the
> lock). In this way the optimization might make a call in that instant
> that results in *many* time slices of latency (particularly if there
> are lots other unimportant tasks on that cpu).
>
> By letting the proxy logic handle the proxy migration and
> rescheduling, the blocked_on tasks are just in the same pool of
> selectable tasks on that cpu and the scheduler on that cpu gets to
> decide what is the most important thing to run next.
>
> Now, I can see the benefit of potentially saving the resched kick on
> the target cpu when we do a proxy-migration - it is an interesting
> idea I should think more on to see how we might do that better in the
> core logic.
>
> > That said, I don't see a correctness dependency on exposing this information.
> > If the latency-policy use case is not strong enough to justify the API, I'm
> > happy to drop the query kfuncs and this preparatory lock-scope change for now,
> > the blocked-donor admission support can stand on its own.
>
> Yeah. If that's the case I wonder if it might be worth waiting a bit
> on these optimizations until after the rest of the core proxy logic
> makes it upstream and settles a bit?
Agreed. I've already dropped that in my local tree. Let's keep it simple for
now. :)
>
> It might also help make sure the demand for this
> optimization/interface is strong before we start adding interfaces
> prematurely.
>
> Really, I'd like it to all be transparent to the class schedulers, but
> given the complexities, just having the ability for the scx bpf
> schedulers to disable keeping blocked_on tasks on the rq (effectively
> disabling scx proxy donation) seems like a good initial step.
>
> Again, I am really excited about your work here! I've sadly really not
> had any time since Dec to look into sched_ext details, and I've had to
> tell a few folks who are interested in both sched_ext and proxy_exec
> that they should turn proxy_exec off if they want to use sched_ext.
> So I'm very eager to have a better answer there!
>
> Thanks for all your great work here!
> -john
Thank you!
-Andrea
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 07/10] sched_ext: Add proxy destination query kfuncs
2026-07-10 21:54 ` John Stultz
@ 2026-07-11 9:07 ` Andrea Righi
0 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-11 9:07 UTC (permalink / raw)
To: John Stultz
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
On Fri, Jul 10, 2026 at 02:54:39PM -0700, John Stultz wrote:
> On Fri, Jul 10, 2026 at 1:40 AM Andrea Righi <arighi@nvidia.com> wrote:
> >
> > BPF schedulers admitting blocked proxy donors may want to know the CPU
> > or cid where the mutex owner will execute.
> >
>
> As I mentioned before, this probably needs some more context as to why
> this is useful/important to the bpf scheduler.
>
>
> > Introduce scx_bpf_task_proxy_cpu() and scx_bpf_task_proxy_cid() to
> > return the CPU or cid of the next mutex owner in the proxy chain.
> >
> > The owner relationship may change immediately after the query, so expose
> > the result only as a scheduling hint. Return a negative errno when no
> > valid proxy destination or cid mapping is available.
> >
> > Provide compatibility wrappers that return -EOPNOTSUPP when the kfuncs
> > are unavailable.
> >
> > Signed-off-by: Andrea Righi <arighi@nvidia.com>
> > ---
> > kernel/sched/core.c | 28 ++++++++++++++++
> > kernel/sched/ext/ext.c | 42 ++++++++++++++++++++++++
> > kernel/sched/ext/internal.h | 6 ++--
> > kernel/sched/sched.h | 2 ++
> > tools/sched_ext/include/scx/common.bpf.h | 2 ++
> > tools/sched_ext/include/scx/compat.bpf.h | 18 ++++++++++
> > 6 files changed, 96 insertions(+), 2 deletions(-)
> >
> > diff --git a/kernel/sched/core.c b/kernel/sched/core.c
> > index 3d72f64ffe627..39e2689ea6c3b 100644
> > --- a/kernel/sched/core.c
> > +++ b/kernel/sched/core.c
> > @@ -7046,6 +7046,34 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
> > return NULL;
> > }
> >
> > +int task_proxy_cpu(struct task_struct *p)
> > +{
> > + struct task_struct *owner;
> > + struct mutex *mutex;
> > +
> > + if (!sched_proxy_exec() || !READ_ONCE(p->is_blocked))
> > + return -ENOENT;
> > +
> > + guard(raw_spinlock_irqsave)(&p->blocked_lock);
> > +
> > + mutex = __get_task_blocked_on(p);
> > + if (!mutex)
> > + return -ENOENT;
> > +
> > + /*
> > + * @blocked_lock stabilizes @blocked_on and thus the mutex lifetime.
> > + * The owner is an atomic snapshot used only as a scheduling hint and
> > + * may change as soon as this function returns, so wait_lock is not
> > + * needed here.
> > + */
> > + owner = __mutex_owner(mutex);
> > + if (!owner)
> > + return -ENOENT;
> > + if (!READ_ONCE(owner->on_rq) || owner->se.sched_delayed)
> > + return -ENOENT;
> > +
> > + return task_cpu(owner);
> > +}
>
> So, I'm somewhat skeptical of this. You do disclaim in the commit log
> above that this is only a hint and it might change, but I fret it
> might be to a point it's not worth much as a hint.
>
> First: you're only looking at the immediate mutex owner, not the
> actual runnable owner of the full chain. So current could be on cpu1,
> the next owner on cpu2, but that task also just blocked on a mutex
> owned on cpu3. And the task on cpu2 may be about to migrate to cpu3
> (where current should ideally also proxy migrate to). So I'm not sure
> what such an ephemeral value might be worth. Again, maybe the
> optimization you have in mind is worth it, but it probably just needs
> some additional explanation in the commit message.
>
> Second: The blocked_lock does stabilize the mutex lifetime, but not
> the owner's. Once you've read the __mutex_owner(), without holding the
> mutex wait_lock, the mutex owner can be releasing the lock and then
> immediately exit, causing the owner dereferences that follow here to
> cause a UAF.
Yep, as mentioned in the previous email, let's get rid of this kfuncs completely
for now. In this way we can get rid of unnecessary complexity, fix these issues,
make the patch more self-consitent in sched_ext, it's a win-win. :)
Thanks,
-Andrea
^ permalink raw reply [flat|nested] 21+ messages in thread
* Re: [PATCH 02/10] sched_ext: Fix ops.running/stopping() pairing for proxy-exec donors
2026-07-10 21:33 ` John Stultz
@ 2026-07-11 9:37 ` Andrea Righi
0 siblings, 0 replies; 21+ messages in thread
From: Andrea Righi @ 2026-07-11 9:37 UTC (permalink / raw)
To: John Stultz
Cc: Tejun Heo, David Vernet, Changwoo Min, Ingo Molnar,
Peter Zijlstra, Juri Lelli, Vincent Guittot, Dietmar Eggemann,
Steven Rostedt, Ben Segall, Mel Gorman, Valentin Schneider,
K Prateek Nayak, Christian Loehle, David Dai, Koba Ko, Aiqun Yu,
Shuah Khan, sched-ext, linux-kernel
On Fri, Jul 10, 2026 at 02:33:55PM -0700, John Stultz wrote:
> On Fri, Jul 10, 2026 at 1:39 AM Andrea Righi <arighi@nvidia.com> wrote:
> >
> > With proxy-exec, pick_next_task() can return a task with blocked_on set
> > (a proxy donor). put_prev_set_next_task() then calls set_next_task_scx()
> > on this "ghost" task even though the task only provides scheduling
> > context and never actually runs.
> >
> > Calling ops.running() for such a donor produces a spurious running
> > event. Simply suppressing ops.running() is not sufficient because the
> > following put_prev_task_scx() would still invoke ops.stopping(),
> > resulting in an unpaired stopping event.
> >
> > Introduce SCX_TASK_IS_RUNNING to track whether a task entered a real
> > running transition. Set and clear the flag independently of
> > ops.running() and ops.stopping(), as the callbacks are independently
> > optional. Invoke ops.running() only for non-blocked tasks and invoke
> > ops.stopping() only after a real running transition. This keeps the
> > callbacks paired for proxy donors while preserving stopping
> > notifications for schedulers which only implement ops.stopping().
>
> It took me a while to understand this.
>
> It seems you're wanting to distinguish normal task selection and
> execution (without proxy) from just task selection for proxy-donation
> (where it doesn't run).
>
> I think what makes it confusing is that TASK_IS_RUNNING is not set for
> the case when the task is running (rq->curr) as a lock-owning proxy
> for a waiting donor.
>
> Would it maybe make it easier to follow if the flag was
> TASK_BLOCKED_DONOR? And the logic was flipped a bit?
>
> That might more clearly cover the case you intend here without extra
> edge cases that you'll have to explain (well, you're running but
> you're not the donor and running... ).
I agree that SCX_TASK_IS_RUNNING is confusing, because it doesn't describe
really well the rq->curr/physical execution.
What the flag records is whether a task entered the sched_ext running/stopping
state. With proxy-exec, the selected scheduling context (rq->donor) and the
physical execution context (rq->curr) can differ, which creates two relevant
cases:
1. a blocked EXT donor goes through set_next_task_scx() even though its mutex
owner executes instead. We must suppress ops.running() for the donor and
remember that it did not enter the running state, so that a later
ops.stopping() is also suppressed,
2. an EXT mutex owner can execute as rq->curr for a non-EXT donor. The owner is
not a blocked donor, but it did not go through set_next_task_scx(), so it
must not receive ops.stopping() if it is subsequently dequeued.
So, I don't think changing the flag to SCX_TASK_BLOCKED_DONOR captures the
required state. SCX_TASK_IS_RUNNING handles both cases by recording whether the
task entered the sched_ext ops.running/stopping() state, independently of
whether either callback is implemented.
How about renaming it to SCX_TASK_IN_RUNNING_TRANSITION and explicitly
documenting that this is the sched_ext callback state, not physical rq->curr
execution? Something like:
/* entered the SCX ops.running/stopping() state, not necessarily rq->curr */
SCX_TASK_IN_RUNNING_TRANSITION = 1 << 6,
Any other ideas for a more clear name?
Thanks,
-Andrea
^ permalink raw reply [flat|nested] 21+ messages in thread
end of thread, other threads:[~2026-07-11 9:37 UTC | newest]
Thread overview: 21+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-07-10 8:36 [PATCHSET v4 sched_ext/for-7.3] sched: Make proxy execution compatible with sched_ext Andrea Righi
2026-07-10 8:36 ` [PATCH 01/10] sched/core: Drop mutex locks before proxy rescheduling Andrea Righi
2026-07-10 18:56 ` John Stultz
2026-07-10 20:47 ` Andrea Righi
2026-07-11 0:21 ` John Stultz
2026-07-11 9:04 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 02/10] sched_ext: Fix ops.running/stopping() pairing for proxy-exec donors Andrea Righi
2026-07-10 21:33 ` John Stultz
2026-07-11 9:37 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 03/10] sched_ext: Split curr|donor references properly Andrea Righi
2026-07-10 8:36 ` [PATCH 04/10] sched_ext: Fix TOCTOU race in consume_remote_task() Andrea Righi
2026-07-10 8:36 ` [PATCH 05/10] sched_ext: Handle blocked donor migration with proxy execution Andrea Righi
2026-07-10 8:36 ` [PATCH 06/10] sched_ext: Delegate proxy donor admission to BPF schedulers Andrea Righi
2026-07-11 1:43 ` John Stultz
2026-07-11 8:24 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 07/10] sched_ext: Add proxy destination query kfuncs Andrea Righi
2026-07-10 21:54 ` John Stultz
2026-07-11 9:07 ` Andrea Righi
2026-07-10 8:36 ` [PATCH 08/10] sched_ext: Add selftest for blocked donor admission Andrea Righi
2026-07-10 8:36 ` [PATCH 09/10] sched_ext: scx_qmap: Add proxy execution support Andrea Righi
2026-07-10 8:36 ` [PATCH 10/10] sched: Allow enabling proxy exec with sched_ext Andrea Righi
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox