Writing a sched_ext Scheduler with struct_ops
A sched_ext scheduler is a set of BPF programs that together implement
struct sched_ext_ops— a kernel-defined operations table — loaded into the kernel through the BPFstruct_opsmechanism (sched-ext.rst, v6.12). The developer fills in callbacks (.select_cpu,.enqueue,.dispatch,.running,.stopping,.init,.exit, and more), moves tasks around using dispatch queues (DSQs), and callsscx_bpf_*kfuncs to talk to the scheduler core. The only mandatory field isops.name; every callback is optional, and the kernel supplies sensible defaults for the ones you omit. A userspace loader (built on libbpf, typically via a generated skeleton) opens, loads, and attaches thestruct_ops— and attaching is what switches the ext scheduling class on. This note walks the callback lifecycle, the DSQ model, the key kfuncs, and two complete in-tree examples (scx_simpleandscx_central), all pinned to Linux 6.12 LTS.
This is the how-to companion to sched_ext and BPF-Defined Schedulers, which covers what sched_ext is, its position in the class stack, and the safety model. Read that first for the architecture.
Version pin and a confirmed rename (6.12 → 6.18)
The doc’s “ABI Instability” section states the ops callbacks, the
include/linux/sched/ext.hconstants, and thescx_bpf_*kfuncs “are subject to change without warning between kernel versions” — so the kfunc names andsched_ext_opsfields below are correct for 6.12 and may differ on other kernels. A concrete, source-verified instance across our two LTS targets: 6.12 usesscx_bpf_dispatch()andscx_bpf_consume(), but by 6.18 LTS these were renamed toscx_bpf_dsq_insert()andscx_bpf_dsq_move_to_local()respectively (verified by readingkernel/sched/ext.cat both the v6.12 and v6.18 tags — 6.18 also adds ascx_bpf_dsq_move()/scx_bpf_dsq_move_set_slice()/scx_bpf_dsq_move_set_vtime()DSQ-iteration family). The vtime variant likewise wentscx_bpf_dispatch_vtime()(6.12) →scx_bpf_dsq_insert_vtime()(6.18). Everything in this note is read from the v6.12 git tag; if you target 6.18, substitute the renamed kfuncs.
Mental model: a function table, filled in by BPF, dispatched through DSQs
Every Linux scheduling class is a struct sched_class of function pointers. sched_ext exposes a parallel table, struct sched_ext_ops, whose fields the BPF program fills in; the kernel’s ext_sched_class callbacks (enqueue_task_scx, pick_next_task_scx, …) are thin shims that invoke the corresponding ops->... BPF callback. The glue is the BPF struct_ops facility (struct_ops docs; struct_ops and sched_ext): you declare a SEC(".struct_ops") object whose members are pointers to your BPF programs, libbpf registers it against the kernel-side bpf_struct_ops named "sched_ext_ops" (kernel/sched/ext.c: static struct bpf_struct_ops bpf_sched_ext_ops = { .name = "sched_ext_ops", ... }), the verifier checks each member program, and attaching the resulting link turns the scheduler on.
The data-flow primitive is the dispatch queue (DSQ). A CPU always runs the head task of its own local DSQ. The BPF scheduler’s job, across its callbacks, is to get the right task into the right local DSQ at the right time.
flowchart TB WAKE["task wakes / forks / exec"] --> SEL[".select_cpu(p, prev_cpu, wake_flags)<br/>hint a CPU; may direct-dispatch"] SEL -- "direct-dispatched?" --> SKIP["skip .enqueue()"] SEL -- "no" --> ENQ[".enqueue(p, enq_flags)<br/>dispatch now, or queue on BPF side"] ENQ --> BPFQ["BPF-side queue<br/>(a BPF map you own)"] ENQ -- "scx_bpf_dispatch(p, DSQ, slice)" --> DSQ["a DSQ<br/>(GLOBAL / custom / LOCAL)"] SKIP --> LOCAL CPUIDLE["CPU's local DSQ empty?"] --> DISP[".dispatch(cpu, prev)<br/>move tasks into local DSQ"] DISP -- "scx_bpf_dispatch()" --> DSQ DISP -- "scx_bpf_consume(DSQ)" --> LOCAL["this CPU's LOCAL DSQ"] BPFQ -.dispatched in .dispatch().-> DSQ LOCAL --> RUN["CPU runs head task<br/>(.running → .stopping)"]
The scheduling cycle of a sched_ext scheduler. What it shows: a waking task flows select_cpu → enqueue → (queue) → dispatch → local DSQ → run. At two points the BPF scheduler can immediately place a task in a DSQ via scx_bpf_dispatch() (in .select_cpu or .enqueue); otherwise it parks the task on its own BPF map and feeds the per-CPU local DSQ from .dispatch() when that CPU runs dry, using scx_bpf_dispatch() (queue elsewhere) or scx_bpf_consume() (pull from a non-local DSQ into the local one). The insight to take: the BPF scheduler never directly “runs” a task — it only ever decides which DSQ a task lands in; the kernel core handles the actual context switch. Get a task into a CPU’s local DSQ and that CPU will run it.
The dispatch-queue (DSQ) model
DSQs are the impedance-matching layer between the scheduler core and the BPF scheduler (sched-ext.rst, v6.12). A DSQ can behave as a FIFO or as a vtime-ordered priority queue. There are two built-in kinds plus user-created ones:
SCX_DSQ_LOCAL— each CPU has one local DSQ. A CPU always executes the head task of its own local DSQ. This is the terminal destination: to make a CPU run a task, that task must end up in that CPU’s local DSQ. (SCX_DSQ_LOCAL_ON | cputargets a specific CPU’s local DSQ.)SCX_DSQ_GLOBAL— one system-wide built-in FIFO. When a CPU’s local DSQ is empty, the core automatically tries to consume the global DSQ before calling.dispatch(). So a trivial scheduler can just dump every task intoSCX_DSQ_GLOBALand never implement.dispatch()at all.- Custom DSQs — created with
scx_bpf_create_dsq(dsq_id, node)and destroyed withscx_bpf_destroy_dsq(dsq_id). The BPF scheduler can make as many as it likes (e.g. one per NUMA node, one per priority band) and manage them itself.
The verbs:
- “dispatch” a task to a DSQ —
scx_bpf_dispatch(p, dsq_id, slice, enq_flags)enqueuespon the FIFO of the target DSQ with a time slice ofslicenanoseconds. For a priority (vtime-ordered) DSQ, usescx_bpf_dispatch_vtime(p, dsq_id, slice, vtime, enq_flags). Built-in DSQs (LOCAL,GLOBAL) cannot be vtime-ordered — callingscx_bpf_dispatch_vtime()on them raises an error; you must create a custom DSQ for priority ordering. (scx_bpf_dispatch()schedules the dispatch rather than performing it instantly; up toops.dispatch_max_batchmay be pending.) - “consume” a task from a non-local DSQ into the dispatching CPU’s local DSQ —
scx_bpf_consume(dsq_id)transfers one task fromdsq_idto the local DSQ so the CPU can run it. It flushes pending dispatches first and cannot be called with BPF locks held.
SCX_SLICE_DFL is the default slice constant; SCX_SLICE_INF is an infinite slice (used by tickless designs — see scx_central below).
The callback lifecycle
A task’s life under sched_ext is a sequence of callback invocations. The doc groups them into the scheduling-decision callbacks (select_cpu, enqueue, dispatch) and the state-transition notifiers (runnable, running, stopping, quiescent). The struct itself is defined in kernel/sched/ext.c (ext.c, v6.12). The core decision callbacks:
| Callback | Signature (6.12) | When / purpose |
|---|---|---|
select_cpu | s32 (*)(struct task_struct *p, s32 prev_cpu, u64 wake_flags) | First op on wakeup. Returns a CPU hint (not binding) and may wake an idle CPU. May direct-dispatch via scx_bpf_dispatch(), which then skips enqueue. |
enqueue | void (*)(struct task_struct *p, u64 enq_flags) | Task is runnable and not yet placed. Either dispatch it to a DSQ now, or stash it on a BPF-side queue. If the BPF side owns it and never dispatches it, the task stalls (and the watchdog fires). |
dequeue | void (*)(struct task_struct *p, u64 deq_flags) | Remove a task from the BPF scheduler, e.g. to change its attributes. Optional — the core tracks ownership and ignores spurious dispatches. |
dispatch | void (*)(s32 cpu, struct task_struct *prev) | Called when cpu’s local DSQ is empty and the global DSQ yielded nothing. Populate the local DSQ via scx_bpf_dispatch() and/or scx_bpf_consume(). |
The state-transition notifiers let the scheduler track each task’s execution lifecycle — a task becomes runnable, then cycles through running/stopping pairs as it is scheduled and descheduled, and finally goes quiescent when it sleeps or migrates:
| Callback | Purpose |
|---|---|
runnable(p, enq_flags) | p is becoming runnable on this CPU (waking, migrating in, or restored after an attribute change). |
running(p) | p is starting to run on its CPU. |
stopping(p, runnable) | p is stopping; runnable says whether it is still runnable (vs. blocking). |
quiescent(p, deq_flags) | p is becoming not-runnable (sleeping SCX_DEQ_SLEEP, migrating, or saved for an attribute change). |
There is a rich tail of further callbacks in 6.12’s sched_ext_ops (ext.c, v6.12): tick(p) (per-1/HZ tick on CPUs running an SCX task — set p->scx.slice = 0 to force a reschedule); yield(from, to); core_sched_before(a, b) (ordering for core scheduling); set_weight(p, weight) and set_cpumask(p, mask) (react to priority/affinity changes); update_idle(cpu, idle) (idle-state transitions — implementing this disables the built-in idle tracking and the scx_bpf_select_cpu_dfl()/scx_bpf_pick_idle_cpu() helpers, unless you set SCX_OPS_KEEP_BUILTIN_IDLE); cpu_acquire/cpu_release (a CPU enters/leaves SCX control, e.g. when preempted by a higher class); a full set of cgroup hooks (cgroup_init, cgroup_exit, cgroup_prep_move, cgroup_move, cgroup_cancel_move, cgroup_set_weight) for group scheduling; and dump(ctx) for emitting custom debug state on error.
The per-task lifecycle callbacks are init_task(p, args) (called exactly once per task, may block/allocate, returns -errno to abort), exit_task(p, args), and the per-SCX-entry pair enable(p)/disable(p). Finally the scheduler-wide lifecycle: init(void) (called once when the scheduler loads — BPF_STRUCT_OPS_SLEEPABLE, so it may allocate and create DSQs) and exit(info) (called on unload/abort, receives a struct scx_exit_info carrying the exit reason). Non-callback fields tune behaviour: dispatch_max_batch, flags (e.g. SCX_OPS_SWITCH_PARTIAL, SCX_OPS_ENQ_LAST, SCX_OPS_KEEP_BUILTIN_IDLE), timeout_ms (watchdog stall threshold), exit_dump_len, hotplug_seq, and the mandatory name.
Key kfuncs, by calling context
The scx_bpf_* functions are kfuncs — whitelisted kernel functions the verifier permits BPF programs to call (Kfuncs and Kernel Function Calls). Critically, each kfunc is only callable from specific callbacks: kernel/sched/ext.c registers them in context-scoped sets (ext.c, v6.12), and calling one out of context raises a runtime error (“kfunc with mask 0x%x called from an operation only allowing 0x%x”). The 6.12 sets:
select_cpuonly:scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle)— the default CPU picker; returns a CPU and setsis_idleif it found an idle one.enqueue/select_cpu:scx_bpf_dispatch(),scx_bpf_dispatch_vtime()— place a task in a DSQ.dispatchonly:scx_bpf_consume(),scx_bpf_dispatch_nr_slots(),scx_bpf_dispatch_cancel(), and thescx_bpf_dispatch_from_dsq*family (move a task between DSQs).cpu_releaseonly:scx_bpf_reenqueue_local()— re-enqueue the local DSQ’s tasks (used when a CPU is taken away).- unlocked / sleepable contexts:
scx_bpf_create_dsq()(KF_SLEEPABLE— typically frominit). - “any” context (callable almost anywhere):
scx_bpf_kick_cpu(cpu, flags)(wake/preempt a CPU —SCX_KICK_PREEMPTforces a reschedule),scx_bpf_dsq_nr_queued(dsq_id),scx_bpf_destroy_dsq(), the idle-mask helpers (scx_bpf_pick_idle_cpu,scx_bpf_test_and_clear_cpu_idle,scx_bpf_get_idle_cpumask/smtmask), CPU-perf helpers (scx_bpf_cpuperf_cap/cur/setfor frequency hints),scx_bpf_nr_cpu_ids(),scx_bpf_task_cpu(p),scx_bpf_task_running(p),scx_bpf_task_cgroup(p), and the diagnostic trioscx_bpf_error_bstr/scx_bpf_exit_bstr/scx_bpf_dump_bstr(wrapped by the variadic macrosscx_bpf_error(),scx_bpf_exit(),scx_bpf_dump()incommon.bpf.h).
Worked example 1: scx_simple — a global weighted-vtime scheduler
The in-tree tools/sched_ext/scx_simple.bpf.c is the canonical minimal scheduler. By default it is a global weighted virtual-time scheduler; with a -f flag it degrades to plain global FIFO. Here is the BPF side, with commentary (scx_simple.bpf.c, v6.12):
char _license[] SEC("license") = "GPL"; /* kfuncs require a GPL-compatible license */
const volatile bool fifo_sched; /* set by userspace via skel->rodata before load */
static u64 vtime_now; /* global virtual-time clock */
UEI_DEFINE(uei); /* "user exit info": carries exit reason to userspace */
#define SHARED_DSQ 0 /* a custom DSQ; built-in GLOBAL can't be vtime-ordered, so we make our own */
/* select_cpu: ask the default picker for an idle CPU; if one is free, dispatch
straight to its LOCAL DSQ (this also skips enqueue()). */
s32 BPF_STRUCT_OPS(simple_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags)
{
bool is_idle = false;
s32 cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle);
if (is_idle) {
stat_inc(0); /* count local dispatches */
scx_bpf_dispatch(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
}
return cpu;
}
/* enqueue: reached only when select_cpu did NOT direct-dispatch (no idle CPU).
FIFO mode → plain FIFO dispatch; vtime mode → priority dispatch keyed on the
task's accumulated virtual time, clamped so an idle task can't hoard budget. */
void BPF_STRUCT_OPS(simple_enqueue, struct task_struct *p, u64 enq_flags)
{
stat_inc(1); /* count global dispatches */
if (fifo_sched) {
scx_bpf_dispatch(p, SHARED_DSQ, SCX_SLICE_DFL, enq_flags);
} else {
u64 vtime = p->scx.dsq_vtime;
if (vtime_before(vtime, vtime_now - SCX_SLICE_DFL))
vtime = vtime_now - SCX_SLICE_DFL; /* cap accumulated lead */
scx_bpf_dispatch_vtime(p, SHARED_DSQ, SCX_SLICE_DFL, vtime, enq_flags);
}
}
/* dispatch: a CPU's local DSQ ran dry → pull the next task from our shared DSQ. */
void BPF_STRUCT_OPS(simple_dispatch, s32 cpu, struct task_struct *prev)
{
scx_bpf_consume(SHARED_DSQ);
}
/* running: advance the global vtime clock so it never lags the task starting to run. */
void BPF_STRUCT_OPS(simple_running, struct task_struct *p)
{
if (fifo_sched) return;
if (vtime_before(vtime_now, p->scx.dsq_vtime))
vtime_now = p->scx.dsq_vtime;
}
/* stopping: charge the task for the slice it consumed, scaled INVERSELY by weight —
a higher-weight (higher-priority) task accrues vtime more slowly, so it is picked
sooner next time. This is the fair-share heart of the scheduler. */
void BPF_STRUCT_OPS(simple_stopping, struct task_struct *p, bool runnable)
{
if (fifo_sched) return;
p->scx.dsq_vtime += (SCX_SLICE_DFL - p->scx.slice) * 100 / p->scx.weight;
}
void BPF_STRUCT_OPS(simple_enable, struct task_struct *p) /* new task: seed its vtime to now */
{
p->scx.dsq_vtime = vtime_now;
}
s32 BPF_STRUCT_OPS_SLEEPABLE(simple_init) /* once at load: create the shared DSQ */
{
return scx_bpf_create_dsq(SHARED_DSQ, -1); /* -1 = no NUMA-node preference */
}
void BPF_STRUCT_OPS(simple_exit, struct scx_exit_info *ei) /* on unload/abort: record why */
{
UEI_RECORD(uei, ei);
}
SCX_OPS_DEFINE(simple_ops, /* the struct_ops object itself */
.select_cpu = (void *)simple_select_cpu,
.enqueue = (void *)simple_enqueue,
.dispatch = (void *)simple_dispatch,
.running = (void *)simple_running,
.stopping = (void *)simple_stopping,
.enable = (void *)simple_enable,
.init = (void *)simple_init,
.exit = (void *)simple_exit,
.name = "simple");The whole fair-share mechanism is two lines: simple_stopping charges each task (slice_used) * 100 / weight of virtual time, and simple_enqueue dispatches by ascending vtime — so lighter-charged (higher-weight) tasks sort to the front. This is a miniature of EEVDF vruntime, implemented in ~40 lines of BPF. Note the use of p->scx.dsq_vtime and p->scx.slice — the task_struct carries an embedded struct sched_ext_entity scx (include/linux/sched/ext.h) that BPF programs read and write directly.
SCX_OPS_DEFINE, BPF_STRUCT_OPS, BPF_STRUCT_OPS_SLEEPABLE, UEI_DEFINE/UEI_RECORD are convenience macros from tools/sched_ext/include/scx/common.bpf.h; they expand to the SEC(".struct_ops.link") object and the correctly-typed program wrappers.
Loading via libbpf
The userspace side (tools/sched_ext/scx_simple.c) is a small libbpf program driving a generated skeleton (scx_simple.bpf.skel.h, produced by bpftool gen skeleton) (scx_simple.c, v6.12). The lifecycle is open → set config → load → attach → poll → destroy:
skel = SCX_OPS_OPEN(simple_ops, scx_simple); /* open the skeleton, bind to the simple_ops struct_ops */
skel->rodata->fifo_sched = true; /* set read-only config BEFORE load (here: -f flag) */
SCX_OPS_LOAD(skel, simple_ops, scx_simple, uei); /* load: verifier runs, BPF JITs, DSQs not yet live */
link = SCX_OPS_ATTACH(skel, simple_ops, scx_simple); /* ATTACH → ext_sched_class goes live, tasks switch in */
while (!exit_req && !UEI_EXITED(skel, uei)) { /* poll stats / watch for an exit */
read_stats(skel, stats);
printf("local=%llu global=%llu\n", stats[0], stats[1]);
sleep(1);
}
bpf_link__destroy(link); /* detach → ALL tasks revert to CFS/EEVDF */
ecode = UEI_REPORT(skel, uei); /* print the exit reason (incl. errors/stalls) */
scx_simple__destroy(skel);
if (UEI_ECODE_RESTART(ecode)) goto restart; /* some exits (e.g. CPU hotplug) ask for a reload */The pivotal lines are SCX_OPS_ATTACH (this is the moment the scheduler takes over — registering the struct_ops link calls into bpf_scx_reg() in the kernel) and bpf_link__destroy(link) (detaching reverts every task to the in-kernel scheduler, calling bpf_scx_unreg()). Crucially, the loader must stay alive for the scheduler to keep running (unless the link is explicitly pinned to bpffs): the struct_ops link is owned by a file descriptor in the loader process, so when the loader exits, that fd closes, the unpinned link is released, bpf_scx_unreg() runs, and all tasks revert to CFS/EEVDF. This is exactly why the kernel doc lists “terminating the sched_ext scheduler program” as one of the ways the BPF scheduler is aborted (sched-ext.rst, v6.12), and why production scx schedulers run as long-lived daemons — killing the daemon is itself a clean kill switch. The loader’s poll loop reads stats and exit info; if the kernel aborts the scheduler from its side (error, stall, SysRq-S), UEI_EXITED becomes true and the loop ends. read_stats() reads a BPF_MAP_TYPE_PERCPU_ARRAY the BPF side increments — the standard pattern for surfacing scheduler internals to userspace via BPF Maps.
Worked example 2: scx_central — one CPU schedules them all (tickless)
tools/sched_ext/scx_central.bpf.c is a deliberately educational scheduler where a single “central” CPU makes every scheduling decision and the others kick it when idle (scx_central.bpf.c, v6.12). It exercises mechanisms scx_simple doesn’t: cross-CPU kicking, infinite slices for tickless operation on CONFIG_NO_HZ_FULL kernels (see CPU Isolation isolcpus and nohz_full), a BPF bpf_timer for periodic preemption, and forward-progress handling for per-CPU kthreads.
/* select_cpu: steer every wakeup to the central CPU. It's only a hint; if the
task can't run there the kernel picks a fallback, so a blind return is safe. */
s32 BPF_STRUCT_OPS(central_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags)
{
return central_cpu;
}
void BPF_STRUCT_OPS(central_enqueue, struct task_struct *p, u64 enq_flags)
{
/* Per-CPU kthreads (e.g. ksoftirqd) jump the queue: dispatch to the head of
the LOCAL DSQ with an INFINITE slice and SCX_ENQ_PREEMPT. This is a
forward-progress guarantee — the central design leans on a BPF timer that
may run from ksoftirqd, so ksoftirqd must never be starved. */
if ((p->flags & PF_KTHREAD) && p->nr_cpus_allowed == 1) {
scx_bpf_dispatch(p, SCX_DSQ_LOCAL, SCX_SLICE_INF, enq_flags | SCX_ENQ_PREEMPT);
return;
}
/* Everyone else: push the pid onto a BPF QUEUE map the central CPU drains. */
if (bpf_map_push_elem(¢ral_q, &p->pid, 0)) { /* queue full → fallback DSQ */
scx_bpf_dispatch(p, FALLBACK_DSQ_ID, SCX_SLICE_INF, enq_flags);
return;
}
/* Wake the central CPU so it runs dispatch() and hands work out. */
if (!scx_bpf_task_running(p))
scx_bpf_kick_cpu(central_cpu, SCX_KICK_PREEMPT);
}Two ideas worth extracting. First, scx_bpf_kick_cpu(central_cpu, SCX_KICK_PREEMPT) is how one CPU forces another to reschedule — the central design is built entirely on kicking. Second, the kthread special-case shows why a real scheduler must respect forward-progress constraints: per-CPU kthreads pinned to one CPU (nr_cpus_allowed == 1) get unconditional priority via SCX_ENQ_PREEMPT so the machinery the scheduler itself depends on (softirqs, timers) never deadlocks behind user work. The non-kthread path stashes the task’s pid in a BPF_MAP_TYPE_QUEUE map (central_q), which the central CPU later drains in its dispatch() callback using bpf_map_pop_elem() + bpf_task_from_pid() — a clean illustration of “queue on the BPF side, dispatch from .dispatch().”
scx_central also dispatches with SCX_SLICE_INF so that, combined with nohz_full, the periodic tick can be stopped entirely — periodic preemption is then driven by an explicit bpf_timer rather than the kernel tick, observable in /proc/interrupts.
Failure modes and how to diagnose them
- Verifier rejection at load. The most common wall for newcomers: an uninitialized variable, an unbounded loop, or reading memory the verifier can’t prove safe. The fix is the same as any verifier fight — initialize everything (note the
bool direct = falsein the doc’sselect_cpuexample, commented “or the BPF verifier will reject the program”), bound loops withbpf_repeat()/BPF_MAX_LOOPS, and read the verifier log. - Stalled tasks → watchdog abort. If
enqueueparks a task on a BPF map anddispatchnever dispatches it, the task is runnable but never runs; the watchdog raisesSCX_EXIT_ERROR_STALLaftertimeout_msand reverts to CFS. Symptom: scheduler suddenly unloads,UEI_REPORTshows a stall. Diagnose by checking that every code path that enqueues also has a path that dispatches. - kfunc-out-of-context error. Calling, say,
scx_bpf_consume()fromenqueue(it’sdispatch-only) triggersscx_ops_error("kfunc with mask ... called from an operation only allowing ..."). Match each kfunc to its allowed callbacks (table above). - vtime on a built-in DSQ.
scx_bpf_dispatch_vtime()toSCX_DSQ_GLOBAL/LOCALerrors with “cannot use vtime ordering for built-in DSQs” — create a custom DSQ (this is exactly whyscx_simplemakesSHARED_DSQ). - Debugging live state.
SysRq-Dtriggers a debug dump without killing the scheduler; thesched_ext_dumptracepoint and theops.dump()callback let you emit custom state;tools/sched_ext/scx_show_state.py(drgn) printsenable_state,nr_rejected,bypass_depth, etc.
See Also
- sched_ext and BPF-Defined Schedulers — the architecture, class-stack position, and safety model (read first)
- struct_ops and sched_ext — the BPF
struct_opsmechanism this note builds on (eBPF MOC) - Scheduling Classes and the sched_class Interface — the
struct sched_classthatsched_ext_opsmirrors - Virtual Runtime and the Fair Scheduling Invariant — the vruntime idea
scx_simplereimplements in BPF - eBPF Verifier · BPF Maps · Kfuncs and Kernel Function Calls — the BPF primitives a scheduler relies on
- CPU Isolation isolcpus and nohz_full — the tickless context
scx_centraltargets - Hierarchical Group Scheduling and Task Groups — the cgroup hooks in
sched_ext_ops - Linux Process Scheduling MOC · Linux eBPF MOC