Re: [PATCH sched_ext/for-7.3] tools/sched_ext: scx_pair: Convert to sched_switch TP
Cheng-Yang Chou <[email protected]> Wed, 22 Jul 2026 21:56:18 +0800
| Newsgroups | dev.linux.lists.sched-ext |
|---|---|
| Message-ID | <[email protected]> |
Hi Andrea, thanks for the review :)
On Tue, Jul 21, 2026 at 09:37:50PM +0200, Andrea Righi wrote:
> 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
> >
--
Cheers,
Cheng-Yang