[PATCH 09/12] sched_ext: Delegate proxy donor admission to BPF schedulers

From: Andrea Righi

Date: Thu Jul 02 2026 - 13:26:45 EST


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's ordering policy and can give donors more
CPU priority than intended to perform the proxy execution handoff to the
mutex owner.

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() and can use
scx_bpf_task_is_blocked() to apply their own admission policy.

The donor starts associated with its original CPU. A BPF scheduler may
dispatch it directly into that CPU's local DSQ to let the core resolve
the mutex owner and execute it with the donor's scheduling context.

Knowing the mutex owner's location in BPF is not strictly required, but
steering the donation there can reduce handoff latency when migration
fits the donor's affinity constraints and the scheduler's policy. Add
scx_bpf_task_proxy_cpu() and scx_bpf_task_proxy_cid() to report that
destination in the corresponding address space, and let the BPF
scheduler decide to migrate or not the donor to the mutex owner's
location.

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@xxxxxxxxxx>
---
kernel/sched/core.c | 36 +++++++-
kernel/sched/ext/ext.c | 108 +++++++++++++++++++----
kernel/sched/ext/ext.h | 2 +
kernel/sched/ext/internal.h | 19 +++-
kernel/sched/sched.h | 6 ++
tools/sched_ext/include/scx/common.bpf.h | 3 +
tools/sched_ext/include/scx/compat.h | 1 +
7 files changed, 156 insertions(+), 19 deletions(-)

diff --git a/kernel/sched/core.c b/kernel/sched/core.c
index 6aedb26c08ee7..f0edfbe8ce232 100644
--- a/kernel/sched/core.c
+++ b/kernel/sched/core.c
@@ -7018,6 +7018,39 @@ find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
proxy_migrate_task(rq, rf, p, owner_cpu);
return NULL;
}
+
+int task_proxy_cpu(struct task_struct *p)
+{
+ struct task_struct *owner;
+ struct mutex *mutex;
+ int owner_cpu;
+
+ if (!task_is_blocked(p))
+ return -ENOENT;
+
+ mutex = READ_ONCE(p->blocked_on);
+ if (!mutex)
+ return -ENOENT;
+
+ guard(raw_spinlock)(&mutex->wait_lock);
+ guard(raw_spinlock)(&p->blocked_lock);
+
+ if (mutex != __get_task_blocked_on(p))
+ return -EAGAIN;
+
+ owner = __mutex_owner(mutex);
+ if (!owner)
+ return -ENOENT;
+ if (!READ_ONCE(owner->on_rq) || owner->se.sched_delayed)
+ return -EAGAIN;
+
+ owner_cpu = task_cpu(owner);
+ if (owner_cpu != task_cpu(p) &&
+ (p->nr_cpus_allowed == 1 || is_migration_disabled(p)))
+ return -EOPNOTSUPP;
+
+ return owner_cpu;
+}
#else /* SCHED_PROXY_EXEC */
static struct task_struct *
find_proxy_task(struct rq *rq, struct task_struct *donor, struct rq_flags *rf)
@@ -7148,7 +7181,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 c48d043dbe58f..8ffbf857acd51 100644
--- a/kernel/sched/ext/ext.c
+++ b/kernel/sched/ext/ext.c
@@ -23,6 +23,14 @@

DEFINE_RAW_SPINLOCK(scx_sched_lock);

+bool scx_allow_proxy_exec(const struct task_struct *p)
+{
+ if (!task_on_scx(p))
+ return true;
+
+ return scx_task_sched(p)->ops.flags & SCX_OPS_ENQ_BLOCKED;
+}
+
/*
* 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
@@ -1700,6 +1708,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));
@@ -1732,15 +1741,20 @@ 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 = task_is_blocked(p) &&
+ (sch->ops.flags & SCX_OPS_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;
@@ -2042,11 +2056,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.
@@ -2837,19 +2858,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 be invoked on
- * next->blocked_on and either run the chain owner here, or call
- * proxy_force_return() and let BPF make a new dispatch decision
- * once the task is no longer blocked.
+ * 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)) {
- 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;
}

@@ -6581,6 +6595,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
@@ -9294,6 +9313,57 @@ __bpf_kfunc bool scx_bpf_task_running(const struct task_struct *p)
return task_rq(p)->curr == p;
}

+/**
+ * scx_bpf_task_is_blocked - Is a task currently blocked?
+ * @p: task of interest
+ *
+ * A BPF scheduler using %SCX_OPS_ENQ_BLOCKED receives blocked donors through
+ * ops.enqueue() and can decide when to make them available for proxy
+ * execution.
+ */
+__bpf_kfunc bool scx_bpf_task_is_blocked(struct task_struct *p)
+{
+ return task_is_blocked(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. 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
@@ -9597,6 +9667,9 @@ 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_is_blocked, 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)
@@ -9632,6 +9705,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/ext.h b/kernel/sched/ext/ext.h
index c7fa4d06ac7d3..08c64547b0143 100644
--- a/kernel/sched/ext/ext.h
+++ b/kernel/sched/ext/ext.h
@@ -20,6 +20,7 @@ void scx_rq_deactivate(struct rq *rq);
int scx_check_setscheduler(struct task_struct *p, int policy);
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)
@@ -60,6 +61,7 @@ static inline int scx_check_setscheduler(struct task_struct *p, int policy) { re
static inline bool task_on_scx(const struct task_struct *p) { return false; }
static inline bool is_ext_class(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 1774e44aebcf7..a958c9d3144b3 100644
--- a/kernel/sched/ext/internal.h
+++ b/kernel/sched/ext/internal.h
@@ -212,6 +212,22 @@ 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(). The BPF scheduler can identify them with
+ * scx_bpf_task_is_blocked() and 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.
+ *
+ * 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 +236,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,
diff --git a/kernel/sched/sched.h b/kernel/sched/sched.h
index 56acf502ba260..c4eb70371f7af 100644
--- a/kernel/sched/sched.h
+++ b/kernel/sched/sched.h
@@ -2470,6 +2470,12 @@ static inline bool task_is_blocked(struct task_struct *p)
return !!p->blocked_on;
}

+#ifdef CONFIG_SCHED_PROXY_EXEC
+int task_proxy_cpu(struct task_struct *p);
+#else
+static inline int task_proxy_cpu(struct task_struct *p) { return -EOPNOTSUPP; }
+#endif
+
static inline int task_on_cpu(struct rq *rq, struct task_struct *p)
{
return p->on_cpu;
diff --git a/tools/sched_ext/include/scx/common.bpf.h b/tools/sched_ext/include/scx/common.bpf.h
index bd51986c4c42e..4b82af377a15d 100644
--- a/tools/sched_ext/include/scx/common.bpf.h
+++ b/tools/sched_ext/include/scx/common.bpf.h
@@ -95,6 +95,9 @@ 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;
+bool scx_bpf_task_is_blocked(struct task_struct *p) __ksym __weak;
+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.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)

--
2.55.0