xref: /linux/Documentation/scheduler/sched-ext.rst (revision 3a2c4d55e32ad65efebdb6de44eef3bfa08bb49d)
1.. _sched-ext:
2
3==========================
4Extensible Scheduler Class
5==========================
6
7sched_ext is a scheduler class whose behavior can be defined by a set of BPF
8programs - the BPF scheduler.
9
10* sched_ext exports a full scheduling interface so that any scheduling
11  algorithm can be implemented on top.
12
13* The BPF scheduler can group CPUs however it sees fit and schedule them
14  together, as tasks aren't tied to specific CPUs at the time of wakeup.
15
16* The BPF scheduler can be turned on and off dynamically anytime.
17
18* The system integrity is maintained no matter what the BPF scheduler does.
19  The default scheduling behavior is restored anytime an error is detected,
20  a runnable task stalls, or on invoking the SysRq key sequence
21  `SysRq-S`.
22
23* When the BPF scheduler triggers an error, debug information is dumped to
24  aid debugging. The debug dump is passed to and printed out by the
25  scheduler binary. The debug dump can also be accessed through the
26  `sched_ext_dump` tracepoint. The SysRq key sequence `SysRq-D`
27  triggers a debug dump. This doesn't terminate the BPF scheduler and can
28  only be read through the tracepoint.
29
30Switching to and from sched_ext
31===============================
32
33``CONFIG_SCHED_CLASS_EXT`` is the config option to enable sched_ext and
34``tools/sched_ext`` contains the example schedulers. The following config
35options should be enabled to use sched_ext:
36
37.. code-block:: none
38
39    CONFIG_BPF=y
40    CONFIG_SCHED_CLASS_EXT=y
41    CONFIG_BPF_SYSCALL=y
42    CONFIG_BPF_JIT=y
43    CONFIG_DEBUG_INFO_BTF=y
44    CONFIG_BPF_JIT_ALWAYS_ON=y
45    CONFIG_BPF_JIT_DEFAULT_ON=y
46
47sched_ext is used only when the BPF scheduler is loaded and running.
48
49If a task explicitly sets its scheduling policy to ``SCHED_EXT``, it will be
50treated as ``SCHED_NORMAL`` and scheduled by the fair-class scheduler until the
51BPF scheduler is loaded.
52
53When the BPF scheduler is loaded and ``SCX_OPS_SWITCH_PARTIAL`` is not set
54in ``ops->flags``, all ``SCHED_NORMAL``, ``SCHED_BATCH``, ``SCHED_IDLE``, and
55``SCHED_EXT`` tasks are scheduled by sched_ext.
56
57However, when the BPF scheduler is loaded and ``SCX_OPS_SWITCH_PARTIAL`` is
58set in ``ops->flags``, only tasks with the ``SCHED_EXT`` policy are scheduled
59by sched_ext, while tasks with ``SCHED_NORMAL``, ``SCHED_BATCH`` and
60``SCHED_IDLE`` policies are scheduled by the fair-class scheduler which has
61higher sched_class precedence than ``SCHED_EXT``.
62
63Terminating the sched_ext scheduler program, triggering `SysRq-S`, or
64detection of any internal error including stalled runnable tasks aborts the
65BPF scheduler and reverts all tasks back to the fair-class scheduler.
66
67.. code-block:: none
68
69    # make -j16 -C tools/sched_ext
70    # tools/sched_ext/build/bin/scx_simple
71    local=0 global=3
72    local=5 global=24
73    local=9 global=44
74    local=13 global=56
75    local=17 global=72
76    ^CEXIT: BPF scheduler unregistered
77
78The current status of the BPF scheduler can be determined as follows:
79
80.. code-block:: none
81
82    # cat /sys/kernel/sched_ext/state
83    enabled
84    # cat /sys/kernel/sched_ext/root/ops
85    simple
86
87You can check if any BPF scheduler has ever been loaded since boot by examining
88this monotonically incrementing counter (a value of zero indicates that no BPF
89scheduler has been loaded):
90
91.. code-block:: none
92
93    # cat /sys/kernel/sched_ext/enable_seq
94    1
95
96Each running scheduler exposes an ``events`` file under its sysfs kobject
97(``/sys/kernel/sched_ext/root/events`` for the root scheduler) that tracks
98diagnostic counters. Each counter occupies one ``name value`` line:
99
100.. code-block:: none
101
102    # cat /sys/kernel/sched_ext/root/events
103    SCX_EV_SELECT_CPU_FALLBACK 0
104    SCX_EV_DISPATCH_LOCAL_DSQ_OFFLINE 0
105    SCX_EV_DISPATCH_KEEP_LAST 123
106    SCX_EV_ENQ_SKIP_EXITING 0
107    SCX_EV_ENQ_SKIP_MIGRATION_DISABLED 0
108    SCX_EV_REENQ_IMMED 0
109    SCX_EV_REENQ_REPEAT 0
110    SCX_EV_REFILL_SLICE_DFL 456789
111    SCX_EV_BYPASS_DURATION 0
112    SCX_EV_BYPASS_DISPATCH 0
113    SCX_EV_BYPASS_ACTIVATE 0
114    SCX_EV_INSERT_NOT_OWNED 0
115    SCX_EV_SUB_BYPASS_DISPATCH 0
116
117The counters are described in ``kernel/sched/ext/internal.h``; briefly:
118
119* ``SCX_EV_SELECT_CPU_FALLBACK``: ops.select_cpu() returned a CPU unusable by
120  the task and the core scheduler silently picked a fallback CPU.
121* ``SCX_EV_DISPATCH_LOCAL_DSQ_OFFLINE``: a local-DSQ dispatch was redirected
122  to the global DSQ because the target CPU went offline.
123* ``SCX_EV_DISPATCH_KEEP_LAST``: a task continued running because no other
124  task was available (only when ``SCX_OPS_ENQ_LAST`` is not set).
125* ``SCX_EV_ENQ_SKIP_EXITING``: an exiting task was dispatched to the local DSQ
126  directly, bypassing ops.enqueue() (only when ``SCX_OPS_ENQ_EXITING`` is not set).
127* ``SCX_EV_ENQ_SKIP_MIGRATION_DISABLED``: a migration-disabled task was
128  dispatched to its local DSQ directly (only when
129  ``SCX_OPS_ENQ_MIGRATION_DISABLED`` is not set).
130* ``SCX_EV_REENQ_IMMED``: a task dispatched with ``SCX_ENQ_IMMED`` was
131  re-enqueued because the target CPU was not available for immediate execution.
132* ``SCX_EV_REENQ_REPEAT``: a reenqueue led to another reenqueue without the
133  task running in between; recurring counts indicate that the BPF scheduler
134  keeps re-deciding placements it can't honor.
135* ``SCX_EV_REFILL_SLICE_DFL``: a task's time slice was refilled with the
136  default value (``SCX_SLICE_DFL``).
137* ``SCX_EV_BYPASS_DURATION``: total nanoseconds spent in bypass mode.
138* ``SCX_EV_BYPASS_DISPATCH``: number of tasks dispatched while in bypass mode.
139* ``SCX_EV_BYPASS_ACTIVATE``: number of times bypass mode was activated.
140* ``SCX_EV_INSERT_NOT_OWNED``: attempted to insert a task not owned by this
141  scheduler into a DSQ; such attempts are silently ignored.
142* ``SCX_EV_SUB_BYPASS_DISPATCH``: tasks dispatched from sub-scheduler bypass
143  DSQs (only relevant with ``CONFIG_EXT_SUB_SCHED``).
144
145``tools/sched_ext/scx_show_state.py`` is a drgn script which shows more
146detailed information:
147
148.. code-block:: none
149
150    # tools/sched_ext/scx_show_state.py
151    ops           : simple
152    enabled       : 1
153    switching_all : 1
154    switched_all  : 1
155    enable_state  : enabled (2)
156    aborting      : False
157    bypass_depth  : 0
158    nr_rejected   : 0
159    enable_seq    : 1
160
161Whether a given task is on sched_ext can be determined as follows:
162
163.. code-block:: none
164
165    # grep ext /proc/self/sched
166    ext.enabled                                  :                    1
167
168The Basics
169==========
170
171Userspace can implement an arbitrary BPF scheduler by loading a set of BPF
172programs that implement ``struct sched_ext_ops``. The only mandatory field
173is ``ops.name`` which must be a valid BPF object name. All operations are
174optional. The following modified excerpt is from
175``tools/sched_ext/scx_simple.bpf.c`` showing a minimal global FIFO scheduler.
176
177.. code-block:: c
178
179    /*
180     * Decide which CPU a task should be migrated to before being
181     * enqueued (either at wakeup, fork time, or exec time). If an
182     * idle core is found by the default ops.select_cpu() implementation,
183     * then insert the task directly into SCX_DSQ_LOCAL and skip the
184     * ops.enqueue() callback.
185     *
186     * Note that this implementation has exactly the same behavior as the
187     * default ops.select_cpu implementation. The behavior of the scheduler
188     * would be exactly same if the implementation just didn't define the
189     * simple_select_cpu() struct_ops prog.
190     */
191    s32 BPF_STRUCT_OPS(simple_select_cpu, struct task_struct *p,
192                       s32 prev_cpu, u64 wake_flags)
193    {
194            s32 cpu;
195            /* Need to initialize or the BPF verifier will reject the program */
196            bool direct = false;
197
198            cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &direct);
199
200            if (direct)
201                    scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
202
203            return cpu;
204    }
205
206    /*
207     * Do a direct insertion of a task to the global DSQ. This ops.enqueue()
208     * callback will only be invoked if we failed to find a core to insert
209     * into in ops.select_cpu() above.
210     *
211     * Note that this implementation has exactly the same behavior as the
212     * default ops.enqueue implementation, which just dispatches the task
213     * to SCX_DSQ_GLOBAL. The behavior of the scheduler would be exactly same
214     * if the implementation just didn't define the simple_enqueue struct_ops
215     * prog.
216     */
217    void BPF_STRUCT_OPS(simple_enqueue, struct task_struct *p, u64 enq_flags)
218    {
219            scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
220    }
221
222    s32 BPF_STRUCT_OPS_SLEEPABLE(simple_init)
223    {
224            /*
225             * By default, all SCHED_EXT, SCHED_OTHER, SCHED_IDLE, and
226             * SCHED_BATCH tasks should use sched_ext.
227             */
228            return 0;
229    }
230
231    void BPF_STRUCT_OPS(simple_exit, struct scx_exit_info *ei)
232    {
233            exit_type = ei->kind;
234    }
235
236    SEC(".struct_ops")
237    struct sched_ext_ops simple_ops = {
238            .select_cpu             = (void *)simple_select_cpu,
239            .enqueue                = (void *)simple_enqueue,
240            .init                   = (void *)simple_init,
241            .exit                   = (void *)simple_exit,
242            .name                   = "simple",
243    };
244
245Scheduler-Dependent Knobs
246-------------------------
247
248The fair-class scheduler enforces CPU controller settings such as
249``cpu.max``, ``cpu.weight`` and ``cpu.idle``. For sched_ext tasks, the
250scheduler core communicates these settings to the BPF scheduler
251through ``ops.cgroup_init()`` and reports subsequent changes through
252the corresponding ``ops.cgroup_set_*()`` callbacks. Similarly, per-task
253nice changes are converted to weights and reported through
254``ops.set_weight()``.
255
256Each BPF scheduler is responsible for implementing the scheduling
257semantics of these settings and may choose to ignore them. Consult the
258loaded scheduler's documentation before relying on these controls.
259
260Dispatch Queues
261---------------
262
263To match the impedance between the scheduler core and the BPF scheduler,
264sched_ext uses DSQs (dispatch queues) which can operate as both a FIFO and a
265priority queue. By default, there is one global FIFO (``SCX_DSQ_GLOBAL``),
266and one local DSQ per CPU (``SCX_DSQ_LOCAL``). The BPF scheduler can manage
267an arbitrary number of DSQs using ``scx_bpf_create_dsq()`` and
268``scx_bpf_destroy_dsq()``.
269
270A CPU always executes a task from its local DSQ. A task is "inserted" into a
271DSQ. A task in a non-local DSQ is "move"d into the target CPU's local DSQ.
272
273When a CPU is looking for the next task to run, if the local DSQ is not
274empty, the first task is picked. Otherwise, the CPU tries to move a task
275from the global DSQ. If that doesn't yield a runnable task either,
276``ops.dispatch()`` is invoked.
277
278Scheduling Cycle
279----------------
280
281The following briefly shows how a waking task is scheduled and executed.
282
2831. When a task is waking up, ``ops.select_cpu()`` is the first operation
284   invoked. This serves two purposes. First, CPU selection optimization
285   hint. Second, waking up the selected CPU if idle.
286
287   The CPU selected by ``ops.select_cpu()`` is an optimization hint and not
288   binding. The actual decision is made at the last step of scheduling.
289   However, there is a small performance gain if the CPU
290   ``ops.select_cpu()`` returns matches the CPU the task eventually runs on.
291
292   A side-effect of selecting a CPU is waking it up from idle. While a BPF
293   scheduler can wake up any cpu using the ``scx_bpf_kick_cpu()`` helper,
294   using ``ops.select_cpu()`` judiciously can be simpler and more efficient.
295
296   Note that the scheduler core will ignore an invalid CPU selection, for
297   example, if it's outside the allowed cpumask of the task.
298
299   A task can be immediately inserted into a DSQ from ``ops.select_cpu()``
300   by calling ``scx_bpf_dsq_insert()`` or ``scx_bpf_dsq_insert_vtime()``.
301
302   If the task is inserted into ``SCX_DSQ_LOCAL`` from
303   ``ops.select_cpu()``, it will be added to the local DSQ of whichever CPU
304   is returned from ``ops.select_cpu()``. Additionally, inserting directly
305   from ``ops.select_cpu()`` will cause the ``ops.enqueue()`` callback to
306   be skipped.
307
308   Any other attempt to store a task in BPF-internal data structures from
309   ``ops.select_cpu()`` does not prevent ``ops.enqueue()`` from being
310   invoked. This is discouraged, as it can introduce racy behavior or
311   inconsistent state.
312
3132. Once the target CPU is selected, ``ops.enqueue()`` is invoked (unless the
314   task was inserted directly from ``ops.select_cpu()``). ``ops.enqueue()``
315   can make one of the following decisions:
316
317   * Immediately insert the task into either the global or a local DSQ by
318     calling ``scx_bpf_dsq_insert()`` with one of the following options:
319     ``SCX_DSQ_GLOBAL``, ``SCX_DSQ_LOCAL``, or ``SCX_DSQ_LOCAL_ON | cpu``.
320
321   * Immediately insert the task into a custom DSQ by calling
322     ``scx_bpf_dsq_insert()`` with a DSQ ID which is smaller than 2^63.
323
324   * Queue the task on the BPF side.
325
326   **Task State Tracking and ops.dequeue() Semantics**
327
328   A task is in the "BPF scheduler's custody" when the BPF scheduler is
329   responsible for managing its lifecycle. A task enters custody when it is
330   dispatched to a user DSQ or stored in the BPF scheduler's internal data
331   structures. Custody is entered only from ``ops.enqueue()`` for those
332   operations. The only exception is dispatching to a user DSQ from
333   ``ops.select_cpu()``: although the task is not yet technically in BPF
334   scheduler custody at that point, the dispatch has the same semantic
335   effect as dispatching from ``ops.enqueue()`` for custody-related
336   purposes.
337
338   Once ``ops.enqueue()`` is called, the task may or may not enter custody
339   depending on what the scheduler does:
340
341   * **Directly dispatched to terminal DSQs** (``SCX_DSQ_LOCAL``,
342     ``SCX_DSQ_LOCAL_ON | cpu``, or ``SCX_DSQ_GLOBAL``): the BPF scheduler
343     is done with the task - it either goes straight to a CPU's local run
344     queue or to the global DSQ as a fallback. The task never enters (or
345     exits) BPF custody, and ``ops.dequeue()`` will not be called.
346
347   * **Dispatch to user-created DSQs** (custom DSQs): the task enters the
348     BPF scheduler's custody. When the task later leaves BPF custody
349     (dispatched to a terminal DSQ, picked by core-sched, or dequeued for
350     sleep/property changes), ``ops.dequeue()`` will be called exactly
351     once.
352
353   * **Stored in BPF data structures** (e.g., internal BPF queues): the
354     task is in BPF custody. ``ops.dequeue()`` will be called when it
355     leaves (e.g., when ``ops.dispatch()`` moves it to a terminal DSQ, or
356     on property change / sleep).
357
358   Note that ``ops.enqueue()`` can be called multiple times in a row without
359   an intervening call to ``ops.dequeue()``. This can happen, for example,
360   when a task on a user-created DSQ is re-enqueued using
361   ``scx_bpf_dsq_reenq()``. The task stays in BPF custody the entire time.
362
363   When a task leaves BPF scheduler custody, ``ops.dequeue()`` is invoked.
364   The dequeue can happen for different reasons, distinguished by flags:
365
366   1. **Regular dispatch**: when a task in BPF custody is dispatched to a
367      terminal DSQ from ``ops.dispatch()`` (leaving BPF custody for
368      execution), ``ops.dequeue()`` is triggered without any special flags.
369
370   2. **Core scheduling pick**: when ``CONFIG_SCHED_CORE`` is enabled and
371      core scheduling picks a task for execution while it's still in BPF
372      custody, ``ops.dequeue()`` is called with the
373      ``SCX_DEQ_CORE_SCHED_EXEC`` flag.
374
375   3. **Scheduling property change**: when a task property changes (via
376      operations like ``sched_setaffinity()``, ``sched_setscheduler()``,
377      priority changes, CPU migrations, etc.) while the task is still in
378      BPF custody, ``ops.dequeue()`` is called with the
379      ``SCX_DEQ_SCHED_CHANGE`` flag set in ``deq_flags``.
380
381   **Important**: Once a task has left BPF custody (e.g., after being
382   dispatched to a terminal DSQ), property changes will not trigger
383   ``ops.dequeue()``, since the task is no longer managed by the BPF
384   scheduler.
385
3863. When a CPU is ready to schedule, it first looks at its local DSQ. If
387   empty, it then looks at the global DSQ. If there still isn't a task to
388   run, ``ops.dispatch()`` is invoked which can use the following two
389   functions to populate the local DSQ.
390
391   * ``scx_bpf_dsq_insert()`` inserts a task to a DSQ. Any target DSQ can be
392     used - ``SCX_DSQ_LOCAL``, ``SCX_DSQ_LOCAL_ON | cpu``,
393     ``SCX_DSQ_GLOBAL`` or a custom DSQ. While ``scx_bpf_dsq_insert()``
394     currently can't be called with BPF locks held, this is being worked on
395     and will be supported. ``scx_bpf_dsq_insert()`` schedules insertion
396     rather than performing them immediately. There can be up to
397     ``ops.dispatch_max_batch`` pending tasks.
398
399   * ``scx_bpf_dsq_move_to_local()`` moves a task from the specified non-local
400     DSQ to the dispatching DSQ. This function cannot be called with any BPF
401     locks held. ``scx_bpf_dsq_move_to_local()`` flushes the pending insertions
402     tasks before trying to move from the specified DSQ.
403
4044. After ``ops.dispatch()`` returns, if there are tasks in the local DSQ,
405   the CPU runs the first one. If empty, the following steps are taken:
406
407   * Try to move from the global DSQ. If successful, run the task.
408
409   * If ``ops.dispatch()`` has dispatched any tasks, retry #3.
410
411   * If the previous task is an SCX task and still runnable, keep executing
412     it (see ``SCX_OPS_ENQ_LAST``).
413
414   * Go idle.
415
416Note that the BPF scheduler can always choose to dispatch tasks immediately
417in ``ops.enqueue()`` as illustrated in the above simple example. If only the
418built-in DSQs are used, there is no need to implement ``ops.dispatch()`` as
419a task is never queued on the BPF scheduler and both the local and global
420DSQs are executed automatically.
421
422``scx_bpf_dsq_insert()`` inserts the task on the FIFO of the target DSQ. Use
423``scx_bpf_dsq_insert_vtime()`` for the priority queue. Internal DSQs such as
424``SCX_DSQ_LOCAL`` and ``SCX_DSQ_GLOBAL`` do not support priority-queue
425dispatching, and must be dispatched to with ``scx_bpf_dsq_insert()``. See
426the function documentation and usage in ``tools/sched_ext/scx_simple.bpf.c``
427for more information.
428
429Task Lifecycle
430--------------
431
432The following pseudo-code presents a rough overview of the entire lifecycle
433of a task managed by a sched_ext scheduler:
434
435.. code-block:: c
436
437    ops.init_task();            /* A new task is created */
438    ops.enable();               /* Enable BPF scheduling for the task */
439
440    while (task in SCHED_EXT) {
441        if (task can migrate)
442            ops.select_cpu();   /* Called on wakeup (optimization) */
443
444        ops.runnable();         /* Task becomes ready to run */
445
446        while (task_is_runnable(task)) {
447            if (task is not in a DSQ || task->scx.slice == 0) {
448                ops.enqueue();  /* Task can be added to a DSQ */
449
450                /* Task property change (i.e., affinity, nice, etc.)? */
451                if (sched_change(task)) {
452                    ops.dequeue(); /* Exiting BPF scheduler custody */
453                    ops.quiescent();
454
455                    /* Property change callback, e.g. ops.set_weight() */
456
457                    ops.runnable();
458                    continue;
459                }
460
461                /* Any usable CPU becomes available */
462
463                ops.dispatch();     /* Task is moved to a local DSQ */
464                ops.dequeue();      /* Exiting BPF scheduler custody */
465            }
466
467            ops.running();      /* Task starts running on its assigned CPU */
468
469            while (task_is_runnable(task) && task->scx.slice > 0) {
470                ops.tick();     /* Called every 1/HZ seconds */
471
472                if (task->scx.slice == 0)
473                    ops.dispatch(); /* task->scx.slice can be refilled */
474            }
475
476            ops.stopping();     /* Task stops running (time slice expires or wait) */
477        }
478
479        ops.quiescent();        /* Task releases its assigned CPU (wait) */
480    }
481
482    ops.disable();              /* Disable BPF scheduling for the task */
483    ops.exit_task();            /* Task is destroyed */
484
485Note that the above pseudo-code does not cover all possible state transitions
486and edge cases, to name a few examples:
487
488* ``ops.dispatch()`` may fail to move the task to a local DSQ due to a racing
489  property change on that task, in which case ``ops.dispatch()`` will be
490  retried.
491
492* The task may be direct-dispatched to a local DSQ from ``ops.enqueue()``,
493  in which case ``ops.dispatch()`` and ``ops.dequeue()`` are skipped and we go
494  straight to ``ops.running()``.
495
496* Property changes may occur at virtually any point during the task's lifecycle,
497  not just when the task is queued and waiting to be dispatched. For example,
498  changing a property of a running task will lead to the callback sequence
499  ``ops.stopping()`` -> ``ops.quiescent()`` -> (property change callback) ->
500  ``ops.runnable()`` -> ``ops.running()``.
501
502* A sched_ext task can be preempted by a task from a higher-priority scheduling
503  class, in which case it will exit the tick-dispatch loop even though it is runnable
504  and has a non-zero slice.
505
506See the "Scheduling Cycle" section for a more detailed description of how
507a freshly woken up task gets on a CPU.
508
509Where to Look
510=============
511
512* ``include/linux/sched/ext.h`` defines the core data structures and
513  constants, while the ops table (``struct sched_ext_ops``) is defined in
514  ``kernel/sched/ext/internal.h``.
515
516* ``kernel/sched/ext/ext.c`` contains sched_ext core implementation and helpers.
517  The functions prefixed with ``scx_bpf_`` can be called from the BPF
518  scheduler.
519
520* ``kernel/sched/ext/idle.c`` contains the built-in idle CPU selection policy.
521
522* ``tools/sched_ext/`` hosts example BPF scheduler implementations.
523
524  * ``scx_simple[.bpf].c``: Minimal global FIFO scheduler example using a
525    custom DSQ.
526
527  * ``scx_qmap[.bpf].c``: A multi-level FIFO scheduler supporting five
528    levels of priority implemented with arena-backed doubly-linked lists.
529
530  * ``scx_central[.bpf].c``: A central FIFO scheduler where all scheduling
531    decisions are made on one CPU, demonstrating ``LOCAL_ON`` dispatching,
532    tickless operation, and kthread preemption.
533
534  * ``scx_cpu0[.bpf].c``: A scheduler that queues all tasks to a shared DSQ
535    and only dispatches them on CPU0 in FIFO order. Useful for testing bypass
536    behavior.
537
538  * ``scx_flatcg[.bpf].c``: A flattened cgroup hierarchy scheduler
539    implementing hierarchical weight-based cgroup CPU control by compounding
540    each cgroup's share at every level into a single flat scheduling layer.
541
542  * ``scx_pair[.bpf].c``: A core-scheduling example that always makes
543    sibling CPU pairs execute tasks from the same CPU cgroup.
544
545  * ``scx_sdt[.bpf].c``: A variation of ``scx_simple`` demonstrating BPF
546    arena memory management for per-task data.
547
548  * ``scx_userland[.bpf].c``: A minimal scheduler demonstrating user space
549    scheduling. Tasks with CPU affinity are direct-dispatched in FIFO order;
550    all others are scheduled in user space by a simple vruntime scheduler.
551
552Module Parameters
553=================
554
555sched_ext exposes two module parameters under the ``sched_ext.`` prefix that
556control bypass-mode behaviour. These knobs are primarily for debugging; there
557is usually no reason to change them during normal operation. They can be read
558and written at runtime (mode 0600) via
559``/sys/module/sched_ext/parameters/``.
560
561``sched_ext.slice_bypass_us`` (default: 5000 µs)
562    The time slice assigned to all tasks when the scheduler is in bypass mode,
563    i.e. during BPF scheduler load, unload, and error recovery. Valid range is
564    100 µs to 100 ms.
565
566``sched_ext.bypass_lb_intv_us`` (default: 500000 µs)
567    The interval at which the bypass-mode load balancer redistributes tasks
568    across CPUs. Set to 0 to disable load balancing during bypass mode. Valid
569    range is 0 to 10 s.
570
571ABI Instability
572===============
573
574The APIs provided by sched_ext to BPF schedulers programs have no stability
575guarantees. This includes the ops table callbacks defined in
576``kernel/sched/ext/internal.h`` and the constants defined in
577``include/linux/sched/ext.h``, as well as the ``scx_bpf_`` kfuncs defined in
578``kernel/sched/ext/ext.c`` and ``kernel/sched/ext/idle.c``.
579
580While we will attempt to provide a relatively stable API surface when
581possible, they are subject to change without warning between kernel
582versions.
583