Google Summer of Code 0x04: sched_ext

Task scheduling in a dyanmic, garbage collected, interpreted language like Lua

Scheduler Class

After adding XDP and TC bindings to Lunatik, the next subsystem is Linux’s sched_ext or Extensible Scheduler Framework. Unlike XDP and TC, however, scheduling is extremely latency-sensitive, which makes running Lua code directly from the scheduling path an interesting problem.

There is a concept of Scheduler class in the kernel. The struct is defined in the source code at linux/kernel/sched.h.

There are multiple scheduler classes in Linux, for example:

Linux kernel 6.12 introduced sched_ext as a framework 1 that allows pluggable custom CPU schedulers via eBPF. This enables implementing and dynamically loading thread schedulers. No need for recompiling the kernel and rebooting.

sched-ext/scx project is a collection of sched_ext schedulers and tools. Schedulers in scx range from simple demonstrative policies to production-oriented ones tailored for specific use cases:

  • scx_simple : basic FIFO or least-run-time policy
  • scx_nest : places tasks on high-frequency cores
  • scx_lavd : is optimized for gaming workloads
  • scx_rusty : partitions CPUs by last-level cache to improve locality
  • scx_bpfland : threads that block frequently (i.e. perform many voluntary context switches per second) are assumed to be interactive, and thus prioritized

BPF Scheduler

As I already mentioned, with sched_ext we can load schedulers during runtime using eBPF. The important point is eBPF here. Because this means sched_ext/scx schedulers are only extensible through eBPF. Much like XDP and TC. So we can go with a similar approach and add support for this via luasched a Lunatik binding for scheduling.

Motivation

Suppose we want:

  • Nginx workers to get low latency scheduling
  • Firefox background processes to receive larger time slices
  • Everything else to use the default policy

Traditionally this logic would be hardcoded inside the scheduler like so:

if (is_nginx(task))
    ...
else if (is_firefox(task))
    ...

There are two pain points:

  • Every change is in eBPF, and the eBPF code is a pain to write.
  • Every policy change requires recompilation.

With luasched, the scheduler asks Lua how a task should be treated in Lua which is relatively simple to write.

local sched = require("sched")

local function schedule(ctx)
    local task = ctx:task()
    if task:comm():match("^nginx") then
        ctx:dsq(REALTIME)
        ctx:slice(20000000)
    else
        ctx:dsq(DEFAULT)
        ctx:slice(10000000)
    end
end

sched.attach(schedule)

Caching results

Lua is a dynamic, garbage collected, interpreted language. These are some words which typically don’t go together with a latency sensitive work like task scheduling. So we’d not want to invoke the Lua runtime each time a task gets enqueued for scheduling. We can design an architecture where we leverage the flexibility of Lua only when it is actually required.

The following diagram summarizes the architecture described above:

diagram