Re: [PATCH sched_ext/for-7.3] tools/sched_ext: scx_pair: Convert to sched_switch TP
Andrea Righi <[email protected]> Tue, 21 Jul 2026 21:37:50 +0200
| Newsgroups | dev.linux.lists.sched-ext |
|---|---|
| Message-ID | <al_Kjns3FrkBgR4W@gpd4> |
Hi Cheng-Yang,
On Sun, Jul 19, 2026 at 10:28:45PM +0800, Cheng-Yang Chou wrote:
> ops.cpu_acquire/release() are deprecated in favor of tracking CPU
> preemption from a sched_switch tracepoint, see
> commit a3f5d4822253 ("sched_ext: Allow scx_bpf_reenqueue_local() to
> be called from anywhere"). Loading scx_pair currently emits a
> deprecation warning.
>
> Replace the pair_cpu_acquire/release() callbacks with a
> tp_btf/sched_switch program that edge-detects the same transitions the
> core used to deliver: a release when a running SCX task loses its CPU
> to a higher-priority class, and an acquire when the CPU switches back
> to an SCX task or idle while marked preempted.
>
> Tasks are classified by effective priority (p->prio) rather than by
> policy: rt_mutex_setprio() boosts a PI beneficiary into the rt/dl
> classes while leaving its policy untouched, so a policy test would
> both miss the release when a boosted task takes the CPU and fire a
> spurious acquire when a boosted task replaces a real rt task.
>
> A switch from idle straight to a higher-priority task is deliberately
> not treated as a release. The CPU was not running an SCX task, so
> there is nothing to drain, and kicking SCX_KICK_PREEMPT |
> SCX_KICK_WAIT on every rt wakeup would make the pair CPU wait out rt
> bursts it was never coupled to. The old callbacks behaved the same
> way, firing ops.cpu_release() only from switch_class() when an SCX
> task was put for a higher class.
>
> The tracepoint runs on every context switch in the system, so the
> common no-transition case is filtered before taking the pair-shared
> lock. This is safe because a CPU's own preempted_mask bit is only ever
> written by this tracepoint running on that CPU.
>
> sched_setscheduler() on a running task changes class in place without
> a context switch, so such transitions are only observed at the task's
> next switch. The old callbacks had the same blind spot in
> switch_class(), and try_dispatch() already bounds the resulting wait.
>
> Verified in virtme-ng with the script below. The scheduler must load
> without the deprecation warning, stay enabled through the rt churn and
> the idle soak (the watchdog would otherwise abort it with "runnable
> task stall"), keep its preemption counter advancing, and unregister
> cleanly at the end. A PI rt-mutex churn that repeatedly boosts
> SCX tasks into the rt class was exercised separately:
>
> #!/bin/bash
> # vng --verbose --cpus 8 -m 4G --user root -- ./verify.sh
> # FIFO harness: survives even if all SCHED_NORMAL tasks stall
> [ "${RT:-0}" = 1 ] || exec chrt -f 5 env RT=1 "$0"
>
> chrt -o 0 ./tools/sched_ext/build/bin/scx_pair &
> PAIR=$!
> sleep 3
> for round in $(seq 10); do
> pids=""
> for i in 0 1 2 3; do # SCHED_FIFO churn
> chrt -f 10 bash -c \
> 'e=$((SECONDS+1)); while [ $SECONDS -lt $e ]; do :; done' &
> pids="$pids $!"
> done
> for i in 0 1; do # SCHED_NORMAL load under scx
> chrt -o 0 bash -c \
> 'n=0; while [ $n -lt 200000 ]; do n=$((n+1)); done' &
> pids="$pids $!"
> done
> wait $pids # explicit pids, not the scx_pair job
> done
> sleep 300 # idle soak
> kill -INT $PAIR # expect clean unregister in dmesg
>
> Signed-off-by: Cheng-Yang Chou <[email protected]>
This looks good to me.
Reviewed-by: Andrea Righi <[email protected]>
Thanks,
-Andrea
> ---
> tools/sched_ext/scx_pair.bpf.c | 136 ++++++++++++++++++++++-----------
> 1 file changed, 90 insertions(+), 46 deletions(-)
>
> diff --git a/tools/sched_ext/scx_pair.bpf.c b/tools/sched_ext/scx_pair.bpf.c
> index 267011b57cba..0d61b7b812db 100644
> --- a/tools/sched_ext/scx_pair.bpf.c
> +++ b/tools/sched_ext/scx_pair.bpf.c
> @@ -93,12 +93,13 @@
> * -----------------------
> *
> * SCX is the lowest priority sched_class, and could be preempted by them at
> - * any time. To address this, the scheduler implements pair_cpu_release() and
> - * pair_cpu_acquire() callbacks which are invoked by the core scheduler when
> - * the scheduler loses and gains control of the CPU respectively.
> + * any time. To address this, the scheduler watches every sched_switch from
> + * a tracepoint and edge-detects when a CPU leaves and returns to SCX
> + * control.
> *
> - * In pair_cpu_release(), we mark the pair_ctx as having been preempted, and
> - * then invoke:
> + * When a higher-priority class takes a CPU away from a running SCX task -
> + * a sched_switch from an SCX task to a higher-priority task - we mark the
> + * pair_ctx as having been preempted and then invoke:
> *
> * scx_bpf_kick_cpu(pair_cpu, SCX_KICK_PREEMPT | SCX_KICK_WAIT);
> *
> @@ -107,9 +108,19 @@
> * sched_class that preempted our scheduler does not schedule a task
> * concurrently with our pair CPU.
> *
> - * When the CPU is re-acquired in pair_cpu_acquire(), we unmark the preemption
> - * in the pair_ctx, and send another resched IPI to the pair CPU to re-enable
> - * pair scheduling.
> + * When the CPU returns to SCX or idle, we unmark the preemption in the
> + * pair_ctx and send another resched IPI to the pair CPU to re-enable pair
> + * scheduling.
> + *
> + * A switch from idle straight to a higher-priority task is not a release:
> + * the CPU was not running an SCX task, so there is nothing to drain and no
> + * reason to make the pair wait. Kicking SCX_KICK_WAIT on every such wakeup
> + * would stall the pair CPU behind rt bursts it was never coupled to.
> + *
> + * Note that sched_setscheduler() on a running task changes its class in
> + * place without a context switch, so such transitions are only observed at
> + * the task's next switch. Until then the stale active_mask bit makes the
> + * pair wait in try_dispatch(), which is bounded by that next switch.
> *
> * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
> * Copyright (c) 2022 Tejun Heo <[email protected]>
> @@ -118,6 +129,8 @@
> #include <scx/common.bpf.h>
> #include "scx_pair.h"
>
> +#define MAX_RT_PRIO 100
> +
> char _license[] SEC("license") = "GPL";
>
> /* !0 for veristat, set during init */
> @@ -308,6 +321,40 @@ static int lookup_pairc_and_mask(s32 cpu, struct pair_ctx **pairc, u32 *mask)
> return 0;
> }
>
> +/*
> + * A task is above SCX whenever its effective priority is in the rt/dl
> + * range. Test p->prio rather than p->policy: rt_mutex_setprio() boosts
> + * a PI beneficiary into the rt/dl classes with its policy left
> + * untouched, so a policy test would misclassify boosted tasks in both
> + * directions. p->prio follows the boost and the deboost.
> + *
> + * This still cannot tell fair and SCX tasks apart. It is complete only
> + * because scx_pair runs in switch-all mode, where no fair class task
> + * exists; in partial mode fair is also above SCX and can take the CPU.
> + */
> +static bool pair_task_is_highpri(struct task_struct *p)
> +{
> + return p->prio < MAX_RT_PRIO;
> +}
> +
> +static void pair_cpu_acquire_locked(struct pair_ctx *pairc, u32 in_pair_mask,
> + u32 *kick_flags)
> +{
> + pairc->preempted_mask &= ~in_pair_mask;
> + /* Kick the pair CPU, unless it was also preempted. */
> + *kick_flags = !pairc->preempted_mask ? SCX_KICK_PREEMPT : 0;
> +}
> +
> +static void pair_cpu_release_locked(struct pair_ctx *pairc, u32 in_pair_mask,
> + u32 *kick_flags)
> +{
> + pairc->preempted_mask |= in_pair_mask;
> + pairc->active_mask &= ~in_pair_mask;
> + /* Kick the pair CPU if it's still running. */
> + *kick_flags = pairc->active_mask ? SCX_KICK_PREEMPT | SCX_KICK_WAIT : 0;
> + pairc->draining = true;
> +}
> +
> __attribute__((noinline))
> static int try_dispatch(s32 cpu)
> {
> @@ -500,61 +547,60 @@ void BPF_STRUCT_OPS(pair_dispatch, s32 cpu, struct task_struct *prev)
> }
> }
>
> -void BPF_STRUCT_OPS(pair_cpu_acquire, s32 cpu, struct scx_cpu_acquire_args *args)
> +SEC("tp_btf/sched_switch")
> +int BPF_PROG(pair_sched_switch, bool preempt, struct task_struct *prev,
> + struct task_struct *next, unsigned int prev_state)
> {
> int ret;
> + s32 cpu = bpf_get_smp_processor_id();
> u32 in_pair_mask;
> struct pair_ctx *pairc;
> - bool kick_pair;
> + u32 kick_flags = 0;
> + bool preempted;
> + bool release, acquire;
>
> ret = lookup_pairc_and_mask(cpu, &pairc, &in_pair_mask);
> if (ret)
> - return;
> -
> - bpf_spin_lock(&pairc->lock);
> - pairc->preempted_mask &= ~in_pair_mask;
> - /* Kick the pair CPU, unless it was also preempted. */
> - kick_pair = !pairc->preempted_mask;
> - bpf_spin_unlock(&pairc->lock);
> -
> - if (kick_pair) {
> - s32 *pair = (s32 *)ARRAY_ELEM_PTR(pair_cpu, cpu, nr_cpu_ids);
> + return 0;
>
> - if (pair) {
> - __sync_fetch_and_add(&nr_kicks, 1);
> - scx_bpf_kick_cpu(*pair, SCX_KICK_PREEMPT);
> - }
> + /*
> + * This runs on every context switch in the system. A CPU's own
> + * preempted_mask bit is only ever written by this tracepoint
> + * running on that CPU, so the unlocked read is exact and the
> + * pair-shared lock is only taken on actual transitions.
> + */
> + preempted = pairc->preempted_mask & in_pair_mask;
> + if (next->pid && pair_task_is_highpri(next)) {
> + /* an SCX task lost the CPU to a higher-priority class */
> + release = !preempted && prev->pid && !pair_task_is_highpri(prev);
> + acquire = false;
> + } else {
> + /* the CPU is back under SCX control (or idle) */
> + release = false;
> + acquire = preempted;
> }
> -}
> -
> -void BPF_STRUCT_OPS(pair_cpu_release, s32 cpu, struct scx_cpu_release_args *args)
> -{
> - int ret;
> - u32 in_pair_mask;
> - struct pair_ctx *pairc;
> - bool kick_pair;
> -
> - ret = lookup_pairc_and_mask(cpu, &pairc, &in_pair_mask);
> - if (ret)
> - return;
> + if (!release && !acquire)
> + return 0;
>
> bpf_spin_lock(&pairc->lock);
> - pairc->preempted_mask |= in_pair_mask;
> - pairc->active_mask &= ~in_pair_mask;
> - /* Kick the pair CPU if it's still running. */
> - kick_pair = pairc->active_mask;
> - pairc->draining = true;
> + if (release) {
> + pair_cpu_release_locked(pairc, in_pair_mask, &kick_flags);
> + __sync_fetch_and_add(&nr_preemptions, 1);
> + } else {
> + pair_cpu_acquire_locked(pairc, in_pair_mask, &kick_flags);
> + }
> bpf_spin_unlock(&pairc->lock);
>
> - if (kick_pair) {
> + if (kick_flags) {
> s32 *pair = (s32 *)ARRAY_ELEM_PTR(pair_cpu, cpu, nr_cpu_ids);
>
> if (pair) {
> __sync_fetch_and_add(&nr_kicks, 1);
> - scx_bpf_kick_cpu(*pair, SCX_KICK_PREEMPT | SCX_KICK_WAIT);
> + scx_bpf_kick_cpu(*pair, kick_flags);
> }
> }
> - __sync_fetch_and_add(&nr_preemptions, 1);
> +
> + return 0;
> }
>
> s32 BPF_STRUCT_OPS(pair_cgroup_init, struct cgroup *cgrp)
> @@ -602,8 +648,6 @@ void BPF_STRUCT_OPS(pair_exit, struct scx_exit_info *ei)
> SCX_OPS_DEFINE(pair_ops,
> .enqueue = (void *)pair_enqueue,
> .dispatch = (void *)pair_dispatch,
> - .cpu_acquire = (void *)pair_cpu_acquire,
> - .cpu_release = (void *)pair_cpu_release,
> .cgroup_init = (void *)pair_cgroup_init,
> .cgroup_exit = (void *)pair_cgroup_exit,
> .exit = (void *)pair_exit,
> --
> 2.43.0
>