Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Scheduler Definitions

A Scheduler tells the framework how to find, configure, and launch the scheduler under test. declare_scheduler! builds one and registers it so both #[ktstr_test] and the verifier sweep can see it:

use ktstr::declare_scheduler;
use ktstr::prelude::*;

declare_scheduler!(MY_SCHED, {
    name = "my_sched",
    binary = "scx_my_sched",
    sched_args = ["--exit-dump-len", "1048576"],
    topology = (1, 2, 4, 1),
});

#[ktstr_test(scheduler = MY_SCHED)]
fn basic(ctx: &Ctx) -> Result<AssertResult> {
    execute_defs(ctx, vec![ctx.cgroup_def("cg_0"), ctx.cgroup_def("cg_1")])
}

MY_SCHED is the Rust handle tests reference; name = "my_sched" is the user-visible label in nextest output, sidecars, and the CLI. Rename either independently. Once declared, the scheduler shows up in the verifier sweep’s cells with no further wiring:

 Nextest run ID 3522bea7-... with nextest profile: default
    Starting 4 tests across 1 binary (55 tests skipped)
        PASS [  12.406s] (1/4) ktstr::kaslr_axis_e2e verifier/ktstr_sched/kernel_7_0/odd-3llc
        PASS [  12.432s] (2/4) ktstr::kaslr_axis_e2e verifier/ktstr_sched/kernel_7_0/smt-2llc
...

Defining a scheduler

declare_scheduler! emits a pub static MY_SCHED: Scheduler and registers a reference to it in the KTSTR_SCHEDULERS distributed slice, which is what cargo ktstr verifier enumerates. #[ktstr_test(scheduler = ...)] expects the bare ident; the macro takes the reference internally. The ident can carry a visibility prefix (pub, pub(crate)).

Accepted fields

name plus exactly one binary-source key (binary, binary_path, or the kernel_builtin_enable/kernel_builtin_disable pair) are required; every other key is optional.

  • name = "..." — short human name (required).
  • binary = "scx_name" — discover a binary by name. Resolution happens entirely on the host, before the VM boots, and the resolved binary is packed into the guest initramfs — nothing is resolved inside the guest. The cascade: a per-name KTSTR_SCHEDULER_BIN_<NAME> env override, then the global KTSTR_SCHEDULER, then a fresh workspace build via cargo build -p <name> — a failed build refuses to serve a possibly-stale pre-built binary unless KTSTR_SCHEDULER_ALLOW_STALE_FALLBACK is set, which enables the pre-built fallbacks (a sibling of the test binary, then target/{release,debug}/). Test binaries run outside the cargo-ktstr pipeline (KTSTR_CARGO_TEST_MODE=1) skip the build and consult the host PATH and the pre-built fallbacks first instead.
  • binary_path = "/abs/path" — explicit pre-built binary; must exist on the host, packed into the initramfs as-is.
  • kernel_builtin_enable = [...] + kernel_builtin_disable = [...] — paired guest shell-command lists for a scheduler compiled into the kernel (no userspace binary). Both keys must appear together.
  • sched_args = ["--a", "--b"] — scheduler CLI args applied to every test; per-test extra_sched_args append after them.
  • kargs = ["nosmt"] — extra guest kernel command line (not the scheduler’s CLI — that’s sched_args). Do not override the kargs ktstr injects itself (console=, loglevel=, rdinit=); those break guest init.
  • sysctls = [Sysctl::new("kernel.foo", "1")] — applied at guest boot, before the scheduler starts (see below).
  • topology = (numa_nodes, llcs, cores, threads) — default VM topology tests inherit dimension-by-dimension.
  • constraints = TopologyConstraints { ... } — gauntlet constraints tests inherit; see the macro reference.
  • cgroup_parent = "/path" — cgroup subtree the guest creates for the scheduler before it starts (see below).
  • config_file = "configs/my.toml" / config_file_def = (...) — the two config-file seams (see below).
  • assert = Assert::NO_OVERRIDES.max_imbalance_ratio(2.0) — scheduler-level checking overrides, merged between the library defaults and per-test attributes. See Customize Checking.
  • kernels = ["6.14", "6.15..=7.0"] — filters which kernel-list entries this scheduler verifies against in the verifier sweep. Entries use the same grammar as cargo ktstr verifier --kernel (exact versions, inclusive ranges, paths, git specs); empty means no filter. Match semantics live in BPF Verifier Sweep.

Manual definition

The const builder still works when the macro doesn’t fit — e.g. a programmatically composed scheduler, or a fixture that must stay out of the verifier sweep:

use ktstr::prelude::*;

const MITOSIS: Scheduler = Scheduler::named("scx_mitosis")
    .binary(SchedulerSpec::Discover("scx_mitosis"))
    .topology(1, 2, 4, 1)
    .sched_args(&["--exit-dump-len", "1048576"])
    .cgroup_parent("/ktstr")
    .assert(Assert::NO_OVERRIDES.max_imbalance_ratio(2.0));

