Skip to content

sched_ext — Callback Reference

Javadoc: Scheduler · SchedulerBase · DispatchQueue
BPF reference: sched_ext_ops — docs.ebpf.io · sched-ext kernel docs
Source: Scheduler.java · SchedulerBase.java
See also: Overview · Writing a Scheduler · Cookbook

How callbacks work

Every sched_ext scheduler is a Java class that extends SchedulerBase and implements the Scheduler interface (which wraps the kernel's sched_ext_ops struct_ops). Each method you override in Java becomes a BPF callback wired into the kernel at load time. The annotation processor generates the struct_ops skeleton and all glue code automatically — you do not annotate callback overrides with @BPFFunction. Method names follow a strict camelCase-to-snake_case mapping: selectCPU becomes select_cpu, initTask becomes init_task, and so on.

The minimal scheduler overrides exactly two methods: enqueue and dispatch. Everything else is optional. SchedulerBase provides working default implementations for dispatch and init, so many schedulers only need to override enqueue.


Callback reference table

Method Required Description
enqueue(p, flags) Yes Task becomes runnable; insert it into a DSQ
dispatch(cpu, prev) Yes (if not using SchedulerBase) CPU needs work; move tasks from DSQs to the local DSQ
init() No Called once at load; create DSQs here
exit(ei) No Called when the scheduler is detached
selectCPU(p, prev_cpu, wake_flags) No Choose which CPU to wake for this task
runnable(p, flags) No Task became runnable (fires before enqueue)
running(p) No Task is about to execute on a CPU
stopping(p, runnable) No Task left the CPU
enable(p) No Task entered SCX scheduling
disable(p) No Task left SCX scheduling
tick(p) No Periodic callback (fires roughly every 1/HZ seconds)
initTask(p, args) No New task created; initialize per-task state
exitTask(p, args) No Task exiting; free per-task state created in initTask
dequeue(p, flags) No Task removed from the scheduler (e.g. priority change)
quiescent(p, flags) No Task became blocked; counterpart to runnable
updateIdle(cpu, idle) No CPU idle state changed
cpuAcquire(cpu, args) No CPU returned to SCX after being preempted
cpuRelease(cpu, args) No CPU preempted by RT/deadline task; call scx_bpf_reenqueue_local()
cpuOnline(cpu) No CPU came online (hotplug)
cpuOffline(cpu) No CPU went offline (hotplug)
yield(from, to) No Task called sched_yield(); return true to honour, false to ignore
setWeight(p, weight) No Task scheduling weight changed (e.g. via setpriority(2))
setCpumask(p, cpumask) No Task CPU affinity changed (e.g. via sched_setaffinity(2))
coreSchedBefore(a, b) No Core scheduling: return true if a should run before b on a shared physical core
dump(ctx) No Global scheduler state dump (sched-ext debug interface)
dumpCpu(ctx, cpu, idle) No Per-CPU state dump
dumpTask(ctx, p) No Per-task state dump

Cgroup-aware schedulers can additionally implement cgroupInit, cgroupExit, cgroupPrepMove, cgroupCancelMove, cgroupMove, cgroupSetWeight, and cgroupSetBandwidth. See the sched-ext overview for details.


The core loop: enqueue and dispatch

These two callbacks are the heart of every scheduler. The kernel calls enqueue whenever a task becomes runnable and calls dispatch whenever a CPU runs out of work.

enqueue

enqueue receives a pointer to the task (p) and a flags bitmask. Its only responsibility is to place the task into a dispatch queue (DSQ). If enqueue returns without inserting the task, the sched_ext watchdog detects the stall and detaches the scheduler.

The simplest implementation inserts every task into the global shared DSQ:

@Override
public void enqueue(Ptr<task_struct> p, long flags) {
    scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, flags);
}

A scheduler with its own DSQ (id stored in SHARED_DSQ) inserts there instead:

@Override
public void enqueue(Ptr<task_struct> p, long flags) {
    scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, flags);
}

dispatch

dispatch receives the id of the CPU that needs work and a pointer to the task that just stopped running (prev, may be null). Its job is to move tasks from a DSQ into the CPU's local DSQ so the kernel can execute them.

