$RodHat_
MOTD

You can write a CPU scheduler in BPF now. Most of you shouldn't. Some of you really should.

Published by

You can write a CPU scheduler in BPF now. Most of you shouldn't. Some of you really should.
Photo: AI-generated — no human photographer / RodHat AI Cover

The sched_ext framework merged into Linux 6.12 in November 2024. One kernel release cycle and a handful of distro updates later, it is in the kernel your servers are already running if you are on anything current. Which means you can now write a CPU scheduler as a BPF program, load it onto a live system, and have the kernel execute it. No reboot. No out-of-tree patches. No rebuilding from source every time upstream moves.

The kernel verifier checks your BPF for correctness before it runs. If your scheduler lets a runqueue stall long enough to trigger the watchdog, the system falls back to the Completely Fair Scheduler automatically and keeps running.

I spent a weekend with it. My read changed. This is not irresponsible. This is the right design.

Why the existing options were bad

The Completely Fair Scheduler has been the Linux default since 2.6.23 in 2007. It handles general-purpose workloads competently. It is correctly inflexible when your workload is not general-purpose.

If you are running a latency-sensitive service and CFS scheduling decisions are eating your tail latency, you had four options before 6.12:

Tune the scheduler tunables and hope. There are sched_latency_ns, sched_min_granularity_ns, and assorted other knobs. They move the problem around rather than solving it.

Use a real-time priority class. SCHED_FIFO or SCHED_RR gets you priority but adds its own failure modes: a misbehaving RT process starves everything else. You are trading one scheduling problem for a different, more dangerous one.

Patch a custom scheduler into your kernel. Now you maintain kernel patches, you rebuild on every upstream update, and your ops team hates you. Congratulations, you now own that.

Run the PREEMPT_RT kernel. Legitimate for actual real-time requirements. Requires out-of-tree patches and its own build and maintenance burden.

sched_ext adds a fifth option: write the policy yourself, in BPF, load it at runtime, and let the kernel’s safety infrastructure catch the ways you will inevitably be wrong.

The safety net is the whole argument

BPF programs pass through the kernel verifier before execution. The verifier statically proves that your program terminates, accesses only valid memory, and does not contain unbounded loops. This is not optional and it is not advisory. Your BPF scheduler either passes the verifier or it does not load. Full stop.

The watchdog layer sits on top of that. If your scheduler lets a runqueue get stuck for too long, the built-in watchdog fires and the system reverts to CFS. You do not kernel panic. You do not need console access. The machine keeps running while you go look at what your BPF program did wrong.

This is the specific design decision that changed my read. Writing a custom scheduler you trust on production hardware without a net is a massive ask. Writing one where the verifier checks correctness first and the watchdog catches the cases the verifier cannot statically reason about is a tractable engineering problem. The failure modes were anticipated and handled before the feature shipped. That is not nothing.

The scx schedulers are production code

The scx repository collects BPF schedulers written by people who know what they are doing. A few worth understanding:

scx_lavd is a latency-aware virtual deadline scheduler built for interactive and gaming workloads. It tracks per-task “performance criticality” derived from actual observed scheduling behavior, then prefers to run high-criticality tasks on the most capable CPUs. On asymmetric core systems (Intel P-cores and E-cores, AMD’s mixed topology), this matters. CFS does not have a model of per-core single-thread performance. Its placement decisions reflect that gap. scx_lavd addresses it.

scx_rusty demonstrates the two-component architecture that more complex schedulers use: a BPF program handles the fast path (which CPU runs this task, when to preempt), and a userspace process written in Rust handles policy decisions that tolerate latency (inter-NUMA load balancing, weight tuning). The userspace process runs as a normal process. If it crashes, the watchdog handles the transition back to CFS. The separation keeps the fast path as lean as possible while allowing arbitrarily complex policy logic off the hot path.

scx_simple is the starting point for reading: the minimum viable scheduler implementation with no production ambitions and clear structure. Read this before reading anything else if you want to understand the hook model.

What this is actually for

You are not going to write a better general-purpose scheduler than CFS in a weekend. CFS has seventeen years of Linux kernel developer time in it, has been beaten on by workload analysis from every major cloud vendor, and its pathological cases are well-documented and partially addressed. Write a naive FIFO scheduler in BPF and it will perform worse than CFS in almost every benchmark you run.

What you might actually do:

Write a scheduler for a database machine with asymmetric cores that pins query threads to P-cores and background compaction to E-cores, based on cgroup membership, instead of hoping CFS figures it out. Write a game server scheduler that prioritizes the network input thread over physics and asset streaming threads using a simple policy that CFS’s fairness model cannot express. Prototype a custom scheduling policy for a specific workload, measure it against CFS on your actual hardware with your actual traffic, and iterate on it without rebuilding the kernel between experiments.

The last one is the real unlock. The edit-load-measure cycle for a BPF scheduler is seconds. The same cycle for a kernel patch is measured in minutes at best, longer in practice when you add the reboot. That iteration speed compounds into significantly better outcomes for anyone who cares enough to use it.

The feature is not a toy. It is also not for anyone who does not know what a runqueue is or why scheduler latency shows up in application tail latency. Those two things are both true simultaneously.

One more thing, and I mean this: the watchdog fallback and the BPF verifier together make sched_ext safer than I expected when I first read the patchset description. The design clearly modeled its own failure modes and built containment for each one before merging. More kernel features should work this way.

See also: cgroups v2 without systemd for the resource isolation layer your custom scheduler will want to integrate with, and bpftrace one-liners for IO latency profiling for the observability tooling that makes sense of what your scheduler is actually doing to your workload.

Sources

  1. sched_ext: BPF-extensible scheduler class — Linux kernel documentation (The Linux Kernel project)
  2. sched-ext/scx: sched_ext schedulers and tools (sched-ext project (GitHub))