Scheduler::named("foo").binary_discover("scx_foo") is shorthand for .binary(SchedulerSpec::Discover("scx_foo")) — the argument is the binary name to discover, not the scheduler name. A manual const is not registered in KTSTR_SCHEDULERS, so the verifier sweep does not see it; use declare_scheduler! for anything that should participate in cargo ktstr verifier.

SchedulerSpec

pub enum SchedulerSpec {
    Eevdf,                   // no sched_ext binary — kernel EEVDF
    Discover(&'static str),  // host-side discovery by name
    Path(&'static str),      // explicit host path
    KernelBuiltin {          // compiled into the kernel
        enable: &'static [&'static str],
        disable: &'static [&'static str],
    },
}

Scheduler::EEVDF (binary SchedulerSpec::Eevdf) runs tests under the kernel’s default scheduler and is what #[ktstr_test] uses when scheduler = is omitted. It is not reachable via declare_scheduler! — reference Scheduler::EEVDF directly. Eevdf and KernelBuiltin are excluded from the verifier sweep: neither has a userspace binary to load BPF programs from.

Kernel-builtin example

declare_scheduler!(MINLAT, {
    name = "minlat",
    kernel_builtin_enable = ["echo minlat > /sys/kernel/debug/sched/ext/root/ops"],
    kernel_builtin_disable = ["echo none > /sys/kernel/debug/sched/ext/root/ops"],
});

The enable commands run in the guest before scenarios start; disable runs after they complete.

Sysctls

sysctls takes Sysctl::new("key", "value") pairs (dot-separated keys; duplicates apply in order, last write wins). The framework injects each as sysctl.<key>=<value> on the guest kernel command line, so the kernel applies them at boot — each test gets a fresh VM, so there is no apply/revert step. Sysctl::new is const fn, so a shared tuning block can live in a const slice:

const RT_TUNING: &[Sysctl] = &[
    Sysctl::new("kernel.sched_rt_runtime_us", "950000"),
    Sysctl::new("kernel.numa_balancing", "0"),
];

declare_scheduler!(RT_TUNED, {
    name = "rt_tuned_scx",
    binary = "scx_rt_tuned",
    sysctls = RT_TUNING,
});

Config files

Pick one of config_file / config_file_def — they are alternatives.

  • The config is the same file for every test → config_file = "configs/my_sched.toml". The framework packs the host file into the guest at /include-files/{filename} and prepends --config /include-files/{filename} to the scheduler args. The --config flag name is fixed; a scheduler that uses a different flag can still take the packed path via sched_args, but must tolerate the extra --config argument.
  • The config varies per test → config_file_def = ("--config={file}", "/include-files/my.json") declares the arg-template + guest-path pair, and each test supplies content via #[ktstr_test(config = …)]. The pairing is enforced both ways at compile time — see Inline scheduler config.

Both fields may technically coexist (the config_file path is always packed and its flag prepended; the inline config is written when a test supplies config = …), but a two-config launch is rarely what anyone wants — pick one.

Cgroup parent

cgroup_parent = "/ktstr" makes guest init create /sys/fs/cgroup/ktstr (enabling cpuset/cpu controllers on its ancestors) before the scheduler starts. It does not pass --cell-parent-cgroup to the scheduler — a cell-aware scheduler that needs the flag must carry it in sched_args or per-test extra_sched_args, and the guest then also creates the directory named by the flag. Paths are validated at compile time by CgroupPath: they must start with /, must not be / alone, and must not contain ...

The same validation applies to any --cell-parent-cgroup value found in sched_args / extra_sched_args at test setup: empty values, bare /, relative paths, and a trailing flag with no value all panic with an actionable message instead of resolving to (or next to) the host cgroup root and corrupting host state.

Default topology

topology = (numa_nodes, llcs, cores_per_llc, threads_per_core) sets the VM topology tests inherit. Scheduler::named() defaults to (1, 1, 2, 1) — a minimal 2-CPU VM. Tests override individual dimensions; unset ones still inherit:

// Inherits llcs=2, cores=4 from MITOSIS; overrides threads to 2.
#[ktstr_test(scheduler = MITOSIS, threads = 2)]
fn smt_test(ctx: &Ctx) -> Result<AssertResult> { /* ... */ }

Two #[ktstr_test] attributes complement the scheduler definition: staged_schedulers = [PATH, …] packs extra scheduler binaries for runtime swaps via Op::ReplaceScheduler / Op::AttachScheduler, and workload_root_cgroup = "/path" roots workload cgroups independently of the scheduler’s cgroup_parent. Both are documented in the macro reference.

Payloads

Payload authoring — #[derive(Payload)], metric hints, include files — lives on its own page: Payloads and Included Files.