SchedulerBase provides a default implementation that consumes from SCX_DSQ_GLOBAL, which is sufficient for most simple schedulers. Override it when you maintain your own DSQs:

@Override
public void dispatch(int cpu, Ptr<task_struct> prev) {
    scx_bpf_dsq_move_to_local(SHARED_DSQ);
}

For round-robin or weighted dispatch across multiple DSQs, call scx_bpf_dsq_move_to_local once per DSQ until the local queue is satisfied.


Lifecycle: init and exit

init

init is annotated @Sleepable by SchedulerBase, which means it runs in a sleepable BPF context and may call helpers that can block. This is the correct place to create custom DSQs and initialize global state.

@Override
@Sleepable
public int init() {
    int rc = super.init();
    if (rc != 0) return rc;
    return scx_bpf_create_dsq(SHARED_DSQ, -1);
}

super.init() sets up the built-in DSQs used by SchedulerBase; call it first unless you are managing everything yourself.

exit

exit receives a pointer to an scx_exit_info struct with two useful fields: .reason (an integer code) and .msg (a null-terminated string describing why the scheduler was detached). Use them for diagnostics or clean shutdown logic.

@Override
public void exit(Ptr<scx_exit_info> ei) {
    int reason = ei.val().reason;
    BPFHelpers.bpf_printk("scheduler exiting, reason=%d", reason);
}

Per-task state: initTask and exitTask

initTask and exitTask are the BPF equivalent of per-task memory allocation. The kernel calls initTask when a task first enters SCX scheduling and exitTask when it leaves. Use a BPFHashMap keyed by PID (the task's pid field, typed int but stored as long to fit the map key type) to associate state with each task.

@BPFMapDefinition(maxEntries = 4096)
BPFHashMap<Integer, TaskData> taskData = BPFHashMap.newInstance();

@Override
public void initTask(Ptr<task_struct> p, Ptr<scx_init_task_args> args) {
    TaskData td = new TaskData();
    td.vruntime = 0;
    taskData.bpf_put(p.val().pid, td);
}

@Override
public void exitTask(Ptr<task_struct> p, Ptr<scx_exit_task_args> args) {
    taskData.bpf_delete(p.val().pid);
}

Failing to clean up in exitTask leaks map entries. BPF hash maps have a fixed maximum size set at creation time, so leaks eventually cause bpf_put to fail with -E2BIG.


CPU lifecycle callbacks

Hotplug: cpuOnline and cpuOffline

cpuOnline fires when a CPU is brought online and cpuOffline fires just before one goes offline. Use these to adjust bitmasks or per-CPU data structures that track which CPUs are available for scheduling.

@Override
public void cpuOnline(int cpu) {
    BPFHelpers.bpf_printk("cpu %d online", cpu);
}

@Override
public void cpuOffline(int cpu) {
    BPFHelpers.bpf_printk("cpu %d offline", cpu);
}

RT preemption: cpuAcquire and cpuRelease

cpuRelease fires when a real-time or deadline task preempts a CPU away from SCX. Any tasks that were queued in that CPU's local DSQ are stranded; call scx_bpf_reenqueue_local() to move them back to a global or shared DSQ so other CPUs can pick them up.

cpuAcquire fires when the CPU is returned to SCX after the RT task finishes. Use it to re-seed the CPU's local DSQ if your scheduler maintains per-CPU state.

@Override
public void cpuRelease(int cpu, Ptr<scx_cpu_release_args> args) {
    scx_bpf_reenqueue_local();
}

Cgroup callbacks

Schedulers that participate in cgroup-based resource control implement a second set of callbacks: cgroupInit, cgroupExit, cgroupPrepMove, cgroupCancelMove, cgroupMove, cgroupSetWeight, and cgroupSetBandwidth. These mirror the cgroup lifecycle (creation, destruction, task migration, and weight/bandwidth updates) and receive pointers to the relevant cgroup and task structs alongside a flags or weight argument.

Cgroup support requires the scheduler to declare it at load time. See the sched-ext overview for the full setup and an annotated example.


Next: Cookbook