summaryrefslogtreecommitdiffstats
path: root/tools/sched_ext
diff options
context:
space:
mode:
authorLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
committerLinus Torvalds <torvalds@linux-foundation.org>2026-10-02 12:17:24 -0700
commit3f1fe48a36b0b6722dc3fd421d93512bac138e9a (patch)
treeb767d7f6e26bc64334d3f14f8422d187239dc45d /tools/sched_ext
downloadlinux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.tar.gz
linux-stable-3f1fe48a36b0b6722dc3fd421d93512bac138e9a.zip
Merge tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linuxgrafted
Pull io_uring fixes from Jens Axboe: - Fix a task_work add use-after-free with SQPOLL. The sqpoll thread could pop and complete the last request while io_req_normal_work_add() was still looking at them after the mpscq push. Use the same approach as DEFER_TASKRUN to protect from that, holding an RCU read lock across the add, and have exit wait for an RCU grace period for SQPOLL rings as well. - CQE32 ring fixes: correct the free entry check for 32b CQEs, zero the big_cqe for aux CQEs, and only post the dummy skip CQE on CQE_MIXED rings - Mark the source filter table as COW when cloning bpf filters, so registering another filter on the source doesn't modify the shared table in place - Initialize the task context before running the BPF loop - Requeue zcrx multishot receives stopped by a local resource - End a TX_TIMESTAMP multishot cmd when the CQ is full (lollipopkit) * tag 'io_uring-7.3-20261002' of git://git.kernel.org/pub/scm/linux/kernel/git/axboe/linux: io_uring: fix task_work add use-after-free with SQPOLL io_uring/cmd_net: end TX_TIMESTAMP multishot when the CQ is full io_uring/zcrx: requeue multishot receives stopped by a local resource io_uring: initialize task context before running the BPF loop io_uring: zero big_cqe for aux CQEs on CQE32 rings io_uring: fix free entry check for 32b CQEs on CQE32 rings io_uring: only post the dummy skip CQE on CQE_MIXED rings io_uring/bpf_filter: mark source as COW when cloning filters
Diffstat (limited to 'tools/sched_ext')
-rw-r--r--tools/sched_ext/.gitignore2
-rw-r--r--tools/sched_ext/Kconfig61
-rw-r--r--tools/sched_ext/Makefile261
-rw-r--r--tools/sched_ext/README.md263
-rw-r--r--tools/sched_ext/include/bpf-compat/gnu/stubs.h11
-rw-r--r--tools/sched_ext/include/scx/bpf_arena_common.bpf.h179
-rw-r--r--tools/sched_ext/include/scx/bpf_arena_common.h33
-rw-r--r--tools/sched_ext/include/scx/cid.bpf.h777
-rw-r--r--tools/sched_ext/include/scx/common.bpf.h1192
-rw-r--r--tools/sched_ext/include/scx/common.h80
-rw-r--r--tools/sched_ext/include/scx/compat.bpf.h495
-rw-r--r--tools/sched_ext/include/scx/compat.h403
-rw-r--r--tools/sched_ext/include/scx/enum_defs.autogen.h210
-rw-r--r--tools/sched_ext/include/scx/enums.autogen.bpf.h143
-rw-r--r--tools/sched_ext/include/scx/enums.autogen.h54
-rw-r--r--tools/sched_ext/include/scx/enums.bpf.h12
-rw-r--r--tools/sched_ext/include/scx/enums.h28
-rw-r--r--tools/sched_ext/include/scx/enums_abi.autogen.h223
-rw-r--r--tools/sched_ext/include/scx/user_exit_info.bpf.h43
-rw-r--r--tools/sched_ext/include/scx/user_exit_info.h75
-rw-r--r--tools/sched_ext/include/scx/user_exit_info_common.h35
-rw-r--r--tools/sched_ext/scx_central.bpf.c388
-rw-r--r--tools/sched_ext/scx_central.c126
-rw-r--r--tools/sched_ext/scx_cpu0.bpf.c94
-rw-r--r--tools/sched_ext/scx_cpu0.c105
-rw-r--r--tools/sched_ext/scx_flatcg.bpf.c978
-rw-r--r--tools/sched_ext/scx_flatcg.c239
-rw-r--r--tools/sched_ext/scx_flatcg.h51
-rw-r--r--tools/sched_ext/scx_pair.bpf.c654
-rw-r--r--tools/sched_ext/scx_pair.c196
-rw-r--r--tools/sched_ext/scx_pair.h9
-rw-r--r--tools/sched_ext/scx_qmap.bpf.c2095
-rw-r--r--tools/sched_ext/scx_qmap.c501
-rw-r--r--tools/sched_ext/scx_qmap.h189
-rw-r--r--tools/sched_ext/scx_sdt.bpf.c717
-rw-r--r--tools/sched_ext/scx_sdt.c102
-rw-r--r--tools/sched_ext/scx_sdt.h113
-rw-r--r--tools/sched_ext/scx_show_state.py51
-rw-r--r--tools/sched_ext/scx_simple.bpf.c161
-rw-r--r--tools/sched_ext/scx_simple.c110
-rw-r--r--tools/sched_ext/scx_userland.bpf.c344
-rw-r--r--tools/sched_ext/scx_userland.c447
-rw-r--r--tools/sched_ext/scx_userland.h17
43 files changed, 12267 insertions, 0 deletions
diff --git a/tools/sched_ext/.gitignore b/tools/sched_ext/.gitignore
new file mode 100644
index 000000000..d6264fe1c
--- /dev/null
+++ b/tools/sched_ext/.gitignore
@@ -0,0 +1,2 @@
+tools/
+build/
diff --git a/tools/sched_ext/Kconfig b/tools/sched_ext/Kconfig
new file mode 100644
index 000000000..275bd97ae
--- /dev/null
+++ b/tools/sched_ext/Kconfig
@@ -0,0 +1,61 @@
+# sched-ext mandatory options
+#
+CONFIG_BPF=y
+CONFIG_BPF_SYSCALL=y
+CONFIG_BPF_JIT=y
+CONFIG_DEBUG_INFO_BTF=y
+CONFIG_BPF_JIT_ALWAYS_ON=y
+CONFIG_BPF_JIT_DEFAULT_ON=y
+CONFIG_SCHED_CLASS_EXT=y
+
+# Required by some rust schedulers (e.g. scx_p2dq)
+#
+CONFIG_KALLSYMS_ALL=y
+
+# Required on arm64
+#
+# CONFIG_DEBUG_INFO_REDUCED is not set
+
+# LAVD tracks futex to give an additional time slice for futex holder
+# (i.e., avoiding lock holder preemption) for better system-wide progress.
+# LAVD first tries to use ftrace to trace futex function calls.
+# If that is not available, it tries to use a tracepoint.
+CONFIG_FUNCTION_TRACER=y
+
+# Enable scheduling debugging
+#
+CONFIG_SCHED_DEBUG=y
+
+# Enable extra scheduling features (for a better code coverage while testing
+# the schedulers)
+#
+CONFIG_SCHED_AUTOGROUP=y
+CONFIG_SCHED_CORE=y
+CONFIG_SCHED_MC=y
+
+# Enable fully preemptible kernel for a better test coverage of the schedulers
+#
+# CONFIG_PREEMPT_NONE is not set
+# CONFIG_PREEMPT_VOLUNTARY is not set
+CONFIG_PREEMPT=y
+CONFIG_PREEMPT_DYNAMIC=y
+
+# Additional debugging information (useful to catch potential locking issues)
+CONFIG_DEBUG_LOCKDEP=y
+CONFIG_DEBUG_ATOMIC_SLEEP=y
+CONFIG_PROVE_LOCKING=y
+
+# Bpftrace headers (for additional debug info)
+CONFIG_BPF_EVENTS=y
+CONFIG_FTRACE_SYSCALLS=y
+CONFIG_DYNAMIC_FTRACE=y
+CONFIG_KPROBES=y
+CONFIG_KPROBE_EVENTS=y
+CONFIG_UPROBES=y
+CONFIG_UPROBE_EVENTS=y
+CONFIG_DEBUG_FS=y
+
+# Enable access to kernel configuration and headers at runtime
+CONFIG_IKHEADERS=y
+CONFIG_IKCONFIG_PROC=y
+CONFIG_IKCONFIG=y
diff --git a/tools/sched_ext/Makefile b/tools/sched_ext/Makefile
new file mode 100644
index 000000000..21554f089
--- /dev/null
+++ b/tools/sched_ext/Makefile
@@ -0,0 +1,261 @@
+# SPDX-License-Identifier: GPL-2.0
+# Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+include ../build/Build.include
+include ../scripts/Makefile.arch
+include ../scripts/Makefile.include
+
+all: all_targets
+
+ifneq ($(LLVM),)
+ifneq ($(filter %/,$(LLVM)),)
+LLVM_PREFIX := $(LLVM)
+else ifneq ($(filter -%,$(LLVM)),)
+LLVM_SUFFIX := $(LLVM)
+endif
+
+CLANG_TARGET_FLAGS_arm := arm-linux-gnueabi
+CLANG_TARGET_FLAGS_arm64 := aarch64-linux-gnu
+CLANG_TARGET_FLAGS_hexagon := hexagon-linux-musl
+CLANG_TARGET_FLAGS_m68k := m68k-linux-gnu
+CLANG_TARGET_FLAGS_mips := mipsel-linux-gnu
+CLANG_TARGET_FLAGS_powerpc := powerpc64le-linux-gnu
+CLANG_TARGET_FLAGS_riscv := riscv64-linux-gnu
+CLANG_TARGET_FLAGS_s390 := s390x-linux-gnu
+CLANG_TARGET_FLAGS_x86 := x86_64-linux-gnu
+CLANG_TARGET_FLAGS := $(CLANG_TARGET_FLAGS_$(ARCH))
+
+ifeq ($(CROSS_COMPILE),)
+ifeq ($(CLANG_TARGET_FLAGS),)
+$(error Specify CROSS_COMPILE or add '--target=' option to lib.mk)
+else
+CLANG_FLAGS += --target=$(CLANG_TARGET_FLAGS)
+endif # CLANG_TARGET_FLAGS
+else
+CLANG_FLAGS += --target=$(notdir $(CROSS_COMPILE:%-=%))
+endif # CROSS_COMPILE
+
+CC := $(LLVM_PREFIX)clang$(LLVM_SUFFIX) $(CLANG_FLAGS) -fintegrated-as
+else
+CC := $(CROSS_COMPILE)gcc
+endif # LLVM
+
+CURDIR := $(abspath .)
+TOOLSDIR := $(abspath ..)
+LIBDIR := $(TOOLSDIR)/lib
+BPFDIR := $(LIBDIR)/bpf
+TOOLSINCDIR := $(TOOLSDIR)/include
+BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool
+APIDIR := $(TOOLSINCDIR)/uapi
+GENDIR := $(abspath ../../include/generated)
+GENHDR := $(GENDIR)/autoconf.h
+
+ifeq ($(O),)
+OUTPUT_DIR := $(CURDIR)/build
+else
+OUTPUT_DIR := $(O)/build
+endif # O
+OBJ_DIR := $(OUTPUT_DIR)/obj
+INCLUDE_DIR := $(OUTPUT_DIR)/include
+BPFOBJ_DIR := $(OBJ_DIR)/libbpf
+SCXOBJ_DIR := $(OBJ_DIR)/sched_ext
+BINDIR := $(OUTPUT_DIR)/bin
+BPFOBJ := $(BPFOBJ_DIR)/libbpf.a
+ifneq ($(CROSS_COMPILE),)
+HOST_BUILD_DIR := $(OBJ_DIR)/host/obj
+HOST_OUTPUT_DIR := $(OBJ_DIR)/host
+HOST_INCLUDE_DIR := $(HOST_OUTPUT_DIR)/include
+else
+HOST_BUILD_DIR := $(OBJ_DIR)
+HOST_OUTPUT_DIR := $(OUTPUT_DIR)
+HOST_INCLUDE_DIR := $(INCLUDE_DIR)
+endif
+HOST_BPFOBJ := $(HOST_BUILD_DIR)/libbpf/libbpf.a
+RESOLVE_BTFIDS := $(HOST_BUILD_DIR)/resolve_btfids/resolve_btfids
+DEFAULT_BPFTOOL := $(HOST_OUTPUT_DIR)/sbin/bpftool
+
+VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux) \
+ $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux) \
+ ../../vmlinux \
+ /sys/kernel/btf/vmlinux \
+ /boot/vmlinux-$(shell uname -r)
+VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS))))
+ifeq ($(VMLINUX_BTF),)
+$(error Cannot find a vmlinux for VMLINUX_BTF at any of "$(VMLINUX_BTF_PATHS)")
+endif
+
+BPFTOOL ?= $(DEFAULT_BPFTOOL)
+
+ifneq ($(wildcard $(GENHDR)),)
+ GENFLAGS := -DHAVE_GENHDR
+endif
+
+CFLAGS += -g -O2 -rdynamic -pthread -Wall -Werror $(GENFLAGS) \
+ -I$(INCLUDE_DIR) -I$(GENDIR) -I$(LIBDIR) \
+ -I$(TOOLSINCDIR) -I$(APIDIR) -I$(CURDIR)/include
+
+# Silence some warnings when compiled with clang
+ifneq ($(LLVM),)
+CFLAGS += -Wno-unused-command-line-argument
+endif
+
+LDFLAGS += -lelf -lz -lpthread
+
+IS_LITTLE_ENDIAN = $(shell $(CC) -dM -E - </dev/null | \
+ grep 'define __BYTE_ORDER__ __ORDER_LITTLE_ENDIAN__')
+
+# Get Clang's default includes on this system, as opposed to those seen by
+# '-target bpf'. This fixes "missing" files on some architectures/distros,
+# such as asm/byteorder.h, asm/socket.h, asm/sockios.h, sys/cdefs.h etc.
+#
+# Use '-idirafter': Don't interfere with include mechanics except where the
+# build would have failed anyways.
+define get_sys_includes
+$(shell $(1) -v -E - </dev/null 2>&1 \
+ | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \
+$(shell $(1) -dM -E - </dev/null | grep '__riscv_xlen ' | awk '{printf("-D__riscv_xlen=%d -D__BITS_PER_LONG=%d", $$3, $$3)}')
+endef
+
+BPF_CFLAGS = -g -D__TARGET_ARCH_$(SRCARCH) \
+ $(if $(IS_LITTLE_ENDIAN),-mlittle-endian,-mbig-endian) \
+ -I$(CURDIR)/include -I$(CURDIR)/include/bpf-compat \
+ -I$(INCLUDE_DIR) -I$(APIDIR) \
+ -I../../include \
+ $(call get_sys_includes,$(CLANG)) \
+ -Wall -Wno-compare-distinct-pointer-types \
+ -Wno-microsoft-anon-tag \
+ -fms-extensions \
+ -O2 -mcpu=v3
+
+# sort removes libbpf duplicates when not cross-building
+MAKE_DIRS := $(sort $(OBJ_DIR)/libbpf $(HOST_BUILD_DIR)/libbpf \
+ $(HOST_BUILD_DIR)/bpftool $(HOST_BUILD_DIR)/resolve_btfids \
+ $(INCLUDE_DIR) $(SCXOBJ_DIR) $(BINDIR))
+
+$(MAKE_DIRS):
+ $(call msg,MKDIR,,$@)
+ $(Q)mkdir -p $@
+
+ifneq ($(CROSS_COMPILE),)
+$(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile) \
+ $(APIDIR)/linux/bpf.h \
+ | $(OBJ_DIR)/libbpf
+ $(Q)$(MAKE) $(submake_extras) CROSS_COMPILE=$(CROSS_COMPILE) \
+ -C $(BPFDIR) OUTPUT=$(OBJ_DIR)/libbpf/ \
+ EXTRA_CFLAGS='-g -O0 -fPIC' \
+ LDFLAGS="$(LDFLAGS)" \
+ DESTDIR=$(OUTPUT_DIR) prefix= all install_headers
+endif
+
+$(HOST_BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile) \
+ $(APIDIR)/linux/bpf.h \
+ | $(HOST_BUILD_DIR)/libbpf
+ $(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) \
+ OUTPUT=$(HOST_BUILD_DIR)/libbpf/ \
+ ARCH= CROSS_COMPILE= CC="$(HOSTCC)" LD=$(HOSTLD) \
+ EXTRA_CFLAGS='-g -O0 -fPIC' \
+ DESTDIR=$(HOST_OUTPUT_DIR) prefix= all install_headers
+
+$(DEFAULT_BPFTOOL): $(wildcard $(BPFTOOLDIR)/*.[ch] $(BPFTOOLDIR)/Makefile) \
+ $(HOST_BPFOBJ) | $(HOST_BUILD_DIR)/bpftool
+ $(Q)$(MAKE) $(submake_extras) -C $(BPFTOOLDIR) \
+ ARCH= CROSS_COMPILE= CC="$(HOSTCC)" LD=$(HOSTLD) \
+ EXTRA_CFLAGS='-g -O0' \
+ OUTPUT=$(HOST_BUILD_DIR)/bpftool/ \
+ LIBBPF_OUTPUT=$(HOST_BUILD_DIR)/libbpf/ \
+ LIBBPF_DESTDIR=$(HOST_OUTPUT_DIR)/ \
+ prefix= DESTDIR=$(HOST_OUTPUT_DIR)/ install-bin
+
+$(INCLUDE_DIR)/vmlinux.h: $(VMLINUX_BTF) $(BPFTOOL) | $(INCLUDE_DIR)
+ifeq ($(VMLINUX_H),)
+ $(call msg,GEN,,$@)
+ $(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@
+else
+ $(call msg,CP,,$@)
+ $(Q)cp "$(VMLINUX_H)" $@
+endif
+
+$(SCXOBJ_DIR)/%.bpf.o: %.bpf.c $(INCLUDE_DIR)/vmlinux.h include/scx/*.h \
+ | $(BPFOBJ) $(SCXOBJ_DIR)
+ $(call msg,CLNG-BPF,,$(notdir $@))
+ $(Q)$(CLANG) $(BPF_CFLAGS) -target bpf -c $< -o $@
+
+$(INCLUDE_DIR)/%.bpf.skel.h: $(SCXOBJ_DIR)/%.bpf.o $(INCLUDE_DIR)/vmlinux.h $(BPFTOOL)
+ $(eval sched=$(notdir $@))
+ $(call msg,GEN-SKEL,,$(sched))
+ $(Q)$(BPFTOOL) gen object $(<:.o=.linked1.o) $<
+ $(Q)$(BPFTOOL) gen object $(<:.o=.linked2.o) $(<:.o=.linked1.o)
+ $(Q)$(BPFTOOL) gen object $(<:.o=.linked3.o) $(<:.o=.linked2.o)
+ $(Q)diff $(<:.o=.linked2.o) $(<:.o=.linked3.o)
+ $(Q)$(BPFTOOL) gen skeleton $(<:.o=.linked3.o) name $(subst .bpf.skel.h,,$(sched)) > $@
+ $(Q)$(BPFTOOL) gen subskeleton $(<:.o=.linked3.o) name $(subst .bpf.skel.h,,$(sched)) > $(@:.skel.h=.subskel.h)
+
+SCX_COMMON_DEPS := include/scx/common.h include/scx/user_exit_info.h | $(BINDIR)
+
+c-sched-targets = scx_simple scx_cpu0 scx_qmap scx_central scx_flatcg scx_userland scx_pair scx_sdt
+
+$(addprefix $(BINDIR)/,$(c-sched-targets)): \
+ $(BINDIR)/%: \
+ $(filter-out %.bpf.c,%.c) \
+ $(INCLUDE_DIR)/%.bpf.skel.h \
+ $(SCX_COMMON_DEPS)
+ $(eval sched=$(notdir $@))
+ $(CC) $(CFLAGS) -c $(sched).c -o $(SCXOBJ_DIR)/$(sched).o
+ $(CC) -o $@ $(SCXOBJ_DIR)/$(sched).o $(BPFOBJ) $(LDFLAGS)
+
+$(c-sched-targets): %: $(BINDIR)/%
+
+install: all
+ $(Q)mkdir -p $(DESTDIR)/usr/local/bin/
+ $(Q)cp $(BINDIR)/* $(DESTDIR)/usr/local/bin/
+
+clean:
+ rm -rf $(OUTPUT_DIR) $(HOST_OUTPUT_DIR)
+ rm -f *.o *.bpf.o *.bpf.skel.h *.bpf.subskel.h
+ rm -f $(c-sched-targets)
+
+help:
+ @echo 'Building targets'
+ @echo '================'
+ @echo ''
+ @echo ' all - Compile all schedulers'
+ @echo ''
+ @echo 'Alternatively, you may compile individual schedulers:'
+ @echo ''
+ @printf ' %s\n' $(c-sched-targets)
+ @echo ''
+ @echo 'For any scheduler build target, you may specify an alternative'
+ @echo 'build output path with the O= environment variable. For example:'
+ @echo ''
+ @echo ' O=/tmp/sched_ext make all'
+ @echo ''
+ @echo 'will compile all schedulers, and emit the build artifacts to'
+ @echo '/tmp/sched_ext/build.'
+ @echo ''
+ @echo ''
+ @echo 'Installing targets'
+ @echo '=================='
+ @echo ''
+ @echo ' install - Compile and install all schedulers to /usr/bin.'
+ @echo ' You may specify the DESTDIR= environment variable'
+ @echo ' to indicate a prefix for /usr/bin. For example:'
+ @echo ''
+ @echo ' DESTDIR=/tmp/sched_ext make install'
+ @echo ''
+ @echo ' will build the schedulers in CWD/build, and'
+ @echo ' install the schedulers to /tmp/sched_ext/usr/bin.'
+ @echo ''
+ @echo ''
+ @echo 'Cleaning targets'
+ @echo '================'
+ @echo ''
+ @echo ' clean - Remove all generated files'
+
+all_targets: $(c-sched-targets)
+
+.PHONY: all all_targets $(c-sched-targets) clean help
+
+# delete failed targets
+.DELETE_ON_ERROR:
+
+# keep intermediate (.bpf.skel.h, .bpf.o, etc) targets
+.SECONDARY:
diff --git a/tools/sched_ext/README.md b/tools/sched_ext/README.md
new file mode 100644
index 000000000..0ee5a3d99
--- /dev/null
+++ b/tools/sched_ext/README.md
@@ -0,0 +1,263 @@
+SCHED_EXT EXAMPLE SCHEDULERS
+============================
+
+# Introduction
+
+This directory contains a number of example sched_ext schedulers. These
+schedulers are meant to provide examples of different types of schedulers
+that can be built using sched_ext, and illustrate how various features of
+sched_ext can be used.
+
+Some of the examples are performant, production-ready schedulers. That is, for
+the correct workload and with the correct tuning, they may be deployed in a
+production environment with acceptable or possibly even improved performance.
+Others are just examples that in practice, would not provide acceptable
+performance (though they could be improved to get there).
+
+This README will describe these example schedulers, including describing the
+types of workloads or scenarios they're designed to accommodate, and whether or
+not they're production ready. For more details on any of these schedulers,
+please see the header comment in their .bpf.c file.
+
+
+# Compiling the examples
+
+There are a few toolchain dependencies for compiling the example schedulers.
+
+## Toolchain dependencies
+
+1. clang >= 16.0.0
+
+The schedulers are BPF programs, and therefore must be compiled with clang. gcc
+is actively working on adding a BPF backend compiler as well, but are still
+missing some features such as BTF type tags which are necessary for using
+kptrs.
+
+2. pahole >= 1.25
+
+You may need pahole in order to generate BTF from DWARF.
+
+3. rust >= 1.70.0
+
+Rust schedulers uses features present in the rust toolchain >= 1.70.0. You
+should be able to use the stable build from rustup, but if that doesn't
+work, try using the rustup nightly build.
+
+There are other requirements as well, such as make, but these are the main /
+non-trivial ones.
+
+## Compiling the kernel
+
+In order to run a sched_ext scheduler, you'll have to run a kernel compiled
+with the patches in this repository, and with a minimum set of necessary
+Kconfig options:
+
+```
+CONFIG_BPF=y
+CONFIG_SCHED_CLASS_EXT=y
+CONFIG_BPF_SYSCALL=y
+CONFIG_BPF_JIT=y
+CONFIG_DEBUG_INFO_BTF=y
+CONFIG_BPF_JIT_ALWAYS_ON=y
+CONFIG_BPF_JIT_DEFAULT_ON=y
+```
+
+There is a `Kconfig` file in this directory whose contents you can append to
+your local `.config` file, as long as there are no conflicts with any existing
+options in the file.
+
+## Getting a vmlinux.h file
+
+You may notice that most of the example schedulers include a "vmlinux.h" file.
+This is a large, auto-generated header file that contains all of the types
+defined in some vmlinux binary that was compiled with
+[BTF](https://docs.kernel.org/bpf/btf.html) (i.e. with the BTF-related Kconfig
+options specified above).
+
+The header file is created using `bpftool`, by passing it a vmlinux binary
+compiled with BTF as follows:
+
+```bash
+$ bpftool btf dump file /path/to/vmlinux format c > vmlinux.h
+```
+
+`bpftool` analyzes all of the BTF encodings in the binary, and produces a
+header file that can be included by BPF programs to access those types. For
+example, using vmlinux.h allows a scheduler to access fields defined directly
+in vmlinux as follows:
+
+```c
+#include "vmlinux.h"
+// vmlinux.h is also implicitly included by scx_common.bpf.h.
+#include "scx_common.bpf.h"
+
+/*
+ * vmlinux.h provides definitions for struct task_struct and
+ * struct scx_enable_args.
+ */
+void BPF_STRUCT_OPS(example_enable, struct task_struct *p,
+ struct scx_enable_args *args)
+{
+ bpf_printk("Task %s enabled in example scheduler", p->comm);
+}
+
+// vmlinux.h provides the definition for struct sched_ext_ops.
+SEC(".struct_ops.link")
+struct sched_ext_ops example_ops {
+ .enable = (void *)example_enable,
+ .name = "example",
+}
+```
+
+The scheduler build system will generate this vmlinux.h file as part of the
+scheduler build pipeline. It looks for a vmlinux file in the following
+dependency order:
+
+1. If the O= environment variable is defined, at `$O/vmlinux`
+2. If the KBUILD_OUTPUT= environment variable is defined, at
+ `$KBUILD_OUTPUT/vmlinux`
+3. At `../../vmlinux` (i.e. at the root of the kernel tree where you're
+ compiling the schedulers)
+3. `/sys/kernel/btf/vmlinux`
+4. `/boot/vmlinux-$(uname -r)`
+
+In other words, if you have compiled a kernel in your local repo, its vmlinux
+file will be used to generate vmlinux.h. Otherwise, it will be the vmlinux of
+the kernel you're currently running on. This means that if you're running on a
+kernel with sched_ext support, you may not need to compile a local kernel at
+all.
+
+### Aside on CO-RE
+
+One of the cooler features of BPF is that it supports
+[CO-RE](https://nakryiko.com/posts/bpf-core-reference-guide/) (Compile Once Run
+Everywhere). This feature allows you to reference fields inside of structs with
+types defined internal to the kernel, and not have to recompile if you load the
+BPF program on a different kernel with the field at a different offset. In our
+example above, we print out a task name with `p->comm`. CO-RE would perform
+relocations for that access when the program is loaded to ensure that it's
+referencing the correct offset for the currently running kernel.
+
+## Compiling the schedulers
+
+Once you have your toolchain setup, and a vmlinux that can be used to generate
+a full vmlinux.h file, you can compile the schedulers using `make`:
+
+```bash
+$ make -j($nproc)
+```
+
+# Example schedulers
+
+This directory contains the following example schedulers. These schedulers are
+for testing and demonstrating different aspects of sched_ext. While some may be
+useful in limited scenarios, they are not intended to be practical.
+
+For more scheduler implementations, tools and documentation, visit
+https://github.com/sched-ext/scx.
+
+## scx_simple
+
+A simple scheduler that provides an example of a minimal sched_ext scheduler.
+scx_simple can be run in either global weighted vtime mode, or FIFO mode.
+
+Though very simple, in limited scenarios, this scheduler can perform reasonably
+well on single-socket systems with a unified L3 cache.
+
+## scx_qmap
+
+Another simple, yet slightly more complex scheduler that provides an example of
+a basic weighted FIFO queuing policy. It also provides examples of some common
+useful BPF features, such as arena-backed doubly-linked lists threaded through
+per-task context and `bpf_res_spin_lock` for per-queue synchronization. It also
+illustrates how core-sched support could be implemented.
+
+## scx_central
+
+A "central" scheduler where scheduling decisions are made from a single CPU.
+This scheduler illustrates how scheduling decisions can be dispatched from a
+single CPU, allowing other cores to run with infinite slices, without timer
+ticks, and without having to incur the overhead of making scheduling decisions.
+
+The approach demonstrated by this scheduler may be useful for any workload that
+benefits from minimizing scheduling overhead and timer ticks. An example of
+where this could be particularly useful is running VMs, where running with
+infinite slices and no timer ticks allows the VM to avoid unnecessary expensive
+vmexits.
+
+## scx_flatcg
+
+A flattened cgroup hierarchy scheduler. This scheduler implements hierarchical
+weight-based cgroup CPU control by flattening the cgroup hierarchy into a single
+layer, by compounding the active weight share at each level. The effect of this
+is a much more performant CPU controller, which does not need to descend down
+cgroup trees in order to properly compute a cgroup's share.
+
+Similar to scx_simple, in limited scenarios, this scheduler can perform
+reasonably well on single socket-socket systems with a unified L3 cache and show
+significantly lowered hierarchical scheduling overhead.
+
+
+# Troubleshooting
+
+There are a number of common issues that you may run into when building the
+schedulers. We'll go over some of the common ones here.
+
+## Build Failures
+
+### Old version of clang
+
+```
+error: static assertion failed due to requirement 'SCX_DSQ_FLAG_BUILTIN': bpftool generated vmlinux.h is missing high bits for 64bit enums, upgrade clang and pahole
+ _Static_assert(SCX_DSQ_FLAG_BUILTIN,
+ ^~~~~~~~~~~~~~~~~~~~
+1 error generated.
+```
+
+This means you built the kernel or the schedulers with an older version of
+clang than what's supported (i.e. older than 16.0.0). To remediate this:
+
+1. `which clang` to make sure you're using a sufficiently new version of clang.
+
+2. `make fullclean` in the root path of the repository, and rebuild the kernel
+ and schedulers.
+
+3. Rebuild the kernel, and then your example schedulers.
+
+The schedulers are also cleaned if you invoke `make mrproper` in the root
+directory of the tree.
+
+### Stale kernel build / incomplete vmlinux.h file
+
+As described above, you'll need a `vmlinux.h` file that was generated from a
+vmlinux built with BTF, and with sched_ext support enabled. If you don't,
+you'll see errors such as the following which indicate that a type being
+referenced in a scheduler is unknown:
+
+```
+/path/to/sched_ext/tools/sched_ext/user_exit_info.h:25:23: note: forward declaration of 'struct scx_exit_info'
+
+const struct scx_exit_info *ei)
+
+^
+```
+
+In order to resolve this, please follow the steps above in
+[Getting a vmlinux.h file](#getting-a-vmlinuxh-file) in order to ensure your
+schedulers are using a vmlinux.h file that includes the requisite types.
+
+## Misc
+
+### llvm: [OFF]
+
+You may see the following output when building the schedulers:
+
+```
+Auto-detecting system features:
+... clang-bpf-co-re: [ on ]
+... llvm: [ OFF ]
+... libcap: [ on ]
+... libbfd: [ on ]
+```
+
+Seeing `llvm: [ OFF ]` here is not an issue. You can safely ignore.
diff --git a/tools/sched_ext/include/bpf-compat/gnu/stubs.h b/tools/sched_ext/include/bpf-compat/gnu/stubs.h
new file mode 100644
index 000000000..ad7d139ce
--- /dev/null
+++ b/tools/sched_ext/include/bpf-compat/gnu/stubs.h
@@ -0,0 +1,11 @@
+/*
+ * Dummy gnu/stubs.h. clang can end up including /usr/include/gnu/stubs.h when
+ * compiling BPF files although its content doesn't play any role. The file in
+ * turn includes stubs-64.h or stubs-32.h depending on whether __x86_64__ is
+ * defined. When compiling a BPF source, __x86_64__ isn't set and thus
+ * stubs-32.h is selected. However, the file is not there if the system doesn't
+ * have 32bit glibc devel package installed leading to a build failure.
+ *
+ * The problem is worked around by making this file available in the include
+ * search paths before the system one when building BPF.
+ */
diff --git a/tools/sched_ext/include/scx/bpf_arena_common.bpf.h b/tools/sched_ext/include/scx/bpf_arena_common.bpf.h
new file mode 100644
index 000000000..2043d6694
--- /dev/null
+++ b/tools/sched_ext/include/scx/bpf_arena_common.bpf.h
@@ -0,0 +1,179 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/* Copyright (c) 2024 Meta Platforms, Inc. and affiliates. */
+#pragma once
+
+#ifndef PAGE_SIZE
+#define PAGE_SIZE __PAGE_SIZE
+/*
+ * for older kernels try sizeof(struct genradix_node)
+ * or flexible:
+ * static inline long __bpf_page_size(void) {
+ * return bpf_core_enum_value(enum page_size_enum___l, __PAGE_SIZE___l) ?: sizeof(struct genradix_node);
+ * }
+ * but generated code is not great.
+ */
+#endif
+
+#if defined(__BPF_FEATURE_ADDR_SPACE_CAST) && !defined(BPF_ARENA_FORCE_ASM)
+#ifndef __arena
+#define __arena __attribute__((address_space(1)))
+#endif
+#define __arena_global __attribute__((address_space(1)))
+#define cast_kern(ptr) /* nop for bpf prog. emitted by LLVM */
+#define cast_user(ptr) /* nop for bpf prog. emitted by LLVM */
+#else
+
+/* emit instruction:
+ * rX = rX .off = BPF_ADDR_SPACE_CAST .imm32 = (dst_as << 16) | src_as
+ *
+ * This is a workaround for LLVM compiler versions without
+ * __BPF_FEATURE_ADDR_SPACE_CAST that do not automatically cast between arena
+ * pointers and native kernel/userspace ones. In this case we explicitly do so
+ * with cast_kern() and cast_user(). E.g., in the Linux kernel tree,
+ * tools/testing/selftests/bpf includes tests that use these macros to implement
+ * linked lists and hashtables backed by arena memory. In sched_ext, we use
+ * cast_kern() and cast_user() for compatibility with older LLVM toolchains.
+ */
+#ifndef bpf_addr_space_cast
+#define bpf_addr_space_cast(var, dst_as, src_as)\
+ asm volatile(".byte 0xBF; \
+ .ifc %[reg], r0; \
+ .byte 0x00; \
+ .endif; \
+ .ifc %[reg], r1; \
+ .byte 0x11; \
+ .endif; \
+ .ifc %[reg], r2; \
+ .byte 0x22; \
+ .endif; \
+ .ifc %[reg], r3; \
+ .byte 0x33; \
+ .endif; \
+ .ifc %[reg], r4; \
+ .byte 0x44; \
+ .endif; \
+ .ifc %[reg], r5; \
+ .byte 0x55; \
+ .endif; \
+ .ifc %[reg], r6; \
+ .byte 0x66; \
+ .endif; \
+ .ifc %[reg], r7; \
+ .byte 0x77; \
+ .endif; \
+ .ifc %[reg], r8; \
+ .byte 0x88; \
+ .endif; \
+ .ifc %[reg], r9; \
+ .byte 0x99; \
+ .endif; \
+ .short %[off]; \
+ .long %[as]" \
+ : [reg]"+r"(var) \
+ : [off]"i"(BPF_ADDR_SPACE_CAST) \
+ , [as]"i"((dst_as << 16) | src_as));
+#endif
+
+#define __arena
+#define __arena_global SEC(".addr_space.1")
+#define cast_kern(ptr) bpf_addr_space_cast(ptr, 0, 1)
+#define cast_user(ptr) bpf_addr_space_cast(ptr, 1, 0)
+#endif
+
+void __arena* bpf_arena_alloc_pages(void *map, void __arena *addr, __u32 page_cnt,
+ int node_id, __u64 flags) __ksym __weak;
+void bpf_arena_free_pages(void *map, void __arena *ptr, __u32 page_cnt) __ksym __weak;
+int bpf_arena_reserve_pages(void *map, void __arena *ptr, __u32 page_cnt) __ksym __weak;
+
+/*
+ * Note that cond_break can only be portably used in the body of a breakable
+ * construct, whereas can_loop can be used anywhere.
+ */
+#ifdef SCX_BPF_UNITTEST
+#define can_loop true
+#define __cond_break(expr) expr
+#else
+#ifdef __BPF_FEATURE_MAY_GOTO
+#define can_loop \
+ ({ __label__ l_break, l_continue; \
+ bool ret = true; \
+ asm volatile goto("may_goto %l[l_break]" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: ret = false; \
+ l_continue:; \
+ ret; \
+ })
+
+#define __cond_break(expr) \
+ ({ __label__ l_break, l_continue; \
+ asm volatile goto("may_goto %l[l_break]" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: expr; \
+ l_continue:; \
+ })
+#else
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define can_loop \
+ ({ __label__ l_break, l_continue; \
+ bool ret = true; \
+ asm volatile goto("1:.byte 0xe5; \
+ .byte 0; \
+ .long ((%l[l_break] - 1b - 8) / 8) & 0xffff; \
+ .short 0" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: ret = false; \
+ l_continue:; \
+ ret; \
+ })
+
+#define __cond_break(expr) \
+ ({ __label__ l_break, l_continue; \
+ asm volatile goto("1:.byte 0xe5; \
+ .byte 0; \
+ .long ((%l[l_break] - 1b - 8) / 8) & 0xffff; \
+ .short 0" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: expr; \
+ l_continue:; \
+ })
+#else
+#define can_loop \
+ ({ __label__ l_break, l_continue; \
+ bool ret = true; \
+ asm volatile goto("1:.byte 0xe5; \
+ .byte 0; \
+ .long (((%l[l_break] - 1b - 8) / 8) & 0xffff) << 16; \
+ .short 0" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: ret = false; \
+ l_continue:; \
+ ret; \
+ })
+
+#define __cond_break(expr) \
+ ({ __label__ l_break, l_continue; \
+ asm volatile goto("1:.byte 0xe5; \
+ .byte 0; \
+ .long (((%l[l_break] - 1b - 8) / 8) & 0xffff) << 16; \
+ .short 0" \
+ :::: l_break); \
+ goto l_continue; \
+ l_break: expr; \
+ l_continue:; \
+ })
+#endif /* __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__ */
+#endif /* __BPF_FEATURE_MAY_GOTO */
+#endif /* SCX_BPF_UNITTEST */
+
+#define cond_break __cond_break(break)
+#define cond_break_label(label) __cond_break(goto label)
+
+
+void bpf_preempt_disable(void) __weak __ksym;
+void bpf_preempt_enable(void) __weak __ksym;
+ssize_t bpf_arena_mapping_nr_pages(void *p__map) __weak __ksym;
diff --git a/tools/sched_ext/include/scx/bpf_arena_common.h b/tools/sched_ext/include/scx/bpf_arena_common.h
new file mode 100644
index 000000000..10141db0b
--- /dev/null
+++ b/tools/sched_ext/include/scx/bpf_arena_common.h
@@ -0,0 +1,33 @@
+/* SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause) */
+/* Copyright (c) 2024 Meta Platforms, Inc. and affiliates. */
+#pragma once
+
+#ifndef arena_container_of
+#define arena_container_of(ptr, type, member) \
+ ({ \
+ void __arena *__mptr = (void __arena *)(ptr); \
+ ((type *)(__mptr - offsetof(type, member))); \
+ })
+#endif
+
+/* Provide the definition of PAGE_SIZE. */
+#include <sys/user.h>
+
+#define __arena
+#define __arg_arena
+#define cast_kern(ptr) /* nop for user space */
+#define cast_user(ptr) /* nop for user space */
+char __attribute__((weak)) arena[1];
+
+#ifndef offsetof
+#define offsetof(type, member) ((unsigned long)&((type *)0)->member)
+#endif
+
+static inline void __arena* bpf_arena_alloc_pages(void *map, void *addr, __u32 page_cnt,
+ int node_id, __u64 flags)
+{
+ return NULL;
+}
+static inline void bpf_arena_free_pages(void *map, void __arena *ptr, __u32 page_cnt)
+{
+}
diff --git a/tools/sched_ext/include/scx/cid.bpf.h b/tools/sched_ext/include/scx/cid.bpf.h
new file mode 100644
index 000000000..69fb4e97b
--- /dev/null
+++ b/tools/sched_ext/include/scx/cid.bpf.h
@@ -0,0 +1,777 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * BPF-side helpers for cids and cmasks. See kernel/sched/ext/cid.h for the
+ * authoritative layout and semantics. The BPF-side helpers use the cmask_*
+ * naming (no scx_ prefix); cmask is the SCX bitmap type so the prefix is
+ * redundant in BPF code. Atomics use __sync_val_compare_and_swap and every
+ * helper is inline (no .c counterpart).
+ *
+ * Included by scx/common.bpf.h; don't include directly.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2026 Tejun Heo <tj@kernel.org>
+ */
+#ifndef __SCX_CID_BPF_H
+#define __SCX_CID_BPF_H
+
+#include "bpf_arena_common.bpf.h"
+
+#ifndef BIT_U64
+#define BIT_U64(nr) (1ULL << (nr))
+#endif
+#ifndef GENMASK_U64
+#define GENMASK_U64(h, l) ((~0ULL << (l)) & (~0ULL >> (63 - (h))))
+#endif
+
+/*
+ * Storage cap for bounded loops over bits[]. Sized to cover NR_CPUS=8192 with
+ * one extra word for head-misalignment. Increase if deployment targets larger
+ * NR_CPUS.
+ */
+#ifndef CMASK_MAX_WORDS
+#define CMASK_MAX_WORDS 129
+#endif
+
+/*
+ * Mirrors SCX_CMASK_NR_WORDS in kernel/sched/ext/types.h. The u64 cast keeps
+ * the +63 from wrapping when @nr_cids is near U32_MAX, so cmask_reframe()
+ * bounds-checking the result against alloc_words catches the overflow instead
+ * of seeing a small value.
+ */
+#define CMASK_NR_WORDS(nr_cids) ((u32)(((u64)(nr_cids) + 63) / 64 + 1))
+
+static __always_inline bool __cmask_contains(u32 cid, const struct scx_cmask __arena *m)
+{
+ return cid >= m->base && cid < m->base + m->nr_cids;
+}
+
+static __always_inline u64 __arena *__cmask_word(u32 cid, const struct scx_cmask __arena *m)
+{
+ return (u64 __arena *)&m->bits[cid / 64 - m->base / 64];
+}
+
+/**
+ * __cmask_init - Initialize @m with explicit storage capacity
+ * @m: cmask to initialize
+ * @base: first cid of the active range
+ * @nr_cids: number of cids in the active range
+ * @alloc_cids: storage capacity in cids, at least @nr_cids
+ *
+ * Use when storage is sized larger than the initial active range. All of
+ * bits[] is zeroed.
+ */
+static __always_inline void __cmask_init(struct scx_cmask __arena *m, u32 base,
+ u32 nr_cids, u32 alloc_cids)
+{
+ u32 alloc_words, i;
+
+ if (unlikely(nr_cids > alloc_cids)) {
+ scx_bpf_error("__cmask_init: nr_cids=%u exceeds alloc_cids=%u",
+ nr_cids, alloc_cids);
+ return;
+ }
+ alloc_words = CMASK_NR_WORDS(alloc_cids);
+
+ m->base = base;
+ m->nr_cids = nr_cids;
+ m->alloc_words = alloc_words;
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ if (i >= alloc_words)
+ break;
+ m->bits[i] = 0;
+ }
+}
+
+/**
+ * cmask_init - Initialize @m on tight storage
+ * @m: cmask to initialize
+ * @base: first cid of the active range
+ * @nr_cids: number of cids in the active range
+ *
+ * All of bits[] is zeroed.
+ */
+static __always_inline void cmask_init(struct scx_cmask __arena *m, u32 base, u32 nr_cids)
+{
+ __cmask_init(m, base, nr_cids, nr_cids);
+}
+
+/**
+ * cmask_reframe - Reshape @m's active range without resizing storage
+ * @m: cmask to reframe
+ * @base: new active range base
+ * @nr_cids: new active range length, must fit within @m->alloc_words
+ *
+ * Body bits within the new range become garbage - only the head and tail
+ * words are zeroed to keep the padding invariant.
+ */
+static __always_inline void cmask_reframe(struct scx_cmask __arena *m, u32 base, u32 nr_cids)
+{
+ if (CMASK_NR_WORDS(nr_cids) > m->alloc_words) {
+ scx_bpf_error("cmask_reframe: nr_cids=%u exceeds alloc_words=%u",
+ nr_cids, m->alloc_words);
+ return;
+ }
+ if (nr_cids) {
+ u32 last_word = ((base & 63) + nr_cids - 1) / 64;
+
+ m->bits[0] = 0;
+ m->bits[last_word] = 0;
+ }
+ m->base = base;
+ m->nr_cids = nr_cids;
+}
+
+static __always_inline bool cmask_test(u32 cid, const struct scx_cmask __arena *m)
+{
+ if (!__cmask_contains(cid, m))
+ return false;
+ return *__cmask_word(cid, m) & BIT_U64(cid & 63);
+}
+
+/*
+ * x86 BPF JIT rejects BPF_OR | BPF_FETCH and BPF_AND | BPF_FETCH on arena
+ * pointers (see bpf_jit_supports_insn() in arch/x86/net/bpf_jit_comp.c). Only
+ * BPF_CMPXCHG / BPF_XCHG / BPF_ADD with FETCH are allowed. Implement
+ * test_and_{set,clear} and the atomic set/clear via a cmpxchg loop.
+ *
+ * CMASK_CAS_TRIES is sized so exhausting it means seconds of real spinning
+ * on one word - past any plausible contention. Abort hard.
+ */
+#define CMASK_CAS_TRIES (1U << 23)
+
+static __always_inline void cmask_set(u32 cid, struct scx_cmask __arena *m)
+{
+ u64 __arena *w;
+ u64 bit, old, new;
+ u32 i;
+
+ if (!__cmask_contains(cid, m))
+ return;
+ w = __cmask_word(cid, m);
+ bit = BIT_U64(cid & 63);
+ bpf_for(i, 0, CMASK_CAS_TRIES) {
+ old = *w;
+ if (old & bit)
+ return;
+ new = old | bit;
+ if (__sync_val_compare_and_swap(w, old, new) == old)
+ return;
+ }
+ scx_bpf_error("cmask_set CAS exhausted at cid %u", cid);
+}
+
+static __always_inline void cmask_clear(u32 cid, struct scx_cmask __arena *m)
+{
+ u64 __arena *w;
+ u64 bit, old, new;
+ u32 i;
+
+ if (!__cmask_contains(cid, m))
+ return;
+ w = __cmask_word(cid, m);
+ bit = BIT_U64(cid & 63);
+ bpf_for(i, 0, CMASK_CAS_TRIES) {
+ old = *w;
+ if (!(old & bit))
+ return;
+ new = old & ~bit;
+ if (__sync_val_compare_and_swap(w, old, new) == old)
+ return;
+ }
+ scx_bpf_error("cmask_clear CAS exhausted at cid %u", cid);
+}
+
+static __always_inline bool cmask_test_and_set(u32 cid, struct scx_cmask __arena *m)
+{
+ u64 __arena *w;
+ u64 bit, old, new;
+ u32 i;
+
+ if (!__cmask_contains(cid, m))
+ return false;
+ w = __cmask_word(cid, m);
+ bit = BIT_U64(cid & 63);
+ bpf_for(i, 0, CMASK_CAS_TRIES) {
+ old = *w;
+ if (old & bit)
+ return true;
+ new = old | bit;
+ if (__sync_val_compare_and_swap(w, old, new) == old)
+ return false;
+ }
+ scx_bpf_error("cmask_test_and_set CAS exhausted at cid %u", cid);
+ return false;
+}
+
+static __always_inline bool cmask_test_and_clear(u32 cid, struct scx_cmask __arena *m)
+{
+ u64 __arena *w;
+ u64 bit, old, new;
+ u32 i;
+
+ if (!__cmask_contains(cid, m))
+ return false;
+ w = __cmask_word(cid, m);
+ bit = BIT_U64(cid & 63);
+ bpf_for(i, 0, CMASK_CAS_TRIES) {
+ old = *w;
+ if (!(old & bit))
+ return false;
+ new = old & ~bit;
+ if (__sync_val_compare_and_swap(w, old, new) == old)
+ return true;
+ }
+ scx_bpf_error("cmask_test_and_clear CAS exhausted at cid %u", cid);
+ return false;
+}
+
+static __always_inline void __cmask_set(u32 cid, struct scx_cmask __arena *m)
+{
+ if (!__cmask_contains(cid, m))
+ return;
+ *__cmask_word(cid, m) |= BIT_U64(cid & 63);
+}
+
+static __always_inline void __cmask_clear(u32 cid, struct scx_cmask __arena *m)
+{
+ if (!__cmask_contains(cid, m))
+ return;
+ *__cmask_word(cid, m) &= ~BIT_U64(cid & 63);
+}
+
+static __always_inline bool __cmask_test_and_set(u32 cid, struct scx_cmask __arena *m)
+{
+ u64 bit = BIT_U64(cid & 63);
+ u64 __arena *w;
+ u64 prev;
+
+ if (!__cmask_contains(cid, m))
+ return false;
+ w = __cmask_word(cid, m);
+ prev = *w & bit;
+ *w |= bit;
+ return prev;
+}
+
+static __always_inline bool __cmask_test_and_clear(u32 cid, struct scx_cmask __arena *m)
+{
+ u64 bit = BIT_U64(cid & 63);
+ u64 __arena *w;
+ u64 prev;
+
+ if (!__cmask_contains(cid, m))
+ return false;
+ w = __cmask_word(cid, m);
+ prev = *w & bit;
+ *w &= ~bit;
+ return prev;
+}
+
+static __always_inline void cmask_zero(struct scx_cmask __arena *m)
+{
+ u32 nr_words = CMASK_NR_WORDS(m->nr_cids), i;
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ if (i >= nr_words)
+ break;
+ m->bits[i] = 0;
+ }
+}
+
+/*
+ * BPF_-prefixed to avoid colliding with the kernel's anonymous CMASK_OP_*
+ * enum in ext/cid.c, which is exported via BTF and reachable through
+ * vmlinux.h.
+ */
+enum {
+ BPF_CMASK_OP_AND,
+ BPF_CMASK_OP_OR,
+ BPF_CMASK_OP_COPY,
+ BPF_CMASK_OP_ANDNOT,
+};
+
+static __always_inline void cmask_op_word(struct scx_cmask __arena *dst,
+ const struct scx_cmask __arena *src,
+ u32 di, u32 si, u64 mask, int op)
+{
+ u64 dv = dst->bits[di];
+ u64 sv = src->bits[si];
+ u64 rv;
+
+ if (op == BPF_CMASK_OP_AND)
+ rv = dv & sv;
+ else if (op == BPF_CMASK_OP_OR)
+ rv = dv | sv;
+ else if (op == BPF_CMASK_OP_ANDNOT)
+ rv = dv & ~sv;
+ else
+ rv = sv;
+
+ dst->bits[di] = (dv & ~mask) | (rv & mask);
+}
+
+static __always_inline void cmask_op(struct scx_cmask __arena *dst,
+ const struct scx_cmask __arena *src, int op)
+{
+ u32 d_end = dst->base + dst->nr_cids;
+ u32 s_end = src->base + src->nr_cids;
+ u32 lo = dst->base > src->base ? dst->base : src->base;
+ u32 hi = d_end < s_end ? d_end : s_end;
+ u32 d_base = dst->base / 64;
+ u32 s_base = src->base / 64;
+ u32 lo_word, hi_word, i;
+ u64 head_mask, tail_mask;
+
+ if (lo >= hi)
+ return;
+
+ lo_word = lo / 64;
+ hi_word = (hi - 1) / 64;
+ head_mask = GENMASK_U64(63, lo & 63);
+ tail_mask = GENMASK_U64((hi - 1) & 63, 0);
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ u32 w = lo_word + i;
+ u64 m;
+
+ if (w > hi_word)
+ break;
+
+ m = GENMASK_U64(63, 0);
+ if (w == lo_word)
+ m &= head_mask;
+ if (w == hi_word)
+ m &= tail_mask;
+
+ cmask_op_word(dst, src, w - d_base, w - s_base, m, op);
+ }
+}
+
+/*
+ * cmask_and/or/copy only modify @dst bits that lie in the intersection of
+ * [@dst->base, @dst->base + @dst->nr_cids) and [@src->base,
+ * @src->base + @src->nr_cids). Bits in @dst outside that window
+ * keep their prior values - in particular, cmask_copy() does NOT zero @dst
+ * bits that lie outside @src's range.
+ */
+static __always_inline void cmask_and(struct scx_cmask __arena *dst,
+ const struct scx_cmask __arena *src)
+{
+ cmask_op(dst, src, BPF_CMASK_OP_AND);
+}
+
+static __always_inline void cmask_or(struct scx_cmask __arena *dst,
+ const struct scx_cmask __arena *src)
+{
+ cmask_op(dst, src, BPF_CMASK_OP_OR);
+}
+
+static __always_inline void cmask_copy(struct scx_cmask __arena *dst,
+ const struct scx_cmask __arena *src)
+{
+ cmask_op(dst, src, BPF_CMASK_OP_COPY);
+}
+
+static __always_inline void cmask_andnot(struct scx_cmask __arena *dst,
+ const struct scx_cmask __arena *src)
+{
+ cmask_op(dst, src, BPF_CMASK_OP_ANDNOT);
+}
+
+/*
+ * True iff @a and @b have identical bits over their (assumed equal) range.
+ * Callers are expected to pass same-shape cmasks; differing shapes always
+ * compare unequal.
+ */
+static __always_inline bool cmask_equal(const struct scx_cmask __arena *a,
+ const struct scx_cmask __arena *b)
+{
+ u32 nr_words, i;
+
+ if (a->base != b->base || a->nr_cids != b->nr_cids)
+ return false;
+ if (a->nr_cids == 0)
+ return true;
+ nr_words = (a->base + a->nr_cids - 1) / 64 - a->base / 64 + 1;
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ if (i >= nr_words)
+ break;
+ if (a->bits[i] != b->bits[i])
+ return false;
+ }
+ return true;
+}
+
+/**
+ * cmask_next_set - find the first set bit at or after @cid
+ * @m: cmask to search
+ * @cid: starting cid (clamped to @m->base if below)
+ *
+ * Returns the smallest set cid in [@cid, @m->base + @m->nr_cids), or
+ * @m->base + @m->nr_cids if none (the out-of-range sentinel matches the
+ * termination condition used by cmask_for_each()).
+ */
+static __always_inline u32 cmask_next_set(const struct scx_cmask __arena *m, u32 cid)
+{
+ u32 end = m->base + m->nr_cids;
+ u32 base = m->base / 64;
+ u32 last_wi = (end - 1) / 64 - base;
+ u32 start_wi, start_bit, i;
+
+ if (cid < m->base)
+ cid = m->base;
+ if (cid >= end)
+ return end;
+
+ start_wi = cid / 64 - base;
+ start_bit = cid & 63;
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ u32 wi = start_wi + i;
+ u64 word;
+ u32 found;
+
+ if (wi > last_wi)
+ break;
+
+ word = m->bits[wi];
+ if (i == 0)
+ word &= GENMASK_U64(63, start_bit);
+ if (!word)
+ continue;
+
+ found = (base + wi) * 64 + ctzll(word);
+ if (found >= end)
+ return end;
+ return found;
+ }
+ return end;
+}
+
+static __always_inline u32 cmask_first_set(const struct scx_cmask __arena *m)
+{
+ return cmask_next_set(m, m->base);
+}
+
+#define cmask_for_each(cid, m) \
+ for ((cid) = cmask_first_set(m); \
+ (cid) < (m)->base + (m)->nr_cids; \
+ (cid) = cmask_next_set((m), (cid) + 1))
+
+/*
+ * True iff every bit set in @a is also set in @b. Matches the kernel-side
+ * scx_cmask_subset(): ranges don't need to nest, and set bits of @a outside
+ * @b's range fail the test.
+ */
+static __always_inline bool cmask_subset(const struct scx_cmask __arena *a,
+ const struct scx_cmask __arena *b)
+{
+ u32 a_end = a->base + a->nr_cids;
+ u32 b_end = b->base + b->nr_cids;
+ u32 a_wbase = a->base / 64;
+ u32 b_wbase = b->base / 64;
+ u32 lo = a->base > b->base ? a->base : b->base;
+ u32 hi = a_end < b_end ? a_end : b_end;
+ u32 lo_word, hi_word, i;
+
+ /* set bits of @a outside @b's range can't be in @b */
+ if (a->base < b->base &&
+ cmask_next_set(a, a->base) < (b->base < a_end ? b->base : a_end))
+ return false;
+ if (a_end > b_end &&
+ cmask_next_set(a, a->base > b_end ? a->base : b_end) < a_end)
+ return false;
+
+ if (lo >= hi)
+ return true;
+
+ /*
+ * Walk the words the range intersection spans. Plain word tests
+ * suffice: the scans above guarantee @a has no set bit outside @b's
+ * range and padding bits are kept clear by all cmask helpers.
+ */
+ lo_word = lo / 64;
+ hi_word = (hi - 1) / 64;
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ u32 w = lo_word + i;
+
+ if (w > hi_word)
+ break;
+ if (a->bits[w - a_wbase] & ~b->bits[w - b_wbase])
+ return false;
+ }
+ return true;
+}
+
+/*
+ * Population count over [base, base + nr_cids). Padding bits in the head/tail
+ * words are guaranteed zero by the mutating helpers, so a flat popcount over
+ * the words the range spans is correct.
+ */
+static __always_inline u32 cmask_weight(const struct scx_cmask __arena *m)
+{
+ u32 nr_words, i;
+ u32 count = 0;
+
+ if (!m->nr_cids)
+ return 0;
+ nr_words = (m->base + m->nr_cids - 1) / 64 - m->base / 64 + 1;
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ if (i >= nr_words)
+ break;
+ count += __builtin_popcountll(m->bits[i]);
+ }
+ return count;
+}
+
+/*
+ * True if @a and @b share any set bit. Walk only the intersection of their
+ * ranges, matching the semantics of cmask_and().
+ */
+static __always_inline bool cmask_intersects(const struct scx_cmask __arena *a,
+ const struct scx_cmask __arena *b)
+{
+ u32 a_end = a->base + a->nr_cids;
+ u32 b_end = b->base + b->nr_cids;
+ u32 lo = a->base > b->base ? a->base : b->base;
+ u32 hi = a_end < b_end ? a_end : b_end;
+ u32 a_base = a->base / 64;
+ u32 b_base = b->base / 64;
+ u32 lo_word, hi_word, i;
+ u64 head_mask, tail_mask;
+
+ if (lo >= hi)
+ return false;
+
+ lo_word = lo / 64;
+ hi_word = (hi - 1) / 64;
+ head_mask = GENMASK_U64(63, lo & 63);
+ tail_mask = GENMASK_U64((hi - 1) & 63, 0);
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ u32 w = lo_word + i;
+ u64 mask, av, bv;
+
+ if (w > hi_word)
+ break;
+
+ mask = GENMASK_U64(63, 0);
+ if (w == lo_word)
+ mask &= head_mask;
+ if (w == hi_word)
+ mask &= tail_mask;
+
+ av = a->bits[w - a_base] & mask;
+ bv = b->bits[w - b_base] & mask;
+ if (av & bv)
+ return true;
+ }
+ return false;
+}
+
+/*
+ * Find the next cid set in both @a and @b at or after @start, bounded by the
+ * intersection of the two ranges. Return a->base + a->nr_cids if none found.
+ *
+ * Building block for cmask_next_and_set_wrap(). Callers that want a bounded
+ * scan without wrap call this directly.
+ */
+static __always_inline u32 cmask_next_and_set(const struct scx_cmask __arena *a,
+ const struct scx_cmask __arena *b,
+ u32 start)
+{
+ u32 a_end = a->base + a->nr_cids;
+ u32 b_end = b->base + b->nr_cids;
+ u32 a_wbase = a->base / 64;
+ u32 b_wbase = b->base / 64;
+ u32 lo = a->base > b->base ? a->base : b->base;
+ u32 hi = a_end < b_end ? a_end : b_end;
+ u32 last_wi, start_wi, start_bit, i;
+
+ if (lo >= hi)
+ return a_end;
+ if (start < lo)
+ start = lo;
+ if (start >= hi)
+ return a_end;
+
+ last_wi = (hi - 1) / 64;
+ start_wi = start / 64;
+ start_bit = start & 63;
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ u32 abs_wi = start_wi + i;
+ u64 word;
+ u32 found;
+
+ if (abs_wi > last_wi)
+ break;
+
+ word = a->bits[abs_wi - a_wbase] & b->bits[abs_wi - b_wbase];
+ if (i == 0)
+ word &= GENMASK_U64(63, start_bit);
+ if (!word)
+ continue;
+
+ found = abs_wi * 64 + ctzll(word);
+ if (found >= hi)
+ return a_end;
+ return found;
+ }
+ return a_end;
+}
+
+/*
+ * Find the next set cid in @m at or after @start, wrapping to @m->base if no
+ * set bit is found in [start, m->base + m->nr_cids). Return m->base +
+ * m->nr_cids if @m is empty.
+ *
+ * Callers do round-robin distribution by passing (last_cid + 1) as @start.
+ */
+static __always_inline u32 cmask_next_set_wrap(const struct scx_cmask __arena *m,
+ u32 start)
+{
+ u32 end = m->base + m->nr_cids;
+ u32 found;
+
+ found = cmask_next_set(m, start);
+ if (found < end || start <= m->base)
+ return found;
+
+ found = cmask_next_set(m, m->base);
+ return found < start ? found : end;
+}
+
+/*
+ * Find the next cid set in both @a and @b at or after @start, wrapping to
+ * @a->base if none found in the forward half. Return a->base + a->nr_cids
+ * if the intersection is empty.
+ *
+ * Callers do round-robin distribution by passing (last_cid + 1) as @start.
+ */
+static __always_inline u32 cmask_next_and_set_wrap(const struct scx_cmask __arena *a,
+ const struct scx_cmask __arena *b,
+ u32 start)
+{
+ u32 a_end = a->base + a->nr_cids;
+ u32 found;
+
+ found = cmask_next_and_set(a, b, start);
+ if (found < a_end || start <= a->base)
+ return found;
+
+ found = cmask_next_and_set(a, b, a->base);
+ return found < start ? found : a_end;
+}
+
+/*
+ * Like cmask_next_and_set() but over the intersection of THREE masks. Return
+ * a->base + a->nr_cids if no cid is set in all three at or after @start.
+ */
+static __always_inline u32 cmask_next_and2_set(const struct scx_cmask __arena *a,
+ const struct scx_cmask __arena *b,
+ const struct scx_cmask __arena *c,
+ u32 start)
+{
+ u32 a_end = a->base + a->nr_cids;
+ u32 b_end = b->base + b->nr_cids;
+ u32 c_end = c->base + c->nr_cids;
+ u32 a_wbase = a->base / 64;
+ u32 b_wbase = b->base / 64;
+ u32 c_wbase = c->base / 64;
+ u32 lo = a->base > b->base ? a->base : b->base;
+ u32 hi = a_end < b_end ? a_end : b_end;
+ u32 last_wi, start_wi, start_bit, i;
+
+ lo = lo > c->base ? lo : c->base;
+ hi = hi < c_end ? hi : c_end;
+
+ if (lo >= hi)
+ return a_end;
+ if (start < lo)
+ start = lo;
+ if (start >= hi)
+ return a_end;
+
+ last_wi = (hi - 1) / 64;
+ start_wi = start / 64;
+ start_bit = start & 63;
+
+ bpf_for(i, 0, CMASK_MAX_WORDS) {
+ u32 abs_wi = start_wi + i;
+ u64 word;
+ u32 found;
+
+ if (abs_wi > last_wi)
+ break;
+
+ word = a->bits[abs_wi - a_wbase] & b->bits[abs_wi - b_wbase] &
+ c->bits[abs_wi - c_wbase];
+ if (i == 0)
+ word &= GENMASK_U64(63, start_bit);
+ if (!word)
+ continue;
+
+ found = abs_wi * 64 + ctzll(word);
+ if (found >= hi)
+ return a_end;
+ return found;
+ }
+ return a_end;
+}
+
+/*
+ * Round-robin variant of cmask_next_and2_set(): wrap to @a->base if the
+ * three-way intersection has no cid in the forward half. Return a->base +
+ * a->nr_cids if empty.
+ */
+static __always_inline u32 cmask_next_and2_set_wrap(const struct scx_cmask __arena *a,
+ const struct scx_cmask __arena *b,
+ const struct scx_cmask __arena *c,
+ u32 start)
+{
+ u32 a_end = a->base + a->nr_cids;
+ u32 found;
+
+ found = cmask_next_and2_set(a, b, c, start);
+ if (found < a_end || start <= a->base)
+ return found;
+
+ found = cmask_next_and2_set(a, b, c, a->base);
+ return found < start ? found : a_end;
+}
+
+/**
+ * cmask_from_cpumask - translate a kernel cpumask to a cid-space cmask
+ * @m: cmask to fill. Zeroed first; only bits within [@m->base, @m->base +
+ * @m->nr_cids) are updated - cpus mapping to cids outside that range
+ * are ignored.
+ * @cpumask: kernel cpumask to translate
+ *
+ * For each cpu in @cpumask, set the cpu's cid in @m. Caller must ensure
+ * @cpumask stays stable across the call (e.g. RCU read lock for
+ * task->cpus_ptr).
+ */
+static __always_inline void cmask_from_cpumask(struct scx_cmask __arena *m,
+ const struct cpumask *cpumask)
+{
+ u32 nr_cpu_ids = scx_bpf_nr_cpu_ids();
+ s32 cpu;
+
+ cmask_zero(m);
+ bpf_for(cpu, 0, nr_cpu_ids) {
+ s32 cid;
+
+ if (!bpf_cpumask_test_cpu(cpu, cpumask))
+ continue;
+ cid = scx_bpf_cpu_to_cid(cpu);
+ if (cid >= 0)
+ __cmask_set(cid, m);
+ }
+}
+
+#endif /* __SCX_CID_BPF_H */
diff --git a/tools/sched_ext/include/scx/common.bpf.h b/tools/sched_ext/include/scx/common.bpf.h
new file mode 100644
index 000000000..22f24ebef
--- /dev/null
+++ b/tools/sched_ext/include/scx/common.bpf.h
@@ -0,0 +1,1192 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#ifndef __SCX_COMMON_BPF_H
+#define __SCX_COMMON_BPF_H
+
+/*
+ * The generated kfunc prototypes in vmlinux.h are missing address space
+ * attributes which cause build failures. For now, suppress the generated
+ * prototypes. See https://github.com/sched-ext/scx/issues/1111.
+ */
+#define BPF_NO_KFUNC_PROTOTYPES
+
+#ifdef LSP
+#define __bpf__
+#include "../vmlinux.h"
+#else
+#include "vmlinux.h"
+#endif
+
+#include <bpf/bpf_helpers.h>
+#include <bpf/bpf_tracing.h>
+#include <asm-generic/errno.h>
+#include "user_exit_info.bpf.h"
+#include "enum_defs.autogen.h"
+#include "bpf_arena_common.bpf.h"
+
+#define PF_IDLE 0x00000002 /* I am an IDLE thread */
+#define PF_IO_WORKER 0x00000010 /* Task is an IO worker */
+#define PF_WQ_WORKER 0x00000020 /* I'm a workqueue worker */
+#define PF_KCOMPACTD 0x00010000 /* I am kcompactd */
+#define PF_KSWAPD 0x00020000 /* I am kswapd */
+#define PF_KTHREAD 0x00200000 /* I am a kernel thread */
+#define PF_EXITING 0x00000004
+#define CLOCK_MONOTONIC 1
+
+#ifndef NR_CPUS
+#define NR_CPUS 1024
+#endif
+
+#ifndef NUMA_NO_NODE
+#define NUMA_NO_NODE (-1)
+#endif
+
+extern int LINUX_KERNEL_VERSION __kconfig;
+extern const char CONFIG_CC_VERSION_TEXT[64] __kconfig __weak;
+extern const char CONFIG_LOCALVERSION[64] __kconfig __weak;
+extern bool CONFIG_PREEMPT_RCU __kconfig __weak;
+
+/*
+ * Earlier versions of clang/pahole lost upper 32bits in 64bit enums which can
+ * lead to really confusing misbehaviors. Let's trigger a build failure.
+ */
+static inline void ___vmlinux_h_sanity_check___(void)
+{
+ _Static_assert(SCX_DSQ_FLAG_BUILTIN,
+ "bpftool generated vmlinux.h is missing high bits for 64bit enums, upgrade clang and pahole");
+}
+
+s32 scx_bpf_create_dsq(u64 dsq_id, s32 node) __ksym;
+s32 scx_bpf_select_cpu_dfl(struct task_struct *p, s32 prev_cpu, u64 wake_flags, bool *is_idle) __ksym;
+s32 __scx_bpf_select_cpu_and(struct task_struct *p, const struct cpumask *cpus_allowed,
+ struct scx_bpf_select_cpu_and_args *args) __ksym __weak;
+bool __scx_bpf_dsq_insert_vtime(struct task_struct *p, struct scx_bpf_dsq_insert_vtime_args *args) __ksym __weak;
+u32 scx_bpf_dispatch_nr_slots(void) __ksym;
+void scx_bpf_dispatch_cancel(void) __ksym;
+void scx_bpf_kick_cpu(s32 cpu, u64 flags) __ksym;
+s32 scx_bpf_dsq_nr_queued(u64 dsq_id) __ksym;
+void scx_bpf_destroy_dsq(u64 dsq_id) __ksym;
+struct task_struct *scx_bpf_dsq_peek(u64 dsq_id) __ksym __weak;
+int bpf_iter_scx_dsq_new(struct bpf_iter_scx_dsq *it, u64 dsq_id, u64 flags) __ksym __weak;
+struct task_struct *bpf_iter_scx_dsq_next(struct bpf_iter_scx_dsq *it) __ksym __weak;
+void bpf_iter_scx_dsq_destroy(struct bpf_iter_scx_dsq *it) __ksym __weak;
+void scx_bpf_exit_bstr(s64 exit_code, char *fmt, unsigned long long *data, u32 data__sz) __ksym __weak;
+void scx_bpf_error_bstr(char *fmt, unsigned long long *data, u32 data_len) __ksym;
+void scx_bpf_dump_bstr(char *fmt, unsigned long long *data, u32 data_len) __ksym __weak;
+u32 scx_bpf_cpuperf_cap(s32 cpu) __ksym __weak;
+u32 scx_bpf_cpuperf_cur(s32 cpu) __ksym __weak;
+void scx_bpf_cpuperf_set(s32 cpu, u32 perf) __ksym __weak;
+u32 scx_bpf_nr_node_ids(void) __ksym __weak;
+u32 scx_bpf_nr_cpu_ids(void) __ksym __weak;
+int scx_bpf_cpu_node(s32 cpu) __ksym __weak;
+const struct cpumask *scx_bpf_get_possible_cpumask(void) __ksym __weak;
+const struct cpumask *scx_bpf_get_online_cpumask(void) __ksym __weak;
+void scx_bpf_put_cpumask(const struct cpumask *cpumask) __ksym __weak;
+const struct cpumask *scx_bpf_get_idle_cpumask_node(int node) __ksym __weak;
+const struct cpumask *scx_bpf_get_idle_cpumask(void) __ksym;
+const struct cpumask *scx_bpf_get_idle_smtmask_node(int node) __ksym __weak;
+const struct cpumask *scx_bpf_get_idle_smtmask(void) __ksym;
+void scx_bpf_put_idle_cpumask(const struct cpumask *cpumask) __ksym;
+bool scx_bpf_test_and_clear_cpu_idle(s32 cpu) __ksym;
+s32 scx_bpf_pick_idle_cpu_node(const cpumask_t *cpus_allowed, int node, u64 flags) __ksym __weak;
+s32 scx_bpf_pick_idle_cpu(const cpumask_t *cpus_allowed, u64 flags) __ksym;
+s32 scx_bpf_pick_any_cpu_node(const cpumask_t *cpus_allowed, int node, u64 flags) __ksym __weak;
+s32 scx_bpf_pick_any_cpu(const cpumask_t *cpus_allowed, u64 flags) __ksym;
+bool scx_bpf_task_running(const struct task_struct *p) __ksym;
+s32 scx_bpf_task_cpu(const struct task_struct *p) __ksym;
+struct rq *scx_bpf_cpu_rq(s32 cpu) __ksym __weak;
+struct rq *scx_bpf_locked_rq(void) __ksym;
+struct task_struct *scx_bpf_cpu_curr(s32 cpu) __ksym __weak;
+struct task_struct *scx_bpf_tid_to_task(u64 tid) __ksym __weak;
+u64 scx_bpf_now(void) __ksym __weak;
+void scx_bpf_events(struct scx_event_stats *events, size_t events__sz) __ksym __weak;
+s32 scx_bpf_cpu_to_cid(s32 cpu) __ksym __weak;
+s32 scx_bpf_cid_to_cpu(s32 cid) __ksym __weak;
+void scx_bpf_cid_topo(s32 cid, struct scx_cid_topo *out, size_t out__sz) __ksym __weak;
+void scx_bpf_kick_cid(s32 cid, u64 flags) __ksym __weak;
+s32 scx_bpf_task_cid(const struct task_struct *p) __ksym __weak;
+s32 scx_bpf_this_cid(void) __ksym __weak;
+struct task_struct *scx_bpf_cid_curr(s32 cid) __ksym __weak;
+u32 scx_bpf_nr_cids(void) __ksym __weak;
+u32 scx_bpf_nr_online_cids(void) __ksym __weak;
+const void __arena *scx_bpf_online_cmask(void) __ksym __weak;
+u32 scx_bpf_cidperf_cap(s32 cid) __ksym __weak;
+u32 scx_bpf_cidperf_cur(s32 cid) __ksym __weak;
+s32 scx_bpf_cidperf_set(s32 cid, u32 perf) __ksym __weak;
+
+/* sub-scheduler cap control, scx_bpf_sub_caps() cgroup_id 0 == self */
+s32 scx_bpf_sub_grant(u64 cgroup_id, u64 caps, const struct scx_cmask __arena *cmask__arena, struct scx_cmask __arena *denied_out__arena__nullable) __ksym __weak;
+void scx_bpf_sub_revoke(u64 cgroup_id, u64 caps, const struct scx_cmask __arena *cmask__arena) __ksym __weak;
+s32 scx_bpf_sub_caps(u64 cgroup_id, u64 caps, struct scx_cmask __arena *out__arena) __ksym __weak;
+s32 scx_bpf_sub_kill_bstr(u64 cgroup_id, char *fmt, unsigned long long *data, u32 data__sz) __ksym __weak;
+
+/*
+ * Use the following as @it__iter when calling scx_bpf_dsq_move[_vtime]() from
+ * within bpf_for_each() loops.
+ */
+#define BPF_FOR_EACH_ITER (&___it)
+
+#define scx_read_event(e, name) \
+ (bpf_core_field_exists((e)->name) ? (e)->name : 0)
+
+static inline __attribute__((format(printf, 1, 2)))
+void ___scx_bpf_bstr_format_checker(const char *fmt, ...) {}
+
+#define SCX_STRINGIFY(x) #x
+#define SCX_TOSTRING(x) SCX_STRINGIFY(x)
+
+/*
+ * Helper macro for initializing the fmt and variadic argument inputs to both
+ * bstr exit kfuncs. Callers to this function should use ___fmt and ___param to
+ * refer to the initialized list of inputs to the bstr kfunc.
+ */
+#define scx_bpf_bstr_preamble(fmt, args...) \
+ static char ___fmt[] = fmt; \
+ /* \
+ * Note that __param[] must have at least one \
+ * element to keep the verifier happy. \
+ */ \
+ unsigned long long ___param[___bpf_narg(args) ?: 1] = {}; \
+ \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wint-conversion\"") \
+ ___bpf_fill(___param, args); \
+ _Pragma("GCC diagnostic pop")
+
+/*
+ * scx_bpf_exit() wraps the scx_bpf_exit_bstr() kfunc with variadic arguments
+ * instead of an array of u64. Using this macro will cause the scheduler to
+ * exit cleanly with the specified exit code being passed to user space.
+ */
+#define scx_bpf_exit(code, fmt, args...) \
+({ \
+ scx_bpf_bstr_preamble(fmt, args) \
+ scx_bpf_exit_bstr(code, ___fmt, ___param, sizeof(___param)); \
+ ___scx_bpf_bstr_format_checker(fmt, ##args); \
+})
+
+/*
+ * scx_bpf_sub_kill() wraps the scx_bpf_sub_kill_bstr() kfunc with variadic
+ * arguments instead of an array of u64. It kills the direct child sub-scheduler
+ * @cgid, passing the formatted reason to its user space, and evaluates to the
+ * kfunc's return value. On a kernel without sub-scheduler support the kfunc is
+ * absent and it returns -EOPNOTSUPP.
+ */
+#define scx_bpf_sub_kill(cgid, fmt, args...) \
+({ \
+ scx_bpf_bstr_preamble(fmt, args) \
+ ___scx_bpf_bstr_format_checker(fmt, ##args); \
+ bpf_ksym_exists(scx_bpf_sub_kill_bstr) ? \
+ scx_bpf_sub_kill_bstr((cgid), ___fmt, ___param, \
+ sizeof(___param)) : -EOPNOTSUPP; \
+})
+
+/*
+ * scx_bpf_error() wraps the scx_bpf_error_bstr() kfunc with variadic arguments
+ * instead of an array of u64. Invoking this macro will cause the scheduler to
+ * exit in an erroneous state, with diagnostic information being passed to the
+ * user. It appends the file and line number to aid debugging.
+ */
+#define scx_bpf_error(fmt, args...) \
+({ \
+ scx_bpf_bstr_preamble( \
+ __FILE__ ":" SCX_TOSTRING(__LINE__) ": " fmt, ##args) \
+ scx_bpf_error_bstr(___fmt, ___param, sizeof(___param)); \
+ ___scx_bpf_bstr_format_checker( \
+ __FILE__ ":" SCX_TOSTRING(__LINE__) ": " fmt, ##args); \
+})
+
+/*
+ * scx_bpf_dump() wraps the scx_bpf_dump_bstr() kfunc with variadic arguments
+ * instead of an array of u64. To be used from ops.dump() and friends.
+ */
+#define scx_bpf_dump(fmt, args...) \
+({ \
+ scx_bpf_bstr_preamble(fmt, args) \
+ scx_bpf_dump_bstr(___fmt, ___param, sizeof(___param)); \
+ ___scx_bpf_bstr_format_checker(fmt, ##args); \
+})
+
+/*
+ * scx_bpf_dump_header() is a wrapper around scx_bpf_dump that adds a header
+ * of system information for debugging.
+ */
+#define scx_bpf_dump_header() \
+({ \
+ scx_bpf_dump("kernel: %d.%d.%d %s\ncc: %s\n", \
+ LINUX_KERNEL_VERSION >> 16, \
+ LINUX_KERNEL_VERSION >> 8 & 0xFF, \
+ LINUX_KERNEL_VERSION & 0xFF, \
+ CONFIG_LOCALVERSION, \
+ CONFIG_CC_VERSION_TEXT); \
+})
+
+#define BPF_STRUCT_OPS(name, args...) \
+SEC("struct_ops/"#name) \
+BPF_PROG(name, ##args)
+
+#define BPF_STRUCT_OPS_SLEEPABLE(name, args...) \
+SEC("struct_ops.s/"#name) \
+BPF_PROG(name, ##args)
+
+/**
+ * RESIZABLE_ARRAY - Generates annotations for an array that may be resized
+ * @elfsec: the data section of the BPF program in which to place the array
+ * @arr: the name of the array
+ *
+ * libbpf has an API for setting map value sizes. Since data sections (i.e.
+ * bss, data, rodata) themselves are maps, a data section can be resized. If
+ * a data section has an array as its last element, the BTF info for that
+ * array will be adjusted so that length of the array is extended to meet the
+ * new length of the data section. This macro annotates an array to have an
+ * element count of one with the assumption that this array can be resized
+ * within the userspace program. It also annotates the section specifier so
+ * this array exists in a custom sub data section which can be resized
+ * independently.
+ *
+ * See RESIZE_ARRAY() for the userspace convenience macro for resizing an
+ * array declared with RESIZABLE_ARRAY().
+ */
+#define RESIZABLE_ARRAY(elfsec, arr) arr[1] SEC("."#elfsec"."#arr)
+
+/**
+ * MEMBER_VPTR - Obtain the verified pointer to a struct or array member
+ * @base: struct or array to index
+ * @member: dereferenced member (e.g. .field, [idx0][idx1], .field[idx0] ...)
+ *
+ * The verifier often gets confused by the instruction sequence the compiler
+ * generates for indexing struct fields or arrays. This macro forces the
+ * compiler to generate a code sequence which first calculates the byte offset,
+ * checks it against the struct or array size and add that byte offset to
+ * generate the pointer to the member to help the verifier.
+ *
+ * Ideally, we want to abort if the calculated offset is out-of-bounds. However,
+ * BPF currently doesn't support abort, so evaluate to %NULL instead. The caller
+ * must check for %NULL and take appropriate action to appease the verifier. To
+ * avoid confusing the verifier, it's best to check for %NULL and dereference
+ * immediately.
+ *
+ * vptr = MEMBER_VPTR(my_array, [i][j]);
+ * if (!vptr)
+ * return error;
+ * *vptr = new_value;
+ *
+ * sizeof(@base) should encompass the memory area to be accessed and thus can't
+ * be a pointer to the area. Use `MEMBER_VPTR(*ptr, .member)` instead of
+ * `MEMBER_VPTR(ptr, ->member)`.
+ */
+#ifndef MEMBER_VPTR
+#define MEMBER_VPTR(base, member) (typeof((base) member) *) \
+({ \
+ u64 __base = (u64)&(base); \
+ u64 __addr = (u64)&((base) member) - __base; \
+ _Static_assert(sizeof(base) >= sizeof((base) member), \
+ "@base is smaller than @member, is @base a pointer?"); \
+ asm volatile ( \
+ "if %0 <= %[max] goto +2\n" \
+ "%0 = 0\n" \
+ "goto +1\n" \
+ "%0 += %1\n" \
+ : "+r"(__addr) \
+ : "r"(__base), \
+ [max]"i"(sizeof(base) - sizeof((base) member))); \
+ __addr; \
+})
+#endif /* MEMBER_VPTR */
+
+/**
+ * ARRAY_ELEM_PTR - Obtain the verified pointer to an array element
+ * @arr: array to index into
+ * @i: array index
+ * @n: number of elements in array
+ *
+ * Similar to MEMBER_VPTR() but is intended for use with arrays where the
+ * element count needs to be explicit.
+ * It can be used in cases where a global array is defined with an initial
+ * size but is intended to be be resized before loading the BPF program.
+ * Without this version of the macro, MEMBER_VPTR() will use the compile time
+ * size of the array to compute the max, which will result in rejection by
+ * the verifier.
+ */
+#ifndef ARRAY_ELEM_PTR
+#define ARRAY_ELEM_PTR(arr, i, n) (typeof(arr[i]) *) \
+({ \
+ u64 __base = (u64)arr; \
+ u64 __addr = (u64)&(arr[i]) - __base; \
+ asm volatile ( \
+ "if %0 <= %[max] goto +2\n" \
+ "%0 = 0\n" \
+ "goto +1\n" \
+ "%0 += %1\n" \
+ : "+r"(__addr) \
+ : "r"(__base), \
+ [max]"r"(sizeof(arr[0]) * ((n) - 1))); \
+ __addr; \
+})
+#endif /* ARRAY_ELEM_PTR */
+
+/**
+ * __sink - Hide @expr's value from the compiler and BPF verifier
+ * @expr: The expression whose value should be opacified
+ *
+ * No-op at runtime. The empty inline assembly with a read-write constraint
+ * ("+g") has two effects at compile/verify time:
+ *
+ * 1. Compiler: treats @expr as both read and written, preventing dead-code
+ * elimination and keeping @expr (and any side effects that produced it)
+ * alive.
+ *
+ * 2. BPF verifier: forgets the precise value/range of @expr ("makes it
+ * imprecise"). The verifier normally tracks exact ranges for every register
+ * and stack slot. While useful, precision means each distinct value creates a
+ * separate verifier state. Inside loops this leads to state explosion - each
+ * iteration carries different precise values so states never merge and the
+ * verifier explores every iteration individually.
+ *
+ * Example - preventing loop state explosion::
+ *
+ * u32 nr_intersects = 0, nr_covered = 0;
+ * __sink(nr_intersects);
+ * __sink(nr_covered);
+ * bpf_for(i, 0, nr_nodes) {
+ * if (intersects(cpumask, node_mask[i]))
+ * nr_intersects++;
+ * if (covers(cpumask, node_mask[i]))
+ * nr_covered++;
+ * }
+ *
+ * Without __sink(), the verifier tracks every possible (nr_intersects,
+ * nr_covered) pair across iterations, causing "BPF program is too large". With
+ * __sink(), the values become unknown scalars so all iterations collapse into
+ * one reusable state.
+ *
+ * Example - keeping a reference alive::
+ *
+ * struct task_struct *t = bpf_task_acquire(task);
+ * __sink(t);
+ *
+ * Follows the convention from BPF selftests (bpf_misc.h).
+ */
+#define __sink(expr) asm volatile ("" : "+g"(expr))
+
+/*
+ * BPF declarations and helpers
+ */
+
+/* list and rbtree */
+#define __contains(name, node) __attribute__((btf_decl_tag("contains:" #name ":" #node)))
+#define private(name) SEC(".data." #name) __hidden __attribute__((aligned(8)))
+
+void *bpf_obj_new_impl(__u64 local_type_id, void *meta) __ksym;
+void bpf_obj_drop_impl(void *kptr, void *meta) __ksym;
+
+#define bpf_obj_new(type) ((type *)bpf_obj_new_impl(bpf_core_type_id_local(type), NULL))
+#define bpf_obj_drop(kptr) bpf_obj_drop_impl(kptr, NULL)
+
+int bpf_list_push_front_impl(struct bpf_list_head *head,
+ struct bpf_list_node *node,
+ void *meta, __u64 off) __ksym;
+#define bpf_list_push_front(head, node) bpf_list_push_front_impl(head, node, NULL, 0)
+
+int bpf_list_push_back_impl(struct bpf_list_head *head,
+ struct bpf_list_node *node,
+ void *meta, __u64 off) __ksym;
+#define bpf_list_push_back(head, node) bpf_list_push_back_impl(head, node, NULL, 0)
+
+struct bpf_list_node *bpf_list_pop_front(struct bpf_list_head *head) __ksym;
+struct bpf_list_node *bpf_list_pop_back(struct bpf_list_head *head) __ksym;
+struct bpf_rb_node *bpf_rbtree_remove(struct bpf_rb_root *root,
+ struct bpf_rb_node *node) __ksym;
+int bpf_rbtree_add_impl(struct bpf_rb_root *root, struct bpf_rb_node *node,
+ bool (less)(struct bpf_rb_node *a, const struct bpf_rb_node *b),
+ void *meta, __u64 off) __ksym;
+#define bpf_rbtree_add(head, node, less) bpf_rbtree_add_impl(head, node, less, NULL, 0)
+
+struct bpf_rb_node *bpf_rbtree_first(struct bpf_rb_root *root) __ksym;
+
+void *bpf_refcount_acquire_impl(void *kptr, void *meta) __ksym;
+#define bpf_refcount_acquire(kptr) bpf_refcount_acquire_impl(kptr, NULL)
+
+/* task */
+struct task_struct *bpf_task_from_pid(s32 pid) __ksym;
+struct task_struct *bpf_task_acquire(struct task_struct *p) __ksym;
+void bpf_task_release(struct task_struct *p) __ksym;
+
+/* cgroup */
+struct cgroup *bpf_cgroup_ancestor(struct cgroup *cgrp, int level) __ksym;
+struct cgroup *bpf_cgroup_acquire(struct cgroup *cgrp) __ksym;
+void bpf_cgroup_release(struct cgroup *cgrp) __ksym;
+struct cgroup *bpf_cgroup_from_id(u64 cgid) __ksym;
+
+/* css iteration */
+struct bpf_iter_css;
+struct cgroup_subsys_state;
+extern int bpf_iter_css_new(struct bpf_iter_css *it,
+ struct cgroup_subsys_state *start,
+ unsigned int flags) __weak __ksym;
+extern struct cgroup_subsys_state *
+bpf_iter_css_next(struct bpf_iter_css *it) __weak __ksym;
+extern void bpf_iter_css_destroy(struct bpf_iter_css *it) __weak __ksym;
+
+/* cpumask */
+struct bpf_cpumask *bpf_cpumask_create(void) __ksym;
+struct bpf_cpumask *bpf_cpumask_acquire(struct bpf_cpumask *cpumask) __ksym;
+void bpf_cpumask_release(struct bpf_cpumask *cpumask) __ksym;
+u32 bpf_cpumask_first(const struct cpumask *cpumask) __ksym;
+u32 bpf_cpumask_first_zero(const struct cpumask *cpumask) __ksym;
+void bpf_cpumask_set_cpu(u32 cpu, struct bpf_cpumask *cpumask) __ksym;
+void bpf_cpumask_clear_cpu(u32 cpu, struct bpf_cpumask *cpumask) __ksym;
+bool bpf_cpumask_test_cpu(u32 cpu, const struct cpumask *cpumask) __ksym;
+bool bpf_cpumask_test_and_set_cpu(u32 cpu, struct bpf_cpumask *cpumask) __ksym;
+bool bpf_cpumask_test_and_clear_cpu(u32 cpu, struct bpf_cpumask *cpumask) __ksym;
+void bpf_cpumask_setall(struct bpf_cpumask *cpumask) __ksym;
+void bpf_cpumask_clear(struct bpf_cpumask *cpumask) __ksym;
+bool bpf_cpumask_and(struct bpf_cpumask *dst, const struct cpumask *src1,
+ const struct cpumask *src2) __ksym;
+void bpf_cpumask_or(struct bpf_cpumask *dst, const struct cpumask *src1,
+ const struct cpumask *src2) __ksym;
+void bpf_cpumask_xor(struct bpf_cpumask *dst, const struct cpumask *src1,
+ const struct cpumask *src2) __ksym;
+bool bpf_cpumask_equal(const struct cpumask *src1, const struct cpumask *src2) __ksym;
+bool bpf_cpumask_intersects(const struct cpumask *src1, const struct cpumask *src2) __ksym;
+bool bpf_cpumask_subset(const struct cpumask *src1, const struct cpumask *src2) __ksym;
+bool bpf_cpumask_empty(const struct cpumask *cpumask) __ksym;
+bool bpf_cpumask_full(const struct cpumask *cpumask) __ksym;
+void bpf_cpumask_copy(struct bpf_cpumask *dst, const struct cpumask *src) __ksym;
+u32 bpf_cpumask_any_distribute(const struct cpumask *cpumask) __ksym;
+u32 bpf_cpumask_any_and_distribute(const struct cpumask *src1,
+ const struct cpumask *src2) __ksym;
+u32 bpf_cpumask_weight(const struct cpumask *cpumask) __ksym;
+
+int bpf_iter_bits_new(struct bpf_iter_bits *it, const u64 *unsafe_ptr__ign, u32 nr_words) __ksym;
+int *bpf_iter_bits_next(struct bpf_iter_bits *it) __ksym;
+void bpf_iter_bits_destroy(struct bpf_iter_bits *it) __ksym;
+
+#define def_iter_struct(name) \
+struct bpf_iter_##name { \
+ struct bpf_iter_bits it; \
+ const struct cpumask *bitmap; \
+};
+
+#define def_iter_new(name) \
+static inline int bpf_iter_##name##_new( \
+ struct bpf_iter_##name *it, const u64 *unsafe_ptr__ign, u32 nr_words) \
+{ \
+ it->bitmap = scx_bpf_get_##name##_cpumask(); \
+ return bpf_iter_bits_new(&it->it, (const u64 *)it->bitmap, \
+ sizeof(struct cpumask) / 8); \
+}
+
+#define def_iter_next(name) \
+static inline int *bpf_iter_##name##_next(struct bpf_iter_##name *it) { \
+ return bpf_iter_bits_next(&it->it); \
+}
+
+#define def_iter_destroy(name) \
+static inline void bpf_iter_##name##_destroy(struct bpf_iter_##name *it) { \
+ scx_bpf_put_cpumask(it->bitmap); \
+ bpf_iter_bits_destroy(&it->it); \
+}
+#define def_for_each_cpu(cpu, name) for_each_##name##_cpu(cpu)
+
+/// Provides iterator for possible and online cpus.
+///
+/// # Example
+///
+/// ```
+/// static inline void example_use() {
+/// int *cpu;
+///
+/// for_each_possible_cpu(cpu){
+/// bpf_printk("CPU %d is possible", *cpu);
+/// }
+///
+/// for_each_online_cpu(cpu){
+/// bpf_printk("CPU %d is online", *cpu);
+/// }
+/// }
+/// ```
+def_iter_struct(possible);
+def_iter_new(possible);
+def_iter_next(possible);
+def_iter_destroy(possible);
+#define for_each_possible_cpu(cpu) bpf_for_each(possible, cpu, NULL, 0)
+
+def_iter_struct(online);
+def_iter_new(online);
+def_iter_next(online);
+def_iter_destroy(online);
+#define for_each_online_cpu(cpu) bpf_for_each(online, cpu, NULL, 0)
+
+/*
+ * Access a cpumask in read-only mode (typically to check bits).
+ */
+static __always_inline const struct cpumask *cast_mask(struct bpf_cpumask *mask)
+{
+ return (const struct cpumask *)mask;
+}
+
+/*
+ * True if the non-sleepable BPF trampoline prolog (__bpf_prog_enter) calls
+ * migrate_disable() for the current task. Recorded once by
+ * scx_lib_init_probe, an fentry program on bpf_scx_reg() that fires during
+ * the natural scheduler-attach call chain (auto-attached by scx_ops_attach!).
+ *
+ * Defaults to true (conservative). Over-reporting in is_migration_disabled()
+ * causes local-only dispatch, which is safe. Under-reporting can crash the
+ * scheduler, so we err high if the probe somehow fails to run.
+ */
+bool __scx_prolog_disables_migration __weak = true;
+
+/*
+ * scx_lib_init_probe - non-sleepable prolog probe.
+ *
+ * Attached to bpf_scx_reg(), the .reg callback in bpf_sched_ext_ops
+ * (kernel/sched/ext.c). The kernel's struct_ops machinery invokes
+ * bpf_scx_reg when userspace creates the scheduler link, before
+ * ops.init() fires. Its address is taken in the vtable, so the symbol
+ * is non-inlinable and has been stable since introduction.
+ *
+ * Entering via fentry runs us through __bpf_prog_enter -- the
+ * non-sleepable prolog that consumers of is_migration_disabled() live
+ * under.
+ *
+ * Loud warning: the prolog adds at most 1 to migration_disabled.
+ * Reading > 1 means something upstream in the
+ * bpf_struct_ops_link_create -> bpf_scx_reg path disabled migration
+ * before the prolog ran, invalidating the probe; audit and adjust.
+ */
+SEC("fentry/bpf_scx_reg") __weak
+int scx_lib_init_probe(void *ctx)
+{
+ if (bpf_core_field_exists(((struct task_struct *)0)->migration_disabled)) {
+ const struct task_struct *p = bpf_get_current_task_btf();
+ unsigned int md = p->migration_disabled;
+
+ if (md > 1)
+ bpf_printk("scx_lib_init_probe: unexpected migration_disabled=%u "
+ "upstream of BPF prolog; probe result unreliable",
+ md);
+
+ __scx_prolog_disables_migration = md > 0;
+ }
+ return 0;
+}
+
+/*
+ * Return true if task @p cannot migrate to a different CPU, false
+ * otherwise.
+ *
+ * IMPORTANT: designed for NON-SLEEPABLE BPF contexts only. Sleepable
+ * contexts (BPF_STRUCT_OPS_SLEEPABLE, SEC("syscall"),
+ * SEC("fentry.s/...")) enter via __bpf_prog_enter_sleepable() or
+ * __bpf_prog_enter_sleepable_recur(), both of which unconditionally
+ * call migrate_disable(); this helper can yield a false negative for
+ * p == current there, which can crash the scheduler.
+ */
+static inline bool is_migration_disabled(const struct task_struct *p)
+{
+ /*
+ * Testing p->migration_disabled in BPF is tricky because the BPF prolog
+ * (__bpf_prog_enter) may call migrate_disable() for the current task,
+ * making migration_disabled == 1 even for tasks that are not truly
+ * migration-disabled.
+ *
+ * Since commit 8e4f0b1ebcf2 ("bpf: use rcu_read_lock_dont_migrate() for
+ * trampoline.c"), the BPF prolog calls migrate_disable() only when
+ * CONFIG_PREEMPT_RCU is enabled. Two fast paths cover the common cases:
+ *
+ * 1) CONFIG_PREEMPT_RCU: prolog always calls migrate_disable(), so
+ * migration_disabled == 1 for the current task is ambiguous.
+ * Disambiguate by checking p == current.
+ *
+ * 2) v6.18+ without CONFIG_PREEMPT_RCU: prolog never calls
+ * migrate_disable(), so migration_disabled == 1 is unambiguously
+ * a real migrate_disable() call.
+ *
+ * A slow path handles pre-v6.18 kernels without CONFIG_PREEMPT_RCU,
+ * where the prolog historically called migrate_disable() unconditionally
+ * but a cherry-picked downstream kernel may not. The runtime-probed flag
+ * __scx_prolog_disables_migration (set by scx_lib_init_probe) distinguishes
+ * the two cases without relying on the kernel version alone.
+ */
+ if (bpf_core_field_exists(p->migration_disabled)) {
+ if (p->migration_disabled == 1) {
+ /* Fast path: prolog always disables migration */
+ if (CONFIG_PREEMPT_RCU)
+ return bpf_get_current_task_btf() != p;
+ /* Fast path: prolog never disables migration */
+ if (LINUX_KERNEL_VERSION >= KERNEL_VERSION(6, 18, 0))
+ return true;
+ /* Slow path: pre-v6.18, !PREEMPT_RCU - use runtime flag */
+ return __scx_prolog_disables_migration ?
+ bpf_get_current_task_btf() != p : true;
+ }
+ return p->migration_disabled;
+ }
+ return false;
+}
+
+/* rcu */
+void bpf_rcu_read_lock(void) __ksym;
+void bpf_rcu_read_unlock(void) __ksym;
+
+/* resilient qspinlock */
+int bpf_res_spin_lock(struct bpf_res_spin_lock *lock) __ksym __weak;
+void bpf_res_spin_unlock(struct bpf_res_spin_lock *lock) __ksym __weak;
+
+/*
+ * Time helpers, most of which are from jiffies.h.
+ */
+
+/**
+ * time_delta - Calculate the delta between new and old time stamp
+ * @after: first comparable as u64
+ * @before: second comparable as u64
+ *
+ * Return: the time difference, which is >= 0
+ */
+static inline s64 time_delta(u64 after, u64 before)
+{
+ return (s64)(after - before) > 0 ? (s64)(after - before) : 0;
+}
+
+/**
+ * time_after - returns true if the time a is after time b.
+ * @a: first comparable as u64
+ * @b: second comparable as u64
+ *
+ * Do this with "<0" and ">=0" to only test the sign of the result. A
+ * good compiler would generate better code (and a really good compiler
+ * wouldn't care). Gcc is currently neither.
+ *
+ * Return: %true is time a is after time b, otherwise %false.
+ */
+static inline bool time_after(u64 a, u64 b)
+{
+ return (s64)(b - a) < 0;
+}
+
+/**
+ * time_before - returns true if the time a is before time b.
+ * @a: first comparable as u64
+ * @b: second comparable as u64
+ *
+ * Return: %true is time a is before time b, otherwise %false.
+ */
+static inline bool time_before(u64 a, u64 b)
+{
+ return time_after(b, a);
+}
+
+/**
+ * time_after_eq - returns true if the time a is after or the same as time b.
+ * @a: first comparable as u64
+ * @b: second comparable as u64
+ *
+ * Return: %true is time a is after or the same as time b, otherwise %false.
+ */
+static inline bool time_after_eq(u64 a, u64 b)
+{
+ return (s64)(a - b) >= 0;
+}
+
+/**
+ * time_before_eq - returns true if the time a is before or the same as time b.
+ * @a: first comparable as u64
+ * @b: second comparable as u64
+ *
+ * Return: %true is time a is before or the same as time b, otherwise %false.
+ */
+static inline bool time_before_eq(u64 a, u64 b)
+{
+ return time_after_eq(b, a);
+}
+
+/**
+ * time_in_range - Calculate whether a is in the range of [b, c].
+ * @a: time to test
+ * @b: beginning of the range
+ * @c: end of the range
+ *
+ * Return: %true is time a is in the range [b, c], otherwise %false.
+ */
+static inline bool time_in_range(u64 a, u64 b, u64 c)
+{
+ return time_after_eq(a, b) && time_before_eq(a, c);
+}
+
+/**
+ * time_in_range_open - Calculate whether a is in the range of [b, c).
+ * @a: time to test
+ * @b: beginning of the range
+ * @c: end of the range
+ *
+ * Return: %true is time a is in the range [b, c), otherwise %false.
+ */
+static inline bool time_in_range_open(u64 a, u64 b, u64 c)
+{
+ return time_after_eq(a, b) && time_before(a, c);
+}
+
+
+/*
+ * Other helpers
+ */
+
+/* useful compiler attributes */
+#ifndef likely
+#define likely(x) __builtin_expect(!!(x), 1)
+#endif
+#ifndef unlikely
+#define unlikely(x) __builtin_expect(!!(x), 0)
+#endif
+#ifndef __maybe_unused
+#define __maybe_unused __attribute__((__unused__))
+#endif
+
+/*
+ * READ/WRITE_ONCE() are from kernel (include/asm-generic/rwonce.h). They
+ * prevent compiler from caching, redoing or reordering reads or writes.
+ */
+typedef __u8 __attribute__((__may_alias__)) __u8_alias_t;
+typedef __u16 __attribute__((__may_alias__)) __u16_alias_t;
+typedef __u32 __attribute__((__may_alias__)) __u32_alias_t;
+typedef __u64 __attribute__((__may_alias__)) __u64_alias_t;
+
+static __always_inline void __read_once_size(const volatile void *p, void *res, int size)
+{
+ switch (size) {
+ case 1: *(__u8_alias_t *) res = *(volatile __u8_alias_t *) p; break;
+ case 2: *(__u16_alias_t *) res = *(volatile __u16_alias_t *) p; break;
+ case 4: *(__u32_alias_t *) res = *(volatile __u32_alias_t *) p; break;
+ case 8: *(__u64_alias_t *) res = *(volatile __u64_alias_t *) p; break;
+ default:
+ barrier();
+ __builtin_memcpy((void *)res, (const void *)p, size);
+ barrier();
+ }
+}
+
+static __always_inline void __write_once_size(volatile void *p, void *res, int size)
+{
+ switch (size) {
+ case 1: *(volatile __u8_alias_t *) p = *(__u8_alias_t *) res; break;
+ case 2: *(volatile __u16_alias_t *) p = *(__u16_alias_t *) res; break;
+ case 4: *(volatile __u32_alias_t *) p = *(__u32_alias_t *) res; break;
+ case 8: *(volatile __u64_alias_t *) p = *(__u64_alias_t *) res; break;
+ default:
+ barrier();
+ __builtin_memcpy((void *)p, (const void *)res, size);
+ barrier();
+ }
+}
+
+/*
+ * __unqual_typeof(x) - Declare an unqualified scalar type, leaving
+ * non-scalar types unchanged,
+ *
+ * Prefer C11 _Generic for better compile-times and simpler code. Note: 'char'
+ * is not type-compatible with 'signed char', and we define a separate case.
+ *
+ * This is copied verbatim from kernel's include/linux/compiler_types.h, but
+ * with default expression (for pointers) changed from (x) to (typeof(x)0).
+ *
+ * This is because LLVM has a bug where for lvalue (x), it does not get rid of
+ * an extra address_space qualifier, but does in case of rvalue (typeof(x)0).
+ * Hence, for pointers, we need to create an rvalue expression to get the
+ * desired type. See https://github.com/llvm/llvm-project/issues/53400.
+ */
+#define __scalar_type_to_expr_cases(type) \
+ unsigned type : (unsigned type)0, signed type : (signed type)0
+
+#define __unqual_typeof(x) \
+ typeof(_Generic((x), \
+ char: (char)0, \
+ __scalar_type_to_expr_cases(char), \
+ __scalar_type_to_expr_cases(short), \
+ __scalar_type_to_expr_cases(int), \
+ __scalar_type_to_expr_cases(long), \
+ __scalar_type_to_expr_cases(long long), \
+ default: (typeof(x))0))
+
+#define READ_ONCE(x) \
+({ \
+ union { __unqual_typeof(x) __val; char __c[1]; } __u = \
+ { .__c = { 0 } }; \
+ __read_once_size((__unqual_typeof(x) *)&(x), __u.__c, sizeof(x)); \
+ __u.__val; \
+})
+
+#define WRITE_ONCE(x, val) \
+({ \
+ union { __unqual_typeof(x) __val; char __c[1]; } __u = \
+ { .__val = (val) }; \
+ __write_once_size((__unqual_typeof(x) *)&(x), __u.__c, sizeof(x)); \
+ __u.__val; \
+})
+
+/*
+ * __calc_avg - Calculate exponential weighted moving average (EWMA) with
+ * @old and @new values. @decay represents how large the @old value remains.
+ * With a larger @decay value, the moving average changes slowly, exhibiting
+ * fewer fluctuations.
+ */
+#define __calc_avg(old, new, decay) ({ \
+ typeof(decay) thr = 1 << (decay); \
+ typeof(old) ret; \
+ if (((old) < thr) || ((new) < thr)) { \
+ if (((old) == 1) && ((new) == 0)) \
+ ret = 0; \
+ else \
+ ret = ((old) - ((old) >> 1)) + ((new) >> 1); \
+ } else { \
+ ret = ((old) - ((old) >> (decay))) + ((new) >> (decay)); \
+ } \
+ ret; \
+})
+
+/*
+ * log2_u32 - Compute the base 2 logarithm of a 32-bit exponential value.
+ * @v: The value for which we're computing the base 2 logarithm.
+ */
+static inline u32 log2_u32(u32 v)
+{
+ u32 r;
+ u32 shift;
+
+ r = (v > 0xFFFF) << 4; v >>= r;
+ shift = (v > 0xFF) << 3; v >>= shift; r |= shift;
+ shift = (v > 0xF) << 2; v >>= shift; r |= shift;
+ shift = (v > 0x3) << 1; v >>= shift; r |= shift;
+ r |= (v >> 1);
+ return r;
+}
+
+/*
+ * log2_u64 - Compute the base 2 logarithm of a 64-bit exponential value.
+ * @v: The value for which we're computing the base 2 logarithm.
+ */
+static inline u32 log2_u64(u64 v)
+{
+ u32 hi = v >> 32;
+ if (hi)
+ return log2_u32(hi) + 32 + 1;
+ else
+ return log2_u32(v) + 1;
+}
+
+/*
+ * sqrt_u64 - Calculate the square root of value @x using Newton's method.
+ */
+static inline u64 __sqrt_u64(u64 x)
+{
+ if (x == 0 || x == 1)
+ return x;
+
+ u64 r = ((1ULL << 32) > x) ? x : (1ULL << 32);
+
+ for (int i = 0; i < 8; ++i) {
+ u64 q = x / r;
+ if (r <= q)
+ break;
+ r = (r + q) >> 1;
+ }
+ return r;
+}
+
+/*
+ * ctzll -- Counts trailing zeros in an unsigned long long. If the input value
+ * is zero, the return value is undefined.
+ */
+static inline int ctzll(u64 v)
+{
+#if (!defined(__BPF__) && defined(__SCX_TARGET_ARCH_x86)) || \
+ (defined(__BPF__) && defined(__clang_major__) && __clang_major__ >= 19)
+ /*
+ * Use the ctz builtin when: (1) building for native x86, or
+ * (2) building for BPF with clang >= 19 (BPF backend supports
+ * the intrinsic from clang 19 onward; earlier versions hit
+ * "unimplemented opcode" in the backend).
+ */
+ return __builtin_ctzll(v);
+#else
+ /*
+ * If neither the target architecture nor the toolchains support ctzll,
+ * use software-based emulation. Let's use the De Bruijn sequence-based
+ * approach to find LSB fastly. See the details of De Bruijn sequence:
+ *
+ * https://en.wikipedia.org/wiki/De_Bruijn_sequence
+ * https://www.chessprogramming.org/BitScan#De_Bruijn_Multiplication
+ */
+ const int lookup_table[64] = {
+ 0, 1, 48, 2, 57, 49, 28, 3, 61, 58, 50, 42, 38, 29, 17, 4,
+ 62, 55, 59, 36, 53, 51, 43, 22, 45, 39, 33, 30, 24, 18, 12, 5,
+ 63, 47, 56, 27, 60, 41, 37, 16, 54, 35, 52, 21, 44, 32, 23, 11,
+ 46, 26, 40, 15, 34, 20, 31, 10, 25, 14, 19, 9, 13, 8, 7, 6,
+ };
+ const u64 DEBRUIJN_CONSTANT = 0x03f79d71b4cb0a89ULL;
+ unsigned int index;
+ u64 lowest_bit;
+ const int *lt;
+
+ if (v == 0)
+ return -1;
+
+ /*
+ * Isolate the least significant bit (LSB).
+ * For example, if v = 0b...10100, then v & -v = 0b...00100
+ */
+ lowest_bit = v & -v;
+
+ /*
+ * Each isolated bit produces a unique 6-bit value, guaranteed by the
+ * De Bruijn property. Calculate a unique index into the lookup table
+ * using the magic constant and a right shift.
+ *
+ * Multiplying by the 64-bit constant "spreads out" that 1-bit into a
+ * unique pattern in the top 6 bits. This uniqueness property is
+ * exactly what a De Bruijn sequence guarantees: Every possible 6-bit
+ * pattern (in top bits) occurs exactly once for each LSB position. So,
+ * the constant 0x03f79d71b4cb0a89ULL is carefully chosen to be a
+ * De Bruijn sequence, ensuring no collisions in the table index.
+ */
+ index = (lowest_bit * DEBRUIJN_CONSTANT) >> 58;
+
+ /*
+ * Lookup in a precomputed table. No collision is guaranteed by the
+ * De Bruijn property.
+ */
+ lt = MEMBER_VPTR(lookup_table, [index]);
+ return (lt)? *lt : -1;
+#endif
+}
+
+/*
+ * Return a value proportionally scaled to the task's weight.
+ */
+static inline u64 scale_by_task_weight(const struct task_struct *p, u64 value)
+{
+ return (value * p->scx.weight) / 100;
+}
+
+/*
+ * Return a value inversely proportional to the task's weight.
+ */
+static inline u64 scale_by_task_weight_inverse(const struct task_struct *p, u64 value)
+{
+ return value * 100 / p->scx.weight;
+}
+
+
+/*
+ * Get a random u64 from the kernel's pseudo-random generator.
+ */
+static inline u64 get_prandom_u64()
+{
+ return ((u64)bpf_get_prandom_u32() << 32) | bpf_get_prandom_u32();
+}
+
+/*
+ * Define the shadow structure to avoid a compilation error when
+ * vmlinux.h does not enable necessary kernel configs. The ___local
+ * suffix is a CO-RE convention that tells the loader to match this
+ * against the base struct rq in the kernel. The attribute
+ * preserve_access_index tells the compiler to generate a CO-RE
+ * relocation for these fields.
+ */
+struct rq___local {
+ /*
+ * A monotonically increasing clock per CPU. It is rq->clock minus
+ * cumulative IRQ time and hypervisor steal time. Unlike rq->clock,
+ * it does not advance during IRQ processing or hypervisor preemption.
+ * It does advance during idle (the idle task counts as a running task
+ * for this purpose).
+ */
+ u64 clock_task;
+ /*
+ * Invariant version of clock_task scaled by CPU capacity and
+ * frequency. For example, clock_pelt advances 2x slower on a CPU
+ * with half the capacity.
+ *
+ * At idle exit, rq->clock_pelt jumps forward to resync with
+ * clock_task. The kernel's rq_clock_pelt() corrects for this jump
+ * by subtracting lost_idle_time, yielding a clock that appears
+ * continuous across idle transitions. scx_clock_pelt() mirrors
+ * rq_clock_pelt() by performing the same subtraction.
+ */
+ u64 clock_pelt;
+ /*
+ * Accumulates the magnitude of each clock_pelt jump at idle exit.
+ * Subtracting this from clock_pelt gives rq_clock_pelt(): a
+ * continuous, capacity-invariant clock suitable for both task
+ * execution time stamping and cross-idle measurements.
+ */
+ unsigned long lost_idle_time;
+ /*
+ * Shadow of paravirt_steal_clock() (the hypervisor's cumulative
+ * stolen time counter). Stays frozen while the hypervisor preempts
+ * the vCPU; catches up the next time update_rq_clock_task() is
+ * called. The delta is the stolen time not yet subtracted from
+ * clock_task.
+ *
+ * Unlike irqtime->total (a plain kernel-side field), the live stolen
+ * time counter lives in hypervisor-specific shared memory and has no
+ * kernel-side equivalent readable from BPF in a hypervisor-agnostic
+ * way. This field is therefore the only portable BPF-accessible
+ * approximation of cumulative steal time.
+ *
+ * Available only when CONFIG_PARAVIRT_TIME_ACCOUNTING is on.
+ */
+ u64 prev_steal_time_rq;
+} __attribute__((preserve_access_index));
+
+extern struct rq runqueues __ksym;
+
+/*
+ * Define the shadow structure to avoid a compilation error when
+ * vmlinux.h does not enable necessary kernel configs.
+ */
+struct irqtime___local {
+ /*
+ * Cumulative IRQ time counter for this CPU, in nanoseconds. Advances
+ * immediately at the exit of every hardirq and non-ksoftirqd softirq
+ * via irqtime_account_irq(). ksoftirqd time is counted as normal
+ * task time and is NOT included. NMI time is also NOT included.
+ *
+ * The companion field irqtime->sync (struct u64_stats_sync) protects
+ * against 64-bit tearing on 32-bit architectures. On 64-bit kernels,
+ * u64_stats_sync is an empty struct and all seqcount operations are
+ * no-ops, so a plain BPF_CORE_READ of this field is safe.
+ *
+ * Available only when CONFIG_IRQ_TIME_ACCOUNTING is on.
+ */
+ u64 total;
+} __attribute__((preserve_access_index));
+
+/*
+ * cpu_irqtime is a per-CPU variable defined only when
+ * CONFIG_IRQ_TIME_ACCOUNTING is on. Declare it as __weak so the BPF
+ * loader sets its address to 0 (rather than failing) when the symbol
+ * is absent from the running kernel.
+ */
+extern struct irqtime___local cpu_irqtime __ksym __weak;
+
+static inline struct rq___local *get_current_rq(u32 cpu)
+{
+ /*
+ * This is a workaround to get an rq pointer now that
+ * scx_bpf_cpu_rq() has been removed.
+ *
+ * WARNING: The caller must hold the rq lock for @cpu. This is
+ * guaranteed when called from scheduling callbacks (ops.running,
+ * ops.stopping, ops.enqueue, ops.dequeue, ops.dispatch, etc.).
+ * There is no runtime check available in BPF for kernel spinlock
+ * state — correctness is enforced by calling context only.
+ */
+ return (void *)bpf_per_cpu_ptr(&runqueues, cpu);
+}
+
+static inline u64 scx_clock_task(u32 cpu)
+{
+ struct rq___local *rq = get_current_rq(cpu);
+
+ /*
+ * Equivalent to the kernel's rq_clock_task(): wall-clock time minus
+ * cumulative IRQ time (CONFIG_IRQ_TIME_ACCOUNTING) and hypervisor
+ * steal time (CONFIG_PARAVIRT_TIME_ACCOUNTING). Without those configs,
+ * it equals rq->clock.
+ *
+ * Conceptually this clock advances during idle (the idle task counts
+ * as a running task), but rq->clock_task is only updated on scheduling
+ * events. With NO_HZ_IDLE (the default), the periodic tick is stopped
+ * on idle CPUs, so rq->clock_task is not refreshed while a CPU is
+ * idle. Reading this clock for a remote idle CPU from a BPF timer
+ * callback returns the value from when the CPU last went idle, making
+ * the delta over an idle interval effectively zero.
+ */
+ return rq ? rq->clock_task : 0;
+}
+
+static inline u64 scx_clock_pelt(u32 cpu)
+{
+ struct rq___local *rq = get_current_rq(cpu);
+
+ /*
+ * Equivalent to the kernel's rq_clock_pelt(): subtracts
+ * lost_idle_time from clock_pelt to absorb the jump that occurs
+ * when clock_pelt resyncs with clock_task at idle exit. The intent
+ * is a continuous, capacity- and frequency-invariant clock that is
+ * frozen during idle, IRQ, and hypervisor steal.
+ *
+ * However, like scx_clock_task(), this clock has a stale-read issue
+ * for remote idle CPUs with NO_HZ_IDLE (the default). clock_pelt
+ * itself advances at wall-clock rate (hardware-clock based), but
+ * lost_idle_time is only updated via update_rq_clock_pelt(), which
+ * requires update_rq_clock() to be called. With NO_HZ_IDLE, the
+ * periodic tick is stopped on idle CPUs, so lost_idle_time is not
+ * refreshed during idle. Reading this clock for a remote idle CPU
+ * from a BPF timer callback therefore returns a value that drifts
+ * at wall-clock rate -- the same stale behaviour as scx_clock_task().
+ *
+ * Without NO_HZ_IDLE, periodic ticks keep lost_idle_time nearly in
+ * sync (stale by at most one tick period, ~1 ms), so the result is
+ * accurate.
+ */
+ return rq ? (rq->clock_pelt - rq->lost_idle_time) : 0;
+}
+
+static inline u64 scx_clock_virt(u32 cpu)
+{
+ struct rq___local *rq;
+
+ /*
+ * Check field existence before calling get_current_rq() so we avoid
+ * the per_cpu lookup entirely on kernels built without
+ * CONFIG_PARAVIRT_TIME_ACCOUNTING.
+ */
+ if (!bpf_core_field_exists(((struct rq___local *)0)->prev_steal_time_rq))
+ return 0;
+
+ /* Lagging shadow of the kernel's paravirt_steal_clock(). */
+ rq = get_current_rq(cpu);
+ return rq ? BPF_CORE_READ(rq, prev_steal_time_rq) : 0;
+}
+
+static inline u64 scx_clock_irq(u32 cpu)
+{
+ struct irqtime___local *irqt;
+
+ /*
+ * bpf_core_type_exists() resolves at load time: if struct irqtime is
+ * absent from kernel BTF (CONFIG_IRQ_TIME_ACCOUNTING off), the loader
+ * patches this into an unconditional return 0, making the
+ * bpf_per_cpu_ptr() call below dead code that the verifier never sees.
+ */
+ if (!bpf_core_type_exists(struct irqtime___local))
+ return 0;
+
+ /* Equivalent to the kernel's irq_time_read(). */
+ irqt = bpf_per_cpu_ptr(&cpu_irqtime, cpu);
+ return irqt ? BPF_CORE_READ(irqt, total) : 0;
+}
+
+/* Abbreviated forms of <linux/overflow.h>'s struct_size() family. */
+#define flex_array_size(p, member, count) \
+ ((count) * sizeof(*(p)->member))
+
+#define struct_size(p, member, count) \
+ (offsetof(typeof(*(p)), member) + flex_array_size(p, member, count))
+
+#define struct_size_t(type, member, count) \
+ struct_size((type *)NULL, member, count)
+
+#include "compat.bpf.h"
+#include "enums.bpf.h"
+#include "cid.bpf.h"
+
+#endif /* __SCX_COMMON_BPF_H */
diff --git a/tools/sched_ext/include/scx/common.h b/tools/sched_ext/include/scx/common.h
new file mode 100644
index 000000000..60f551378
--- /dev/null
+++ b/tools/sched_ext/include/scx/common.h
@@ -0,0 +1,80 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2023 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2023 David Vernet <dvernet@meta.com>
+ */
+#ifndef __SCHED_EXT_COMMON_H
+#define __SCHED_EXT_COMMON_H
+
+#ifdef __KERNEL__
+#error "Should not be included by BPF programs"
+#endif
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <errno.h>
+#include "enum_defs.autogen.h"
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef uint32_t u32;
+typedef uint64_t u64;
+typedef int8_t s8;
+typedef int16_t s16;
+typedef int32_t s32;
+typedef int64_t s64;
+
+#define SCX_BUG(__fmt, ...) \
+ do { \
+ fprintf(stderr, "[SCX_BUG] %s:%d", __FILE__, __LINE__); \
+ if (errno) \
+ fprintf(stderr, " (%s)\n", strerror(errno)); \
+ else \
+ fprintf(stderr, "\n"); \
+ fprintf(stderr, __fmt __VA_OPT__(,) __VA_ARGS__); \
+ fprintf(stderr, "\n"); \
+ \
+ exit(EXIT_FAILURE); \
+ } while (0)
+
+#define SCX_BUG_ON(__cond, __fmt, ...) \
+ do { \
+ if (__cond) \
+ SCX_BUG((__fmt) __VA_OPT__(,) __VA_ARGS__); \
+ } while (0)
+
+/**
+ * RESIZE_ARRAY - Convenience macro for resizing a BPF array
+ * @__skel: the skeleton containing the array
+ * @elfsec: the data section of the BPF program in which the array exists
+ * @arr: the name of the array
+ * @n: the desired array element count
+ *
+ * For BPF arrays declared with RESIZABLE_ARRAY(), this macro performs two
+ * operations. It resizes the map which corresponds to the custom data
+ * section that contains the target array. As a side effect, the BTF info for
+ * the array is adjusted so that the array length is sized to cover the new
+ * data section size. The second operation is reassigning the skeleton pointer
+ * for that custom data section so that it points to the newly memory mapped
+ * region.
+ */
+#define RESIZE_ARRAY(__skel, elfsec, arr, n) \
+ do { \
+ size_t __sz; \
+ bpf_map__set_value_size((__skel)->maps.elfsec##_##arr, \
+ sizeof((__skel)->elfsec##_##arr->arr[0]) * (n)); \
+ (__skel)->elfsec##_##arr = \
+ (typeof((__skel)->elfsec##_##arr)) \
+ bpf_map__initial_value((__skel)->maps.elfsec##_##arr, &__sz); \
+ } while (0)
+
+#include "user_exit_info.h"
+#include "compat.h"
+#include "enums.h"
+
+#include "bpf_arena_common.h"
+
+#endif /* __SCHED_EXT_COMMON_H */
diff --git a/tools/sched_ext/include/scx/compat.bpf.h b/tools/sched_ext/include/scx/compat.bpf.h
new file mode 100644
index 000000000..6944221f9
--- /dev/null
+++ b/tools/sched_ext/include/scx/compat.bpf.h
@@ -0,0 +1,495 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2024 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2024 David Vernet <dvernet@meta.com>
+ */
+#ifndef __SCX_COMPAT_BPF_H
+#define __SCX_COMPAT_BPF_H
+
+#define __COMPAT_ENUM_OR_ZERO(__type, __ent) \
+({ \
+ __type __ret = 0; \
+ if (bpf_core_enum_value_exists(__type, __ent)) \
+ __ret = __ent; \
+ __ret; \
+})
+
+/* v6.12: 819513666966 ("sched_ext: Add cgroup support") */
+struct cgroup *scx_bpf_task_cgroup___new(struct task_struct *p) __ksym __weak;
+
+#define scx_bpf_task_cgroup(p) \
+ (bpf_ksym_exists(scx_bpf_task_cgroup___new) ? \
+ scx_bpf_task_cgroup___new((p)) : NULL)
+
+/*
+ * v6.13: The verb `dispatch` was too overloaded and confusing. kfuncs are
+ * renamed to unload the verb.
+ *
+ * scx_bpf_dispatch_from_dsq() and friends were added during v6.12 by
+ * 4c30f5ce4f7a ("sched_ext: Implement scx_bpf_dispatch[_vtime]_from_dsq()").
+ *
+ * v7.1: scx_bpf_dsq_move_to_local___v2() to add @enq_flags.
+ */
+bool scx_bpf_dsq_move_to_local___v2___compat(u64 dsq_id, u64 enq_flags) __ksym __weak;
+bool scx_bpf_dsq_move_to_local___v1(u64 dsq_id) __ksym __weak;
+void scx_bpf_dsq_move_set_slice___new(struct bpf_iter_scx_dsq *it__iter, u64 slice) __ksym __weak;
+void scx_bpf_dsq_move_set_vtime___new(struct bpf_iter_scx_dsq *it__iter, u64 vtime) __ksym __weak;
+bool scx_bpf_dsq_move___new(struct bpf_iter_scx_dsq *it__iter, struct task_struct *p, u64 dsq_id, u64 enq_flags) __ksym __weak;
+bool scx_bpf_dsq_move_vtime___new(struct bpf_iter_scx_dsq *it__iter, struct task_struct *p, u64 dsq_id, u64 enq_flags) __ksym __weak;
+
+bool scx_bpf_consume___old(u64 dsq_id) __ksym __weak;
+void scx_bpf_dispatch_from_dsq_set_slice___old(struct bpf_iter_scx_dsq *it__iter, u64 slice) __ksym __weak;
+void scx_bpf_dispatch_from_dsq_set_vtime___old(struct bpf_iter_scx_dsq *it__iter, u64 vtime) __ksym __weak;
+bool scx_bpf_dispatch_from_dsq___old(struct bpf_iter_scx_dsq *it__iter, struct task_struct *p, u64 dsq_id, u64 enq_flags) __ksym __weak;
+bool scx_bpf_dispatch_vtime_from_dsq___old(struct bpf_iter_scx_dsq *it__iter, struct task_struct *p, u64 dsq_id, u64 enq_flags) __ksym __weak;
+
+#define scx_bpf_dsq_move_to_local(dsq_id, enq_flags) \
+ (bpf_ksym_exists(scx_bpf_dsq_move_to_local___v2___compat) ? \
+ scx_bpf_dsq_move_to_local___v2___compat((dsq_id), (enq_flags)) : \
+ (bpf_ksym_exists(scx_bpf_dsq_move_to_local___v1) ? \
+ scx_bpf_dsq_move_to_local___v1((dsq_id)) : \
+ scx_bpf_consume___old((dsq_id))))
+
+#define scx_bpf_dsq_move_set_slice(it__iter, slice) \
+ (bpf_ksym_exists(scx_bpf_dsq_move_set_slice___new) ? \
+ scx_bpf_dsq_move_set_slice___new((it__iter), (slice)) : \
+ (bpf_ksym_exists(scx_bpf_dispatch_from_dsq_set_slice___old) ? \
+ scx_bpf_dispatch_from_dsq_set_slice___old((it__iter), (slice)) : \
+ (void)0))
+
+#define scx_bpf_dsq_move_set_vtime(it__iter, vtime) \
+ (bpf_ksym_exists(scx_bpf_dsq_move_set_vtime___new) ? \
+ scx_bpf_dsq_move_set_vtime___new((it__iter), (vtime)) : \
+ (bpf_ksym_exists(scx_bpf_dispatch_from_dsq_set_vtime___old) ? \
+ scx_bpf_dispatch_from_dsq_set_vtime___old((it__iter), (vtime)) : \
+ (void)0))
+
+#define scx_bpf_dsq_move(it__iter, p, dsq_id, enq_flags) \
+ (bpf_ksym_exists(scx_bpf_dsq_move___new) ? \
+ scx_bpf_dsq_move___new((it__iter), (p), (dsq_id), (enq_flags)) : \
+ (bpf_ksym_exists(scx_bpf_dispatch_from_dsq___old) ? \
+ scx_bpf_dispatch_from_dsq___old((it__iter), (p), (dsq_id), (enq_flags)) : \
+ false))
+
+#define scx_bpf_dsq_move_vtime(it__iter, p, dsq_id, enq_flags) \
+ (bpf_ksym_exists(scx_bpf_dsq_move_vtime___new) ? \
+ scx_bpf_dsq_move_vtime___new((it__iter), (p), (dsq_id), (enq_flags)) : \
+ (bpf_ksym_exists(scx_bpf_dispatch_vtime_from_dsq___old) ? \
+ scx_bpf_dispatch_vtime_from_dsq___old((it__iter), (p), (dsq_id), (enq_flags)) : \
+ false))
+
+/*
+ * v6.15: 950ad93df2fc ("bpf: add kfunc for populating cpumask bits")
+ *
+ * Compat macro will be dropped on v6.19 release.
+ */
+int bpf_cpumask_populate(struct bpf_cpumask *dst, void *src, size_t src__sz) __ksym __weak;
+
+#define __COMPAT_bpf_cpumask_populate(cpumask, src, size__sz) \
+ (bpf_ksym_exists(bpf_cpumask_populate) ? \
+ (bpf_cpumask_populate(cpumask, src, size__sz)) : -EOPNOTSUPP)
+
+/*
+ * v6.19: Introduce lockless peek API for user DSQs.
+ * v7.1: Fix scx_bpf_dsq_peek() spuriously returning NULL on non-empty
+ * FIFO DSQs (2f2ea7709266).
+ *
+ * The kfunc exists from v6.19 but can return NULL for a non-empty FIFO DSQ
+ * before the v7.1 fix. Require kernel version >= 7.1.0 before calling it;
+ * otherwise fall through to the bpf_iter_scx_dsq fallback below.
+ */
+static inline struct task_struct *__COMPAT_scx_bpf_dsq_peek(u64 dsq_id)
+{
+ struct task_struct *p = NULL;
+ struct bpf_iter_scx_dsq it;
+
+ if (bpf_ksym_exists(scx_bpf_dsq_peek) &&
+ LINUX_KERNEL_VERSION >= KERNEL_VERSION(7, 1, 0))
+ return scx_bpf_dsq_peek(dsq_id);
+ if (!bpf_iter_scx_dsq_new(&it, dsq_id, 0))
+ p = bpf_iter_scx_dsq_next(&it);
+ bpf_iter_scx_dsq_destroy(&it);
+ return p;
+}
+
+/*
+ * v7.1: scx_bpf_sub_dispatch() for sub-sched dispatch. Preserve until
+ * we drop the compat layer for older kernels that lack the kfunc.
+ */
+bool scx_bpf_sub_dispatch___compat(u64 cgroup_id) __ksym __weak;
+
+static inline bool scx_bpf_sub_dispatch(u64 cgroup_id)
+{
+ if (bpf_ksym_exists(scx_bpf_sub_dispatch___compat))
+ return scx_bpf_sub_dispatch___compat(cgroup_id);
+ return false;
+}
+
+/*
+ * v7.3: scx_bpf_cid_override() for explicit cid and shard mapping. Ignore if
+ * missing.
+ */
+void scx_bpf_cid_override___compat(const s32 __arena *cpu_to_cid__arena,
+ u32 cpu_to_cid_cnt,
+ const s32 __arena *shard_start__arena,
+ u32 shard_start_cnt) __ksym __weak;
+
+static inline void scx_bpf_cid_override(const s32 __arena *cpu_to_cid, u32 cpu_to_cid_cnt,
+ const s32 __arena *shard_start, u32 shard_start_cnt)
+{
+ if (bpf_ksym_exists(scx_bpf_cid_override___compat))
+ scx_bpf_cid_override___compat(cpu_to_cid, cpu_to_cid_cnt,
+ shard_start, shard_start_cnt);
+}
+
+/**
+ * __COMPAT_is_enq_cpu_selected - Test if SCX_ENQ_CPU_SELECTED is on
+ * in a compatible way. We will preserve this __COMPAT helper until v6.16.
+ *
+ * @enq_flags: enqueue flags from ops.enqueue()
+ *
+ * Return: True if SCX_ENQ_CPU_SELECTED is turned on in @enq_flags
+ */
+static inline bool __COMPAT_is_enq_cpu_selected(u64 enq_flags)
+{
+#ifdef HAVE_SCX_ENQ_CPU_SELECTED
+ /*
+ * This is the case that a BPF code compiled against vmlinux.h
+ * where the enum SCX_ENQ_CPU_SELECTED exists.
+ */
+
+ /*
+ * We should temporarily suspend the macro expansion of
+ * 'SCX_ENQ_CPU_SELECTED'. This avoids 'SCX_ENQ_CPU_SELECTED' being
+ * rewritten to '__SCX_ENQ_CPU_SELECTED' when 'SCX_ENQ_CPU_SELECTED'
+ * is defined in 'scripts/gen_enums.py'.
+ */
+#pragma push_macro("SCX_ENQ_CPU_SELECTED")
+#undef SCX_ENQ_CPU_SELECTED
+ u64 flag;
+
+ /*
+ * When the kernel did not have SCX_ENQ_CPU_SELECTED,
+ * select_task_rq_scx() has never been skipped. Thus, this case
+ * should be considered that the CPU has already been selected.
+ */
+ if (!bpf_core_enum_value_exists(enum scx_enq_flags,
+ SCX_ENQ_CPU_SELECTED))
+ return true;
+
+ flag = bpf_core_enum_value(enum scx_enq_flags, SCX_ENQ_CPU_SELECTED);
+ return enq_flags & flag;
+
+ /*
+ * Once done, resume the macro expansion of 'SCX_ENQ_CPU_SELECTED'.
+ */
+#pragma pop_macro("SCX_ENQ_CPU_SELECTED")
+#else
+ /*
+ * This is the case that a BPF code compiled against vmlinux.h
+ * where the enum SCX_ENQ_CPU_SELECTED does NOT exist.
+ */
+ return true;
+#endif /* HAVE_SCX_ENQ_CPU_SELECTED */
+}
+
+
+#define scx_bpf_now() \
+ (bpf_ksym_exists(scx_bpf_now) ? \
+ scx_bpf_now() : \
+ bpf_ktime_get_ns())
+
+/*
+ * v6.15: Introduce event counters.
+ *
+ * Preserve the following macro until v6.17.
+ */
+#define __COMPAT_scx_bpf_events(events, size) \
+ (bpf_ksym_exists(scx_bpf_events) ? \
+ scx_bpf_events(events, size) : ({}))
+
+/*
+ * v6.15: Introduce NUMA-aware kfuncs to operate with per-node idle
+ * cpumasks.
+ *
+ * Preserve the following __COMPAT_scx_*_node macros until v6.17.
+ */
+#define __COMPAT_scx_bpf_nr_node_ids() \
+ (bpf_ksym_exists(scx_bpf_nr_node_ids) ? \
+ scx_bpf_nr_node_ids() : 1U)
+
+#define __COMPAT_scx_bpf_cpu_node(cpu) \
+ (bpf_ksym_exists(scx_bpf_cpu_node) ? \
+ scx_bpf_cpu_node(cpu) : 0)
+
+#define __COMPAT_scx_bpf_get_idle_cpumask_node(node) \
+ (bpf_ksym_exists(scx_bpf_get_idle_cpumask_node) ? \
+ scx_bpf_get_idle_cpumask_node(node) : \
+ scx_bpf_get_idle_cpumask()) \
+
+#define __COMPAT_scx_bpf_get_idle_smtmask_node(node) \
+ (bpf_ksym_exists(scx_bpf_get_idle_smtmask_node) ? \
+ scx_bpf_get_idle_smtmask_node(node) : \
+ scx_bpf_get_idle_smtmask())
+
+#define __COMPAT_scx_bpf_pick_idle_cpu_node(cpus_allowed, node, flags) \
+ (bpf_ksym_exists(scx_bpf_pick_idle_cpu_node) ? \
+ scx_bpf_pick_idle_cpu_node(cpus_allowed, node, flags) : \
+ scx_bpf_pick_idle_cpu(cpus_allowed, flags))
+
+#define __COMPAT_scx_bpf_pick_any_cpu_node(cpus_allowed, node, flags) \
+ (bpf_ksym_exists(scx_bpf_pick_any_cpu_node) ? \
+ scx_bpf_pick_any_cpu_node(cpus_allowed, node, flags) : \
+ scx_bpf_pick_any_cpu(cpus_allowed, flags))
+
+/*
+ * v6.18: Add a helper to retrieve the current task running on a CPU.
+ *
+ * The kernel tree dropped this helper and scx_bpf_cpu_rq(), but schedulers in
+ * this tree still support pre-v6.18 kernels where scx_bpf_cpu_curr() doesn't
+ * resolve and the scx_bpf_cpu_rq() fallback still exists. Keep it until
+ * pre-v6.18 kernels fall out of the support window.
+ */
+static inline struct task_struct *__COMPAT_scx_bpf_cpu_curr(int cpu)
+{
+ struct rq *rq;
+
+ if (bpf_ksym_exists(scx_bpf_cpu_curr))
+ return scx_bpf_cpu_curr(cpu);
+
+ rq = scx_bpf_cpu_rq(cpu);
+
+ return rq ? rq->curr : NULL;
+}
+
+/*
+ * v6.19: To work around BPF maximum parameter limit, the following kfuncs are
+ * replaced with variants that pack scalar arguments in a struct. Wrappers are
+ * provided to maintain source compatibility.
+ *
+ * v6.13: scx_bpf_dsq_insert_vtime() renaming is also handled here. See the
+ * block on dispatch renaming above for more details.
+ *
+ * The kernel will carry the compat variants until v6.23 to maintain binary
+ * compatibility. After v6.23 release, remove the compat handling and move the
+ * wrappers to common.bpf.h.
+ */
+s32 scx_bpf_select_cpu_and___compat(struct task_struct *p, s32 prev_cpu, u64 wake_flags,
+ const struct cpumask *cpus_allowed, u64 flags) __ksym __weak;
+void scx_bpf_dispatch_vtime___compat(struct task_struct *p, u64 dsq_id, u64 slice, u64 vtime, u64 enq_flags) __ksym __weak;
+void scx_bpf_dsq_insert_vtime___compat(struct task_struct *p, u64 dsq_id, u64 slice, u64 vtime, u64 enq_flags) __ksym __weak;
+
+/**
+ * scx_bpf_select_cpu_and - Pick an idle CPU usable by task @p
+ * @p: task_struct to select a CPU for
+ * @prev_cpu: CPU @p was on previously
+ * @wake_flags: %SCX_WAKE_* flags
+ * @cpus_allowed: cpumask of allowed CPUs
+ * @flags: %SCX_PICK_IDLE* flags
+ *
+ * Inline wrapper that packs scalar arguments into a struct and calls
+ * __scx_bpf_select_cpu_and(). See __scx_bpf_select_cpu_and() for details.
+ */
+static inline s32
+scx_bpf_select_cpu_and(struct task_struct *p, s32 prev_cpu, u64 wake_flags,
+ const struct cpumask *cpus_allowed, u64 flags)
+{
+ if (bpf_core_type_exists(struct scx_bpf_select_cpu_and_args)) {
+ struct scx_bpf_select_cpu_and_args args = {
+ .prev_cpu = prev_cpu,
+ .wake_flags = wake_flags,
+ .flags = flags,
+ };
+
+ return __scx_bpf_select_cpu_and(p, cpus_allowed, &args);
+ } else {
+ return scx_bpf_select_cpu_and___compat(p, prev_cpu, wake_flags,
+ cpus_allowed, flags);
+ }
+}
+
+/*
+ * scx_bpf_select_cpu_and() is now an inline wrapper. Use this instead of
+ * bpf_ksym_exists(scx_bpf_select_cpu_and) to test availability.
+ */
+#define __COMPAT_HAS_scx_bpf_select_cpu_and \
+ (bpf_core_type_exists(struct scx_bpf_select_cpu_and_args) || \
+ bpf_ksym_exists(scx_bpf_select_cpu_and___compat))
+
+/**
+ * scx_bpf_dsq_insert_vtime - Insert a task into the vtime priority queue of a DSQ
+ * @p: task_struct to insert
+ * @dsq_id: DSQ to insert into
+ * @slice: duration @p can run for in nsecs, 0 to keep the current value
+ * @vtime: @p's ordering inside the vtime-sorted queue of the target DSQ
+ * @enq_flags: SCX_ENQ_*
+ *
+ * Inline wrapper that packs scalar arguments into a struct and calls
+ * __scx_bpf_dsq_insert_vtime(). See __scx_bpf_dsq_insert_vtime() for details.
+ */
+static inline bool
+scx_bpf_dsq_insert_vtime(struct task_struct *p, u64 dsq_id, u64 slice, u64 vtime,
+ u64 enq_flags)
+{
+ if (bpf_core_type_exists(struct scx_bpf_dsq_insert_vtime_args)) {
+ struct scx_bpf_dsq_insert_vtime_args args = {
+ .dsq_id = dsq_id,
+ .slice = slice,
+ .vtime = vtime,
+ .enq_flags = enq_flags,
+ };
+
+ return __scx_bpf_dsq_insert_vtime(p, &args);
+ } else if (bpf_ksym_exists(scx_bpf_dsq_insert_vtime___compat)) {
+ scx_bpf_dsq_insert_vtime___compat(p, dsq_id, slice, vtime,
+ enq_flags);
+ return true;
+ } else {
+ scx_bpf_dispatch_vtime___compat(p, dsq_id, slice, vtime,
+ enq_flags);
+ return true;
+ }
+}
+
+/*
+ * v6.19: scx_bpf_dsq_insert() now returns bool instead of void. Move
+ * scx_bpf_dsq_insert() decl to common.bpf.h and drop compat helper after v6.22.
+ * The extra ___compat suffix is to work around libbpf not ignoring __SUFFIX on
+ * kernel side. The entire suffix can be dropped later.
+ *
+ * v6.13: scx_bpf_dsq_insert() renaming is also handled here. See the block on
+ * dispatch renaming above for more details.
+ */
+bool scx_bpf_dsq_insert___v2___compat(struct task_struct *p, u64 dsq_id, u64 slice, u64 enq_flags) __ksym __weak;
+void scx_bpf_dsq_insert___v1(struct task_struct *p, u64 dsq_id, u64 slice, u64 enq_flags) __ksym __weak;
+void scx_bpf_dispatch___compat(struct task_struct *p, u64 dsq_id, u64 slice, u64 enq_flags) __ksym __weak;
+
+static inline bool
+scx_bpf_dsq_insert(struct task_struct *p, u64 dsq_id, u64 slice, u64 enq_flags)
+{
+ if (bpf_ksym_exists(scx_bpf_dsq_insert___v2___compat)) {
+ return scx_bpf_dsq_insert___v2___compat(p, dsq_id, slice, enq_flags);
+ } else if (bpf_ksym_exists(scx_bpf_dsq_insert___v1)) {
+ scx_bpf_dsq_insert___v1(p, dsq_id, slice, enq_flags);
+ return true;
+ } else {
+ scx_bpf_dispatch___compat(p, dsq_id, slice, enq_flags);
+ return true;
+ }
+}
+
+/*
+ * v6.19: scx_bpf_task_set_slice() and scx_bpf_task_set_dsq_vtime() added to for
+ * sub-sched authority checks. Drop the wrappers and move the decls to
+ * common.bpf.h after v6.22.
+ */
+bool scx_bpf_task_set_slice___new(struct task_struct *p, u64 slice) __ksym __weak;
+bool scx_bpf_task_set_dsq_vtime___new(struct task_struct *p, u64 vtime) __ksym __weak;
+
+static inline void scx_bpf_task_set_slice(struct task_struct *p, u64 slice)
+{
+ if (bpf_ksym_exists(scx_bpf_task_set_slice___new))
+ scx_bpf_task_set_slice___new(p, slice);
+ else
+ p->scx.slice = slice;
+}
+
+static inline void scx_bpf_task_set_dsq_vtime(struct task_struct *p, u64 vtime)
+{
+ if (bpf_ksym_exists(scx_bpf_task_set_dsq_vtime___new))
+ scx_bpf_task_set_dsq_vtime___new(p, vtime);
+ else
+ p->scx.dsq_vtime = vtime;
+}
+
+/*
+ * v7.1: New scx_bpf_dsq_reenq() that allows re-enqueues on more DSQs. This
+ * will eventually deprecate scx_bpf_reenqueue_local().
+ */
+void scx_bpf_dsq_reenq___compat(u64 dsq_id, u64 reenq_flags) __ksym __weak;
+
+static inline bool __COMPAT_has_generic_reenq(void)
+{
+ return bpf_ksym_exists(scx_bpf_dsq_reenq___compat);
+}
+
+/*
+ * v6.19: The new void variant can be called from anywhere while the older v1
+ * variant can only be called from ops.cpu_release(). The double ___ prefixes on
+ * the v2 variant need to be removed once libbpf is updated to ignore ___ prefix
+ * on kernel side. Drop the wrapper and move the decl to common.bpf.h after
+ * v6.22.
+ */
+u32 scx_bpf_reenqueue_local___v1(void) __ksym __weak;
+void scx_bpf_reenqueue_local___v2___compat(void) __ksym __weak;
+
+static inline bool __COMPAT_scx_bpf_reenqueue_local_from_anywhere(void)
+{
+ return bpf_ksym_exists(scx_bpf_reenqueue_local___v2___compat);
+}
+
+static inline void scx_bpf_reenqueue_local(void)
+{
+ if (__COMPAT_has_generic_reenq())
+ scx_bpf_dsq_reenq___compat(SCX_DSQ_LOCAL, 0);
+ else if (__COMPAT_scx_bpf_reenqueue_local_from_anywhere())
+ scx_bpf_reenqueue_local___v2___compat();
+ else
+ scx_bpf_reenqueue_local___v1();
+}
+
+static inline int scx_bpf_reenqueue_local_from_anywhere(void)
+{
+ /*
+ * The generic reenq kfunc and the v2 reenqueue-local variant can both be
+ * called from anywhere; v1 cannot. Test each ksym in its own branch with a
+ * distinct call: combining them with || would fold into a bitwise OR of the
+ * two ksym addresses, which the verifier rejects.
+ */
+ if (__COMPAT_has_generic_reenq()) {
+ scx_bpf_dsq_reenq___compat(SCX_DSQ_LOCAL, 0);
+ return 0;
+ }
+ if (__COMPAT_scx_bpf_reenqueue_local_from_anywhere()) {
+ scx_bpf_reenqueue_local___v2___compat();
+ return 0;
+ }
+ return -EOPNOTSUPP;
+}
+
+static inline void scx_bpf_dsq_reenq(u64 dsq_id, u64 reenq_flags)
+{
+ if (bpf_ksym_exists(scx_bpf_dsq_reenq___compat))
+ scx_bpf_dsq_reenq___compat(dsq_id, reenq_flags);
+ else if (dsq_id == SCX_DSQ_LOCAL && reenq_flags == 0)
+ scx_bpf_reenqueue_local();
+ else
+ scx_bpf_error("kernel too old to reenqueue foreign local or user DSQs");
+}
+
+/*
+ * Define sched_ext_ops. See compat.h::SCX_OPS_OPEN() for how backward
+ * compatibility is handled (this macro can be expanded to emit multiple
+ * variants for incompatible op changes; SCX_OPS_OPEN() handles purely
+ * additive changes at load time).
+ */
+#define SCX_OPS_DEFINE(__name, ...) \
+ SEC(".struct_ops.link") \
+ struct sched_ext_ops __name = { \
+ __VA_ARGS__, \
+ };
+
+/*
+ * Define a cid-form sched_ext_ops. Programs targeting this struct_ops type
+ * use cid-form callback signatures (select_cid, set_cmask, cid_online/offline,
+ * dispatch with cid arg, etc.) and may only call the cid-form scx_bpf_*
+ * kfuncs (kick_cid, task_cid, this_cid, ...).
+ */
+#define SCX_OPS_CID_DEFINE(__name, ...) \
+ SEC(".struct_ops.link") \
+ struct sched_ext_ops_cid __name = { \
+ __VA_ARGS__, \
+ };
+
+#endif /* __SCX_COMPAT_BPF_H */
diff --git a/tools/sched_ext/include/scx/compat.h b/tools/sched_ext/include/scx/compat.h
new file mode 100644
index 000000000..7c12df45f
--- /dev/null
+++ b/tools/sched_ext/include/scx/compat.h
@@ -0,0 +1,403 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2024 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2024 David Vernet <dvernet@meta.com>
+ */
+#ifndef __SCX_COMPAT_H
+#define __SCX_COMPAT_H
+
+#include <bpf/btf.h>
+#include <bpf/libbpf.h>
+#include <fcntl.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "enums_abi.autogen.h"
+
+struct btf *__COMPAT_vmlinux_btf __attribute__((weak));
+
+static inline void __COMPAT_load_vmlinux_btf(void)
+{
+ if (!__COMPAT_vmlinux_btf) {
+ __COMPAT_vmlinux_btf = btf__load_vmlinux_btf();
+ SCX_BUG_ON(!__COMPAT_vmlinux_btf, "btf__load_vmlinux_btf()");
+ }
+}
+
+/*
+ * Recover the true value of a 64-bit enum enumerator whose kernel BTF entry
+ * was truncated to its low 32 bits.
+ *
+ * Kernels whose BTF was generated without BTF_KIND_ENUM64 support encode
+ * 64-bit enums as 8-byte BTF_KIND_ENUM entries whose enumerator values only
+ * carry the low 32 bits. This happens with pahole < 1.24, which predates
+ * ENUM64, and with pahole passing --skip_encoding_btf_enum64 (e.g. Google's
+ * Container-Optimized OS / GKE kernels deliberately pass it for backward
+ * compatibility with older BTF consumers). The high bits
+ * can't be recovered from kernel BTF, so substitute the value from the
+ * vmlinux.h this tree was built against, cross-checked against the low 32
+ * bits the kernel did provide.
+ *
+ * Note that this is a best-effort recovery, not a ground truth. The
+ * substitution assumes the running kernel agrees with this tree's vmlinux.h
+ * on the high 32 bits, but only the low 32 bits can actually be verified.
+ * The cross-check is vacuous for enumerators whose value has no low bits
+ * set (e.g. SCX_DSQ_FLAG_BUILTIN, __SCX_ENQ_INTERNAL_MASK,
+ * SCX_ENQ_CLEAR_OPSS, SCX_ECODE_*): their lo32 is 0 and matches anything,
+ * so those substitutions rest entirely on the high bits never moving. An
+ * enumerator missing from the table (a kernel newer than this tree's
+ * vmlinux.h, or a stale autogen table) can't be recovered at all. If a
+ * substitution is ever wrong, the scheduler operates on bogus values (e.g.
+ * dispatching to nonexistent DSQ ids or silently dropping flags) and can
+ * wildly malfunction, which is why the mismatch and table-miss paths refuse
+ * instead of guessing.
+ */
+static inline bool __COMPAT_recover_truncated_enum64(const char *type,
+ const char *name,
+ u32 lo32, u64 *v)
+{
+ static bool warned;
+ size_t i;
+
+ for (i = 0; i < sizeof(__scx_enum_abi_vals) / sizeof(__scx_enum_abi_vals[0]); i++) {
+ const struct __scx_enum_abi_val *e = &__scx_enum_abi_vals[i];
+
+ if (strcmp(e->type, type) || strcmp(e->name, name))
+ continue;
+
+ if (e->val <= (u64)UINT32_MAX) {
+ *v = lo32;
+ return true;
+ }
+
+ if ((u32)e->val != lo32) {
+ fprintf(stderr, "ERROR: kernel BTF value of %s::%s (0x%x) doesn't match the low 32 bits of the vmlinux.h value (0x%llx); refusing to substitute\n",
+ type, name, lo32, (unsigned long long)e->val);
+ return false;
+ }
+
+ if (!warned) {
+ fprintf(stderr,
+ "WARNING: kernel BTF lacks BTF_KIND_ENUM64 encoding (generated by\n"
+ "WARNING: pahole < 1.24 or with --skip_encoding_btf_enum64), so 64-bit\n"
+ "WARNING: scx enum values are truncated to their low 32 bits in kernel\n"
+ "WARNING: BTF. Substituting the full 64-bit values from the vmlinux.h\n"
+ "WARNING: this binary was built against, cross-checked against the low\n"
+ "WARNING: 32 bits the kernel does provide. The high 32 bits cannot be\n"
+ "WARNING: verified: if the running kernel's actual values differ from\n"
+ "WARNING: the build-time vmlinux.h (e.g. an enum that moved in a newer\n"
+ "WARNING: kernel), the scheduler will operate on bogus values, such as\n"
+ "WARNING: dispatching to nonexistent DSQ ids, and can wildly malfunction.\n");
+ warned = true;
+ }
+ *v = e->val;
+ return true;
+ }
+
+ /*
+ * Unknown enumerator (likely a stale autogen table). Fail
+ * pessimistically to avoid returning an invalid value.
+ */
+ fprintf(stderr, "ERROR: kernel BTF truncates 64-bit enum %s::%s to 0x%x; 64-bit variant not found in vmlinux.h\n",
+ type, name, lo32);
+ return false;
+}
+
+static inline bool __COMPAT_read_enum(const char *type, const char *name, u64 *v)
+{
+ const struct btf_type *t;
+ const char *n;
+ s32 tid;
+ __u32 i;
+
+ __COMPAT_load_vmlinux_btf();
+
+ tid = btf__find_by_name(__COMPAT_vmlinux_btf, type);
+ if (tid < 0)
+ return false;
+
+ t = btf__type_by_id(__COMPAT_vmlinux_btf, tid);
+ SCX_BUG_ON(!t, "btf__type_by_id(%d)", tid);
+
+ if (btf_is_enum(t)) {
+ struct btf_enum *e = btf_enum(t);
+
+ for (i = 0; i < btf_vlen(t); i++) {
+ n = btf__name_by_offset(__COMPAT_vmlinux_btf, e[i].name_off);
+ SCX_BUG_ON(!n, "btf__name_by_offset()");
+ if (!strcmp(n, name)) {
+ /*
+ * Try to recover a 64-bit enum from an 8-byte
+ * BTF_KIND_ENUM that was encoded without ENUM64
+ * support (old pahole or
+ * --skip_encoding_btf_enum64). Only scx_*
+ * types are covered by the substitution table;
+ * non-scx types fall through to the raw value
+ * so this generic utility keeps working for
+ * them.
+ */
+ if (t->size == 8 && !strncmp(type, "scx_", 4))
+ return __COMPAT_recover_truncated_enum64(type, name,
+ (u32)e[i].val, v);
+ *v = e[i].val;
+ return true;
+ }
+ }
+ } else if (btf_is_enum64(t)) {
+ struct btf_enum64 *e = btf_enum64(t);
+
+ for (i = 0; i < btf_vlen(t); i++) {
+ n = btf__name_by_offset(__COMPAT_vmlinux_btf, e[i].name_off);
+ SCX_BUG_ON(!n, "btf__name_by_offset()");
+ if (!strcmp(n, name)) {
+ *v = btf_enum64_value(&e[i]);
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+#define __COMPAT_ENUM_OR_ZERO(__type, __ent) \
+({ \
+ u64 __val = 0; \
+ __COMPAT_read_enum(__type, __ent, &__val); \
+ __val; \
+})
+
+static inline bool __COMPAT_has_ksym(const char *ksym)
+{
+ __COMPAT_load_vmlinux_btf();
+ return btf__find_by_name(__COMPAT_vmlinux_btf, ksym) >= 0;
+}
+
+static inline bool __COMPAT_struct_has_field(const char *type, const char *field)
+{
+ const struct btf_type *t;
+ const struct btf_member *m;
+ const char *n;
+ s32 tid;
+ __u32 i;
+
+ __COMPAT_load_vmlinux_btf();
+ tid = btf__find_by_name_kind(__COMPAT_vmlinux_btf, type, BTF_KIND_STRUCT);
+ if (tid < 0)
+ return false;
+
+ t = btf__type_by_id(__COMPAT_vmlinux_btf, tid);
+ SCX_BUG_ON(!t, "btf__type_by_id(%d)", tid);
+
+ m = btf_members(t);
+
+ for (i = 0; i < btf_vlen(t); i++) {
+ n = btf__name_by_offset(__COMPAT_vmlinux_btf, m[i].name_off);
+ SCX_BUG_ON(!n, "btf__name_by_offset()");
+ if (!strcmp(n, field))
+ return true;
+ }
+
+ return false;
+}
+
+#define SCX_OPS_FLAG(name) __COMPAT_ENUM_OR_ZERO("scx_ops_flags", #name)
+
+#define SCX_OPS_KEEP_BUILTIN_IDLE SCX_OPS_FLAG(SCX_OPS_KEEP_BUILTIN_IDLE)
+#define SCX_OPS_ENQ_LAST SCX_OPS_FLAG(SCX_OPS_ENQ_LAST)
+#define SCX_OPS_ENQ_EXITING SCX_OPS_FLAG(SCX_OPS_ENQ_EXITING)
+#define SCX_OPS_SWITCH_PARTIAL SCX_OPS_FLAG(SCX_OPS_SWITCH_PARTIAL)
+#define SCX_OPS_ENQ_MIGRATION_DISABLED SCX_OPS_FLAG(SCX_OPS_ENQ_MIGRATION_DISABLED)
+#define SCX_OPS_ALLOW_QUEUED_WAKEUP SCX_OPS_FLAG(SCX_OPS_ALLOW_QUEUED_WAKEUP)
+#define SCX_OPS_BUILTIN_IDLE_PER_NODE SCX_OPS_FLAG(SCX_OPS_BUILTIN_IDLE_PER_NODE)
+#define SCX_OPS_ALWAYS_ENQ_IMMED SCX_OPS_FLAG(SCX_OPS_ALWAYS_ENQ_IMMED)
+
+#define SCX_PICK_IDLE_FLAG(name) __COMPAT_ENUM_OR_ZERO("scx_pick_idle_cpu_flags", #name)
+
+#define SCX_PICK_IDLE_CORE SCX_PICK_IDLE_FLAG(SCX_PICK_IDLE_CORE)
+#define SCX_PICK_IDLE_IN_NODE SCX_PICK_IDLE_FLAG(SCX_PICK_IDLE_IN_NODE)
+
+static inline long scx_hotplug_seq(void)
+{
+ int fd;
+ char buf[32];
+ char *endptr;
+ ssize_t len;
+ long val;
+
+ fd = open("/sys/kernel/sched_ext/hotplug_seq", O_RDONLY);
+ if (fd < 0)
+ return -ENOENT;
+
+ len = read(fd, buf, sizeof(buf) - 1);
+ SCX_BUG_ON(len <= 0, "read failed (%ld)", len);
+ buf[len] = 0;
+ close(fd);
+
+ errno = 0;
+ val = strtoul(buf, &endptr, 10);
+ SCX_BUG_ON(errno == ERANGE || endptr == buf ||
+ (*endptr != '\n' && *endptr != '\0'), "invalid num hotplug events: %ld", val);
+
+ return val;
+}
+
+/*
+ * Open the sched_ext_ops skeleton.
+ *
+ * struct sched_ext_ops can change over time. Two complementary mechanisms
+ * keep BPF schedulers built against newer headers running on older kernels:
+ *
+ * 1. Load-time fix-up (SCX_OPS_OPEN()). For each optional ops callback or field
+ * added to struct sched_ext_ops, an explicit stanza below probes the
+ * running kernel's BTF via __COMPAT_struct_has_field() and, if the field
+ * is missing, clears it in the in-memory struct_ops (with a warning to
+ * stderr) before load. Handles additive changes - a new stanza must be
+ * added here for each new optional field.
+ *
+ * 2. Multi-variant struct_ops via compat.bpf.h::SCX_OPS_DEFINE(). That
+ * macro can be expanded to emit several variants of struct sched_ext_ops,
+ * and SCX_OPS_LOAD()/ATTACH() can pick the right one based on what the
+ * kernel supports. Needed when an existing operation has to change
+ * incompatibly (e.g. a callback signature changes); the load-time
+ * fix-up above only handles purely additive changes.
+ *
+ * ec7e3b0463e1 ("implement-ops") in https://github.com/sched-ext/sched_ext is
+ * the current minimum required kernel version.
+ *
+ * COMPAT:
+ * - v6.17: ops.cgroup_set_bandwidth()
+ * - v6.19: ops.cgroup_set_idle()
+ * - v7.1: ops.sub_attach(), ops.sub_detach(), ops.sub_cgroup_id
+ * - v7.3: ops.rescue_bandwidth_ppt, ops.rescue_quantum_us
+ */
+#define __SCX_OPS_OPEN(__ops_name, __scx_name, __ops_struct) ({ \
+ struct __scx_name *__oskel; \
+ \
+ SCX_BUG_ON(!__COMPAT_struct_has_field(__ops_struct, "dump"), \
+ __ops_struct ".dump() missing, kernel too old?"); \
+ \
+ __oskel = __scx_name##__open(); \
+ SCX_BUG_ON(!__oskel, "Could not open " #__scx_name); \
+ __oskel->struct_ops.__ops_name->hotplug_seq = scx_hotplug_seq(); \
+ SCX_ENUM_INIT(__oskel); \
+ __oskel; \
+})
+
+#define SCX_OPS_OPEN(__ops_name, __scx_name) ({ \
+ struct __scx_name *__skel; \
+ \
+ __skel = __SCX_OPS_OPEN(__ops_name, __scx_name, "sched_ext_ops"); \
+ if (__skel->struct_ops.__ops_name->cgroup_set_bandwidth && \
+ !__COMPAT_struct_has_field("sched_ext_ops", "cgroup_set_bandwidth")) { \
+ fprintf(stderr, "WARNING: kernel doesn't support ops.cgroup_set_bandwidth()\n"); \
+ __skel->struct_ops.__ops_name->cgroup_set_bandwidth = NULL; \
+ } \
+ if (__skel->struct_ops.__ops_name->cgroup_set_idle && \
+ !__COMPAT_struct_has_field("sched_ext_ops", "cgroup_set_idle")) { \
+ fprintf(stderr, "WARNING: kernel doesn't support ops.cgroup_set_idle()\n"); \
+ __skel->struct_ops.__ops_name->cgroup_set_idle = NULL; \
+ } \
+ if (__skel->struct_ops.__ops_name->sub_attach && \
+ !__COMPAT_struct_has_field("sched_ext_ops", "sub_attach")) { \
+ fprintf(stderr, "WARNING: kernel doesn't support ops.sub_attach()\n"); \
+ __skel->struct_ops.__ops_name->sub_attach = NULL; \
+ } \
+ if (__skel->struct_ops.__ops_name->sub_detach && \
+ !__COMPAT_struct_has_field("sched_ext_ops", "sub_detach")) { \
+ fprintf(stderr, "WARNING: kernel doesn't support ops.sub_detach()\n"); \
+ __skel->struct_ops.__ops_name->sub_detach = NULL; \
+ } \
+ if (__skel->struct_ops.__ops_name->sub_cgroup_id > 0 && \
+ !__COMPAT_struct_has_field("sched_ext_ops", "sub_cgroup_id")) { \
+ fprintf(stderr, "WARNING: kernel doesn't support ops.sub_cgroup_id\n"); \
+ __skel->struct_ops.__ops_name->sub_cgroup_id = 0; \
+ } \
+ if (__skel->struct_ops.__ops_name->rescue_bandwidth_ppt > 0 && \
+ !__COMPAT_struct_has_field("sched_ext_ops", "rescue_bandwidth_ppt")) { \
+ fprintf(stderr, "WARNING: kernel doesn't support ops.rescue_bandwidth_ppt\n"); \
+ __skel->struct_ops.__ops_name->rescue_bandwidth_ppt = 0; \
+ } \
+ if (__skel->struct_ops.__ops_name->rescue_quantum_us > 0 && \
+ !__COMPAT_struct_has_field("sched_ext_ops", "rescue_quantum_us")) { \
+ fprintf(stderr, "WARNING: kernel doesn't support ops.rescue_quantum_us\n"); \
+ __skel->struct_ops.__ops_name->rescue_quantum_us = 0; \
+ } \
+ __skel; \
+})
+
+/*
+ * Open a cid-form (struct sched_ext_ops_cid) skeleton. The cid form postdates
+ * every op the load-time fix-ups above handle, so none of them apply.
+ */
+#define SCX_OPS_CID_OPEN(__ops_name, __scx_name) \
+ __SCX_OPS_OPEN(__ops_name, __scx_name, "sched_ext_ops_cid")
+
+/*
+ * Associate non-struct_ops BPF programs with the scheduler's struct_ops map so
+ * that scx_prog_sched() can determine which scheduler a BPF program belongs
+ * to. Requires libbpf >= 1.7.
+ */
+#if LIBBPF_MAJOR_VERSION > 1 || \
+ (LIBBPF_MAJOR_VERSION == 1 && LIBBPF_MINOR_VERSION >= 7)
+static inline void __scx_ops_assoc_prog(struct bpf_program *prog,
+ struct bpf_map *map,
+ const char *ops_name)
+{
+ s32 err = bpf_program__assoc_struct_ops(prog, map, NULL);
+ if (err)
+ fprintf(stderr,
+ "ERROR: Failed to associate %s with %s: %d\n",
+ bpf_program__name(prog), ops_name, err);
+}
+#else
+static inline void __scx_ops_assoc_prog(struct bpf_program *prog,
+ struct bpf_map *map,
+ const char *ops_name)
+{
+}
+#endif
+
+/* See SCX_OPS_OPEN() above for backward-compatibility handling. */
+#define SCX_OPS_LOAD(__skel, __ops_name, __scx_name, __uei_name) ({ \
+ struct bpf_program *__prog; \
+ UEI_SET_SIZE(__skel, __ops_name, __uei_name); \
+ SCX_BUG_ON(__scx_name##__load((__skel)), "Failed to load skel"); \
+ bpf_object__for_each_program(__prog, (__skel)->obj) { \
+ if (bpf_program__type(__prog) == BPF_PROG_TYPE_STRUCT_OPS) \
+ continue; \
+ __scx_ops_assoc_prog(__prog, (__skel)->maps.__ops_name, \
+ #__ops_name); \
+ } \
+})
+
+/*
+ * New versions of bpftool now emit additional link placeholders for BPF maps,
+ * and set up BPF skeleton in such a way that libbpf will auto-attach BPF maps
+ * automatically, assuming libbpf is recent enough (v1.5+). Old libbpf will do
+ * nothing with those links and won't attempt to auto-attach maps.
+ *
+ * To maintain compatibility with older libbpf while avoiding trying to attach
+ * twice, disable the autoattach feature on newer libbpf.
+ */
+#if LIBBPF_MAJOR_VERSION > 1 || \
+ (LIBBPF_MAJOR_VERSION == 1 && LIBBPF_MINOR_VERSION >= 5)
+#define __SCX_OPS_DISABLE_AUTOATTACH(__skel, __ops_name) \
+ bpf_map__set_autoattach((__skel)->maps.__ops_name, false)
+#else
+#define __SCX_OPS_DISABLE_AUTOATTACH(__skel, __ops_name) do {} while (0)
+#endif
+
+#define SCX_OPS_ATTACH(__skel, __ops_name, __scx_name) ({ \
+ struct bpf_link *__link; \
+ __SCX_OPS_DISABLE_AUTOATTACH(__skel, __ops_name); \
+ SCX_BUG_ON(__scx_name##__attach((__skel)), "Failed to attach skel"); \
+ __link = bpf_map__attach_struct_ops((__skel)->maps.__ops_name); \
+ SCX_BUG_ON(!__link, "Failed to attach struct_ops"); \
+ __link; \
+})
+
+#endif /* __SCX_COMPAT_H */
diff --git a/tools/sched_ext/include/scx/enum_defs.autogen.h b/tools/sched_ext/include/scx/enum_defs.autogen.h
new file mode 100644
index 000000000..63b6b14b1
--- /dev/null
+++ b/tools/sched_ext/include/scx/enum_defs.autogen.h
@@ -0,0 +1,210 @@
+/*
+ * WARNING: This file is autogenerated from gen_enum_defs.py [1].
+ *
+ * [1] https://github.com/sched-ext/scx/blob/main/scripts/gen_enum_defs.py
+ */
+
+#ifndef __ENUM_DEFS_AUTOGEN_H__
+#define __ENUM_DEFS_AUTOGEN_H__
+
+#define HAVE_SCX_ARENA_MIN_ORDER
+#define HAVE_SCX_ARENA_GROW_PAGES
+#define HAVE___SCX_CAP_ENQ_IMMED
+#define HAVE___SCX_CAP_ENQ
+#define HAVE___SCX_CAP_PREEMPT
+#define HAVE___SCX_CAP_PERF
+#define HAVE___SCX_NR_CAPS
+#define HAVE___SCX_CAP_ALL
+#define HAVE_SCX_CAP_ENQ_IMMED
+#define HAVE_SCX_CAP_ENQ
+#define HAVE_SCX_CAP_PREEMPT
+#define HAVE_SCX_CAP_PERF
+#define HAVE_SCX_CAP_BASE
+#define HAVE_SCX_CAPS_REENQ_ON_LOSS
+#define HAVE_SCX_CID_SHARD_SIZE_DFL
+#define HAVE_SCX_CID_SHARD_MAX_CPUS
+#define HAVE_SCX_DSP_DFL_MAX_BATCH
+#define HAVE_SCX_DSP_MAX_LOOPS
+#define HAVE_SCX_WATCHDOG_MAX_TIMEOUT
+#define HAVE_SCX_RESCUE_DFL_BW_PPT
+#define HAVE_SCX_RESCUE_MAX_BW_PPT
+#define HAVE_SCX_RESCUE_DISABLE
+#define HAVE_SCX_RESCUE_DFL_QUANTUM_US
+#define HAVE_SCX_RESCUE_MIN_QUANTUM_US
+#define HAVE_SCX_RESCUE_MAX_QUANTUM_US
+#define HAVE_SCX_RESCUE_MIN_SLICE_US
+#define HAVE_SCX_RESCUE_OVERLOAD_MULT
+#define HAVE_SCX_RESCUE_MIN_OVERLOAD_MS
+#define HAVE_SCX_RESCUE_MAX_OVERLOAD_MS
+#define HAVE_SCX_TID_CHUNK
+#define HAVE_SCX_EXIT_BT_LEN
+#define HAVE_SCX_EXIT_MSG_LEN
+#define HAVE_SCX_EXIT_DUMP_DFL_LEN
+#define HAVE_SCX_CPUPERF_ONE
+#define HAVE_SCX_TASK_ITER_BATCH
+#define HAVE_SCX_BYPASS_HOST_NTH
+#define HAVE_SCX_BYPASS_LB_DFL_INTV_US
+#define HAVE_SCX_BYPASS_LB_DONOR_PCT
+#define HAVE_SCX_BYPASS_LB_MIN_DELTA_DIV
+#define HAVE_SCX_BYPASS_LB_BATCH
+#define HAVE_SCX_REENQ_MAX_REPEAT
+#define HAVE_SCX_SUB_MAX_DEPTH
+#define HAVE_SCX_CPU_PREEMPT_RT
+#define HAVE_SCX_CPU_PREEMPT_DL
+#define HAVE_SCX_CPU_PREEMPT_STOP
+#define HAVE_SCX_CPU_PREEMPT_UNKNOWN
+#define HAVE_SCX_DEQ_SLEEP
+#define HAVE_SCX_DEQ_CORE_SCHED_EXEC
+#define HAVE_SCX_DEQ_SCHED_CHANGE
+#define HAVE_SCX_DSP_NONE
+#define HAVE_SCX_DSP_LOCAL
+#define HAVE_SCX_DSP_PREV
+#define HAVE_SCX_DSP_RETRY
+#define HAVE_SCX_DSQ_FLAG_BUILTIN
+#define HAVE_SCX_DSQ_FLAG_LOCAL_ON
+#define HAVE_SCX_DSQ_INVALID
+#define HAVE_SCX_DSQ_GLOBAL
+#define HAVE_SCX_DSQ_LOCAL
+#define HAVE_SCX_DSQ_BYPASS
+#define HAVE_SCX_DSQ_REJECT
+#define HAVE_SCX_DSQ_RESCUE
+#define HAVE_SCX_DSQ_LOCAL_ON
+#define HAVE_SCX_DSQ_LOCAL_CPU_MASK
+#define HAVE_SCX_DSQ_ITER_REV
+#define HAVE___SCX_DSQ_ITER_HAS_SLICE
+#define HAVE___SCX_DSQ_ITER_HAS_VTIME
+#define HAVE___SCX_DSQ_ITER_USER_FLAGS
+#define HAVE___SCX_DSQ_ITER_ALL_FLAGS
+#define HAVE_SCX_DSQ_LNODE_ITER_CURSOR
+#define HAVE___SCX_DSQ_LNODE_PRIV_SHIFT
+#define HAVE_SCX_ENABLING
+#define HAVE_SCX_ENABLED
+#define HAVE_SCX_DISABLING
+#define HAVE_SCX_DISABLED
+#define HAVE_SCX_ENQ_WAKEUP
+#define HAVE_SCX_ENQ_HEAD
+#define HAVE_SCX_ENQ_CPU_SELECTED
+#define HAVE_SCX_ENQ_PREEMPT
+#define HAVE_SCX_ENQ_IMMED
+#define HAVE_SCX_ENQ_RESCUE
+#define HAVE_SCX_ENQ_REENQ
+#define HAVE_SCX_ENQ_LAST
+#define HAVE___SCX_ENQ_INTERNAL_MASK
+#define HAVE_SCX_ENQ_CLEAR_OPSS
+#define HAVE_SCX_ENQ_DSQ_PRIQ
+#define HAVE_SCX_ENQ_NESTED
+#define HAVE_SCX_ENQ_GDSQ_FALLBACK
+#define HAVE_SCX_ENQ_IGNORE_CAPS
+#define HAVE_SCX_ENQ_APPLY_SLICE
+#define HAVE_SCX_ENQ_SLICE_DFL
+#define HAVE_SCX_TASK_DSQ_ON_PRIQ
+#define HAVE_SCX_TASK_QUEUED
+#define HAVE_SCX_TASK_IN_CUSTODY
+#define HAVE_SCX_TASK_RESET_RUNNABLE_AT
+#define HAVE_SCX_TASK_DEQD_FOR_SLEEP
+#define HAVE_SCX_TASK_SUB_INIT
+#define HAVE_SCX_TASK_IMMED
+#define HAVE_SCX_TASK_PROTECTED
+#define HAVE_SCX_TASK_STATE_SHIFT
+#define HAVE_SCX_TASK_STATE_BITS
+#define HAVE_SCX_TASK_STATE_MASK
+#define HAVE_SCX_TASK_NONE
+#define HAVE_SCX_TASK_INIT_BEGIN
+#define HAVE_SCX_TASK_INIT
+#define HAVE_SCX_TASK_READY
+#define HAVE_SCX_TASK_ENABLED
+#define HAVE_SCX_TASK_DEAD
+#define HAVE_SCX_TASK_REENQ_REASON_SHIFT
+#define HAVE_SCX_TASK_REENQ_REASON_BITS
+#define HAVE_SCX_TASK_REENQ_REASON_MASK
+#define HAVE_SCX_TASK_REENQ_NONE
+#define HAVE_SCX_TASK_REENQ_KFUNC
+#define HAVE_SCX_TASK_REENQ_IMMED
+#define HAVE_SCX_TASK_REENQ_PREEMPTED
+#define HAVE_SCX_TASK_REENQ_CAP
+#define HAVE_SCX_TASK_CURSOR
+#define HAVE_SCX_ECODE_RSN_HOTPLUG
+#define HAVE_SCX_ECODE_RSN_CGROUP_OFFLINE
+#define HAVE_SCX_ECODE_ACT_RESTART
+#define HAVE_SCX_EFLAG_INITIALIZED
+#define HAVE_SCX_EXIT_NONE
+#define HAVE_SCX_EXIT_DONE
+#define HAVE_SCX_EXIT_UNREG
+#define HAVE_SCX_EXIT_UNREG_BPF
+#define HAVE_SCX_EXIT_UNREG_KERN
+#define HAVE_SCX_EXIT_SYSRQ
+#define HAVE_SCX_EXIT_PARENT
+#define HAVE_SCX_EXIT_PARENT_KILL
+#define HAVE_SCX_EXIT_ERROR
+#define HAVE_SCX_EXIT_ERROR_BPF
+#define HAVE_SCX_EXIT_ERROR_STALL
+#define HAVE_SCX_EXIT_ERROR_REENQ
+#define HAVE_SCX_EXIT_ERROR_RESCUE
+#define HAVE_SCX_KF_ALLOW_UNLOCKED
+#define HAVE_SCX_KF_ALLOW_INIT_CIDS
+#define HAVE_SCX_KF_ALLOW_CPU_RELEASE
+#define HAVE_SCX_KF_ALLOW_DISPATCH
+#define HAVE_SCX_KF_ALLOW_ENQUEUE
+#define HAVE_SCX_KF_ALLOW_SELECT_CPU
+#define HAVE_SCX_KICK_IDLE
+#define HAVE_SCX_KICK_PREEMPT
+#define HAVE_SCX_KICK_WAIT
+#define HAVE_SCX_OPI_BEGIN
+#define HAVE_SCX_OPI_NORMAL_BEGIN
+#define HAVE_SCX_OPI_NORMAL_END
+#define HAVE_SCX_OPI_CPU_HOTPLUG_BEGIN
+#define HAVE_SCX_OPI_CPU_HOTPLUG_END
+#define HAVE_SCX_OPI_END
+#define HAVE_SCX_OPS_KEEP_BUILTIN_IDLE
+#define HAVE_SCX_OPS_ENQ_LAST
+#define HAVE_SCX_OPS_ENQ_EXITING
+#define HAVE_SCX_OPS_SWITCH_PARTIAL
+#define HAVE_SCX_OPS_ENQ_MIGRATION_DISABLED
+#define HAVE_SCX_OPS_ALLOW_QUEUED_WAKEUP
+#define HAVE_SCX_OPS_BUILTIN_IDLE_PER_NODE
+#define HAVE_SCX_OPS_ALWAYS_ENQ_IMMED
+#define HAVE_SCX_OPS_TID_TO_TASK
+#define HAVE_SCX_OPS_ALL_FLAGS
+#define HAVE___SCX_OPS_INTERNAL_MASK
+#define HAVE_SCX_OPS_HAS_CPU_PREEMPT
+#define HAVE_SCX_OPSS_NONE
+#define HAVE_SCX_OPSS_QUEUEING
+#define HAVE_SCX_OPSS_QUEUED
+#define HAVE_SCX_OPSS_DISPATCHING
+#define HAVE_SCX_OPSS_QSEQ_SHIFT
+#define HAVE_SCX_PICK_IDLE_CORE
+#define HAVE_SCX_PICK_IDLE_IN_NODE
+#define HAVE_SCX_OPS_NAME_LEN
+#define HAVE_SCX_SLICE_DFL
+#define HAVE_SCX_SLICE_BYPASS
+#define HAVE_SCX_SLICE_INF
+#define HAVE_SCX_REENQ_ANY
+#define HAVE_SCX_REENQ_CAP_REVOKE
+#define HAVE___SCX_REENQ_FILTER_MASK
+#define HAVE___SCX_REENQ_USER_MASK
+#define HAVE_SCX_REENQ_TSR_RQ_OPEN
+#define HAVE_SCX_REENQ_TSR_NOT_FIRST
+#define HAVE___SCX_REENQ_TSR_MASK
+#define HAVE_SCX_RQ_ONLINE
+#define HAVE_SCX_RQ_CAN_STOP_TICK
+#define HAVE_SCX_RQ_CLK_VALID
+#define HAVE_SCX_RQ_BAL_CB_PENDING
+#define HAVE_SCX_RQ_SUB_IDLE_RENOTIFY
+#define HAVE_SCX_RQ_ROOT_IDLE_RENOTIFY
+#define HAVE_SCX_RQ_IN_WAKEUP
+#define HAVE_SCX_RQ_IN_DISPATCH
+#define HAVE_SCX_SCHED_PCPU_BYPASSING
+#define HAVE_SCX_SLICE_OOB_DUR_BITS
+#define HAVE_SCX_SLICE_OOB_ID_BITS
+#define HAVE_SCX_SLICE_OOB_DUR_MASK
+#define HAVE_SCX_SLICE_OOB_ID_SHIFT
+#define HAVE_SCX_SLICE_OOB_ID_MASK
+#define HAVE_SCX_SLICE_OOB_PENDING
+#define HAVE_SCX_TG_ONLINE
+#define HAVE_SCX_TG_INITED
+#define HAVE_SCX_TG_SUB_INIT
+#define HAVE_SCX_WAKE_FORK
+#define HAVE_SCX_WAKE_TTWU
+#define HAVE_SCX_WAKE_SYNC
+
+#endif /* __ENUM_DEFS_AUTOGEN_H__ */
diff --git a/tools/sched_ext/include/scx/enums.autogen.bpf.h b/tools/sched_ext/include/scx/enums.autogen.bpf.h
new file mode 100644
index 000000000..726813101
--- /dev/null
+++ b/tools/sched_ext/include/scx/enums.autogen.bpf.h
@@ -0,0 +1,143 @@
+/*
+ * WARNING: This file is autogenerated from scripts/gen_enums.py. If you would
+ * like to access an enum that is currently missing, add it to the script
+ * and run it from the root directory to update this file.
+ */
+
+const volatile u64 __SCX_OPS_NAME_LEN __weak;
+#define SCX_OPS_NAME_LEN __SCX_OPS_NAME_LEN
+
+const volatile u64 __SCX_SLICE_DFL __weak;
+#define SCX_SLICE_DFL __SCX_SLICE_DFL
+
+const volatile u64 __SCX_SLICE_INF __weak;
+#define SCX_SLICE_INF __SCX_SLICE_INF
+
+const volatile u64 __SCX_RQ_ONLINE __weak;
+#define SCX_RQ_ONLINE __SCX_RQ_ONLINE
+
+const volatile u64 __SCX_RQ_CAN_STOP_TICK __weak;
+#define SCX_RQ_CAN_STOP_TICK __SCX_RQ_CAN_STOP_TICK
+
+const volatile u64 __SCX_RQ_BAL_PENDING __weak;
+#define SCX_RQ_BAL_PENDING __SCX_RQ_BAL_PENDING
+
+const volatile u64 __SCX_RQ_BYPASSING __weak;
+#define SCX_RQ_BYPASSING __SCX_RQ_BYPASSING
+
+const volatile u64 __SCX_RQ_CLK_VALID __weak;
+#define SCX_RQ_CLK_VALID __SCX_RQ_CLK_VALID
+
+const volatile u64 __SCX_RQ_IN_WAKEUP __weak;
+#define SCX_RQ_IN_WAKEUP __SCX_RQ_IN_WAKEUP
+
+const volatile u64 __SCX_RQ_IN_BALANCE __weak;
+#define SCX_RQ_IN_BALANCE __SCX_RQ_IN_BALANCE
+
+const volatile u64 __SCX_RQ_IN_DISPATCH __weak;
+#define SCX_RQ_IN_DISPATCH __SCX_RQ_IN_DISPATCH
+
+const volatile u64 __SCX_DSQ_FLAG_BUILTIN __weak;
+#define SCX_DSQ_FLAG_BUILTIN __SCX_DSQ_FLAG_BUILTIN
+
+const volatile u64 __SCX_DSQ_FLAG_LOCAL_ON __weak;
+#define SCX_DSQ_FLAG_LOCAL_ON __SCX_DSQ_FLAG_LOCAL_ON
+
+const volatile u64 __SCX_DSQ_INVALID __weak;
+#define SCX_DSQ_INVALID __SCX_DSQ_INVALID
+
+const volatile u64 __SCX_DSQ_GLOBAL __weak;
+#define SCX_DSQ_GLOBAL __SCX_DSQ_GLOBAL
+
+const volatile u64 __SCX_DSQ_LOCAL __weak;
+#define SCX_DSQ_LOCAL __SCX_DSQ_LOCAL
+
+const volatile u64 __SCX_DSQ_LOCAL_ON __weak;
+#define SCX_DSQ_LOCAL_ON __SCX_DSQ_LOCAL_ON
+
+const volatile u64 __SCX_DSQ_LOCAL_CPU_MASK __weak;
+#define SCX_DSQ_LOCAL_CPU_MASK __SCX_DSQ_LOCAL_CPU_MASK
+
+const volatile u64 __SCX_TASK_QUEUED __weak;
+#define SCX_TASK_QUEUED __SCX_TASK_QUEUED
+
+const volatile u64 __SCX_TASK_RESET_RUNNABLE_AT __weak;
+#define SCX_TASK_RESET_RUNNABLE_AT __SCX_TASK_RESET_RUNNABLE_AT
+
+const volatile u64 __SCX_TASK_DEQD_FOR_SLEEP __weak;
+#define SCX_TASK_DEQD_FOR_SLEEP __SCX_TASK_DEQD_FOR_SLEEP
+
+const volatile u64 __SCX_TASK_SUB_INIT __weak;
+#define SCX_TASK_SUB_INIT __SCX_TASK_SUB_INIT
+
+const volatile u64 __SCX_TASK_IMMED __weak;
+#define SCX_TASK_IMMED __SCX_TASK_IMMED
+
+const volatile u64 __SCX_TASK_STATE_SHIFT __weak;
+#define SCX_TASK_STATE_SHIFT __SCX_TASK_STATE_SHIFT
+
+const volatile u64 __SCX_TASK_STATE_BITS __weak;
+#define SCX_TASK_STATE_BITS __SCX_TASK_STATE_BITS
+
+const volatile u64 __SCX_TASK_STATE_MASK __weak;
+#define SCX_TASK_STATE_MASK __SCX_TASK_STATE_MASK
+
+const volatile u64 __SCX_TASK_CURSOR __weak;
+#define SCX_TASK_CURSOR __SCX_TASK_CURSOR
+
+const volatile u64 __SCX_TASK_NONE __weak;
+#define SCX_TASK_NONE __SCX_TASK_NONE
+
+const volatile u64 __SCX_TASK_INIT __weak;
+#define SCX_TASK_INIT __SCX_TASK_INIT
+
+const volatile u64 __SCX_TASK_READY __weak;
+#define SCX_TASK_READY __SCX_TASK_READY
+
+const volatile u64 __SCX_TASK_ENABLED __weak;
+#define SCX_TASK_ENABLED __SCX_TASK_ENABLED
+
+const volatile u64 __SCX_TASK_NR_STATES __weak;
+#define SCX_TASK_NR_STATES __SCX_TASK_NR_STATES
+
+const volatile u64 __SCX_TASK_DSQ_ON_PRIQ __weak;
+#define SCX_TASK_DSQ_ON_PRIQ __SCX_TASK_DSQ_ON_PRIQ
+
+const volatile u64 __SCX_KICK_IDLE __weak;
+#define SCX_KICK_IDLE __SCX_KICK_IDLE
+
+const volatile u64 __SCX_KICK_PREEMPT __weak;
+#define SCX_KICK_PREEMPT __SCX_KICK_PREEMPT
+
+const volatile u64 __SCX_KICK_WAIT __weak;
+#define SCX_KICK_WAIT __SCX_KICK_WAIT
+
+const volatile u64 __SCX_ENQ_WAKEUP __weak;
+#define SCX_ENQ_WAKEUP __SCX_ENQ_WAKEUP
+
+const volatile u64 __SCX_ENQ_HEAD __weak;
+#define SCX_ENQ_HEAD __SCX_ENQ_HEAD
+
+const volatile u64 __SCX_ENQ_PREEMPT __weak;
+#define SCX_ENQ_PREEMPT __SCX_ENQ_PREEMPT
+
+const volatile u64 __SCX_ENQ_IMMED __weak;
+#define SCX_ENQ_IMMED __SCX_ENQ_IMMED
+
+const volatile u64 __SCX_ENQ_RESCUE __weak;
+#define SCX_ENQ_RESCUE __SCX_ENQ_RESCUE
+
+const volatile u64 __SCX_ENQ_REENQ __weak;
+#define SCX_ENQ_REENQ __SCX_ENQ_REENQ
+
+const volatile u64 __SCX_ENQ_LAST __weak;
+#define SCX_ENQ_LAST __SCX_ENQ_LAST
+
+const volatile u64 __SCX_ENQ_CLEAR_OPSS __weak;
+#define SCX_ENQ_CLEAR_OPSS __SCX_ENQ_CLEAR_OPSS
+
+const volatile u64 __SCX_ENQ_DSQ_PRIQ __weak;
+#define SCX_ENQ_DSQ_PRIQ __SCX_ENQ_DSQ_PRIQ
+
+const volatile u64 __SCX_DEQ_SCHED_CHANGE __weak;
+#define SCX_DEQ_SCHED_CHANGE __SCX_DEQ_SCHED_CHANGE
diff --git a/tools/sched_ext/include/scx/enums.autogen.h b/tools/sched_ext/include/scx/enums.autogen.h
new file mode 100644
index 000000000..e61632654
--- /dev/null
+++ b/tools/sched_ext/include/scx/enums.autogen.h
@@ -0,0 +1,54 @@
+/*
+ * WARNING: This file is autogenerated from scripts/gen_enums.py. If you would
+ * like to access an enum that is currently missing, add it to the script
+ * and run it from the root directory to update this file.
+ */
+
+#define SCX_ENUM_INIT(skel) do { \
+ SCX_ENUM_SET(skel, scx_public_consts, SCX_OPS_NAME_LEN); \
+ SCX_ENUM_SET(skel, scx_public_consts, SCX_SLICE_DFL); \
+ SCX_ENUM_SET(skel, scx_public_consts, SCX_SLICE_INF); \
+ SCX_ENUM_SET(skel, scx_rq_flags, SCX_RQ_ONLINE); \
+ SCX_ENUM_SET(skel, scx_rq_flags, SCX_RQ_CAN_STOP_TICK); \
+ SCX_ENUM_SET(skel, scx_rq_flags, SCX_RQ_BAL_PENDING); \
+ SCX_ENUM_SET(skel, scx_rq_flags, SCX_RQ_BYPASSING); \
+ SCX_ENUM_SET(skel, scx_rq_flags, SCX_RQ_CLK_VALID); \
+ SCX_ENUM_SET(skel, scx_rq_flags, SCX_RQ_IN_WAKEUP); \
+ SCX_ENUM_SET(skel, scx_rq_flags, SCX_RQ_IN_BALANCE); \
+ SCX_ENUM_SET(skel, scx_rq_flags, SCX_RQ_IN_DISPATCH); \
+ SCX_ENUM_SET(skel, scx_dsq_id_flags, SCX_DSQ_FLAG_BUILTIN); \
+ SCX_ENUM_SET(skel, scx_dsq_id_flags, SCX_DSQ_FLAG_LOCAL_ON); \
+ SCX_ENUM_SET(skel, scx_dsq_id_flags, SCX_DSQ_INVALID); \
+ SCX_ENUM_SET(skel, scx_dsq_id_flags, SCX_DSQ_GLOBAL); \
+ SCX_ENUM_SET(skel, scx_dsq_id_flags, SCX_DSQ_LOCAL); \
+ SCX_ENUM_SET(skel, scx_dsq_id_flags, SCX_DSQ_LOCAL_ON); \
+ SCX_ENUM_SET(skel, scx_dsq_id_flags, SCX_DSQ_LOCAL_CPU_MASK); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_QUEUED); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_RESET_RUNNABLE_AT); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_DEQD_FOR_SLEEP); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_SUB_INIT); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_IMMED); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_STATE_SHIFT); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_STATE_BITS); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_STATE_MASK); \
+ SCX_ENUM_SET(skel, scx_ent_flags, SCX_TASK_CURSOR); \
+ SCX_ENUM_SET(skel, scx_task_state, SCX_TASK_NONE); \
+ SCX_ENUM_SET(skel, scx_task_state, SCX_TASK_INIT); \
+ SCX_ENUM_SET(skel, scx_task_state, SCX_TASK_READY); \
+ SCX_ENUM_SET(skel, scx_task_state, SCX_TASK_ENABLED); \
+ SCX_ENUM_SET(skel, scx_task_state, SCX_TASK_NR_STATES); \
+ SCX_ENUM_SET(skel, scx_ent_dsq_flags, SCX_TASK_DSQ_ON_PRIQ); \
+ SCX_ENUM_SET(skel, scx_kick_flags, SCX_KICK_IDLE); \
+ SCX_ENUM_SET(skel, scx_kick_flags, SCX_KICK_PREEMPT); \
+ SCX_ENUM_SET(skel, scx_kick_flags, SCX_KICK_WAIT); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_WAKEUP); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_HEAD); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_PREEMPT); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_IMMED); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_RESCUE); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_REENQ); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_LAST); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_CLEAR_OPSS); \
+ SCX_ENUM_SET(skel, scx_enq_flags, SCX_ENQ_DSQ_PRIQ); \
+ SCX_ENUM_SET(skel, scx_deq_flags, SCX_DEQ_SCHED_CHANGE); \
+} while (0)
diff --git a/tools/sched_ext/include/scx/enums.bpf.h b/tools/sched_ext/include/scx/enums.bpf.h
new file mode 100644
index 000000000..af704c5d6
--- /dev/null
+++ b/tools/sched_ext/include/scx/enums.bpf.h
@@ -0,0 +1,12 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Convenience macros for getting/setting struct scx_enums instances.
+ *
+ * Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
+ */
+#ifndef __SCX_ENUMS_BPF_H
+#define __SCX_ENUMS_BPF_H
+
+#include "enums.autogen.bpf.h"
+
+#endif /* __SCX_ENUMS_BPF_H */
diff --git a/tools/sched_ext/include/scx/enums.h b/tools/sched_ext/include/scx/enums.h
new file mode 100644
index 000000000..c3b09acce
--- /dev/null
+++ b/tools/sched_ext/include/scx/enums.h
@@ -0,0 +1,28 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Define struct scx_enums that stores the load-time values of enums
+ * used by the BPF program.
+ *
+ * Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
+ */
+
+#ifndef __SCX_ENUMS_H
+#define __SCX_ENUMS_H
+
+static inline void __ENUM_set(u64 *val, const char *type, const char *name)
+{
+ bool res;
+
+ res = __COMPAT_read_enum(type, name, val);
+ if (!res)
+ *val = 0;
+}
+
+#define SCX_ENUM_SET(skel, type, name) do { \
+ __ENUM_set(&skel->rodata->__##name, #type, #name); \
+ } while (0)
+
+
+#include "enums.autogen.h"
+
+#endif /* __SCX_ENUMS_H */
diff --git a/tools/sched_ext/include/scx/enums_abi.autogen.h b/tools/sched_ext/include/scx/enums_abi.autogen.h
new file mode 100644
index 000000000..d53899764
--- /dev/null
+++ b/tools/sched_ext/include/scx/enums_abi.autogen.h
@@ -0,0 +1,223 @@
+/*
+ * WARNING: This file is autogenerated from gen_enum_defs.py [1].
+ *
+ * scx enumerator values from the vmlinux.h this tree is built against.
+ * Used as the substitution source when the running kernel's BTF lacks
+ * BTF_KIND_ENUM64 encoding and 64-bit enum values are truncated.
+ *
+ * [1] https://github.com/sched-ext/scx/blob/main/scripts/gen_enum_defs.py
+ */
+
+#ifndef __ENUMS_ABI_AUTOGEN_H__
+#define __ENUMS_ABI_AUTOGEN_H__
+
+struct __scx_enum_abi_val {
+ const char *type;
+ const char *name;
+ u64 val;
+};
+
+static const struct __scx_enum_abi_val __scx_enum_abi_vals[]
+ __attribute__((unused)) = {
+ { "scx_arena_consts", "SCX_ARENA_MIN_ORDER", 0x3LLU },
+ { "scx_arena_consts", "SCX_ARENA_GROW_PAGES", 0x4LLU },
+ { "scx_cap_flags", "__SCX_CAP_ENQ_IMMED", 0x0LLU },
+ { "scx_cap_flags", "__SCX_CAP_ENQ", 0x1LLU },
+ { "scx_cap_flags", "__SCX_CAP_PREEMPT", 0x2LLU },
+ { "scx_cap_flags", "__SCX_CAP_PERF", 0x3LLU },
+ { "scx_cap_flags", "__SCX_NR_CAPS", 0x4LLU },
+ { "scx_cap_flags", "__SCX_CAP_ALL", 0xfLLU },
+ { "scx_cap_flags", "SCX_CAP_ENQ_IMMED", 0x1LLU },
+ { "scx_cap_flags", "SCX_CAP_ENQ", 0x2LLU },
+ { "scx_cap_flags", "SCX_CAP_PREEMPT", 0x4LLU },
+ { "scx_cap_flags", "SCX_CAP_PERF", 0x8LLU },
+ { "scx_cap_flags", "SCX_CAP_BASE", 0x1LLU },
+ { "scx_cap_flags", "SCX_CAPS_REENQ_ON_LOSS", 0x3LLU },
+ { "scx_cid_consts", "SCX_CID_SHARD_SIZE_DFL", 0x18LLU },
+ { "scx_cid_consts", "SCX_CID_SHARD_MAX_CPUS", 0x200LLU },
+ { "scx_consts", "SCX_DSP_DFL_MAX_BATCH", 0x20LLU },
+ { "scx_consts", "SCX_DSP_MAX_LOOPS", 0x20LLU },
+ { "scx_consts", "SCX_WATCHDOG_MAX_TIMEOUT", 0x7530LLU },
+ { "scx_consts", "SCX_RESCUE_DFL_BW_PPT", 0x14LLU },
+ { "scx_consts", "SCX_RESCUE_MAX_BW_PPT", 0xfaLLU },
+ { "scx_consts", "SCX_RESCUE_DISABLE", 0xffffffffLLU },
+ { "scx_consts", "SCX_RESCUE_DFL_QUANTUM_US", 0x1388LLU },
+ { "scx_consts", "SCX_RESCUE_MIN_QUANTUM_US", 0x3e8LLU },
+ { "scx_consts", "SCX_RESCUE_MAX_QUANTUM_US", 0x186a0LLU },
+ { "scx_consts", "SCX_RESCUE_MIN_SLICE_US", 0x3e8LLU },
+ { "scx_consts", "SCX_RESCUE_OVERLOAD_MULT", 0x10LLU },
+ { "scx_consts", "SCX_RESCUE_MIN_OVERLOAD_MS", 0x3e8LLU },
+ { "scx_consts", "SCX_RESCUE_MAX_OVERLOAD_MS", 0x3a98LLU },
+ { "scx_consts", "SCX_TID_CHUNK", 0x400LLU },
+ { "scx_consts", "SCX_EXIT_BT_LEN", 0x40LLU },
+ { "scx_consts", "SCX_EXIT_MSG_LEN", 0x400LLU },
+ { "scx_consts", "SCX_EXIT_DUMP_DFL_LEN", 0x8000LLU },
+ { "scx_consts", "SCX_CPUPERF_ONE", 0x400LLU },
+ { "scx_consts", "SCX_TASK_ITER_BATCH", 0x20LLU },
+ { "scx_consts", "SCX_BYPASS_HOST_NTH", 0x2LLU },
+ { "scx_consts", "SCX_BYPASS_LB_DFL_INTV_US", 0x7a120LLU },
+ { "scx_consts", "SCX_BYPASS_LB_DONOR_PCT", 0x7dLLU },
+ { "scx_consts", "SCX_BYPASS_LB_MIN_DELTA_DIV", 0x4LLU },
+ { "scx_consts", "SCX_BYPASS_LB_BATCH", 0x100LLU },
+ { "scx_consts", "SCX_REENQ_MAX_REPEAT", 0x100LLU },
+ { "scx_consts", "SCX_SUB_MAX_DEPTH", 0x4LLU },
+ { "scx_cpu_preempt_reason", "SCX_CPU_PREEMPT_RT", 0x0LLU },
+ { "scx_cpu_preempt_reason", "SCX_CPU_PREEMPT_DL", 0x1LLU },
+ { "scx_cpu_preempt_reason", "SCX_CPU_PREEMPT_STOP", 0x2LLU },
+ { "scx_cpu_preempt_reason", "SCX_CPU_PREEMPT_UNKNOWN", 0x3LLU },
+ { "scx_deq_flags", "SCX_DEQ_SLEEP", 0x1LLU },
+ { "scx_deq_flags", "SCX_DEQ_CORE_SCHED_EXEC", 0x100000000LLU },
+ { "scx_deq_flags", "SCX_DEQ_SCHED_CHANGE", 0x200000000LLU },
+ { "scx_dsp_verdict", "SCX_DSP_NONE", 0x0LLU },
+ { "scx_dsp_verdict", "SCX_DSP_LOCAL", 0x1LLU },
+ { "scx_dsp_verdict", "SCX_DSP_PREV", 0x2LLU },
+ { "scx_dsp_verdict", "SCX_DSP_RETRY", 0x3LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_FLAG_BUILTIN", 0x8000000000000000LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_FLAG_LOCAL_ON", 0x4000000000000000LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_INVALID", 0x8000000000000000LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_GLOBAL", 0x8000000000000001LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_LOCAL", 0x8000000000000002LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_BYPASS", 0x8000000000000003LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_REJECT", 0x8000000000000004LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_RESCUE", 0x8000000000000005LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_LOCAL_ON", 0xc000000000000000LLU },
+ { "scx_dsq_id_flags", "SCX_DSQ_LOCAL_CPU_MASK", 0xffffffffLLU },
+ { "scx_dsq_iter_flags", "SCX_DSQ_ITER_REV", 0x10000LLU },
+ { "scx_dsq_iter_flags", "__SCX_DSQ_ITER_HAS_SLICE", 0x40000000LLU },
+ { "scx_dsq_iter_flags", "__SCX_DSQ_ITER_HAS_VTIME", 0x80000000LLU },
+ { "scx_dsq_iter_flags", "__SCX_DSQ_ITER_USER_FLAGS", 0x10000LLU },
+ { "scx_dsq_iter_flags", "__SCX_DSQ_ITER_ALL_FLAGS", 0xc0010000LLU },
+ { "scx_dsq_lnode_flags", "SCX_DSQ_LNODE_ITER_CURSOR", 0x1LLU },
+ { "scx_dsq_lnode_flags", "__SCX_DSQ_LNODE_PRIV_SHIFT", 0x10LLU },
+ { "scx_enable_state", "SCX_ENABLING", 0x0LLU },
+ { "scx_enable_state", "SCX_ENABLED", 0x1LLU },
+ { "scx_enable_state", "SCX_DISABLING", 0x2LLU },
+ { "scx_enable_state", "SCX_DISABLED", 0x3LLU },
+ { "scx_enq_flags", "SCX_ENQ_WAKEUP", 0x1LLU },
+ { "scx_enq_flags", "SCX_ENQ_HEAD", 0x10000LLU },
+ { "scx_enq_flags", "SCX_ENQ_CPU_SELECTED", 0x100000LLU },
+ { "scx_enq_flags", "SCX_ENQ_PREEMPT", 0x100000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_IMMED", 0x200000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_RESCUE", 0x400000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_REENQ", 0x10000000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_LAST", 0x20000000000LLU },
+ { "scx_enq_flags", "__SCX_ENQ_INTERNAL_MASK", 0xff00000000000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_CLEAR_OPSS", 0x100000000000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_DSQ_PRIQ", 0x200000000000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_NESTED", 0x400000000000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_GDSQ_FALLBACK", 0x800000000000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_IGNORE_CAPS", 0x1000000000000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_APPLY_SLICE", 0x2000000000000000LLU },
+ { "scx_enq_flags", "SCX_ENQ_SLICE_DFL", 0x4000000000000000LLU },
+ { "scx_ent_dsq_flags", "SCX_TASK_DSQ_ON_PRIQ", 0x1LLU },
+ { "scx_ent_flags", "SCX_TASK_QUEUED", 0x1LLU },
+ { "scx_ent_flags", "SCX_TASK_IN_CUSTODY", 0x2LLU },
+ { "scx_ent_flags", "SCX_TASK_RESET_RUNNABLE_AT", 0x4LLU },
+ { "scx_ent_flags", "SCX_TASK_DEQD_FOR_SLEEP", 0x8LLU },
+ { "scx_ent_flags", "SCX_TASK_SUB_INIT", 0x10LLU },
+ { "scx_ent_flags", "SCX_TASK_IMMED", 0x20LLU },
+ { "scx_ent_flags", "SCX_TASK_PROTECTED", 0x40LLU },
+ { "scx_ent_flags", "SCX_TASK_STATE_SHIFT", 0x8LLU },
+ { "scx_ent_flags", "SCX_TASK_STATE_BITS", 0x3LLU },
+ { "scx_ent_flags", "SCX_TASK_STATE_MASK", 0x700LLU },
+ { "scx_ent_flags", "SCX_TASK_NONE", 0x0LLU },
+ { "scx_ent_flags", "SCX_TASK_INIT_BEGIN", 0x100LLU },
+ { "scx_ent_flags", "SCX_TASK_INIT", 0x200LLU },
+ { "scx_ent_flags", "SCX_TASK_READY", 0x300LLU },
+ { "scx_ent_flags", "SCX_TASK_ENABLED", 0x400LLU },
+ { "scx_ent_flags", "SCX_TASK_DEAD", 0x500LLU },
+ { "scx_ent_flags", "SCX_TASK_REENQ_REASON_SHIFT", 0xcLLU },
+ { "scx_ent_flags", "SCX_TASK_REENQ_REASON_BITS", 0x3LLU },
+ { "scx_ent_flags", "SCX_TASK_REENQ_REASON_MASK", 0x7000LLU },
+ { "scx_ent_flags", "SCX_TASK_REENQ_NONE", 0x0LLU },
+ { "scx_ent_flags", "SCX_TASK_REENQ_KFUNC", 0x1000LLU },
+ { "scx_ent_flags", "SCX_TASK_REENQ_IMMED", 0x2000LLU },
+ { "scx_ent_flags", "SCX_TASK_REENQ_PREEMPTED", 0x3000LLU },
+ { "scx_ent_flags", "SCX_TASK_REENQ_CAP", 0x4000LLU },
+ { "scx_ent_flags", "SCX_TASK_CURSOR", 0xffffffff80000000LLU },
+ { "scx_exit_code", "SCX_ECODE_RSN_HOTPLUG", 0x100000000LLU },
+ { "scx_exit_code", "SCX_ECODE_RSN_CGROUP_OFFLINE", 0x200000000LLU },
+ { "scx_exit_code", "SCX_ECODE_ACT_RESTART", 0x1000000000000LLU },
+ { "scx_exit_flags", "SCX_EFLAG_INITIALIZED", 0x1LLU },
+ { "scx_exit_kind", "SCX_EXIT_NONE", 0x0LLU },
+ { "scx_exit_kind", "SCX_EXIT_DONE", 0x1LLU },
+ { "scx_exit_kind", "SCX_EXIT_UNREG", 0x40LLU },
+ { "scx_exit_kind", "SCX_EXIT_UNREG_BPF", 0x41LLU },
+ { "scx_exit_kind", "SCX_EXIT_UNREG_KERN", 0x42LLU },
+ { "scx_exit_kind", "SCX_EXIT_SYSRQ", 0x43LLU },
+ { "scx_exit_kind", "SCX_EXIT_PARENT", 0x44LLU },
+ { "scx_exit_kind", "SCX_EXIT_PARENT_KILL", 0x45LLU },
+ { "scx_exit_kind", "SCX_EXIT_ERROR", 0x400LLU },
+ { "scx_exit_kind", "SCX_EXIT_ERROR_BPF", 0x401LLU },
+ { "scx_exit_kind", "SCX_EXIT_ERROR_STALL", 0x402LLU },
+ { "scx_exit_kind", "SCX_EXIT_ERROR_REENQ", 0x403LLU },
+ { "scx_exit_kind", "SCX_EXIT_ERROR_RESCUE", 0x404LLU },
+ { "scx_kf_allow_flags", "SCX_KF_ALLOW_UNLOCKED", 0x1LLU },
+ { "scx_kf_allow_flags", "SCX_KF_ALLOW_INIT_CIDS", 0x2LLU },
+ { "scx_kf_allow_flags", "SCX_KF_ALLOW_CPU_RELEASE", 0x4LLU },
+ { "scx_kf_allow_flags", "SCX_KF_ALLOW_DISPATCH", 0x8LLU },
+ { "scx_kf_allow_flags", "SCX_KF_ALLOW_ENQUEUE", 0x10LLU },
+ { "scx_kf_allow_flags", "SCX_KF_ALLOW_SELECT_CPU", 0x20LLU },
+ { "scx_kick_flags", "SCX_KICK_IDLE", 0x1LLU },
+ { "scx_kick_flags", "SCX_KICK_PREEMPT", 0x2LLU },
+ { "scx_kick_flags", "SCX_KICK_WAIT", 0x4LLU },
+ { "scx_opi", "SCX_OPI_BEGIN", 0x0LLU },
+ { "scx_opi", "SCX_OPI_NORMAL_BEGIN", 0x0LLU },
+ { "scx_opi", "SCX_OPI_NORMAL_END", 0x21LLU },
+ { "scx_opi", "SCX_OPI_CPU_HOTPLUG_BEGIN", 0x21LLU },
+ { "scx_opi", "SCX_OPI_CPU_HOTPLUG_END", 0x23LLU },
+ { "scx_opi", "SCX_OPI_END", 0x23LLU },
+ { "scx_ops_flags", "SCX_OPS_KEEP_BUILTIN_IDLE", 0x1LLU },
+ { "scx_ops_flags", "SCX_OPS_ENQ_LAST", 0x2LLU },
+ { "scx_ops_flags", "SCX_OPS_ENQ_EXITING", 0x4LLU },
+ { "scx_ops_flags", "SCX_OPS_SWITCH_PARTIAL", 0x8LLU },
+ { "scx_ops_flags", "SCX_OPS_ENQ_MIGRATION_DISABLED", 0x10LLU },
+ { "scx_ops_flags", "SCX_OPS_ALLOW_QUEUED_WAKEUP", 0x20LLU },
+ { "scx_ops_flags", "SCX_OPS_BUILTIN_IDLE_PER_NODE", 0x40LLU },
+ { "scx_ops_flags", "SCX_OPS_ALWAYS_ENQ_IMMED", 0x80LLU },
+ { "scx_ops_flags", "SCX_OPS_TID_TO_TASK", 0x100LLU },
+ { "scx_ops_flags", "SCX_OPS_ALL_FLAGS", 0x1ffLLU },
+ { "scx_ops_flags", "__SCX_OPS_INTERNAL_MASK", 0xff00000000000000LLU },
+ { "scx_ops_flags", "SCX_OPS_HAS_CPU_PREEMPT", 0x100000000000000LLU },
+ { "scx_ops_state", "SCX_OPSS_NONE", 0x0LLU },
+ { "scx_ops_state", "SCX_OPSS_QUEUEING", 0x1LLU },
+ { "scx_ops_state", "SCX_OPSS_QUEUED", 0x2LLU },
+ { "scx_ops_state", "SCX_OPSS_DISPATCHING", 0x3LLU },
+ { "scx_ops_state", "SCX_OPSS_QSEQ_SHIFT", 0x2LLU },
+ { "scx_pick_idle_cpu_flags", "SCX_PICK_IDLE_CORE", 0x1LLU },
+ { "scx_pick_idle_cpu_flags", "SCX_PICK_IDLE_IN_NODE", 0x2LLU },
+ { "scx_public_consts", "SCX_OPS_NAME_LEN", 0x80LLU },
+ { "scx_public_consts", "SCX_SLICE_DFL", 0x1312d00LLU },
+ { "scx_public_consts", "SCX_SLICE_BYPASS", 0x4c4b40LLU },
+ { "scx_public_consts", "SCX_SLICE_INF", 0xffffffffffffffffLLU },
+ { "scx_reenq_flags", "SCX_REENQ_ANY", 0x1LLU },
+ { "scx_reenq_flags", "SCX_REENQ_CAP_REVOKE", 0x2LLU },
+ { "scx_reenq_flags", "__SCX_REENQ_FILTER_MASK", 0xffffLLU },
+ { "scx_reenq_flags", "__SCX_REENQ_USER_MASK", 0x1LLU },
+ { "scx_reenq_flags", "SCX_REENQ_TSR_RQ_OPEN", 0x100000000LLU },
+ { "scx_reenq_flags", "SCX_REENQ_TSR_NOT_FIRST", 0x200000000LLU },
+ { "scx_reenq_flags", "__SCX_REENQ_TSR_MASK", 0xf00000000LLU },
+ { "scx_rq_flags", "SCX_RQ_ONLINE", 0x1LLU },
+ { "scx_rq_flags", "SCX_RQ_CAN_STOP_TICK", 0x2LLU },
+ { "scx_rq_flags", "SCX_RQ_CLK_VALID", 0x20LLU },
+ { "scx_rq_flags", "SCX_RQ_BAL_CB_PENDING", 0x40LLU },
+ { "scx_rq_flags", "SCX_RQ_SUB_IDLE_RENOTIFY", 0x80LLU },
+ { "scx_rq_flags", "SCX_RQ_ROOT_IDLE_RENOTIFY", 0x100LLU },
+ { "scx_rq_flags", "SCX_RQ_IN_WAKEUP", 0x10000LLU },
+ { "scx_rq_flags", "SCX_RQ_IN_DISPATCH", 0x20000LLU },
+ { "scx_sched_pcpu_flags", "SCX_SCHED_PCPU_BYPASSING", 0x1LLU },
+ { "scx_slice_oob_consts", "SCX_SLICE_OOB_DUR_BITS", 0x2bLLU },
+ { "scx_slice_oob_consts", "SCX_SLICE_OOB_ID_BITS", 0x14LLU },
+ { "scx_slice_oob_consts", "SCX_SLICE_OOB_DUR_MASK", 0x7ffffffffffLLU },
+ { "scx_slice_oob_consts", "SCX_SLICE_OOB_ID_SHIFT", 0x2bLLU },
+ { "scx_slice_oob_consts", "SCX_SLICE_OOB_ID_MASK", 0xfffffLLU },
+ { "scx_slice_oob_consts", "SCX_SLICE_OOB_PENDING", 0x8000000000000000LLU },
+ { "scx_tg_flags", "SCX_TG_ONLINE", 0x1LLU },
+ { "scx_tg_flags", "SCX_TG_INITED", 0x2LLU },
+ { "scx_tg_flags", "SCX_TG_SUB_INIT", 0x4LLU },
+ { "scx_wake_flags", "SCX_WAKE_FORK", 0x4LLU },
+ { "scx_wake_flags", "SCX_WAKE_TTWU", 0x8LLU },
+ { "scx_wake_flags", "SCX_WAKE_SYNC", 0x10LLU },
+};
+
+#endif /* __ENUMS_ABI_AUTOGEN_H__ */
diff --git a/tools/sched_ext/include/scx/user_exit_info.bpf.h b/tools/sched_ext/include/scx/user_exit_info.bpf.h
new file mode 100644
index 000000000..98cab643c
--- /dev/null
+++ b/tools/sched_ext/include/scx/user_exit_info.bpf.h
@@ -0,0 +1,43 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Define struct user_exit_info which is shared between BPF and userspace parts
+ * to communicate exit status and other information.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+
+#ifndef __USER_EXIT_INFO_BPF_H
+#define __USER_EXIT_INFO_BPF_H
+
+#ifndef LSP
+#include "vmlinux.h"
+#endif
+#include <bpf/bpf_core_read.h>
+
+#include "user_exit_info_common.h"
+
+#define UEI_DEFINE(__name) \
+ char RESIZABLE_ARRAY(data, __name##_dump); \
+ const volatile u32 __name##_dump_len; \
+ struct user_exit_info __name SEC(".data")
+
+#define UEI_RECORD(__uei_name, __ei) ({ \
+ bpf_probe_read_kernel_str(__uei_name.reason, \
+ sizeof(__uei_name.reason), (__ei)->reason); \
+ bpf_probe_read_kernel_str(__uei_name.msg, \
+ sizeof(__uei_name.msg), (__ei)->msg); \
+ bpf_probe_read_kernel_str(__uei_name##_dump, \
+ __uei_name##_dump_len, (__ei)->dump); \
+ if (bpf_core_field_exists((__ei)->exit_code)) \
+ __uei_name.exit_code = (__ei)->exit_code; \
+ __uei_name.exit_cpu = -1; \
+ if (bpf_core_field_exists((__ei)->exit_cpu)) \
+ __uei_name.exit_cpu = (__ei)->exit_cpu; \
+ /* use __sync to force memory barrier */ \
+ __sync_val_compare_and_swap(&__uei_name.kind, __uei_name.kind, \
+ (__ei)->kind); \
+})
+
+#endif /* __USER_EXIT_INFO_BPF_H */
diff --git a/tools/sched_ext/include/scx/user_exit_info.h b/tools/sched_ext/include/scx/user_exit_info.h
new file mode 100644
index 000000000..56a02b549
--- /dev/null
+++ b/tools/sched_ext/include/scx/user_exit_info.h
@@ -0,0 +1,75 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Define struct user_exit_info which is shared between BPF and userspace parts
+ * to communicate exit status and other information.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#ifndef __USER_EXIT_INFO_H
+#define __USER_EXIT_INFO_H
+
+#include <stdio.h>
+#include <stdbool.h>
+
+#include "user_exit_info_common.h"
+
+/* no need to call the following explicitly if SCX_OPS_LOAD() is used */
+#define UEI_SET_SIZE(__skel, __ops_name, __uei_name) ({ \
+ u32 __len = (__skel)->struct_ops.__ops_name->exit_dump_len ?: UEI_DUMP_DFL_LEN; \
+ (__skel)->rodata->__uei_name##_dump_len = __len; \
+ RESIZE_ARRAY((__skel), data, __uei_name##_dump, __len); \
+})
+
+#define UEI_EXITED(__skel, __uei_name) ({ \
+ /* use __sync to force memory barrier */ \
+ __sync_val_compare_and_swap(&(__skel)->data->__uei_name.kind, -1, -1); \
+})
+
+#define UEI_REPORT(__skel, __uei_name) ({ \
+ struct user_exit_info *__uei = &(__skel)->data->__uei_name; \
+ char *__uei_dump = (__skel)->data_##__uei_name##_dump->__uei_name##_dump; \
+ if (__uei_dump[0] != '\0') { \
+ fputs("\nDEBUG DUMP\n", stderr); \
+ fputs("================================================================================\n\n", stderr); \
+ fputs(__uei_dump, stderr); \
+ fputs("\n================================================================================\n\n", stderr); \
+ } \
+ fprintf(stderr, "EXIT: %s", __uei->reason); \
+ if (__uei->msg[0] != '\0') \
+ fprintf(stderr, " (%s)", __uei->msg); \
+ if (__uei->exit_cpu >= 0) \
+ fprintf(stderr, " on CPU %d", __uei->exit_cpu); \
+ fputs("\n", stderr); \
+ __uei->exit_code; \
+})
+
+/*
+ * We can't import vmlinux.h while compiling user C code. Let's duplicate
+ * scx_exit_code definition.
+ */
+enum scx_exit_code {
+ /* Reasons */
+ SCX_ECODE_RSN_HOTPLUG = 1LLU << 32,
+
+ /* Actions */
+ SCX_ECODE_ACT_RESTART = 1LLU << 48,
+};
+
+enum uei_ecode_mask {
+ UEI_ECODE_USER_MASK = ((1LLU << 32) - 1),
+ UEI_ECODE_SYS_RSN_MASK = ((1LLU << 16) - 1) << 32,
+ UEI_ECODE_SYS_ACT_MASK = ((1LLU << 16) - 1) << 48,
+};
+
+/*
+ * These macro interpret the ecode returned from UEI_REPORT().
+ */
+#define UEI_ECODE_USER(__ecode) ((__ecode) & UEI_ECODE_USER_MASK)
+#define UEI_ECODE_SYS_RSN(__ecode) ((__ecode) & UEI_ECODE_SYS_RSN_MASK)
+#define UEI_ECODE_SYS_ACT(__ecode) ((__ecode) & UEI_ECODE_SYS_ACT_MASK)
+
+#define UEI_ECODE_RESTART(__ecode) (UEI_ECODE_SYS_ACT((__ecode)) == SCX_ECODE_ACT_RESTART)
+
+#endif /* __USER_EXIT_INFO_H */
diff --git a/tools/sched_ext/include/scx/user_exit_info_common.h b/tools/sched_ext/include/scx/user_exit_info_common.h
new file mode 100644
index 000000000..76e2a055e
--- /dev/null
+++ b/tools/sched_ext/include/scx/user_exit_info_common.h
@@ -0,0 +1,35 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Define struct user_exit_info which is shared between BPF and userspace parts
+ * to communicate exit status and other information.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#ifndef __USER_EXIT_INFO_COMMON_H
+#define __USER_EXIT_INFO_COMMON_H
+
+#ifdef LSP
+#include "../vmlinux.h"
+#endif
+
+enum uei_sizes {
+ UEI_REASON_LEN = 128,
+ UEI_MSG_LEN = 1024,
+ UEI_DUMP_DFL_LEN = 32768,
+};
+
+struct user_exit_info {
+ int kind;
+ /*
+ * CPU that triggered the exit, or -1 if unset (e.g. running on an
+ * older kernel that does not expose this field).
+ */
+ s32 exit_cpu;
+ s64 exit_code;
+ char reason[UEI_REASON_LEN];
+ char msg[UEI_MSG_LEN];
+};
+
+#endif /* __USER_EXIT_INFO_COMMON_H */
diff --git a/tools/sched_ext/scx_central.bpf.c b/tools/sched_ext/scx_central.bpf.c
new file mode 100644
index 000000000..65dae9e45
--- /dev/null
+++ b/tools/sched_ext/scx_central.bpf.c
@@ -0,0 +1,388 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * A central FIFO sched_ext scheduler which demonstrates the following:
+ *
+ * a. Making all scheduling decisions from one CPU:
+ *
+ * The central CPU is the only one making scheduling decisions. All other
+ * CPUs kick the central CPU when they run out of tasks to run.
+ *
+ * There is one global BPF queue and the central CPU schedules all CPUs by
+ * dispatching from the global queue to each CPU's local dsq from dispatch().
+ * This isn't the most straightforward. e.g. It'd be easier to bounce
+ * through per-CPU BPF queues. The current design is chosen to maximally
+ * utilize and verify various SCX mechanisms such as LOCAL_ON dispatching.
+ *
+ * b. Tickless operation
+ *
+ * All tasks are dispatched with the infinite slice which allows stopping the
+ * ticks on CONFIG_NO_HZ_FULL kernels running with the proper nohz_full
+ * parameter. The tickless operation can be observed through
+ * /proc/interrupts.
+ *
+ * Periodic switching is enforced by a periodic timer checking all CPUs and
+ * preempting them as necessary. Unfortunately, BPF timer currently doesn't
+ * have a way to pin to a specific CPU, so the periodic timer isn't pinned to
+ * the central CPU.
+ *
+ * c. Preemption
+ *
+ * Kthreads are unconditionally queued to the head of a matching local dsq
+ * and dispatched with SCX_DSQ_PREEMPT. This ensures that a kthread is always
+ * prioritized over user threads, which is required for ensuring forward
+ * progress as e.g. the periodic timer may run on a ksoftirqd and if the
+ * ksoftirqd gets starved by a user thread, there may not be anything else to
+ * vacate that user thread.
+ *
+ * SCX_KICK_PREEMPT is used to trigger scheduling and CPUs to move to the
+ * next tasks.
+ *
+ * This scheduler is designed to maximize usage of various SCX mechanisms. A
+ * more practical implementation would likely put the scheduling loop outside
+ * the central CPU's dispatch() path and add some form of priority mechanism.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <scx/common.bpf.h>
+
+char _license[] SEC("license") = "GPL";
+
+enum {
+ FALLBACK_DSQ_ID = 0,
+ MS_TO_NS = 1000LLU * 1000,
+ TIMER_INTERVAL_NS = 1 * MS_TO_NS,
+};
+
+const volatile s32 central_cpu;
+const volatile u32 nr_cpu_ids = 1; /* !0 for veristat, set during init */
+const volatile u64 slice_ns;
+
+bool timer_pinned = true;
+bool timer_started;
+u64 nr_total, nr_locals, nr_queued, nr_lost_pids;
+u64 nr_timers, nr_dispatches, nr_mismatches, nr_retries;
+u64 nr_overflows;
+
+UEI_DEFINE(uei);
+
+struct {
+ __uint(type, BPF_MAP_TYPE_QUEUE);
+ __uint(max_entries, 4096);
+ __type(value, s32);
+} central_q SEC(".maps");
+
+/* can't use percpu map due to bad lookups */
+bool RESIZABLE_ARRAY(data, cpu_gimme_task);
+u64 RESIZABLE_ARRAY(data, cpu_started_at);
+
+struct central_timer {
+ struct bpf_timer timer;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, u32);
+ __type(value, struct central_timer);
+} central_timer SEC(".maps");
+
+s32 BPF_STRUCT_OPS(central_select_cpu, struct task_struct *p,
+ s32 prev_cpu, u64 wake_flags)
+{
+ /*
+ * Steer wakeups to the central CPU as much as possible to avoid
+ * disturbing other CPUs. It's safe to blindly return the central cpu as
+ * select_cpu() is a hint and if @p can't be on it, the kernel will
+ * automatically pick a fallback CPU.
+ */
+ return central_cpu;
+}
+
+void BPF_STRUCT_OPS(central_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ s32 pid = p->pid;
+
+ __sync_fetch_and_add(&nr_total, 1);
+
+ /*
+ * Push per-cpu kthreads at the head of local dsq's and preempt the
+ * corresponding CPU. This ensures that e.g. ksoftirqd isn't blocked
+ * behind other threads which is necessary for forward progress
+ * guarantee as we depend on the BPF timer which may run from ksoftirqd.
+ */
+ if ((p->flags & PF_KTHREAD) && p->nr_cpus_allowed == 1) {
+ __sync_fetch_and_add(&nr_locals, 1);
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_INF,
+ enq_flags | SCX_ENQ_PREEMPT);
+ return;
+ }
+
+ if (bpf_map_push_elem(&central_q, &pid, 0)) {
+ __sync_fetch_and_add(&nr_overflows, 1);
+ scx_bpf_dsq_insert(p, FALLBACK_DSQ_ID, SCX_SLICE_INF, enq_flags);
+ return;
+ }
+
+ __sync_fetch_and_add(&nr_queued, 1);
+
+ if (!scx_bpf_task_running(p))
+ scx_bpf_kick_cpu(central_cpu, SCX_KICK_PREEMPT);
+}
+
+static bool dispatch_to_cpu(s32 cpu)
+{
+ struct task_struct *p;
+ s32 pid;
+
+ bpf_repeat(BPF_MAX_LOOPS) {
+ if (bpf_map_pop_elem(&central_q, &pid))
+ break;
+
+ __sync_fetch_and_sub(&nr_queued, 1);
+
+ p = bpf_task_from_pid(pid);
+ if (!p) {
+ __sync_fetch_and_add(&nr_lost_pids, 1);
+ continue;
+ }
+
+ /*
+ * If we can't run the task at the top for whatever reason,
+ * bounce it to the fallback dsq. Also check
+ * is_migration_disabled() explicitly as p->cpus_ptr may not
+ * reflect the migration-disabled state yet if
+ * migrate_disable_switch() hasn't run.
+ */
+ if (!bpf_cpumask_test_cpu(cpu, p->cpus_ptr) ||
+ (is_migration_disabled(p) && scx_bpf_task_cpu(p) != cpu)) {
+ __sync_fetch_and_add(&nr_mismatches, 1);
+ scx_bpf_dsq_insert(p, FALLBACK_DSQ_ID, SCX_SLICE_INF, 0);
+ bpf_task_release(p);
+ /*
+ * We might run out of dispatch buffer slots if we continue dispatching
+ * to the fallback DSQ, without dispatching to the local DSQ of the
+ * target CPU. In such a case, break the loop now as will fail the
+ * next dispatch operation.
+ */
+ if (!scx_bpf_dispatch_nr_slots())
+ break;
+ continue;
+ }
+
+ /* dispatch to local and mark that @cpu doesn't need more */
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cpu, SCX_SLICE_INF, 0);
+
+ if (cpu != central_cpu)
+ scx_bpf_kick_cpu(cpu, SCX_KICK_IDLE);
+
+ bpf_task_release(p);
+ return true;
+ }
+
+ return false;
+}
+
+static void start_central_timer(void)
+{
+ struct bpf_timer *timer;
+ u32 key = 0;
+ int ret;
+
+ if (likely(timer_started))
+ return;
+
+ timer = bpf_map_lookup_elem(&central_timer, &key);
+ if (!timer) {
+ scx_bpf_error("failed to lookup central timer");
+ return;
+ }
+
+ ret = bpf_timer_start(timer, TIMER_INTERVAL_NS, BPF_F_TIMER_CPU_PIN);
+ /*
+ * BPF_F_TIMER_CPU_PIN is pretty new (>=6.7). If we're running in a
+ * kernel which doesn't have it, bpf_timer_start() will return -EINVAL.
+ * Retry without the PIN. This would be the perfect use case for
+ * bpf_core_enum_value_exists() but the enum type doesn't have a name
+ * and can't be used with bpf_core_enum_value_exists(). Oh well...
+ */
+ if (ret == -EINVAL) {
+ timer_pinned = false;
+ ret = bpf_timer_start(timer, TIMER_INTERVAL_NS, 0);
+ }
+
+ if (ret) {
+ scx_bpf_error("bpf_timer_start failed (%d)", ret);
+ return;
+ }
+
+ timer_started = true;
+}
+
+void BPF_STRUCT_OPS(central_dispatch, s32 cpu, struct task_struct *prev)
+{
+ if (cpu == central_cpu) {
+ start_central_timer();
+
+ /* dispatch for all other CPUs first */
+ __sync_fetch_and_add(&nr_dispatches, 1);
+
+ bpf_for(cpu, 0, nr_cpu_ids) {
+ bool *gimme;
+
+ if (!scx_bpf_dispatch_nr_slots())
+ break;
+
+ /* central's gimme is never set */
+ gimme = ARRAY_ELEM_PTR(cpu_gimme_task, cpu, nr_cpu_ids);
+ if (!gimme || !*gimme)
+ continue;
+
+ if (dispatch_to_cpu(cpu))
+ *gimme = false;
+ }
+
+ /*
+ * Retry if we ran out of dispatch buffer slots as we might have
+ * skipped some CPUs and also need to dispatch for self. The ext
+ * core automatically retries if the local dsq is empty but we
+ * can't rely on that as we're dispatching for other CPUs too.
+ * Kick self explicitly to retry.
+ */
+ if (!scx_bpf_dispatch_nr_slots()) {
+ __sync_fetch_and_add(&nr_retries, 1);
+ scx_bpf_kick_cpu(central_cpu, SCX_KICK_PREEMPT);
+ return;
+ }
+
+ /* look for a task to run on the central CPU */
+ if (scx_bpf_dsq_move_to_local(FALLBACK_DSQ_ID, 0))
+ return;
+ dispatch_to_cpu(central_cpu);
+ } else {
+ bool *gimme;
+
+ if (scx_bpf_dsq_move_to_local(FALLBACK_DSQ_ID, 0))
+ return;
+
+ gimme = ARRAY_ELEM_PTR(cpu_gimme_task, cpu, nr_cpu_ids);
+ if (gimme)
+ *gimme = true;
+
+ /*
+ * Force dispatch on the scheduling CPU so that it finds a task
+ * to run for us.
+ */
+ scx_bpf_kick_cpu(central_cpu, SCX_KICK_PREEMPT);
+ }
+}
+
+void BPF_STRUCT_OPS(central_running, struct task_struct *p)
+{
+ s32 cpu = scx_bpf_task_cpu(p);
+ u64 *started_at = ARRAY_ELEM_PTR(cpu_started_at, cpu, nr_cpu_ids);
+ if (started_at)
+ *started_at = scx_bpf_now() ?: 1; /* 0 indicates idle */
+}
+
+void BPF_STRUCT_OPS(central_stopping, struct task_struct *p, bool runnable)
+{
+ s32 cpu = scx_bpf_task_cpu(p);
+ u64 *started_at = ARRAY_ELEM_PTR(cpu_started_at, cpu, nr_cpu_ids);
+ if (started_at)
+ *started_at = 0;
+}
+
+static int central_timerfn(void *map, int *key, struct bpf_timer *timer)
+{
+ u64 now = scx_bpf_now();
+ u64 nr_to_kick = nr_queued;
+ s32 i, curr_cpu;
+ int ret;
+
+ curr_cpu = bpf_get_smp_processor_id();
+ if (timer_pinned && (curr_cpu != central_cpu)) {
+ scx_bpf_error("Central timer ran on CPU %d, not central CPU %d",
+ curr_cpu, central_cpu);
+ return 0;
+ }
+
+ bpf_for(i, 0, nr_cpu_ids) {
+ s32 cpu = (nr_timers + i) % nr_cpu_ids;
+ u64 *started_at;
+
+ if (cpu == central_cpu)
+ continue;
+
+ /* kick iff the current one exhausted its slice */
+ started_at = ARRAY_ELEM_PTR(cpu_started_at, cpu, nr_cpu_ids);
+ if (started_at && *started_at &&
+ time_before(now, *started_at + slice_ns))
+ continue;
+
+ /* and there's something pending */
+ if (scx_bpf_dsq_nr_queued(FALLBACK_DSQ_ID) ||
+ scx_bpf_dsq_nr_queued(SCX_DSQ_LOCAL_ON | cpu))
+ ;
+ else if (nr_to_kick)
+ nr_to_kick--;
+ else
+ continue;
+
+ scx_bpf_kick_cpu(cpu, SCX_KICK_PREEMPT);
+ }
+
+ ret = bpf_timer_start(timer, TIMER_INTERVAL_NS,
+ timer_pinned ? BPF_F_TIMER_CPU_PIN : 0);
+ if (ret)
+ scx_bpf_error("bpf_timer_start failed (%d)", ret);
+ __sync_fetch_and_add(&nr_timers, 1);
+ return 0;
+}
+
+int BPF_STRUCT_OPS_SLEEPABLE(central_init)
+{
+ u32 key = 0;
+ struct bpf_timer *timer;
+ int ret;
+
+ ret = scx_bpf_create_dsq(FALLBACK_DSQ_ID, -1);
+ if (ret) {
+ scx_bpf_error("scx_bpf_create_dsq failed (%d)", ret);
+ return ret;
+ }
+
+ timer = bpf_map_lookup_elem(&central_timer, &key);
+ if (!timer)
+ return -ESRCH;
+
+ bpf_timer_init(timer, &central_timer, CLOCK_MONOTONIC);
+ bpf_timer_set_callback(timer, central_timerfn);
+
+ scx_bpf_kick_cpu(central_cpu, 0);
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(central_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+SCX_OPS_DEFINE(central_ops,
+ /*
+ * We are offloading all scheduling decisions to the central CPU
+ * and thus being the last task on a given CPU doesn't mean
+ * anything special. Enqueue the last tasks like any other tasks.
+ */
+ .flags = SCX_OPS_ENQ_LAST,
+
+ .select_cpu = (void *)central_select_cpu,
+ .enqueue = (void *)central_enqueue,
+ .dispatch = (void *)central_dispatch,
+ .running = (void *)central_running,
+ .stopping = (void *)central_stopping,
+ .init = (void *)central_init,
+ .exit = (void *)central_exit,
+ .name = "central");
diff --git a/tools/sched_ext/scx_central.c b/tools/sched_ext/scx_central.c
new file mode 100644
index 000000000..e1acaed4e
--- /dev/null
+++ b/tools/sched_ext/scx_central.c
@@ -0,0 +1,126 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <unistd.h>
+#include <inttypes.h>
+#include <signal.h>
+#include <assert.h>
+#include <libgen.h>
+#include <bpf/bpf.h>
+#include <scx/common.h>
+#include "scx_central.bpf.skel.h"
+
+const char help_fmt[] =
+"A central FIFO sched_ext scheduler.\n"
+"\n"
+"See the top-level comment in .bpf.c for more details.\n"
+"\n"
+"Usage: %s [-s SLICE_US] [-c CPU] [-v]\n"
+"\n"
+" -s SLICE_US Override slice duration\n"
+" -c CPU Override the central CPU (default: 0)\n"
+" -v Print libbpf debug messages\n"
+" -h Display this help and exit\n";
+
+static bool verbose;
+static volatile int exit_req;
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ if (level == LIBBPF_DEBUG && !verbose)
+ return 0;
+ return vfprintf(stderr, format, args);
+}
+
+static void sigint_handler(int dummy)
+{
+ exit_req = 1;
+}
+
+int main(int argc, char **argv)
+{
+ struct scx_central *skel;
+ struct bpf_link *link;
+ __u64 seq = 0, ecode;
+ __s32 opt;
+
+ libbpf_set_print(libbpf_print_fn);
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+restart:
+ optind = 1;
+ skel = SCX_OPS_OPEN(central_ops, scx_central);
+
+ skel->rodata->central_cpu = 0;
+ skel->rodata->nr_cpu_ids = libbpf_num_possible_cpus();
+ skel->rodata->slice_ns = __COMPAT_ENUM_OR_ZERO("scx_public_consts", "SCX_SLICE_DFL");
+
+ assert(skel->rodata->nr_cpu_ids > 0);
+ assert(skel->rodata->nr_cpu_ids <= INT32_MAX);
+
+ while ((opt = getopt(argc, argv, "s:c:vh")) != -1) {
+ switch (opt) {
+ case 's':
+ skel->rodata->slice_ns = strtoull(optarg, NULL, 0) * 1000;
+ break;
+ case 'c': {
+ u32 central_cpu = strtoul(optarg, NULL, 0);
+ if (central_cpu >= skel->rodata->nr_cpu_ids) {
+ fprintf(stderr, "invalid central CPU id value, %u given (%u max)\n", central_cpu, skel->rodata->nr_cpu_ids);
+ scx_central__destroy(skel);
+ return -1;
+ }
+ skel->rodata->central_cpu = (s32)central_cpu;
+ break;
+ }
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ fprintf(stderr, help_fmt, basename(argv[0]));
+ return opt != 'h';
+ }
+ }
+
+ /* Resize arrays so their element count is equal to cpu count. */
+ RESIZE_ARRAY(skel, data, cpu_gimme_task, skel->rodata->nr_cpu_ids);
+ RESIZE_ARRAY(skel, data, cpu_started_at, skel->rodata->nr_cpu_ids);
+
+ SCX_OPS_LOAD(skel, central_ops, scx_central, uei);
+
+ link = SCX_OPS_ATTACH(skel, central_ops, scx_central);
+
+ if (!skel->data->timer_pinned)
+ printf("WARNING : BPF_F_TIMER_CPU_PIN not available, timer not pinned to central\n");
+
+ while (!exit_req && !UEI_EXITED(skel, uei)) {
+ printf("[SEQ %llu]\n", seq++);
+ printf("total :%10" PRIu64 " local:%10" PRIu64 " queued:%10" PRIu64 " lost:%10" PRIu64 "\n",
+ skel->bss->nr_total,
+ skel->bss->nr_locals,
+ skel->bss->nr_queued,
+ skel->bss->nr_lost_pids);
+ printf("timer :%10" PRIu64 " dispatch:%10" PRIu64 " mismatch:%10" PRIu64 " retry:%10" PRIu64 "\n",
+ skel->bss->nr_timers,
+ skel->bss->nr_dispatches,
+ skel->bss->nr_mismatches,
+ skel->bss->nr_retries);
+ printf("overflow:%10" PRIu64 "\n",
+ skel->bss->nr_overflows);
+ fflush(stdout);
+ sleep(1);
+ }
+
+ bpf_link__destroy(link);
+ ecode = UEI_REPORT(skel, uei);
+ scx_central__destroy(skel);
+
+ if (!exit_req && UEI_ECODE_RESTART(ecode))
+ goto restart;
+ return 0;
+}
diff --git a/tools/sched_ext/scx_cpu0.bpf.c b/tools/sched_ext/scx_cpu0.bpf.c
new file mode 100644
index 000000000..909d1be1b
--- /dev/null
+++ b/tools/sched_ext/scx_cpu0.bpf.c
@@ -0,0 +1,94 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * A CPU0 scheduler.
+ *
+ * This scheduler queues all tasks to a shared DSQ and only dispatches them on
+ * CPU0 in FIFO order. This is useful for testing bypass behavior when many
+ * tasks are concentrated on a single CPU. If the load balancer doesn't work,
+ * bypass mode can trigger task hangs or RCU stalls as the queue is long and
+ * there's only one CPU working on it.
+ *
+ * - Statistics tracking how many tasks are queued to local and CPU0 DSQs.
+ * - Termination notification for userspace.
+ *
+ * Copyright (c) 2025 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2025 Tejun Heo <tj@kernel.org>
+ */
+#include <scx/common.bpf.h>
+
+char _license[] SEC("license") = "GPL";
+
+UEI_DEFINE(uei);
+
+/*
+ * We create a custom DSQ with ID 0 that we dispatch to and consume from on
+ * CPU0.
+ */
+#define DSQ_CPU0 0
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
+ __uint(key_size, sizeof(u32));
+ __uint(value_size, sizeof(u64));
+ __uint(max_entries, 2); /* [local, cpu0] */
+} stats SEC(".maps");
+
+static void stat_inc(u32 idx)
+{
+ u64 *cnt_p = bpf_map_lookup_elem(&stats, &idx);
+ if (cnt_p)
+ (*cnt_p)++;
+}
+
+s32 BPF_STRUCT_OPS(cpu0_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags)
+{
+ return 0;
+}
+
+void BPF_STRUCT_OPS(cpu0_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ /*
+ * select_cpu() always picks CPU0. If @p is not on CPU0, it can't run on
+ * CPU 0. Queue on whichever CPU it's currently only.
+ */
+ if (scx_bpf_task_cpu(p) != 0) {
+ stat_inc(0); /* count local queueing */
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
+ return;
+ }
+
+ stat_inc(1); /* count cpu0 queueing */
+ scx_bpf_dsq_insert(p, DSQ_CPU0, SCX_SLICE_DFL, enq_flags);
+}
+
+void BPF_STRUCT_OPS(cpu0_dispatch, s32 cpu, struct task_struct *prev)
+{
+ if (cpu == 0)
+ scx_bpf_dsq_move_to_local(DSQ_CPU0, 0);
+}
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(cpu0_init)
+{
+ int ret;
+
+ ret = scx_bpf_create_dsq(DSQ_CPU0, -1);
+ if (ret) {
+ scx_bpf_error("failed to create DSQ %d (%d)", DSQ_CPU0, ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(cpu0_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+SCX_OPS_DEFINE(cpu0_ops,
+ .select_cpu = (void *)cpu0_select_cpu,
+ .enqueue = (void *)cpu0_enqueue,
+ .dispatch = (void *)cpu0_dispatch,
+ .init = (void *)cpu0_init,
+ .exit = (void *)cpu0_exit,
+ .name = "cpu0");
diff --git a/tools/sched_ext/scx_cpu0.c b/tools/sched_ext/scx_cpu0.c
new file mode 100644
index 000000000..1c2b507b8
--- /dev/null
+++ b/tools/sched_ext/scx_cpu0.c
@@ -0,0 +1,105 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2025 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2025 Tejun Heo <tj@kernel.org>
+ */
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+#include <assert.h>
+#include <libgen.h>
+#include <bpf/bpf.h>
+#include <scx/common.h>
+#include "scx_cpu0.bpf.skel.h"
+
+const char help_fmt[] =
+"A cpu0 sched_ext scheduler.\n"
+"\n"
+"See the top-level comment in .bpf.c for more details.\n"
+"\n"
+"Usage: %s [-v]\n"
+"\n"
+" -v Print libbpf debug messages\n"
+" -h Display this help and exit\n";
+
+static bool verbose;
+static volatile int exit_req;
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ if (level == LIBBPF_DEBUG && !verbose)
+ return 0;
+ return vfprintf(stderr, format, args);
+}
+
+static void sigint_handler(int sig)
+{
+ exit_req = 1;
+}
+
+static void read_stats(struct scx_cpu0 *skel, __u64 *stats)
+{
+ int nr_cpus = libbpf_num_possible_cpus();
+ assert(nr_cpus > 0);
+ __u64 cnts[2][nr_cpus];
+ __u32 idx;
+
+ memset(stats, 0, sizeof(stats[0]) * 2);
+
+ for (idx = 0; idx < 2; idx++) {
+ int ret, cpu;
+
+ ret = bpf_map_lookup_elem(bpf_map__fd(skel->maps.stats),
+ &idx, cnts[idx]);
+ if (ret < 0)
+ continue;
+ for (cpu = 0; cpu < nr_cpus; cpu++)
+ stats[idx] += cnts[idx][cpu];
+ }
+}
+
+int main(int argc, char **argv)
+{
+ struct scx_cpu0 *skel;
+ struct bpf_link *link;
+ __s32 opt;
+ __u64 ecode;
+
+ libbpf_set_print(libbpf_print_fn);
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+restart:
+ optind = 1;
+ skel = SCX_OPS_OPEN(cpu0_ops, scx_cpu0);
+
+ while ((opt = getopt(argc, argv, "vh")) != -1) {
+ switch (opt) {
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ fprintf(stderr, help_fmt, basename(argv[0]));
+ return opt != 'h';
+ }
+ }
+
+ SCX_OPS_LOAD(skel, cpu0_ops, scx_cpu0, uei);
+ link = SCX_OPS_ATTACH(skel, cpu0_ops, scx_cpu0);
+
+ while (!exit_req && !UEI_EXITED(skel, uei)) {
+ __u64 stats[2];
+
+ read_stats(skel, stats);
+ printf("local=%llu cpu0=%llu\n", stats[0], stats[1]);
+ fflush(stdout);
+ sleep(1);
+ }
+
+ bpf_link__destroy(link);
+ ecode = UEI_REPORT(skel, uei);
+ scx_cpu0__destroy(skel);
+
+ if (!exit_req && UEI_ECODE_RESTART(ecode))
+ goto restart;
+ return 0;
+}
diff --git a/tools/sched_ext/scx_flatcg.bpf.c b/tools/sched_ext/scx_flatcg.bpf.c
new file mode 100644
index 000000000..454ebb820
--- /dev/null
+++ b/tools/sched_ext/scx_flatcg.bpf.c
@@ -0,0 +1,978 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * A demo sched_ext flattened cgroup hierarchy scheduler. It implements
+ * hierarchical weight-based cgroup CPU control by flattening the cgroup
+ * hierarchy into a single layer by compounding the active weight share at each
+ * level. Consider the following hierarchy with weights in parentheses:
+ *
+ * R + A (100) + B (100)
+ * | \ C (100)
+ * \ D (200)
+ *
+ * Ignoring the root and threaded cgroups, only B, C and D can contain tasks.
+ * Let's say all three have runnable tasks. The total share that each of these
+ * three cgroups is entitled to can be calculated by compounding its share at
+ * each level.
+ *
+ * For example, B is competing against C and in that competition its share is
+ * 100/(100+100) == 1/2. At its parent level, A is competing against D and A's
+ * share in that competition is 100/(200+100) == 1/3. B's eventual share in the
+ * system can be calculated by multiplying the two shares, 1/2 * 1/3 == 1/6. C's
+ * eventual share is the same at 1/6. D is only competing at the top level and
+ * its share is 200/(100+200) == 2/3.
+ *
+ * So, instead of hierarchically scheduling level-by-level, we can consider it
+ * as B, C and D competing each other with respective share of 1/6, 1/6 and 2/3
+ * and keep updating the eventual shares as the cgroups' runnable states change.
+ *
+ * This flattening of hierarchy can bring a substantial performance gain when
+ * the cgroup hierarchy is nested multiple levels. in a simple benchmark using
+ * wrk[8] on apache serving a CGI script calculating sha1sum of a small file, it
+ * outperforms CFS by ~3% with CPU controller disabled and by ~10% with two
+ * apache instances competing with 2:1 weight ratio nested four level deep.
+ *
+ * However, the gain comes at the cost of not being able to properly handle
+ * thundering herd of cgroups. For example, if many cgroups which are nested
+ * behind a low priority parent cgroup wake up around the same time, they may be
+ * able to consume more CPU cycles than they are entitled to. In many use cases,
+ * this isn't a real concern especially given the performance gain. Also, there
+ * are ways to mitigate the problem further by e.g. introducing an extra
+ * scheduling layer on cgroup delegation boundaries.
+ *
+ * The scheduler first picks the cgroup to run and then schedule the tasks
+ * within by using nested weighted vtime scheduling by default. The
+ * cgroup-internal scheduling can be switched to FIFO with the -f option.
+ */
+#include <scx/common.bpf.h>
+#include "scx_flatcg.h"
+
+/*
+ * Maximum amount of retries to find a valid cgroup.
+ */
+enum {
+ FALLBACK_DSQ = 0,
+ CGROUP_MAX_RETRIES = 1024,
+};
+
+char _license[] SEC("license") = "GPL";
+
+const volatile u32 nr_cpus = 32; /* !0 for veristat, set during init */
+const volatile u64 cgrp_slice_ns;
+const volatile bool fifo_sched;
+
+u64 cvtime_now;
+UEI_DEFINE(uei);
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
+ __type(key, u32);
+ __type(value, u64);
+ __uint(max_entries, FCG_NR_STATS);
+} stats SEC(".maps");
+
+static void stat_inc(enum fcg_stat_idx idx)
+{
+ u32 idx_v = idx;
+
+ u64 *cnt_p = bpf_map_lookup_elem(&stats, &idx_v);
+ if (cnt_p)
+ (*cnt_p)++;
+}
+
+struct fcg_cpu_ctx {
+ u64 cur_cgid;
+ u64 cur_at;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
+ __type(key, u32);
+ __type(value, struct fcg_cpu_ctx);
+ __uint(max_entries, 1);
+} cpu_ctx SEC(".maps");
+
+struct {
+ __uint(type, BPF_MAP_TYPE_CGRP_STORAGE);
+ __uint(map_flags, BPF_F_NO_PREALLOC);
+ __type(key, int);
+ __type(value, struct fcg_cgrp_ctx);
+} cgrp_ctx SEC(".maps");
+
+struct cgv_node {
+ struct bpf_rb_node rb_node;
+ __u64 cvtime;
+ __u64 cgid;
+};
+
+private(CGV_TREE) struct bpf_spin_lock cgv_tree_lock;
+private(CGV_TREE) struct bpf_rb_root cgv_tree __contains(cgv_node, rb_node);
+
+struct cgv_node_stash {
+ struct cgv_node __kptr *node;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, 16384);
+ __type(key, __u64);
+ __type(value, struct cgv_node_stash);
+} cgv_node_stash SEC(".maps");
+
+struct fcg_task_ctx {
+ u64 bypassed_at;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_TASK_STORAGE);
+ __uint(map_flags, BPF_F_NO_PREALLOC);
+ __type(key, int);
+ __type(value, struct fcg_task_ctx);
+} task_ctx SEC(".maps");
+
+/* gets inc'd on weight tree changes to expire the cached hweights */
+u64 hweight_gen = 1;
+
+static u64 div_round_up(u64 dividend, u64 divisor)
+{
+ return (dividend + divisor - 1) / divisor;
+}
+
+static bool cgv_node_less(struct bpf_rb_node *a, const struct bpf_rb_node *b)
+{
+ struct cgv_node *cgc_a, *cgc_b;
+
+ cgc_a = container_of(a, struct cgv_node, rb_node);
+ cgc_b = container_of(b, struct cgv_node, rb_node);
+
+ return cgc_a->cvtime < cgc_b->cvtime;
+}
+
+static struct fcg_cpu_ctx *find_cpu_ctx(void)
+{
+ struct fcg_cpu_ctx *cpuc;
+ u32 idx = 0;
+
+ cpuc = bpf_map_lookup_elem(&cpu_ctx, &idx);
+ if (!cpuc) {
+ scx_bpf_error("cpu_ctx lookup failed");
+ return NULL;
+ }
+ return cpuc;
+}
+
+static struct fcg_cgrp_ctx *find_cgrp_ctx(struct cgroup *cgrp)
+{
+ struct fcg_cgrp_ctx *cgc;
+
+ cgc = bpf_cgrp_storage_get(&cgrp_ctx, cgrp, 0, 0);
+ if (!cgc) {
+ scx_bpf_error("cgrp_ctx lookup failed for cgid %llu", cgrp->kn->id);
+ return NULL;
+ }
+ return cgc;
+}
+
+static struct fcg_cgrp_ctx *find_ancestor_cgrp_ctx(struct cgroup *cgrp, int level)
+{
+ struct fcg_cgrp_ctx *cgc;
+
+ cgrp = bpf_cgroup_ancestor(cgrp, level);
+ if (!cgrp) {
+ scx_bpf_error("ancestor cgroup lookup failed");
+ return NULL;
+ }
+
+ cgc = find_cgrp_ctx(cgrp);
+ if (!cgc)
+ scx_bpf_error("ancestor cgrp_ctx lookup failed");
+ bpf_cgroup_release(cgrp);
+ return cgc;
+}
+
+static void cgrp_refresh_hweight(struct cgroup *cgrp, struct fcg_cgrp_ctx *cgc)
+{
+ int level;
+
+ if (!cgc->nr_active) {
+ stat_inc(FCG_STAT_HWT_SKIP);
+ return;
+ }
+
+ if (cgc->hweight_gen == hweight_gen) {
+ stat_inc(FCG_STAT_HWT_CACHE);
+ return;
+ }
+
+ stat_inc(FCG_STAT_HWT_UPDATES);
+ bpf_for(level, 0, cgrp->level + 1) {
+ struct fcg_cgrp_ctx *cgc;
+ bool is_active;
+
+ cgc = find_ancestor_cgrp_ctx(cgrp, level);
+ if (!cgc)
+ break;
+
+ if (!level) {
+ cgc->hweight = FCG_HWEIGHT_ONE;
+ cgc->hweight_gen = hweight_gen;
+ } else {
+ struct fcg_cgrp_ctx *pcgc;
+
+ pcgc = find_ancestor_cgrp_ctx(cgrp, level - 1);
+ if (!pcgc)
+ break;
+
+ /*
+ * We can be opportunistic here and not grab the
+ * cgv_tree_lock and deal with the occasional races.
+ * However, hweight updates are already cached and
+ * relatively low-frequency. Let's just do the
+ * straightforward thing.
+ */
+ bpf_spin_lock(&cgv_tree_lock);
+ is_active = cgc->nr_active;
+ if (is_active) {
+ cgc->hweight_gen = pcgc->hweight_gen;
+ cgc->hweight =
+ div_round_up(pcgc->hweight * cgc->weight,
+ pcgc->child_weight_sum);
+ }
+ bpf_spin_unlock(&cgv_tree_lock);
+
+ if (!is_active) {
+ stat_inc(FCG_STAT_HWT_RACE);
+ break;
+ }
+ }
+ }
+}
+
+static void cgrp_cap_budget(struct cgv_node *cgv_node, struct fcg_cgrp_ctx *cgc)
+{
+ u64 delta, cvtime, max_budget;
+
+ /*
+ * A node which is on the rbtree can't be pointed to from elsewhere yet
+ * and thus can't be updated and repositioned. Instead, we collect the
+ * vtime deltas separately and apply it asynchronously here.
+ */
+ delta = __sync_fetch_and_and(&cgc->cvtime_delta, 0);
+ cvtime = cgv_node->cvtime + delta;
+
+ /*
+ * Allow a cgroup to carry the maximum budget proportional to its
+ * hweight such that a full-hweight cgroup can immediately take up half
+ * of the CPUs at the most while staying at the front of the rbtree.
+ */
+ max_budget = (cgrp_slice_ns * nr_cpus * cgc->hweight) /
+ (2 * FCG_HWEIGHT_ONE);
+ if (time_before(cvtime, cvtime_now - max_budget))
+ cvtime = cvtime_now - max_budget;
+
+ cgv_node->cvtime = cvtime;
+}
+
+static void cgrp_enqueued(struct cgroup *cgrp, struct fcg_cgrp_ctx *cgc)
+{
+ struct cgv_node_stash *stash;
+ struct cgv_node *cgv_node;
+ u64 cgid = cgrp->kn->id;
+
+ /* paired with cmpxchg in try_pick_next_cgroup() */
+ if (__sync_val_compare_and_swap(&cgc->queued, 0, 1)) {
+ stat_inc(FCG_STAT_ENQ_SKIP);
+ return;
+ }
+
+ stash = bpf_map_lookup_elem(&cgv_node_stash, &cgid);
+ if (!stash) {
+ scx_bpf_error("cgv_node lookup failed for cgid %llu", cgid);
+ return;
+ }
+
+ /* NULL if the node is already on the rbtree */
+ cgv_node = bpf_kptr_xchg(&stash->node, NULL);
+ if (!cgv_node) {
+ stat_inc(FCG_STAT_ENQ_RACE);
+ return;
+ }
+
+ bpf_spin_lock(&cgv_tree_lock);
+ cgrp_cap_budget(cgv_node, cgc);
+ bpf_rbtree_add(&cgv_tree, &cgv_node->rb_node, cgv_node_less);
+ bpf_spin_unlock(&cgv_tree_lock);
+}
+
+static void set_bypassed_at(struct task_struct *p, struct fcg_task_ctx *taskc)
+{
+ /*
+ * Tell fcg_stopping() that this bypassed the regular scheduling path
+ * and should be force charged to the cgroup. 0 is used to indicate that
+ * the task isn't bypassing, so if the current runtime is 0, go back by
+ * one nanosecond.
+ */
+ taskc->bypassed_at = p->se.sum_exec_runtime ?: (u64)-1;
+}
+
+s32 BPF_STRUCT_OPS(fcg_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags)
+{
+ struct fcg_task_ctx *taskc;
+ bool is_idle = false;
+ s32 cpu;
+
+ cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle);
+
+ taskc = bpf_task_storage_get(&task_ctx, p, 0, 0);
+ if (!taskc) {
+ scx_bpf_error("task_ctx lookup failed");
+ return cpu;
+ }
+
+ /*
+ * If select_cpu_dfl() is recommending local enqueue, the target CPU is
+ * idle. Follow it and charge the cgroup later in fcg_stopping() after
+ * the fact.
+ */
+ if (is_idle) {
+ set_bypassed_at(p, taskc);
+ stat_inc(FCG_STAT_LOCAL);
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
+ }
+
+ return cpu;
+}
+
+void BPF_STRUCT_OPS(fcg_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ struct fcg_task_ctx *taskc;
+ struct cgroup *cgrp;
+ struct fcg_cgrp_ctx *cgc;
+
+ taskc = bpf_task_storage_get(&task_ctx, p, 0, 0);
+ if (!taskc) {
+ scx_bpf_error("task_ctx lookup failed");
+ return;
+ }
+
+ /*
+ * Use the direct dispatching and force charging to deal with tasks with
+ * custom affinities so that we don't have to worry about per-cgroup
+ * dq's containing tasks that can't be executed from some CPUs.
+ */
+ if (p->nr_cpus_allowed != nr_cpus) {
+ set_bypassed_at(p, taskc);
+
+ /*
+ * The global dq is deprioritized as we don't want to let tasks
+ * to boost themselves by constraining its cpumask. The
+ * deprioritization is rather severe, so let's not apply that to
+ * per-cpu kernel threads. This is ham-fisted. We probably wanna
+ * implement per-cgroup fallback dq's instead so that we have
+ * more control over when tasks with custom cpumask get issued.
+ */
+ if (p->nr_cpus_allowed == 1 && (p->flags & PF_KTHREAD)) {
+ stat_inc(FCG_STAT_LOCAL);
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL,
+ enq_flags);
+ } else {
+ stat_inc(FCG_STAT_GLOBAL);
+ scx_bpf_dsq_insert(p, FALLBACK_DSQ, SCX_SLICE_DFL,
+ enq_flags);
+ }
+ return;
+ }
+
+ cgrp = scx_bpf_task_cgroup(p);
+ cgc = find_cgrp_ctx(cgrp);
+ if (!cgc)
+ goto out_release;
+
+ if (fifo_sched) {
+ scx_bpf_dsq_insert(p, cgrp->kn->id, SCX_SLICE_DFL, enq_flags);
+ } else {
+ u64 tvtime = p->scx.dsq_vtime;
+
+ /*
+ * Limit the amount of budget that an idling task can accumulate
+ * to one slice.
+ */
+ if (time_before(tvtime, cgc->tvtime_now - SCX_SLICE_DFL))
+ tvtime = cgc->tvtime_now - SCX_SLICE_DFL;
+
+ scx_bpf_dsq_insert_vtime(p, cgrp->kn->id, SCX_SLICE_DFL,
+ tvtime, enq_flags);
+ }
+
+ cgrp_enqueued(cgrp, cgc);
+out_release:
+ bpf_cgroup_release(cgrp);
+}
+
+/*
+ * Walk the cgroup tree to update the active weight sums as tasks wake up and
+ * sleep. The weight sums are used as the base when calculating the proportion a
+ * given cgroup or task is entitled to at each level.
+ */
+static void update_active_weight_sums(struct cgroup *cgrp, bool runnable)
+{
+ struct fcg_cgrp_ctx *cgc;
+ bool updated = false;
+ int idx;
+
+ cgc = find_cgrp_ctx(cgrp);
+ if (!cgc)
+ return;
+
+ /*
+ * In most cases, a hot cgroup would have multiple threads going to
+ * sleep and waking up while the whole cgroup stays active. In leaf
+ * cgroups, ->nr_runnable which is updated with __sync operations gates
+ * ->nr_active updates, so that we don't have to grab the cgv_tree_lock
+ * repeatedly for a busy cgroup which is staying active.
+ */
+ if (runnable) {
+ if (__sync_fetch_and_add(&cgc->nr_runnable, 1))
+ return;
+ stat_inc(FCG_STAT_ACT);
+ } else {
+ if (__sync_sub_and_fetch(&cgc->nr_runnable, 1))
+ return;
+ stat_inc(FCG_STAT_DEACT);
+ }
+
+ /*
+ * If @cgrp is becoming runnable, its hweight should be refreshed after
+ * it's added to the weight tree so that enqueue has the up-to-date
+ * value. If @cgrp is becoming quiescent, the hweight should be
+ * refreshed before it's removed from the weight tree so that the usage
+ * charging which happens afterwards has access to the latest value.
+ */
+ if (!runnable)
+ cgrp_refresh_hweight(cgrp, cgc);
+
+ /* propagate upwards */
+ bpf_for(idx, 0, cgrp->level) {
+ int level = cgrp->level - idx;
+ struct fcg_cgrp_ctx *cgc, *pcgc = NULL;
+ bool propagate = false;
+
+ cgc = find_ancestor_cgrp_ctx(cgrp, level);
+ if (!cgc)
+ break;
+ if (level) {
+ pcgc = find_ancestor_cgrp_ctx(cgrp, level - 1);
+ if (!pcgc)
+ break;
+ }
+
+ /*
+ * We need the propagation protected by a lock to synchronize
+ * against weight changes. There's no reason to drop the lock at
+ * each level but bpf_spin_lock() doesn't want any function
+ * calls while locked.
+ */
+ bpf_spin_lock(&cgv_tree_lock);
+
+ if (runnable) {
+ if (!cgc->nr_active++) {
+ updated = true;
+ if (pcgc) {
+ propagate = true;
+ pcgc->child_weight_sum += cgc->weight;
+ }
+ }
+ } else {
+ if (!--cgc->nr_active) {
+ updated = true;
+ if (pcgc) {
+ propagate = true;
+ pcgc->child_weight_sum -= cgc->weight;
+ }
+ }
+ }
+
+ bpf_spin_unlock(&cgv_tree_lock);
+
+ if (!propagate)
+ break;
+ }
+
+ if (updated)
+ __sync_fetch_and_add(&hweight_gen, 1);
+
+ if (runnable)
+ cgrp_refresh_hweight(cgrp, cgc);
+}
+
+void BPF_STRUCT_OPS(fcg_runnable, struct task_struct *p, u64 enq_flags)
+{
+ struct cgroup *cgrp;
+
+ cgrp = scx_bpf_task_cgroup(p);
+ update_active_weight_sums(cgrp, true);
+ bpf_cgroup_release(cgrp);
+}
+
+void BPF_STRUCT_OPS(fcg_running, struct task_struct *p)
+{
+ struct cgroup *cgrp;
+ struct fcg_cgrp_ctx *cgc;
+
+ if (fifo_sched)
+ return;
+
+ cgrp = scx_bpf_task_cgroup(p);
+ cgc = find_cgrp_ctx(cgrp);
+ if (cgc) {
+ /*
+ * @cgc->tvtime_now always progresses forward as tasks start
+ * executing. The test and update can be performed concurrently
+ * from multiple CPUs and thus racy. Any error should be
+ * contained and temporary. Let's just live with it.
+ */
+ if (time_before(cgc->tvtime_now, p->scx.dsq_vtime))
+ cgc->tvtime_now = p->scx.dsq_vtime;
+ }
+ bpf_cgroup_release(cgrp);
+}
+
+void BPF_STRUCT_OPS(fcg_stopping, struct task_struct *p, bool runnable)
+{
+ struct fcg_task_ctx *taskc;
+ struct cgroup *cgrp;
+ struct fcg_cgrp_ctx *cgc;
+
+ /*
+ * Scale the execution time by the inverse of the weight and charge.
+ *
+ * Note that the default yield implementation yields by setting
+ * @p->scx.slice to zero and the following would treat the yielding task
+ * as if it has consumed all its slice. If this penalizes yielding tasks
+ * too much, determine the execution time by taking explicit timestamps
+ * instead of depending on @p->scx.slice.
+ */
+ if (!fifo_sched) {
+ u64 delta = scale_by_task_weight_inverse(p, SCX_SLICE_DFL - p->scx.slice);
+
+ scx_bpf_task_set_dsq_vtime(p, p->scx.dsq_vtime + delta);
+ }
+
+ taskc = bpf_task_storage_get(&task_ctx, p, 0, 0);
+ if (!taskc) {
+ scx_bpf_error("task_ctx lookup failed");
+ return;
+ }
+
+ if (!taskc->bypassed_at)
+ return;
+
+ cgrp = scx_bpf_task_cgroup(p);
+ cgc = find_cgrp_ctx(cgrp);
+ if (cgc) {
+ __sync_fetch_and_add(&cgc->cvtime_delta,
+ (p->se.sum_exec_runtime - taskc->bypassed_at) *
+ FCG_HWEIGHT_ONE / (cgc->hweight ?: 1));
+ taskc->bypassed_at = 0;
+ }
+ bpf_cgroup_release(cgrp);
+}
+
+void BPF_STRUCT_OPS(fcg_quiescent, struct task_struct *p, u64 deq_flags)
+{
+ struct cgroup *cgrp;
+
+ cgrp = scx_bpf_task_cgroup(p);
+ update_active_weight_sums(cgrp, false);
+ bpf_cgroup_release(cgrp);
+}
+
+void BPF_STRUCT_OPS(fcg_cgroup_set_weight, struct cgroup *cgrp, u32 weight)
+{
+ struct fcg_cgrp_ctx *cgc, *pcgc = NULL;
+
+ cgc = find_cgrp_ctx(cgrp);
+ if (!cgc)
+ return;
+
+ if (cgrp->level) {
+ pcgc = find_ancestor_cgrp_ctx(cgrp, cgrp->level - 1);
+ if (!pcgc)
+ return;
+ }
+
+ bpf_spin_lock(&cgv_tree_lock);
+ if (pcgc && cgc->nr_active)
+ pcgc->child_weight_sum += (s64)weight - cgc->weight;
+ cgc->weight = weight;
+ bpf_spin_unlock(&cgv_tree_lock);
+
+ /* expire cached hweights so the new weight propagates */
+ __sync_fetch_and_add(&hweight_gen, 1);
+}
+
+static bool try_pick_next_cgroup(u64 *cgidp)
+{
+ struct bpf_rb_node *rb_node;
+ struct cgv_node_stash *stash;
+ struct cgv_node *cgv_node;
+ struct fcg_cgrp_ctx *cgc;
+ struct cgroup *cgrp;
+ u64 cgid;
+
+ /* pop the front cgroup and wind cvtime_now accordingly */
+ bpf_spin_lock(&cgv_tree_lock);
+
+ rb_node = bpf_rbtree_first(&cgv_tree);
+ if (!rb_node) {
+ bpf_spin_unlock(&cgv_tree_lock);
+ stat_inc(FCG_STAT_PNC_NO_CGRP);
+ *cgidp = 0;
+ return true;
+ }
+
+ rb_node = bpf_rbtree_remove(&cgv_tree, rb_node);
+ bpf_spin_unlock(&cgv_tree_lock);
+
+ if (!rb_node) {
+ /*
+ * This should never happen. bpf_rbtree_first() was called
+ * above while the tree lock was held, so the node should
+ * always be present.
+ */
+ scx_bpf_error("node could not be removed");
+ return true;
+ }
+
+ cgv_node = container_of(rb_node, struct cgv_node, rb_node);
+ cgid = cgv_node->cgid;
+
+ if (time_before(cvtime_now, cgv_node->cvtime))
+ cvtime_now = cgv_node->cvtime;
+
+ /*
+ * If lookup fails, the cgroup's gone. Free and move on. See
+ * fcg_cgroup_exit().
+ */
+ cgrp = bpf_cgroup_from_id(cgid);
+ if (!cgrp) {
+ stat_inc(FCG_STAT_PNC_GONE);
+ goto out_free;
+ }
+
+ cgc = bpf_cgrp_storage_get(&cgrp_ctx, cgrp, 0, 0);
+ if (!cgc) {
+ bpf_cgroup_release(cgrp);
+ stat_inc(FCG_STAT_PNC_GONE);
+ goto out_free;
+ }
+
+ if (!scx_bpf_dsq_move_to_local(cgid, 0)) {
+ bpf_cgroup_release(cgrp);
+ stat_inc(FCG_STAT_PNC_EMPTY);
+ goto out_stash;
+ }
+
+ /*
+ * Successfully consumed from the cgroup. This will be our current
+ * cgroup for the new slice. Refresh its hweight.
+ */
+ cgrp_refresh_hweight(cgrp, cgc);
+
+ bpf_cgroup_release(cgrp);
+
+ /*
+ * As the cgroup may have more tasks, add it back to the rbtree. Note
+ * that here we charge the full slice upfront and then exact later
+ * according to the actual consumption. This prevents lowpri thundering
+ * herd from saturating the machine.
+ */
+ bpf_spin_lock(&cgv_tree_lock);
+ cgv_node->cvtime += cgrp_slice_ns * FCG_HWEIGHT_ONE / (cgc->hweight ?: 1);
+ cgrp_cap_budget(cgv_node, cgc);
+ bpf_rbtree_add(&cgv_tree, &cgv_node->rb_node, cgv_node_less);
+ bpf_spin_unlock(&cgv_tree_lock);
+
+ *cgidp = cgid;
+ stat_inc(FCG_STAT_PNC_NEXT);
+ return true;
+
+out_stash:
+ stash = bpf_map_lookup_elem(&cgv_node_stash, &cgid);
+ if (!stash) {
+ stat_inc(FCG_STAT_PNC_GONE);
+ goto out_free;
+ }
+
+ /*
+ * Paired with cmpxchg in cgrp_enqueued(). If they see the following
+ * transition, they'll enqueue the cgroup. If they are earlier, we'll
+ * see their task in the dq below and requeue the cgroup.
+ */
+ __sync_val_compare_and_swap(&cgc->queued, 1, 0);
+
+ if (scx_bpf_dsq_nr_queued(cgid)) {
+ bpf_spin_lock(&cgv_tree_lock);
+ bpf_rbtree_add(&cgv_tree, &cgv_node->rb_node, cgv_node_less);
+ bpf_spin_unlock(&cgv_tree_lock);
+ stat_inc(FCG_STAT_PNC_RACE);
+ } else {
+ cgv_node = bpf_kptr_xchg(&stash->node, cgv_node);
+ if (cgv_node) {
+ scx_bpf_error("unexpected !NULL cgv_node stash");
+ goto out_free;
+ }
+ }
+
+ return false;
+
+out_free:
+ bpf_obj_drop(cgv_node);
+ return false;
+}
+
+void BPF_STRUCT_OPS(fcg_dispatch, s32 cpu, struct task_struct *prev)
+{
+ struct fcg_cpu_ctx *cpuc;
+ struct fcg_cgrp_ctx *cgc;
+ struct cgroup *cgrp;
+ u64 now = scx_bpf_now();
+ bool picked_next = false;
+
+ cpuc = find_cpu_ctx();
+ if (!cpuc)
+ return;
+
+ if (!cpuc->cur_cgid)
+ goto pick_next_cgroup;
+
+ if (time_before(now, cpuc->cur_at + cgrp_slice_ns)) {
+ if (scx_bpf_dsq_move_to_local(cpuc->cur_cgid, 0)) {
+ stat_inc(FCG_STAT_CNS_KEEP);
+ return;
+ }
+ stat_inc(FCG_STAT_CNS_EMPTY);
+ } else {
+ stat_inc(FCG_STAT_CNS_EXPIRE);
+ }
+
+ /*
+ * The current cgroup is expiring. It was already charged a full slice.
+ * Calculate the actual usage and accumulate the delta.
+ */
+ cgrp = bpf_cgroup_from_id(cpuc->cur_cgid);
+ if (!cgrp) {
+ stat_inc(FCG_STAT_CNS_GONE);
+ goto pick_next_cgroup;
+ }
+
+ cgc = bpf_cgrp_storage_get(&cgrp_ctx, cgrp, 0, 0);
+ if (cgc) {
+ /*
+ * We want to update the vtime delta and then look for the next
+ * cgroup to execute but the latter needs to be done in a loop
+ * and we can't keep the lock held. Oh well...
+ */
+ s64 delta = now - cpuc->cur_at - cgrp_slice_ns;
+
+ bpf_spin_lock(&cgv_tree_lock);
+ /* keep the dividends positive, BPF division is unsigned */
+ if (delta >= 0)
+ __sync_fetch_and_add(&cgc->cvtime_delta,
+ (u64)delta * FCG_HWEIGHT_ONE /
+ (cgc->hweight ?: 1));
+ else
+ __sync_fetch_and_sub(&cgc->cvtime_delta,
+ (u64)-delta * FCG_HWEIGHT_ONE /
+ (cgc->hweight ?: 1));
+ bpf_spin_unlock(&cgv_tree_lock);
+ } else {
+ stat_inc(FCG_STAT_CNS_GONE);
+ }
+
+ bpf_cgroup_release(cgrp);
+
+pick_next_cgroup:
+ cpuc->cur_at = now;
+
+ if (scx_bpf_dsq_move_to_local(FALLBACK_DSQ, 0)) {
+ cpuc->cur_cgid = 0;
+ return;
+ }
+
+ bpf_repeat(CGROUP_MAX_RETRIES) {
+ if (try_pick_next_cgroup(&cpuc->cur_cgid)) {
+ picked_next = true;
+ break;
+ }
+ }
+
+ /*
+ * This only happens if try_pick_next_cgroup() races against enqueue
+ * path for more than CGROUP_MAX_RETRIES times, which is extremely
+ * unlikely and likely indicates an underlying bug. There shouldn't be
+ * any stall risk as the race is against enqueue.
+ */
+ if (!picked_next)
+ stat_inc(FCG_STAT_PNC_FAIL);
+}
+
+s32 BPF_STRUCT_OPS(fcg_init_task, struct task_struct *p,
+ struct scx_init_task_args *args)
+{
+ struct fcg_task_ctx *taskc;
+ struct fcg_cgrp_ctx *cgc;
+
+ /*
+ * @p is new. Let's ensure that its task_ctx is available. We can sleep
+ * in this function and the following will automatically use GFP_KERNEL.
+ */
+ taskc = bpf_task_storage_get(&task_ctx, p, 0,
+ BPF_LOCAL_STORAGE_GET_F_CREATE);
+ if (!taskc)
+ return -ENOMEM;
+
+ taskc->bypassed_at = 0;
+
+ if (!(cgc = find_cgrp_ctx(args->cgroup)))
+ return -ENOENT;
+
+ scx_bpf_task_set_dsq_vtime(p, cgc->tvtime_now);
+
+ return 0;
+}
+
+int BPF_STRUCT_OPS_SLEEPABLE(fcg_cgroup_init, struct cgroup *cgrp,
+ struct scx_cgroup_init_args *args)
+{
+ struct fcg_cgrp_ctx *cgc;
+ struct cgv_node *cgv_node;
+ struct cgv_node_stash empty_stash = {}, *stash;
+ u64 cgid = cgrp->kn->id;
+ int ret;
+
+ /*
+ * Technically incorrect as cgroup ID is full 64bit while dsq ID is
+ * 63bit. Should not be a problem in practice and easy to spot in the
+ * unlikely case that it breaks.
+ */
+ ret = scx_bpf_create_dsq(cgid, -1);
+ if (ret) {
+ scx_bpf_error("scx_bpf_create_dsq failed (%d)", ret);
+ return ret;
+ }
+
+ cgc = bpf_cgrp_storage_get(&cgrp_ctx, cgrp, 0,
+ BPF_LOCAL_STORAGE_GET_F_CREATE);
+ if (!cgc) {
+ ret = -ENOMEM;
+ goto err_destroy_dsq;
+ }
+
+ cgc->weight = args->weight;
+ cgc->hweight = FCG_HWEIGHT_ONE;
+
+ ret = bpf_map_update_elem(&cgv_node_stash, &cgid, &empty_stash,
+ BPF_NOEXIST);
+ if (ret) {
+ if (ret != -ENOMEM)
+ scx_bpf_error("unexpected stash creation error (%d)",
+ ret);
+ goto err_destroy_dsq;
+ }
+
+ stash = bpf_map_lookup_elem(&cgv_node_stash, &cgid);
+ if (!stash) {
+ scx_bpf_error("unexpected cgv_node stash lookup failure");
+ ret = -ENOENT;
+ goto err_destroy_dsq;
+ }
+
+ cgv_node = bpf_obj_new(struct cgv_node);
+ if (!cgv_node) {
+ ret = -ENOMEM;
+ goto err_del_cgv_node;
+ }
+
+ cgv_node->cgid = cgid;
+ cgv_node->cvtime = cvtime_now;
+
+ cgv_node = bpf_kptr_xchg(&stash->node, cgv_node);
+ if (cgv_node) {
+ scx_bpf_error("unexpected !NULL cgv_node stash");
+ ret = -EBUSY;
+ goto err_drop;
+ }
+
+ return 0;
+
+err_drop:
+ bpf_obj_drop(cgv_node);
+err_del_cgv_node:
+ bpf_map_delete_elem(&cgv_node_stash, &cgid);
+err_destroy_dsq:
+ scx_bpf_destroy_dsq(cgid);
+ return ret;
+}
+
+void BPF_STRUCT_OPS(fcg_cgroup_exit, struct cgroup *cgrp)
+{
+ u64 cgid = cgrp->kn->id;
+
+ /*
+ * For now, there's no way find and remove the cgv_node if it's on the
+ * cgv_tree. Let's drain them in the dispatch path as they get popped
+ * off the front of the tree.
+ */
+ bpf_map_delete_elem(&cgv_node_stash, &cgid);
+ scx_bpf_destroy_dsq(cgid);
+}
+
+void BPF_STRUCT_OPS(fcg_cgroup_move, struct task_struct *p,
+ struct cgroup *from, struct cgroup *to)
+{
+ struct fcg_cgrp_ctx *from_cgc, *to_cgc;
+ s64 delta;
+
+ /* find_cgrp_ctx() triggers scx_bpf_error() on lookup failures */
+ if (!(from_cgc = find_cgrp_ctx(from)) || !(to_cgc = find_cgrp_ctx(to)))
+ return;
+
+ delta = (s64)(p->scx.dsq_vtime - from_cgc->tvtime_now);
+ scx_bpf_task_set_dsq_vtime(p, to_cgc->tvtime_now + delta);
+}
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(fcg_init)
+{
+ int ret;
+
+ ret = scx_bpf_create_dsq(FALLBACK_DSQ, -1);
+ if (ret) {
+ scx_bpf_error("failed to create DSQ %d (%d)", FALLBACK_DSQ, ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(fcg_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+SCX_OPS_DEFINE(flatcg_ops,
+ .select_cpu = (void *)fcg_select_cpu,
+ .enqueue = (void *)fcg_enqueue,
+ .dispatch = (void *)fcg_dispatch,
+ .runnable = (void *)fcg_runnable,
+ .running = (void *)fcg_running,
+ .stopping = (void *)fcg_stopping,
+ .quiescent = (void *)fcg_quiescent,
+ .init_task = (void *)fcg_init_task,
+ .cgroup_set_weight = (void *)fcg_cgroup_set_weight,
+ .cgroup_init = (void *)fcg_cgroup_init,
+ .cgroup_exit = (void *)fcg_cgroup_exit,
+ .cgroup_move = (void *)fcg_cgroup_move,
+ .init = (void *)fcg_init,
+ .exit = (void *)fcg_exit,
+ .flags = SCX_OPS_ENQ_EXITING,
+ .name = "flatcg");
diff --git a/tools/sched_ext/scx_flatcg.c b/tools/sched_ext/scx_flatcg.c
new file mode 100644
index 000000000..a223bff37
--- /dev/null
+++ b/tools/sched_ext/scx_flatcg.c
@@ -0,0 +1,239 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2023 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2023 David Vernet <dvernet@meta.com>
+ */
+#include <stdio.h>
+#include <signal.h>
+#include <assert.h>
+#include <unistd.h>
+#include <libgen.h>
+#include <limits.h>
+#include <inttypes.h>
+#include <fcntl.h>
+#include <time.h>
+#include <bpf/bpf.h>
+#include <scx/common.h>
+#include "scx_flatcg.h"
+#include "scx_flatcg.bpf.skel.h"
+
+#ifndef FILEID_KERNFS
+#define FILEID_KERNFS 0xfe
+#endif
+
+const char help_fmt[] =
+"A flattened cgroup hierarchy sched_ext scheduler.\n"
+"\n"
+"See the top-level comment in .bpf.c for more details.\n"
+"\n"
+"Usage: %s [-s SLICE_US] [-i INTERVAL] [-f] [-v]\n"
+"\n"
+" -s SLICE_US Override slice duration\n"
+" -i INTERVAL Report interval\n"
+" -f Use FIFO scheduling instead of weighted vtime scheduling\n"
+" -v Print libbpf debug messages\n"
+" -h Display this help and exit\n";
+
+static bool verbose;
+static volatile int exit_req;
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ if (level == LIBBPF_DEBUG && !verbose)
+ return 0;
+ return vfprintf(stderr, format, args);
+}
+
+static void sigint_handler(int dummy)
+{
+ exit_req = 1;
+}
+
+static float read_cpu_util(__u64 *last_sum, __u64 *last_idle)
+{
+ FILE *fp;
+ char buf[4096];
+ char *line, *cur = NULL, *tok;
+ __u64 sum = 0, idle = 0;
+ __u64 delta_sum, delta_idle;
+ int idx;
+
+ fp = fopen("/proc/stat", "r");
+ if (!fp) {
+ perror("fopen(\"/proc/stat\")");
+ return 0.0;
+ }
+
+ if (!fgets(buf, sizeof(buf), fp)) {
+ perror("fgets(\"/proc/stat\")");
+ fclose(fp);
+ return 0.0;
+ }
+ fclose(fp);
+
+ line = buf;
+ for (idx = 0; (tok = strtok_r(line, " \n", &cur)); idx++) {
+ char *endp = NULL;
+ __u64 v;
+
+ if (idx == 0) {
+ line = NULL;
+ continue;
+ }
+ v = strtoull(tok, &endp, 0);
+ if (!endp || *endp != '\0') {
+ fprintf(stderr, "failed to parse %dth field of /proc/stat (\"%s\")\n",
+ idx, tok);
+ continue;
+ }
+ sum += v;
+ if (idx == 4)
+ idle = v;
+ }
+
+ delta_sum = sum - *last_sum;
+ delta_idle = idle - *last_idle;
+ *last_sum = sum;
+ *last_idle = idle;
+
+ return delta_sum ? (float)(delta_sum - delta_idle) / delta_sum : 0.0;
+}
+
+static void fcg_read_stats(struct scx_flatcg *skel, __u64 *stats)
+{
+ __u64 *cnts;
+ __u32 idx;
+
+ memset(stats, 0, sizeof(stats[0]) * FCG_NR_STATS);
+
+ cnts = calloc(skel->rodata->nr_cpus, sizeof(__u64));
+ if (!cnts)
+ return;
+
+ for (idx = 0; idx < FCG_NR_STATS; idx++) {
+ int ret, cpu;
+
+ ret = bpf_map_lookup_elem(bpf_map__fd(skel->maps.stats),
+ &idx, cnts);
+ if (ret < 0)
+ continue;
+ for (cpu = 0; cpu < skel->rodata->nr_cpus; cpu++)
+ stats[idx] += cnts[cpu];
+ }
+
+ free(cnts);
+}
+
+int main(int argc, char **argv)
+{
+ struct scx_flatcg *skel;
+ struct bpf_link *link;
+ struct timespec intv_ts = { .tv_sec = 2, .tv_nsec = 0 };
+ __u64 last_cpu_sum = 0, last_cpu_idle = 0;
+ __u64 last_stats[FCG_NR_STATS] = {};
+ unsigned long seq = 0;
+ __s32 opt;
+ __u64 ecode;
+
+ libbpf_set_print(libbpf_print_fn);
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+restart:
+ optind = 1;
+ skel = SCX_OPS_OPEN(flatcg_ops, scx_flatcg);
+
+ skel->rodata->nr_cpus = libbpf_num_possible_cpus();
+ assert(skel->rodata->nr_cpus > 0);
+ skel->rodata->cgrp_slice_ns = __COMPAT_ENUM_OR_ZERO("scx_public_consts", "SCX_SLICE_DFL");
+
+ while ((opt = getopt(argc, argv, "s:i:fvh")) != -1) {
+ double v;
+
+ switch (opt) {
+ case 's':
+ v = strtod(optarg, NULL);
+ skel->rodata->cgrp_slice_ns = v * 1000;
+ break;
+ case 'i':
+ v = strtod(optarg, NULL);
+ intv_ts.tv_sec = v;
+ intv_ts.tv_nsec = (v - (float)intv_ts.tv_sec) * 1000000000;
+ break;
+ case 'f':
+ skel->rodata->fifo_sched = true;
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ case 'h':
+ default:
+ fprintf(stderr, help_fmt, basename(argv[0]));
+ return opt != 'h';
+ }
+ }
+
+ printf("slice=%.1lfms intv=%.1lfs",
+ (double)skel->rodata->cgrp_slice_ns / 1000000.0,
+ (double)intv_ts.tv_sec + (double)intv_ts.tv_nsec / 1000000000.0);
+
+
+ SCX_OPS_LOAD(skel, flatcg_ops, scx_flatcg, uei);
+ link = SCX_OPS_ATTACH(skel, flatcg_ops, scx_flatcg);
+
+ while (!exit_req && !UEI_EXITED(skel, uei)) {
+ __u64 acc_stats[FCG_NR_STATS];
+ __u64 stats[FCG_NR_STATS];
+ float cpu_util;
+ int i;
+
+ cpu_util = read_cpu_util(&last_cpu_sum, &last_cpu_idle);
+
+ fcg_read_stats(skel, acc_stats);
+ for (i = 0; i < FCG_NR_STATS; i++)
+ stats[i] = acc_stats[i] - last_stats[i];
+
+ memcpy(last_stats, acc_stats, sizeof(acc_stats));
+
+ printf("\n[SEQ %6lu cpu=%5.1lf hweight_gen=%" PRIu64 "]\n",
+ seq++, cpu_util * 100.0, skel->data->hweight_gen);
+ printf(" act:%6llu deact:%6llu global:%6llu local:%6llu\n",
+ stats[FCG_STAT_ACT],
+ stats[FCG_STAT_DEACT],
+ stats[FCG_STAT_GLOBAL],
+ stats[FCG_STAT_LOCAL]);
+ printf("HWT cache:%6llu update:%6llu skip:%6llu race:%6llu\n",
+ stats[FCG_STAT_HWT_CACHE],
+ stats[FCG_STAT_HWT_UPDATES],
+ stats[FCG_STAT_HWT_SKIP],
+ stats[FCG_STAT_HWT_RACE]);
+ printf("ENQ skip:%6llu race:%6llu\n",
+ stats[FCG_STAT_ENQ_SKIP],
+ stats[FCG_STAT_ENQ_RACE]);
+ printf("CNS keep:%6llu expire:%6llu empty:%6llu gone:%6llu\n",
+ stats[FCG_STAT_CNS_KEEP],
+ stats[FCG_STAT_CNS_EXPIRE],
+ stats[FCG_STAT_CNS_EMPTY],
+ stats[FCG_STAT_CNS_GONE]);
+ printf("PNC next:%6llu empty:%6llu nocgrp:%6llu gone:%6llu race:%6llu fail:%6llu\n",
+ stats[FCG_STAT_PNC_NEXT],
+ stats[FCG_STAT_PNC_EMPTY],
+ stats[FCG_STAT_PNC_NO_CGRP],
+ stats[FCG_STAT_PNC_GONE],
+ stats[FCG_STAT_PNC_RACE],
+ stats[FCG_STAT_PNC_FAIL]);
+ printf("BAD remove:%6llu\n",
+ acc_stats[FCG_STAT_BAD_REMOVAL]);
+ fflush(stdout);
+
+ nanosleep(&intv_ts, NULL);
+ }
+
+ bpf_link__destroy(link);
+ ecode = UEI_REPORT(skel, uei);
+ scx_flatcg__destroy(skel);
+
+ if (!exit_req && UEI_ECODE_RESTART(ecode))
+ goto restart;
+ return 0;
+}
diff --git a/tools/sched_ext/scx_flatcg.h b/tools/sched_ext/scx_flatcg.h
new file mode 100644
index 000000000..6f2ea50ac
--- /dev/null
+++ b/tools/sched_ext/scx_flatcg.h
@@ -0,0 +1,51 @@
+#ifndef __SCX_EXAMPLE_FLATCG_H
+#define __SCX_EXAMPLE_FLATCG_H
+
+enum {
+ FCG_HWEIGHT_ONE = 1LLU << 16,
+};
+
+enum fcg_stat_idx {
+ FCG_STAT_ACT,
+ FCG_STAT_DEACT,
+ FCG_STAT_LOCAL,
+ FCG_STAT_GLOBAL,
+
+ FCG_STAT_HWT_UPDATES,
+ FCG_STAT_HWT_CACHE,
+ FCG_STAT_HWT_SKIP,
+ FCG_STAT_HWT_RACE,
+
+ FCG_STAT_ENQ_SKIP,
+ FCG_STAT_ENQ_RACE,
+
+ FCG_STAT_CNS_KEEP,
+ FCG_STAT_CNS_EXPIRE,
+ FCG_STAT_CNS_EMPTY,
+ FCG_STAT_CNS_GONE,
+
+ FCG_STAT_PNC_NO_CGRP,
+ FCG_STAT_PNC_NEXT,
+ FCG_STAT_PNC_EMPTY,
+ FCG_STAT_PNC_GONE,
+ FCG_STAT_PNC_RACE,
+ FCG_STAT_PNC_FAIL,
+
+ FCG_STAT_BAD_REMOVAL,
+
+ FCG_NR_STATS,
+};
+
+struct fcg_cgrp_ctx {
+ u32 nr_active;
+ u32 nr_runnable;
+ u32 queued;
+ u32 weight;
+ u32 hweight;
+ u64 child_weight_sum;
+ u64 hweight_gen;
+ s64 cvtime_delta;
+ u64 tvtime_now;
+};
+
+#endif /* __SCX_EXAMPLE_FLATCG_H */
diff --git a/tools/sched_ext/scx_pair.bpf.c b/tools/sched_ext/scx_pair.bpf.c
new file mode 100644
index 000000000..0d61b7b81
--- /dev/null
+++ b/tools/sched_ext/scx_pair.bpf.c
@@ -0,0 +1,654 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * A demo sched_ext core-scheduler which always makes every sibling CPU pair
+ * execute from the same CPU cgroup.
+ *
+ * This scheduler is a minimal implementation and would need some form of
+ * priority handling both inside each cgroup and across the cgroups to be
+ * practically useful.
+ *
+ * Each CPU in the system is paired with exactly one other CPU, according to a
+ * "stride" value that can be specified when the BPF scheduler program is first
+ * loaded. Throughout the runtime of the scheduler, these CPU pairs guarantee
+ * that they will only ever schedule tasks that belong to the same CPU cgroup.
+ *
+ * Scheduler Initialization
+ * ------------------------
+ *
+ * The scheduler BPF program is first initialized from user space, before it is
+ * enabled. During this initialization process, each CPU on the system is
+ * assigned several values that are constant throughout its runtime:
+ *
+ * 1. *Pair CPU*: The CPU that it synchronizes with when making scheduling
+ * decisions. Paired CPUs always schedule tasks from the same
+ * CPU cgroup, and synchronize with each other to guarantee
+ * that this constraint is not violated.
+ * 2. *Pair ID*: Each CPU pair is assigned a Pair ID, which is used to access
+ * a struct pair_ctx object that is shared between the pair.
+ * 3. *In-pair-index*: An index, 0 or 1, that is assigned to each core in the
+ * pair. Each struct pair_ctx has an active_mask field,
+ * which is a bitmap used to indicate whether each core
+ * in the pair currently has an actively running task.
+ * This index specifies which entry in the bitmap corresponds
+ * to each CPU in the pair.
+ *
+ * During this initialization, the CPUs are paired according to a "stride" that
+ * may be specified when invoking the user space program that initializes and
+ * loads the scheduler. By default, the stride is 1/2 the total number of CPUs.
+ *
+ * Tasks and cgroups
+ * -----------------
+ *
+ * Every cgroup in the system is registered with the scheduler using the
+ * pair_cgroup_init() callback, and every task in the system is associated with
+ * exactly one cgroup. At a high level, the idea with the pair scheduler is to
+ * always schedule tasks from the same cgroup within a given CPU pair. When a
+ * task is enqueued (i.e. passed to the pair_enqueue() callback function), its
+ * cgroup ID is read from its task struct, and then a corresponding queue map
+ * is used to FIFO-enqueue the task for that cgroup.
+ *
+ * If you look through the implementation of the scheduler, you'll notice that
+ * there is quite a bit of complexity involved with looking up the per-cgroup
+ * FIFO queue that we enqueue tasks in. For example, there is a cgrp_q_idx_hash
+ * BPF hash map that is used to map a cgroup ID to a globally unique ID that's
+ * allocated in the BPF program. This is done because we use separate maps to
+ * store the FIFO queue of tasks, and the length of that map, per cgroup. This
+ * complexity is only present because of current deficiencies in BPF that will
+ * soon be addressed. The main point to keep in mind is that newly enqueued
+ * tasks are added to their cgroup's FIFO queue.
+ *
+ * Dispatching tasks
+ * -----------------
+ *
+ * This section will describe how enqueued tasks are dispatched and scheduled.
+ * Tasks are dispatched in pair_dispatch(), and at a high level the workflow is
+ * as follows:
+ *
+ * 1. Fetch the struct pair_ctx for the current CPU. As mentioned above, this is
+ * the structure that's used to synchronize amongst the two pair CPUs in their
+ * scheduling decisions. After any of the following events have occurred:
+ *
+ * - The cgroup's slice run has expired, or
+ * - The cgroup becomes empty, or
+ * - Either CPU in the pair is preempted by a higher priority scheduling class
+ *
+ * The cgroup transitions to the draining state and stops executing new tasks
+ * from the cgroup.
+ *
+ * 2. If the pair is still executing a task, mark the pair_ctx as draining, and
+ * wait for the pair CPU to be preempted.
+ *
+ * 3. Otherwise, if the pair CPU is not running a task, we can move onto
+ * scheduling new tasks. Pop the next cgroup id from the top_q queue.
+ *
+ * 4. Pop a task from that cgroup's FIFO task queue, and begin executing it.
+ *
+ * Note again that this scheduling behavior is simple, but the implementation
+ * is complex mostly because this it hits several BPF shortcomings and has to
+ * work around in often awkward ways. Most of the shortcomings are expected to
+ * be resolved in the near future which should allow greatly simplifying this
+ * scheduler.
+ *
+ * Dealing with preemption
+ * -----------------------
+ *
+ * SCX is the lowest priority sched_class, and could be preempted by them at
+ * any time. To address this, the scheduler watches every sched_switch from
+ * a tracepoint and edge-detects when a CPU leaves and returns to SCX
+ * control.
+ *
+ * When a higher-priority class takes a CPU away from a running SCX task -
+ * a sched_switch from an SCX task to a higher-priority task - we mark the
+ * pair_ctx as having been preempted and then invoke:
+ *
+ * scx_bpf_kick_cpu(pair_cpu, SCX_KICK_PREEMPT | SCX_KICK_WAIT);
+ *
+ * This preempts the pair CPU, and waits until it has re-entered the scheduler
+ * before returning. This is necessary to ensure that the higher priority
+ * sched_class that preempted our scheduler does not schedule a task
+ * concurrently with our pair CPU.
+ *
+ * When the CPU returns to SCX or idle, we unmark the preemption in the
+ * pair_ctx and send another resched IPI to the pair CPU to re-enable pair
+ * scheduling.
+ *
+ * A switch from idle straight to a higher-priority task is not a release:
+ * the CPU was not running an SCX task, so there is nothing to drain and no
+ * reason to make the pair wait. Kicking SCX_KICK_WAIT on every such wakeup
+ * would stall the pair CPU behind rt bursts it was never coupled to.
+ *
+ * Note that sched_setscheduler() on a running task changes its class in
+ * place without a context switch, so such transitions are only observed at
+ * the task's next switch. Until then the stale active_mask bit makes the
+ * pair wait in try_dispatch(), which is bounded by that next switch.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <scx/common.bpf.h>
+#include "scx_pair.h"
+
+#define MAX_RT_PRIO 100
+
+char _license[] SEC("license") = "GPL";
+
+/* !0 for veristat, set during init */
+const volatile u32 nr_cpu_ids = 1;
+
+/* a pair of CPUs stay on a cgroup for this duration */
+const volatile u32 pair_batch_dur_ns;
+
+/* cpu ID -> pair cpu ID */
+const volatile s32 RESIZABLE_ARRAY(rodata, pair_cpu);
+
+/* cpu ID -> pair_id */
+const volatile u32 RESIZABLE_ARRAY(rodata, pair_id);
+
+/* CPU ID -> CPU # in the pair (0 or 1) */
+const volatile u32 RESIZABLE_ARRAY(rodata, in_pair_idx);
+
+struct pair_ctx {
+ struct bpf_spin_lock lock;
+
+ /* the cgroup the pair is currently executing */
+ u64 cgid;
+
+ /* the pair started executing the current cgroup at */
+ u64 started_at;
+
+ /* whether the current cgroup is draining */
+ bool draining;
+
+ /* the CPUs that are currently active on the cgroup */
+ u32 active_mask;
+
+ /*
+ * the CPUs that are currently preempted and running tasks in a
+ * different scheduler.
+ */
+ u32 preempted_mask;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __type(key, u32);
+ __type(value, struct pair_ctx);
+} pair_ctx SEC(".maps");
+
+/* queue of cgrp_q's possibly with tasks on them */
+struct {
+ __uint(type, BPF_MAP_TYPE_QUEUE);
+ /*
+ * Because it's difficult to build strong synchronization encompassing
+ * multiple non-trivial operations in BPF, this queue is managed in an
+ * opportunistic way so that we guarantee that a cgroup w/ active tasks
+ * is always on it but possibly multiple times. Once we have more robust
+ * synchronization constructs and e.g. linked list, we should be able to
+ * do this in a prettier way but for now just size it big enough.
+ */
+ __uint(max_entries, 4 * MAX_CGRPS);
+ __type(value, u64);
+} top_q SEC(".maps");
+
+/* per-cgroup q which FIFOs the tasks from the cgroup */
+struct cgrp_q {
+ __uint(type, BPF_MAP_TYPE_QUEUE);
+ __uint(max_entries, MAX_QUEUED);
+ __type(value, u32);
+};
+
+/*
+ * Ideally, we want to allocate cgrp_q and cgrq_q_len in the cgroup local
+ * storage; however, a cgroup local storage can only be accessed from the BPF
+ * progs attached to the cgroup. For now, work around by allocating array of
+ * cgrp_q's and then allocating per-cgroup indices.
+ *
+ * Another caveat: It's difficult to populate a large array of maps statically
+ * or from BPF. Initialize it from userland.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY_OF_MAPS);
+ __uint(max_entries, MAX_CGRPS);
+ __type(key, s32);
+ __array(values, struct cgrp_q);
+} cgrp_q_arr SEC(".maps");
+
+static u64 cgrp_q_len[MAX_CGRPS];
+
+/*
+ * This and cgrp_q_idx_hash combine into a poor man's IDR. This likely would be
+ * useful to have as a map type.
+ */
+static u32 cgrp_q_idx_cursor;
+static u64 cgrp_q_idx_busy[MAX_CGRPS];
+
+/*
+ * All added up, the following is what we do:
+ *
+ * 1. When a cgroup is enabled, RR cgroup_q_idx_busy array doing cmpxchg looking
+ * for a free ID. If not found, fail cgroup creation with -EBUSY.
+ *
+ * 2. Hash the cgroup ID to the allocated cgrp_q_idx in the following
+ * cgrp_q_idx_hash.
+ *
+ * 3. Whenever a cgrp_q needs to be accessed, first look up the cgrp_q_idx from
+ * cgrp_q_idx_hash and then access the corresponding entry in cgrp_q_arr.
+ *
+ * This is sadly complicated for something pretty simple. Hopefully, we should
+ * be able to simplify in the future.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_HASH);
+ __uint(max_entries, MAX_CGRPS);
+ __uint(key_size, sizeof(u64)); /* cgrp ID */
+ __uint(value_size, sizeof(s32)); /* cgrp_q idx */
+} cgrp_q_idx_hash SEC(".maps");
+
+/* statistics */
+u64 nr_total, nr_dispatched, nr_missing, nr_kicks, nr_preemptions;
+u64 nr_exps, nr_exp_waits, nr_exp_empty;
+u64 nr_cgrp_next, nr_cgrp_coll, nr_cgrp_empty;
+
+UEI_DEFINE(uei);
+
+void BPF_STRUCT_OPS(pair_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ struct cgroup *cgrp;
+ struct cgrp_q *cgq;
+ s32 pid = p->pid;
+ u64 cgid;
+ u32 *q_idx;
+ u64 *cgq_len;
+
+ __sync_fetch_and_add(&nr_total, 1);
+
+ cgrp = scx_bpf_task_cgroup(p);
+ cgid = cgrp->kn->id;
+ bpf_cgroup_release(cgrp);
+
+ /* find the cgroup's q and push @p into it */
+ q_idx = bpf_map_lookup_elem(&cgrp_q_idx_hash, &cgid);
+ if (!q_idx) {
+ scx_bpf_error("failed to lookup q_idx for cgroup[%llu]", cgid);
+ return;
+ }
+
+ cgq = bpf_map_lookup_elem(&cgrp_q_arr, q_idx);
+ if (!cgq) {
+ scx_bpf_error("failed to lookup q_arr for cgroup[%llu] q_idx[%u]",
+ cgid, *q_idx);
+ return;
+ }
+
+ if (bpf_map_push_elem(cgq, &pid, 0)) {
+ scx_bpf_error("cgroup[%llu] queue overflow", cgid);
+ return;
+ }
+
+ /* bump q len, if going 0 -> 1, queue cgroup into the top_q */
+ cgq_len = MEMBER_VPTR(cgrp_q_len, [*q_idx]);
+ if (!cgq_len) {
+ scx_bpf_error("MEMBER_VTPR malfunction");
+ return;
+ }
+
+ if (!__sync_fetch_and_add(cgq_len, 1) &&
+ bpf_map_push_elem(&top_q, &cgid, 0)) {
+ scx_bpf_error("top_q overflow");
+ return;
+ }
+}
+
+static int lookup_pairc_and_mask(s32 cpu, struct pair_ctx **pairc, u32 *mask)
+{
+ u32 *vptr;
+
+ vptr = (u32 *)ARRAY_ELEM_PTR(pair_id, cpu, nr_cpu_ids);
+ if (!vptr)
+ return -EINVAL;
+
+ *pairc = bpf_map_lookup_elem(&pair_ctx, vptr);
+ if (!(*pairc))
+ return -EINVAL;
+
+ vptr = (u32 *)ARRAY_ELEM_PTR(in_pair_idx, cpu, nr_cpu_ids);
+ if (!vptr)
+ return -EINVAL;
+
+ *mask = 1U << *vptr;
+
+ return 0;
+}
+
+/*
+ * A task is above SCX whenever its effective priority is in the rt/dl
+ * range. Test p->prio rather than p->policy: rt_mutex_setprio() boosts
+ * a PI beneficiary into the rt/dl classes with its policy left
+ * untouched, so a policy test would misclassify boosted tasks in both
+ * directions. p->prio follows the boost and the deboost.
+ *
+ * This still cannot tell fair and SCX tasks apart. It is complete only
+ * because scx_pair runs in switch-all mode, where no fair class task
+ * exists; in partial mode fair is also above SCX and can take the CPU.
+ */
+static bool pair_task_is_highpri(struct task_struct *p)
+{
+ return p->prio < MAX_RT_PRIO;
+}
+
+static void pair_cpu_acquire_locked(struct pair_ctx *pairc, u32 in_pair_mask,
+ u32 *kick_flags)
+{
+ pairc->preempted_mask &= ~in_pair_mask;
+ /* Kick the pair CPU, unless it was also preempted. */
+ *kick_flags = !pairc->preempted_mask ? SCX_KICK_PREEMPT : 0;
+}
+
+static void pair_cpu_release_locked(struct pair_ctx *pairc, u32 in_pair_mask,
+ u32 *kick_flags)
+{
+ pairc->preempted_mask |= in_pair_mask;
+ pairc->active_mask &= ~in_pair_mask;
+ /* Kick the pair CPU if it's still running. */
+ *kick_flags = pairc->active_mask ? SCX_KICK_PREEMPT | SCX_KICK_WAIT : 0;
+ pairc->draining = true;
+}
+
+__attribute__((noinline))
+static int try_dispatch(s32 cpu)
+{
+ struct pair_ctx *pairc;
+ struct bpf_map *cgq_map;
+ struct task_struct *p;
+ u64 now = scx_bpf_now();
+ bool kick_pair = false;
+ bool expired, pair_preempted;
+ u32 *vptr, in_pair_mask;
+ s32 pid, q_idx;
+ u64 cgid;
+ int ret;
+
+ ret = lookup_pairc_and_mask(cpu, &pairc, &in_pair_mask);
+ if (ret) {
+ scx_bpf_error("failed to lookup pairc and in_pair_mask for cpu[%d]",
+ cpu);
+ return -ENOENT;
+ }
+
+ bpf_spin_lock(&pairc->lock);
+ pairc->active_mask &= ~in_pair_mask;
+
+ expired = time_before(pairc->started_at + pair_batch_dur_ns, now);
+ if (expired || pairc->draining) {
+ u64 new_cgid = 0;
+
+ __sync_fetch_and_add(&nr_exps, 1);
+
+ /*
+ * We're done with the current cgid. An obvious optimization
+ * would be not draining if the next cgroup is the current one.
+ * For now, be dumb and always expire.
+ */
+ pairc->draining = true;
+
+ pair_preempted = pairc->preempted_mask;
+ if (pairc->active_mask || pair_preempted) {
+ /*
+ * The other CPU is still active, or is no longer under
+ * our control due to e.g. being preempted by a higher
+ * priority sched_class. We want to wait until this
+ * cgroup expires, or until control of our pair CPU has
+ * been returned to us.
+ *
+ * If the pair controls its CPU, and the time already
+ * expired, kick. When the other CPU arrives at
+ * dispatch and clears its active mask, it'll push the
+ * pair to the next cgroup and kick this CPU.
+ */
+ __sync_fetch_and_add(&nr_exp_waits, 1);
+ bpf_spin_unlock(&pairc->lock);
+ if (expired && !pair_preempted)
+ kick_pair = true;
+ goto out_maybe_kick;
+ }
+
+ bpf_spin_unlock(&pairc->lock);
+
+ /*
+ * Pick the next cgroup. It'd be easier / cleaner to not drop
+ * pairc->lock and use stronger synchronization here especially
+ * given that we'll be switching cgroups significantly less
+ * frequently than tasks. Unfortunately, bpf_spin_lock can't
+ * really protect anything non-trivial. Let's do opportunistic
+ * operations instead.
+ */
+ bpf_repeat(BPF_MAX_LOOPS) {
+ u32 *q_idx;
+ u64 *cgq_len;
+
+ if (bpf_map_pop_elem(&top_q, &new_cgid)) {
+ /* no active cgroup, go idle */
+ __sync_fetch_and_add(&nr_exp_empty, 1);
+ return 0;
+ }
+
+ q_idx = bpf_map_lookup_elem(&cgrp_q_idx_hash, &new_cgid);
+ if (!q_idx)
+ continue;
+
+ /*
+ * This is the only place where empty cgroups are taken
+ * off the top_q.
+ */
+ cgq_len = MEMBER_VPTR(cgrp_q_len, [*q_idx]);
+ if (!cgq_len || !*cgq_len)
+ continue;
+
+ /*
+ * If it has any tasks, requeue as we may race and not
+ * execute it.
+ */
+ bpf_map_push_elem(&top_q, &new_cgid, 0);
+ break;
+ }
+
+ bpf_spin_lock(&pairc->lock);
+
+ /*
+ * The other CPU may already have started on a new cgroup while
+ * we dropped the lock. Make sure that we're still draining and
+ * start on the new cgroup.
+ */
+ if (pairc->draining && !pairc->active_mask) {
+ __sync_fetch_and_add(&nr_cgrp_next, 1);
+ pairc->cgid = new_cgid;
+ pairc->started_at = now;
+ pairc->draining = false;
+ kick_pair = true;
+ } else {
+ __sync_fetch_and_add(&nr_cgrp_coll, 1);
+ }
+ }
+
+ cgid = pairc->cgid;
+ pairc->active_mask |= in_pair_mask;
+ bpf_spin_unlock(&pairc->lock);
+
+ /* again, it'd be better to do all these with the lock held, oh well */
+ vptr = bpf_map_lookup_elem(&cgrp_q_idx_hash, &cgid);
+ if (!vptr) {
+ scx_bpf_error("failed to lookup q_idx for cgroup[%llu]", cgid);
+ return -ENOENT;
+ }
+ q_idx = *vptr;
+
+ /* claim one task from cgrp_q w/ q_idx */
+ bpf_repeat(BPF_MAX_LOOPS) {
+ u64 *cgq_len, len;
+
+ cgq_len = MEMBER_VPTR(cgrp_q_len, [q_idx]);
+ if (!cgq_len || !(len = *(volatile u64 *)cgq_len)) {
+ /* the cgroup must be empty, expire and repeat */
+ __sync_fetch_and_add(&nr_cgrp_empty, 1);
+ bpf_spin_lock(&pairc->lock);
+ pairc->draining = true;
+ pairc->active_mask &= ~in_pair_mask;
+ bpf_spin_unlock(&pairc->lock);
+ return -EAGAIN;
+ }
+
+ if (__sync_val_compare_and_swap(cgq_len, len, len - 1) != len)
+ continue;
+
+ break;
+ }
+
+ cgq_map = bpf_map_lookup_elem(&cgrp_q_arr, &q_idx);
+ if (!cgq_map) {
+ scx_bpf_error("failed to lookup cgq_map for cgroup[%llu] q_idx[%d]",
+ cgid, q_idx);
+ return -ENOENT;
+ }
+
+ if (bpf_map_pop_elem(cgq_map, &pid)) {
+ scx_bpf_error("cgq_map is empty for cgroup[%llu] q_idx[%d]",
+ cgid, q_idx);
+ return -ENOENT;
+ }
+
+ p = bpf_task_from_pid(pid);
+ if (p) {
+ __sync_fetch_and_add(&nr_dispatched, 1);
+ scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0);
+ bpf_task_release(p);
+ } else {
+ /* we don't handle dequeues, retry on lost tasks */
+ __sync_fetch_and_add(&nr_missing, 1);
+ return -EAGAIN;
+ }
+
+out_maybe_kick:
+ if (kick_pair) {
+ s32 *pair = (s32 *)ARRAY_ELEM_PTR(pair_cpu, cpu, nr_cpu_ids);
+ if (pair) {
+ __sync_fetch_and_add(&nr_kicks, 1);
+ scx_bpf_kick_cpu(*pair, SCX_KICK_PREEMPT);
+ }
+ }
+ return 0;
+}
+
+void BPF_STRUCT_OPS(pair_dispatch, s32 cpu, struct task_struct *prev)
+{
+ bpf_repeat(BPF_MAX_LOOPS) {
+ if (try_dispatch(cpu) != -EAGAIN)
+ break;
+ }
+}
+
+SEC("tp_btf/sched_switch")
+int BPF_PROG(pair_sched_switch, bool preempt, struct task_struct *prev,
+ struct task_struct *next, unsigned int prev_state)
+{
+ int ret;
+ s32 cpu = bpf_get_smp_processor_id();
+ u32 in_pair_mask;
+ struct pair_ctx *pairc;
+ u32 kick_flags = 0;
+ bool preempted;
+ bool release, acquire;
+
+ ret = lookup_pairc_and_mask(cpu, &pairc, &in_pair_mask);
+ if (ret)
+ return 0;
+
+ /*
+ * This runs on every context switch in the system. A CPU's own
+ * preempted_mask bit is only ever written by this tracepoint
+ * running on that CPU, so the unlocked read is exact and the
+ * pair-shared lock is only taken on actual transitions.
+ */
+ preempted = pairc->preempted_mask & in_pair_mask;
+ if (next->pid && pair_task_is_highpri(next)) {
+ /* an SCX task lost the CPU to a higher-priority class */
+ release = !preempted && prev->pid && !pair_task_is_highpri(prev);
+ acquire = false;
+ } else {
+ /* the CPU is back under SCX control (or idle) */
+ release = false;
+ acquire = preempted;
+ }
+ if (!release && !acquire)
+ return 0;
+
+ bpf_spin_lock(&pairc->lock);
+ if (release) {
+ pair_cpu_release_locked(pairc, in_pair_mask, &kick_flags);
+ __sync_fetch_and_add(&nr_preemptions, 1);
+ } else {
+ pair_cpu_acquire_locked(pairc, in_pair_mask, &kick_flags);
+ }
+ bpf_spin_unlock(&pairc->lock);
+
+ if (kick_flags) {
+ s32 *pair = (s32 *)ARRAY_ELEM_PTR(pair_cpu, cpu, nr_cpu_ids);
+
+ if (pair) {
+ __sync_fetch_and_add(&nr_kicks, 1);
+ scx_bpf_kick_cpu(*pair, kick_flags);
+ }
+ }
+
+ return 0;
+}
+
+s32 BPF_STRUCT_OPS(pair_cgroup_init, struct cgroup *cgrp)
+{
+ u64 cgid = cgrp->kn->id;
+ s32 i, q_idx;
+
+ bpf_for(i, 0, MAX_CGRPS) {
+ q_idx = __sync_fetch_and_add(&cgrp_q_idx_cursor, 1) % MAX_CGRPS;
+ if (!__sync_val_compare_and_swap(&cgrp_q_idx_busy[q_idx], 0, 1))
+ break;
+ }
+ if (i == MAX_CGRPS)
+ return -EBUSY;
+
+ if (bpf_map_update_elem(&cgrp_q_idx_hash, &cgid, &q_idx, BPF_ANY)) {
+ u64 *busy = MEMBER_VPTR(cgrp_q_idx_busy, [q_idx]);
+ if (busy)
+ *busy = 0;
+ return -EBUSY;
+ }
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(pair_cgroup_exit, struct cgroup *cgrp)
+{
+ u64 cgid = cgrp->kn->id;
+ s32 *q_idx;
+
+ q_idx = bpf_map_lookup_elem(&cgrp_q_idx_hash, &cgid);
+ if (q_idx) {
+ u64 *busy = MEMBER_VPTR(cgrp_q_idx_busy, [*q_idx]);
+ if (busy)
+ *busy = 0;
+ bpf_map_delete_elem(&cgrp_q_idx_hash, &cgid);
+ }
+}
+
+void BPF_STRUCT_OPS(pair_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+SCX_OPS_DEFINE(pair_ops,
+ .enqueue = (void *)pair_enqueue,
+ .dispatch = (void *)pair_dispatch,
+ .cgroup_init = (void *)pair_cgroup_init,
+ .cgroup_exit = (void *)pair_cgroup_exit,
+ .exit = (void *)pair_exit,
+ .name = "pair");
diff --git a/tools/sched_ext/scx_pair.c b/tools/sched_ext/scx_pair.c
new file mode 100644
index 000000000..00f595b58
--- /dev/null
+++ b/tools/sched_ext/scx_pair.c
@@ -0,0 +1,196 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <stdio.h>
+#include <unistd.h>
+#include <inttypes.h>
+#include <signal.h>
+#include <assert.h>
+#include <libgen.h>
+#include <bpf/bpf.h>
+#include <scx/common.h>
+#include "scx_pair.h"
+#include "scx_pair.bpf.skel.h"
+
+const char help_fmt[] =
+"A demo sched_ext core-scheduler which always makes every sibling CPU pair\n"
+"execute from the same CPU cgroup.\n"
+"\n"
+"See the top-level comment in .bpf.c for more details.\n"
+"\n"
+"Usage: %s [-S STRIDE] [-v]\n"
+"\n"
+" -S STRIDE Override CPU pair stride (default: nr_cpus_ids / 2)\n"
+" -v Print libbpf debug messages\n"
+" -h Display this help and exit\n";
+
+static bool verbose;
+static volatile int exit_req;
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ if (level == LIBBPF_DEBUG && !verbose)
+ return 0;
+ return vfprintf(stderr, format, args);
+}
+
+static void sigint_handler(int dummy)
+{
+ exit_req = 1;
+}
+
+int main(int argc, char **argv)
+{
+ struct scx_pair *skel;
+ struct bpf_link *link;
+ __u64 seq = 0, ecode;
+ __s32 stride, i, opt, outer_fd;
+ __u32 pair_id = 0;
+
+ libbpf_set_print(libbpf_print_fn);
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+restart:
+ optind = 1;
+ skel = SCX_OPS_OPEN(pair_ops, scx_pair);
+
+ skel->rodata->nr_cpu_ids = libbpf_num_possible_cpus();
+ skel->rodata->pair_batch_dur_ns = __COMPAT_ENUM_OR_ZERO("scx_public_consts", "SCX_SLICE_DFL");
+
+ /* pair up the earlier half to the latter by default, override with -s */
+ stride = skel->rodata->nr_cpu_ids / 2;
+
+ while ((opt = getopt(argc, argv, "S:vh")) != -1) {
+ switch (opt) {
+ case 'S':
+ stride = strtoul(optarg, NULL, 0);
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ fprintf(stderr, help_fmt, basename(argv[0]));
+ return opt != 'h';
+ }
+ }
+
+ /* Stride must be positive to pair distinct CPUs. */
+ if (stride <= 0) {
+ fprintf(stderr, "Invalid stride %d, must be positive\n", stride);
+ scx_pair__destroy(skel);
+ return -1;
+ }
+
+ if (skel->rodata->nr_cpu_ids & 1) {
+ fprintf(stderr, "scx_pair requires an even CPU count, got %u\n",
+ skel->rodata->nr_cpu_ids);
+ scx_pair__destroy(skel);
+ return -1;
+ }
+
+ bpf_map__set_max_entries(skel->maps.pair_ctx, skel->rodata->nr_cpu_ids / 2);
+
+ /* Resize arrays so their element count is equal to cpu count. */
+ RESIZE_ARRAY(skel, rodata, pair_cpu, skel->rodata->nr_cpu_ids);
+ RESIZE_ARRAY(skel, rodata, pair_id, skel->rodata->nr_cpu_ids);
+ RESIZE_ARRAY(skel, rodata, in_pair_idx, skel->rodata->nr_cpu_ids);
+
+ for (i = 0; i < skel->rodata->nr_cpu_ids; i++)
+ skel->rodata_pair_cpu->pair_cpu[i] = -1;
+
+ printf("Pairs: ");
+ for (i = 0; i < skel->rodata->nr_cpu_ids; i++) {
+ int j = (i + stride) % skel->rodata->nr_cpu_ids;
+
+ if (skel->rodata_pair_cpu->pair_cpu[i] >= 0)
+ continue;
+
+ SCX_BUG_ON(i == j,
+ "Invalid stride %d - CPU%d wants to be its own pair",
+ stride, i);
+
+ SCX_BUG_ON(skel->rodata_pair_cpu->pair_cpu[j] >= 0,
+ "Invalid stride %d - three CPUs (%d, %d, %d) want to be a pair",
+ stride, i, j, skel->rodata_pair_cpu->pair_cpu[j]);
+
+ skel->rodata_pair_cpu->pair_cpu[i] = j;
+ skel->rodata_pair_cpu->pair_cpu[j] = i;
+ skel->rodata_pair_id->pair_id[i] = pair_id;
+ skel->rodata_pair_id->pair_id[j] = pair_id;
+ skel->rodata_in_pair_idx->in_pair_idx[i] = 0;
+ skel->rodata_in_pair_idx->in_pair_idx[j] = 1;
+ pair_id++;
+
+ printf("[%d, %d] ", i, j);
+ }
+ printf("\n");
+
+ SCX_OPS_LOAD(skel, pair_ops, scx_pair, uei);
+
+ /*
+ * Populate the cgrp_q_arr map which is an array containing per-cgroup
+ * queues. It'd probably be better to do this from BPF but there are too
+ * many to initialize statically and there's no way to dynamically
+ * populate from BPF.
+ */
+ outer_fd = bpf_map__fd(skel->maps.cgrp_q_arr);
+ SCX_BUG_ON(outer_fd < 0, "Failed to get outer_fd: %d", outer_fd);
+
+ printf("Initializing");
+ for (i = 0; i < MAX_CGRPS; i++) {
+ __s32 inner_fd;
+
+ if (exit_req)
+ break;
+
+ inner_fd = bpf_map_create(BPF_MAP_TYPE_QUEUE, NULL, 0,
+ sizeof(__u32), MAX_QUEUED, NULL);
+ SCX_BUG_ON(inner_fd < 0, "Failed to get inner_fd: %d",
+ inner_fd);
+ SCX_BUG_ON(bpf_map_update_elem(outer_fd, &i, &inner_fd, BPF_ANY),
+ "Failed to set inner map");
+ close(inner_fd);
+
+ if (!(i % 10))
+ printf(".");
+ fflush(stdout);
+ }
+ printf("\n");
+
+ /*
+ * Fully initialized, attach and run.
+ */
+ link = SCX_OPS_ATTACH(skel, pair_ops, scx_pair);
+
+ while (!exit_req && !UEI_EXITED(skel, uei)) {
+ printf("[SEQ %llu]\n", seq++);
+ printf(" total:%10" PRIu64 " dispatch:%10" PRIu64 " missing:%10" PRIu64 "\n",
+ skel->bss->nr_total,
+ skel->bss->nr_dispatched,
+ skel->bss->nr_missing);
+ printf(" kicks:%10" PRIu64 " preemptions:%7" PRIu64 "\n",
+ skel->bss->nr_kicks,
+ skel->bss->nr_preemptions);
+ printf(" exp:%10" PRIu64 " exp_wait:%10" PRIu64 " exp_empty:%10" PRIu64 "\n",
+ skel->bss->nr_exps,
+ skel->bss->nr_exp_waits,
+ skel->bss->nr_exp_empty);
+ printf("cgnext:%10" PRIu64 " cgcoll:%10" PRIu64 " cgempty:%10" PRIu64 "\n",
+ skel->bss->nr_cgrp_next,
+ skel->bss->nr_cgrp_coll,
+ skel->bss->nr_cgrp_empty);
+ fflush(stdout);
+ sleep(1);
+ }
+
+ bpf_link__destroy(link);
+ ecode = UEI_REPORT(skel, uei);
+ scx_pair__destroy(skel);
+
+ if (!exit_req && UEI_ECODE_RESTART(ecode))
+ goto restart;
+ return 0;
+}
diff --git a/tools/sched_ext/scx_pair.h b/tools/sched_ext/scx_pair.h
new file mode 100644
index 000000000..d9666a447
--- /dev/null
+++ b/tools/sched_ext/scx_pair.h
@@ -0,0 +1,9 @@
+#ifndef __SCX_EXAMPLE_PAIR_H
+#define __SCX_EXAMPLE_PAIR_H
+
+enum {
+ MAX_QUEUED = 4096,
+ MAX_CGRPS = 4096,
+};
+
+#endif /* __SCX_EXAMPLE_PAIR_H */
diff --git a/tools/sched_ext/scx_qmap.bpf.c b/tools/sched_ext/scx_qmap.bpf.c
new file mode 100644
index 000000000..2f3653199
--- /dev/null
+++ b/tools/sched_ext/scx_qmap.bpf.c
@@ -0,0 +1,2095 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * scx_qmap: a demonstration and testing scheduler for sched_ext features.
+ *
+ * A simple scheduler that exercises a broad set of sched_ext features. Unlikely
+ * to be useful for real workloads. It demonstrates:
+ *
+ * - BPF-side queueing using TIDs.
+ * - BPF arena for scheduler state.
+ * - Core-sched support.
+ * - Hierarchical sub-scheduling: delegating cpus to child cgroup schedulers.
+ *
+ * Base design: Five FIFOs (arena-backed doubly-linked lists through per-task
+ * context). A task is assigned to a FIFO by its compound weight. Each cpu
+ * round-robins the FIFOs, dispatching more from higher ones.
+ *
+ * Sub-scheduling: Any qmap sched can delegate cpus to its own child cgroup
+ * schedulers and keep the rest for its tasks. Terminology:
+ *
+ * excl - A cpu the delegatee owns wholly (ENQ_IMMED|ENQ|PREEMPT).
+ * shared - A cpu delegated as ENQ_IMMED only. Time-shared.
+ * held_excl / held_shared - What this node was handed by its parent.
+ * held-excl cpus are re-delegatable. A held-shared cpu is a
+ * time-share that stays self-local.
+ * self - The excl cpus the node kept for itself, plus all of held_shared.
+ * owner - Who holds a cid - a child slot, CID_SELF, or CID_NONE.
+ * avail - Cpus whose caps are in effect, per ops.sub_ecaps_updated().
+ * usable - self AND avail. Placement decisions use this: self is the
+ * delegation split and can run ahead of what the cpus honor.
+ *
+ * The scheduler splits its held-excl cpus among self and the children in
+ * proportion to each node's cpu.weight, handing each the floor of its share as
+ * excl cpus. The leftover from rounding forms a shared pool the round-robin
+ * timer hands around. With no excl cpu to delegate, the node evicts its
+ * children.
+ *
+ * This policy is a demonstration only, not a practical one. The split
+ * considers only direct children and is not work-conserving. It only exists to
+ * drive sub-sched primitives with as simple logic as possible.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <scx/common.bpf.h>
+
+#include "scx_qmap.h"
+
+enum consts {
+ ONE_SEC_IN_NS = 1000000000,
+ ONE_MSEC_IN_NS = 1000000,
+ LOWPRI_INTV_NS = 10 * ONE_MSEC_IN_NS,
+ SHARED_DSQ = 0,
+ HIGHPRI_DSQ = 1,
+ LOWPRI_DSQ = 2,
+ HIGHPRI_WEIGHT = 8668, /* this is what -20 maps to */
+};
+
+char _license[] SEC("license") = "GPL";
+
+const volatile u64 slice_ns;
+const volatile u32 stall_user_nth;
+const volatile u32 stall_kernel_nth;
+const volatile u32 dsp_inf_loop_after;
+const volatile u32 dsp_batch;
+const volatile bool highpri_boosting;
+const volatile bool print_dsqs_and_events;
+const volatile bool print_msgs;
+const volatile u64 sub_cgroup_id;
+const volatile s32 disallow_tgid;
+const volatile bool suppress_dump;
+const volatile u32 immed_stress_nth;
+const volatile u32 max_tasks;
+
+/* sub-sched: period for handing the round-robin cid pool to the next child */
+const volatile u64 round_robin_ns;
+
+/*
+ * Optional cid-override test harness. When cid_override_mode is non-zero,
+ * qmap_init_cids() calls scx_bpf_cid_override() with the caller-supplied arrays
+ * to exercise the kfunc's acceptance and error paths. See enum
+ * qmap_cid_override for the modes.
+ */
+const volatile u32 cid_override_mode;
+const volatile u32 cid_override_nr_shards;
+
+UEI_DEFINE(uei);
+
+/*
+ * All scheduler state - per-cpu context, stats counters, core-sched sequence
+ * numbers, sub-sched cgroup ids - lives in this single BPF arena map. Userspace
+ * reaches it via skel->arena->qa.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ __uint(max_entries, 1 << 16); /* upper bound in pages */
+#if defined(__TARGET_ARCH_arm64) || defined(__aarch64__)
+ __ulong(map_extra, 0x1ull << 32); /* user/BPF mmap base */
+#else
+ __ulong(map_extra, 0x1ull << 44);
+#endif
+} arena SEC(".maps");
+
+struct qmap_arena __arena_global qa;
+
+/* ensure that BPF and userspace are seeing the same size for qmap_cmask */
+_Static_assert(QMAP_CMASK_WORDS == CMASK_NR_WORDS(SCX_QMAP_MAX_CPUS),
+ "QMAP_CMASK_WORDS must equal CMASK_NR_WORDS(SCX_QMAP_MAX_CPUS)");
+_Static_assert(sizeof(struct qmap_cmask) ==
+ struct_size_t(struct scx_cmask, bits, QMAP_CMASK_WORDS),
+ "qmap_cmask must be exactly sized to back a full scx_cmask");
+
+/* Per-queue locks. Each in its own .data section as bpf_res_spin_lock requires. */
+__hidden struct bpf_res_spin_lock qa_q_lock0 SEC(".data.qa_q_lock0");
+__hidden struct bpf_res_spin_lock qa_q_lock1 SEC(".data.qa_q_lock1");
+__hidden struct bpf_res_spin_lock qa_q_lock2 SEC(".data.qa_q_lock2");
+__hidden struct bpf_res_spin_lock qa_q_lock3 SEC(".data.qa_q_lock3");
+__hidden struct bpf_res_spin_lock qa_q_lock4 SEC(".data.qa_q_lock4");
+
+static struct bpf_res_spin_lock *qa_q_lock(s32 qid)
+{
+ switch (qid) {
+ case 0: return &qa_q_lock0;
+ case 1: return &qa_q_lock1;
+ case 2: return &qa_q_lock2;
+ case 3: return &qa_q_lock3;
+ case 4: return &qa_q_lock4;
+ default: return NULL;
+ }
+}
+
+/*
+ * If enabled, CPU performance target is set according to the queue index
+ * according to the following table.
+ */
+static const u32 qidx_to_cpuperf_target[] = {
+ [0] = SCX_CPUPERF_ONE * 0 / 4,
+ [1] = SCX_CPUPERF_ONE * 1 / 4,
+ [2] = SCX_CPUPERF_ONE * 2 / 4,
+ [3] = SCX_CPUPERF_ONE * 3 / 4,
+ [4] = SCX_CPUPERF_ONE * 4 / 4,
+};
+
+/*
+ * Per-queue sequence numbers to implement core-sched ordering.
+ *
+ * Tail seq is assigned to each queued task and incremented. Head seq tracks the
+ * sequence number of the latest dispatched task. The distance between the a
+ * task's seq and the associated queue's head seq is called the queue distance
+ * and used when comparing two tasks for ordering. See qmap_core_sched_before().
+ */
+
+/*
+ * Per-task scheduling context. Allocated from the qa.task_ctxs[] slab in
+ * arena. While the task is alive the entry is referenced from task_ctx_stor;
+ * while it's free the entry sits on the free list singly-linked through
+ * @next_free.
+ *
+ * When the task is queued on one of the five priority FIFOs, @q_idx is the
+ * queue index and @q_next/@q_prev link it in the queue's doubly-linked list.
+ * @q_idx is -1 when the task isn't on any queue.
+ */
+struct task_ctx {
+ struct task_ctx __arena *next_free; /* only valid on free list */
+ struct task_ctx __arena *q_next; /* queue link, NULL if tail */
+ struct task_ctx __arena *q_prev; /* queue link, NULL if head */
+ struct qmap_fifo __arena *fifo; /* queue we're on, NULL if not queued */
+ u64 tid;
+ s32 pid; /* for dump only */
+ bool force_local; /* Dispatch directly to local_dsq */
+ bool highpri;
+ u64 core_sched_seq;
+ struct scx_cmask cpus_allowed; /* per-task affinity in cid space */
+};
+
+/*
+ * Slab stride for task_ctx. cpus_allowed's flex array bits[] overlaps the
+ * tail bytes appended per entry; struct_size() gives the actual per-entry
+ * footprint.
+ */
+#define TASK_CTX_STRIDE \
+ struct_size_t(struct task_ctx, cpus_allowed.bits, \
+ CMASK_NR_WORDS(SCX_QMAP_MAX_CPUS))
+
+/* All task_ctx pointers are arena pointers. */
+typedef struct task_ctx __arena task_ctx_t;
+
+/* Holds an arena pointer to the task's slab entry. */
+struct task_ctx_stor_val {
+ task_ctx_t *taskc;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_TASK_STORAGE);
+ __uint(map_flags, BPF_F_NO_PREALLOC);
+ __type(key, int);
+ __type(value, struct task_ctx_stor_val);
+} task_ctx_stor SEC(".maps");
+
+/* Protects the task_ctx slab free list. */
+__hidden struct bpf_res_spin_lock qa_task_lock SEC(".data.qa_task_lock");
+
+static int qmap_spin_lock(struct bpf_res_spin_lock *lock)
+{
+ if (bpf_res_spin_lock(lock)) {
+ scx_bpf_error("res_spin_lock failed");
+ return -EBUSY;
+ }
+ return 0;
+}
+
+/*
+ * Try prev_cid, then scan cpus_allowed AND idle_cids AND usable_cids
+ * round-robin from prev_cid + 1. Atomic claim retries on race; bounded by
+ * IDLE_PICK_RETRIES to keep the verifier's insn budget in check.
+ */
+#define IDLE_PICK_RETRIES 16
+
+static s32 pick_direct_dispatch_cid(struct task_struct *p, s32 prev_cid,
+ task_ctx_t *taskc)
+{
+ u32 nr_cids = scx_bpf_nr_cids();
+ s32 cid;
+ u32 i;
+
+ if (cmask_test(prev_cid, &qa.usable_cids.mask) &&
+ cmask_test_and_clear(prev_cid, &qa.idle_cids.mask))
+ return prev_cid;
+
+ cid = prev_cid;
+ bpf_for(i, 0, IDLE_PICK_RETRIES) {
+ cid = cmask_next_and2_set_wrap(&taskc->cpus_allowed,
+ &qa.idle_cids.mask,
+ &qa.usable_cids.mask, cid + 1);
+ barrier_var(cid);
+ if (cid >= nr_cids)
+ return -1;
+ if (cmask_test_and_clear(cid, &qa.idle_cids.mask))
+ return cid;
+ }
+ return -1;
+}
+
+/*
+ * Force a reference to the arena map. The verifier associates an arena with
+ * a program by finding an LD_IMM64 instruction that loads the arena's BPF
+ * map; programs that only use arena pointers returned from task-local
+ * storage (like qmap_select_cpu) never reference @arena directly. Without
+ * this, the verifier rejects addr_space_cast with "addr_space_cast insn
+ * can only be used in a program that has an associated arena".
+ */
+#define QMAP_TOUCH_ARENA() do { asm volatile("" :: "r"(&arena)); } while (0)
+
+static task_ctx_t *lookup_task_ctx(struct task_struct *p)
+{
+ struct task_ctx_stor_val *v;
+
+ QMAP_TOUCH_ARENA();
+
+ v = bpf_task_storage_get(&task_ctx_stor, p, 0, 0);
+ if (!v || !v->taskc)
+ return NULL;
+ return v->taskc;
+}
+
+/* Append @taskc to the tail of @fifo. Must not already be queued. */
+static void qmap_fifo_enqueue(struct qmap_fifo __arena *fifo, task_ctx_t *taskc)
+{
+ struct bpf_res_spin_lock *lock = qa_q_lock(fifo->idx);
+
+ if (!lock || qmap_spin_lock(lock))
+ return;
+ taskc->fifo = fifo;
+ taskc->q_next = NULL;
+ taskc->q_prev = fifo->tail;
+ if (fifo->tail)
+ fifo->tail->q_next = taskc;
+ else
+ fifo->head = taskc;
+ fifo->tail = taskc;
+ bpf_res_spin_unlock(lock);
+}
+
+/* Pop the head of @fifo. Returns NULL if empty. */
+static task_ctx_t *qmap_fifo_pop(struct qmap_fifo __arena *fifo)
+{
+ struct bpf_res_spin_lock *lock = qa_q_lock(fifo->idx);
+ task_ctx_t *taskc;
+
+ if (!lock || qmap_spin_lock(lock))
+ return NULL;
+ taskc = fifo->head;
+ if (taskc) {
+ fifo->head = taskc->q_next;
+ if (taskc->q_next)
+ taskc->q_next->q_prev = NULL;
+ else
+ fifo->tail = NULL;
+ taskc->q_next = NULL;
+ taskc->q_prev = NULL;
+ taskc->fifo = NULL;
+ }
+ bpf_res_spin_unlock(lock);
+ return taskc;
+}
+
+/* Remove @taskc from its fifo. No-op if not queued. */
+static void qmap_fifo_remove(task_ctx_t *taskc)
+{
+ struct qmap_fifo __arena *fifo = taskc->fifo;
+ struct bpf_res_spin_lock *lock;
+
+ if (!fifo)
+ return;
+
+ lock = qa_q_lock(fifo->idx);
+ if (!lock || qmap_spin_lock(lock))
+ return;
+
+ /* Re-check under lock — a concurrent pop may have cleared fifo. */
+ if (taskc->fifo != fifo) {
+ bpf_res_spin_unlock(lock);
+ return;
+ }
+
+ if (taskc->q_next)
+ taskc->q_next->q_prev = taskc->q_prev;
+ else
+ fifo->tail = taskc->q_prev;
+ if (taskc->q_prev)
+ taskc->q_prev->q_next = taskc->q_next;
+ else
+ fifo->head = taskc->q_next;
+ taskc->q_next = NULL;
+ taskc->q_prev = NULL;
+ taskc->fifo = NULL;
+ bpf_res_spin_unlock(lock);
+}
+
+s32 BPF_STRUCT_OPS(qmap_select_cid, struct task_struct *p,
+ s32 prev_cid, u64 wake_flags)
+{
+ task_ctx_t *taskc;
+ s32 cid;
+
+ if (!(taskc = lookup_task_ctx(p)))
+ return prev_cid;
+
+ if (p->scx.weight < 2 && !(p->flags & PF_KTHREAD))
+ return prev_cid;
+
+ cid = pick_direct_dispatch_cid(p, prev_cid, taskc);
+
+ if (cid >= 0) {
+ taskc->force_local = true;
+ return cid;
+ } else {
+ return prev_cid;
+ }
+}
+
+/*
+ * A received time-shared cid is held ENQ_IMMED-only, so inserts meant to run
+ * there must set SCX_ENQ_IMMED.
+ */
+static u64 needs_immed(s32 cid)
+{
+ return qa.cid_shared[cid] ? SCX_ENQ_IMMED : 0;
+}
+
+/* first cid this node does NOT hold for fault injection, -1 if none */
+static s32 first_unavail_cid(void)
+{
+ s32 nr_cids = qa.nr_cids, c;
+
+ if (nr_cids > SCX_QMAP_MAX_CPUS) {
+ scx_bpf_error("-ERANGE");
+ return -1;
+ }
+
+ bpf_for(c, 0, nr_cids) {
+ if (!cmask_test(c, &qa.held_excl.mask) &&
+ !cmask_test(c, &qa.held_shared.mask))
+ return c;
+ }
+ return -1;
+}
+
+static int weight_to_idx(u32 weight)
+{
+ /* Coarsely map the compound weight to a FIFO. */
+ if (weight <= 25)
+ return 0;
+ else if (weight <= 50)
+ return 1;
+ else if (weight < 200)
+ return 2;
+ else if (weight < 400)
+ return 3;
+ else
+ return 4;
+}
+
+void BPF_STRUCT_OPS(qmap_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ static u32 user_cnt, kernel_cnt;
+ task_ctx_t *taskc;
+ int idx = weight_to_idx(p->scx.weight);
+ s32 cid;
+
+ if (enq_flags & SCX_ENQ_REENQ) {
+ u64 reason = p->scx.flags & SCX_TASK_REENQ_REASON_MASK;
+
+ __sync_fetch_and_add(&qa.nr_reenqueued, 1);
+ if (scx_bpf_task_cid(p) == 0)
+ __sync_fetch_and_add(&qa.nr_reenqueued_cid0, 1);
+ /* cap-loss and IMMED-handback bounces, relocated below */
+ if (reason == SCX_TASK_REENQ_CAP)
+ __sync_fetch_and_add(&qa.nr_reenq_cap, 1);
+ else if (reason == SCX_TASK_REENQ_IMMED)
+ __sync_fetch_and_add(&qa.nr_reenq_immed, 1);
+ }
+
+ if (p->flags & PF_KTHREAD) {
+ if (stall_kernel_nth && !(++kernel_cnt % stall_kernel_nth))
+ return;
+ } else {
+ if (stall_user_nth && !(++user_cnt % stall_user_nth))
+ return;
+ }
+
+ if (qa.test_error_cnt && !--qa.test_error_cnt)
+ scx_bpf_error("test triggering error");
+
+ if (!(taskc = lookup_task_ctx(p)))
+ return;
+
+ /*
+ * All enqueued tasks must have their core_sched_seq updated for correct
+ * core-sched ordering. Also, take a look at the end of qmap_dispatch().
+ */
+ taskc->core_sched_seq = qa.core_sched_tail_seqs[idx]++;
+
+ /*
+ * A task of ours that can run on none of our self cids - the parent
+ * didn't grant them or we delegated them to children - would starve in
+ * SHARED/FIFO since we only pull from those on self cids.
+ *
+ * Force it onto its first allowed cid's local DSQ with SCX_ENQ_RESCUE.
+ * If we hold ENQ on that cid it runs. Otherwise the kernel diverts the
+ * task to its rescue path. IMMED would turn the insert into a legal
+ * placement on a time-shared cid and the kernel would bounce it back
+ * here instead of rescuing it.
+ */
+ if (!cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) {
+ s32 c = cmask_next_set_wrap(&taskc->cpus_allowed, 0);
+
+ if (c >= 0 && c < scx_bpf_nr_cids()) {
+ taskc->force_local = false;
+ __sync_fetch_and_add(&qa.nr_rescue_dsp, 1);
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | c, slice_ns,
+ enq_flags | SCX_ENQ_RESCUE);
+ return;
+ }
+ }
+
+ /*
+ * Fault injection: deliberately dispatch one of our own tasks to a cid
+ * we don't hold. The inserts carry SCX_ENQ_RESCUE and divert to the
+ * kernel rescue path, a deterministic rescue-traffic generator. Under
+ * -B 0 the kernel cap check rejects and re-enqueues them instead, so
+ * nr_inject_attempts tracks nr_reenq_cap 1:1 and proves delivery-time
+ * enforcement. Throttled.
+ */
+ if (qa.inject_mode == QMAP_INJ_WRONG_CID && p->nr_cpus_allowed > 1 &&
+ !(enq_flags & SCX_ENQ_REENQ)) {
+ static u32 inj_cnt;
+
+ if (!(++inj_cnt % 64)) {
+ s32 bad = first_unavail_cid();
+
+ if (bad >= 0 && cmask_test(bad, &taskc->cpus_allowed)) {
+ __sync_fetch_and_add(&qa.nr_inject_attempts, 1);
+ __sync_fetch_and_add(&qa.nr_rescue_dsp, 1);
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | bad, slice_ns,
+ enq_flags | SCX_ENQ_RESCUE);
+ return;
+ }
+ }
+ }
+
+ /*
+ * IMMED stress testing: Every immed_stress_nth'th enqueue, dispatch
+ * directly to prev_cpu's local DSQ even when busy to force dsq->nr > 1
+ * and exercise the kernel IMMED reenqueue trigger paths.
+ */
+ if (immed_stress_nth && !(enq_flags & SCX_ENQ_REENQ)) {
+ static u32 immed_stress_cnt;
+
+ if (!(++immed_stress_cnt % immed_stress_nth)) {
+ taskc->force_local = false;
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | scx_bpf_task_cid(p),
+ slice_ns, enq_flags);
+ return;
+ }
+ }
+
+ /*
+ * If qmap_select_cid() is telling us to or this is the last runnable
+ * task on the CPU, enqueue locally.
+ */
+ if (taskc->force_local) {
+ taskc->force_local = false;
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, slice_ns,
+ enq_flags | needs_immed(scx_bpf_task_cid(p)));
+ return;
+ }
+
+ /* see lowpri_timerfn() */
+ if (__COMPAT_has_generic_reenq() &&
+ p->scx.weight < 2 && !(p->flags & PF_KTHREAD) && !(enq_flags & SCX_ENQ_REENQ)) {
+ scx_bpf_dsq_insert(p, LOWPRI_DSQ, slice_ns, enq_flags);
+ return;
+ }
+
+ /* if select_cid() wasn't called, try direct dispatch */
+ if (!__COMPAT_is_enq_cpu_selected(enq_flags) &&
+ (cid = pick_direct_dispatch_cid(p, scx_bpf_task_cid(p), taskc)) >= 0) {
+ __sync_fetch_and_add(&qa.nr_ddsp_from_enq, 1);
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cid, slice_ns,
+ enq_flags | needs_immed(cid));
+ return;
+ }
+
+ /*
+ * If the task was re-enqueued due to the CPU being preempted by a
+ * higher priority scheduling class, just re-enqueue the task directly
+ * on the global DSQ. As we want another CPU to pick it up, find and
+ * kick an idle cid.
+ */
+ if (enq_flags & SCX_ENQ_REENQ) {
+ s32 cid;
+
+ scx_bpf_dsq_insert(p, SHARED_DSQ, 0, enq_flags);
+ cid = cmask_next_and2_set_wrap(&taskc->cpus_allowed,
+ &qa.idle_cids.mask,
+ &qa.usable_cids.mask, 0);
+ if (cid < scx_bpf_nr_cids())
+ scx_bpf_kick_cid(cid, SCX_KICK_IDLE);
+ return;
+ }
+
+ /* Queue on the selected FIFO. */
+ qmap_fifo_enqueue(&qa.fifos[idx], taskc);
+
+ if (highpri_boosting && p->scx.weight >= HIGHPRI_WEIGHT) {
+ taskc->highpri = true;
+ __sync_fetch_and_add(&qa.nr_highpri_queued, 1);
+ }
+ __sync_fetch_and_add(&qa.nr_enqueued, 1);
+}
+
+void BPF_STRUCT_OPS(qmap_dequeue, struct task_struct *p, u64 deq_flags)
+{
+ task_ctx_t *taskc;
+
+ __sync_fetch_and_add(&qa.nr_dequeued, 1);
+ if (deq_flags & SCX_DEQ_CORE_SCHED_EXEC)
+ __sync_fetch_and_add(&qa.nr_core_sched_execed, 1);
+
+ taskc = lookup_task_ctx(p);
+ if (taskc && taskc->fifo) {
+ if (taskc->highpri)
+ __sync_fetch_and_sub(&qa.nr_highpri_queued, 1);
+ qmap_fifo_remove(taskc);
+ }
+}
+
+static void update_core_sched_head_seq(struct task_struct *p)
+{
+ int idx = weight_to_idx(p->scx.weight);
+ task_ctx_t *taskc;
+
+ if ((taskc = lookup_task_ctx(p)))
+ qa.core_sched_head_seqs[idx] = taskc->core_sched_seq;
+}
+
+/*
+ * One pass over SHARED_DSQ: rescue stranded tasks and boost highpri ones. A
+ * task whose cids were lost while it was queued in the fifos would strand on
+ * SHARED_DSQ, which is consumed only on self cids it can't run on - move it to
+ * the kernel rescue path. One whose cids were lost after the highpri cull is
+ * likewise rescued out of HIGHPRI_DSQ below.
+ *
+ * To demonstrate the use of scx_bpf_dsq_move(), implement silly selective
+ * priority boosting mechanism by moving highpri tasks to HIGHPRI_DSQ and then
+ * consuming them first. This makes minor difference only when dsp_batch is
+ * larger than 1.
+ *
+ * scx_bpf_dsq_move[_vtime]() are allowed both from ops.dispatch() and
+ * non-rq-lock holding BPF programs. As demonstration, this function is called
+ * from qmap_dispatch() and monitor_timerfn().
+ */
+static bool scan_shared_dsq(bool from_timer)
+{
+ struct task_struct *p;
+ s32 this_cid = scx_bpf_this_cid();
+ u32 nr_cids = scx_bpf_nr_cids();
+
+ /* rescue strands and move highpri tasks to HIGHPRI_DSQ */
+ bpf_for_each(scx_dsq, p, SHARED_DSQ, 0) {
+ static u64 highpri_seq;
+ task_ctx_t *taskc;
+ s32 c;
+
+ if (!(taskc = lookup_task_ctx(p)))
+ return false;
+
+ /* stranded? rescue - it can't be dispatched here either way */
+ if (!cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask)) {
+ c = cmask_next_set_wrap(&taskc->cpus_allowed, 0);
+ if (c >= 0 && c < scx_bpf_nr_cids()) {
+ __sync_fetch_and_add(&qa.nr_rescue_dsp, 1);
+ scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | c,
+ SCX_ENQ_RESCUE);
+ }
+ continue;
+ }
+
+ if (taskc->highpri) {
+ /* exercise the set_*() and vtime interface too */
+ scx_bpf_dsq_move_set_slice(BPF_FOR_EACH_ITER, slice_ns * 2);
+ scx_bpf_dsq_move_set_vtime(BPF_FOR_EACH_ITER, highpri_seq++);
+ scx_bpf_dsq_move_vtime(BPF_FOR_EACH_ITER, p, HIGHPRI_DSQ, 0);
+ }
+ }
+
+ /*
+ * Scan HIGHPRI_DSQ and dispatch until a task that can run here is
+ * found. Prefer this_cid if the task allows it; otherwise RR-scan the
+ * task's cpus_allowed starting after this_cid.
+ */
+ bpf_for_each(scx_dsq, p, HIGHPRI_DSQ, 0) {
+ task_ctx_t *taskc;
+ bool dispatched = false;
+ s32 cid;
+
+ if (!(taskc = lookup_task_ctx(p)))
+ return false;
+
+ /* only run highpri tasks on cids this node can use right now */
+ if (cmask_test(this_cid, &taskc->cpus_allowed) &&
+ cmask_test(this_cid, &qa.usable_cids.mask))
+ cid = this_cid;
+ else
+ cid = cmask_next_and_set_wrap(&taskc->cpus_allowed,
+ &qa.usable_cids.mask,
+ this_cid + 1);
+ if (cid >= nr_cids) {
+ s32 c;
+
+ /* self cids lack caps in effect yet, leave it queued */
+ if (cmask_intersects(&taskc->cpus_allowed, &qa.self_cids.mask))
+ continue;
+
+ /* stranded after the cull - rescue it from here */
+ c = cmask_next_set_wrap(&taskc->cpus_allowed, 0);
+ if (c >= 0 && c < nr_cids) {
+ __sync_fetch_and_add(&qa.nr_rescue_dsp, 1);
+ scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | c,
+ SCX_ENQ_RESCUE);
+ }
+ continue;
+ }
+
+ if (scx_bpf_dsq_move(BPF_FOR_EACH_ITER, p, SCX_DSQ_LOCAL_ON | cid,
+ SCX_ENQ_PREEMPT | needs_immed(cid))) {
+ if (cid == this_cid) {
+ dispatched = true;
+ __sync_fetch_and_add(&qa.nr_expedited_local, 1);
+ } else {
+ __sync_fetch_and_add(&qa.nr_expedited_remote, 1);
+ }
+ if (from_timer)
+ __sync_fetch_and_add(&qa.nr_expedited_from_timer, 1);
+ } else {
+ __sync_fetch_and_add(&qa.nr_expedited_lost, 1);
+ }
+
+ if (dispatched)
+ return true;
+ }
+
+ return false;
+}
+
+void BPF_STRUCT_OPS(qmap_dispatch, s32 cid, struct task_struct *prev)
+{
+ struct task_struct *p;
+ struct cpu_ctx __arena *cpuc;
+ task_ctx_t *taskc;
+ u32 batch = dsp_batch ?: 1;
+ s32 owner, i;
+
+ if (scan_shared_dsq(false))
+ return;
+
+ /*
+ * Sub-sched routing: a child-owned cid goes to its owner. Never run
+ * this node's own tasks on a delegated cid. Read without the guard.
+ */
+ owner = qa.part.cid_owner[cid];
+ if (owner == CID_SHARED) {
+ /* route to the live rr holder (0 = self, runs below) */
+ s32 pos = qa.part.rr_pos;
+ u64 holder_cgid = (pos >= 0 && pos < MAX_PARTS) ?
+ qa.part.rr_slots[pos] : 0;
+
+ if (holder_cgid) {
+ scx_bpf_sub_dispatch(holder_cgid);
+ return;
+ }
+ } else if (owner >= 0 && owner < MAX_SUB_SCHEDS) {
+ u64 cgid = qa.sub_sched_ctxs[owner].cgroup_id;
+
+ if (cgid) {
+ if (scx_bpf_sub_dispatch(cgid))
+ __sync_fetch_and_add(&qa.sub_sched_ctxs[owner].nr_dsps, 1);
+ return;
+ }
+ }
+
+ if (!qa.nr_highpri_queued && scx_bpf_dsq_move_to_local(SHARED_DSQ, needs_immed(cid)))
+ return;
+
+ if (dsp_inf_loop_after && qa.nr_dispatched > dsp_inf_loop_after) {
+ /*
+ * PID 2 should be kthreadd which should mostly be idle and off
+ * the scheduler. Let's keep dispatching it to force the kernel
+ * to call this function over and over again.
+ */
+ p = bpf_task_from_pid(2);
+ if (p) {
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, slice_ns, 0);
+ bpf_task_release(p);
+ return;
+ }
+ }
+
+ cpuc = &qa.cpu_ctxs[scx_bpf_this_cid()];
+
+ for (i = 0; i < 5; i++) {
+ /* Advance the dispatch cursor and pick the fifo. */
+ if (!cpuc->dsp_cnt) {
+ cpuc->dsp_idx = (cpuc->dsp_idx + 1) % 5;
+ cpuc->dsp_cnt = 1 << cpuc->dsp_idx;
+ }
+
+ /* Dispatch or advance. */
+ bpf_repeat(BPF_MAX_LOOPS) {
+ task_ctx_t *taskc;
+
+ taskc = qmap_fifo_pop(&qa.fifos[cpuc->dsp_idx]);
+ if (!taskc)
+ break;
+
+ p = scx_bpf_tid_to_task(taskc->tid);
+ if (!p)
+ continue;
+
+ if (taskc->highpri)
+ __sync_fetch_and_sub(&qa.nr_highpri_queued, 1);
+
+ update_core_sched_head_seq(p);
+ __sync_fetch_and_add(&qa.nr_dispatched, 1);
+
+ scx_bpf_dsq_insert(p, SHARED_DSQ, slice_ns, 0);
+
+ /*
+ * scx_qmap uses a global BPF queue that any CPU's
+ * dispatch can pop from. If this CPU popped a task that
+ * can't run here, it gets stranded on SHARED_DSQ after
+ * consume_dispatch_q() skips it. Kick the task's home
+ * CPU so it drains SHARED_DSQ.
+ *
+ * There's a race between the pop and the flush of the
+ * buffered dsq_insert:
+ *
+ * CPU 0 (dispatching) CPU 1 (home, idle)
+ * ~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
+ * pop from BPF queue
+ * dsq_insert(buffered)
+ * balance:
+ * SHARED_DSQ empty
+ * BPF queue empty
+ * -> goes idle
+ * flush -> on SHARED
+ * kick CPU 1
+ * wakes, drains task
+ *
+ * The kick prevents indefinite stalls but a per-CPU
+ * kthread like ksoftirqd can be briefly stranded when
+ * its home CPU enters idle with softirq pending,
+ * triggering:
+ *
+ * "NOHZ tick-stop error: local softirq work is pending, handler #N!!!"
+ *
+ * from report_idle_softirq(). The kick lands shortly
+ * after and the home CPU drains the task. This could be
+ * avoided by e.g. dispatching pinned tasks to local or
+ * global DSQs, but the current code is left as-is to
+ * document this class of issue -- other schedulers
+ * seeing similar warnings can use this as a reference.
+ */
+ if (!cmask_test(cid, &taskc->cpus_allowed))
+ scx_bpf_kick_cid(scx_bpf_task_cid(p), 0);
+ batch--;
+ cpuc->dsp_cnt--;
+ if (!batch || !scx_bpf_dispatch_nr_slots()) {
+ if (scan_shared_dsq(false) ||
+ scx_bpf_dsq_move_to_local(SHARED_DSQ, needs_immed(cid)))
+ return;
+ goto prev;
+ }
+ if (!cpuc->dsp_cnt)
+ break;
+ }
+
+ cpuc->dsp_cnt = 0;
+ }
+
+ if (scan_shared_dsq(false))
+ return;
+prev:
+ /*
+ * No other tasks. @prev will keep running. Update its core_sched_seq as
+ * if the task were enqueued and dispatched immediately.
+ *
+ * No @prev to keep running means the CPU goes idle. If its claim was
+ * never used, that is not a transition and ops.update_idle() stays
+ * silent. Restore the claim here.
+ */
+ if (prev) {
+ taskc = lookup_task_ctx(prev);
+ if (!taskc)
+ return;
+
+ taskc->core_sched_seq =
+ qa.core_sched_tail_seqs[weight_to_idx(prev->scx.weight)]++;
+ } else {
+ cmask_set(cid, &qa.idle_cids.mask);
+ }
+}
+
+void BPF_STRUCT_OPS(qmap_tick, struct task_struct *p)
+{
+ struct cpu_ctx __arena *cpuc = &qa.cpu_ctxs[scx_bpf_this_cid()];
+ int idx;
+
+ /*
+ * Use the running avg of weights to select the target cpuperf level.
+ * This is a demonstration of the cpuperf feature rather than a
+ * practical strategy to regulate CPU frequency.
+ */
+ cpuc->avg_weight = cpuc->avg_weight * 3 / 4 + p->scx.weight / 4;
+ idx = weight_to_idx(cpuc->avg_weight);
+ cpuc->cpuperf_target = qidx_to_cpuperf_target[idx];
+
+ scx_bpf_cidperf_set(scx_bpf_task_cid(p), cpuc->cpuperf_target);
+}
+
+/*
+ * The distance from the head of the queue scaled by the weight of the queue.
+ * The lower the number, the older the task and the higher the priority.
+ */
+static s64 task_qdist(struct task_struct *p, task_ctx_t *taskc)
+{
+ int idx = weight_to_idx(p->scx.weight);
+ s64 qdist;
+
+ qdist = taskc->core_sched_seq - qa.core_sched_head_seqs[idx];
+
+ /*
+ * As queue index increments, the priority doubles. The queue w/ index 3
+ * is dispatched twice more frequently than 2. Reflect the difference by
+ * scaling qdists accordingly. Note that the shift amount needs to be
+ * flipped depending on the sign to avoid flipping priority direction.
+ */
+ if (qdist >= 0)
+ return qdist << (4 - idx);
+ else
+ return qdist << idx;
+}
+
+/*
+ * This is called to determine the task ordering when core-sched is picking
+ * tasks to execute on SMT siblings and should encode about the same ordering as
+ * the regular scheduling path. Use the priority-scaled distances from the head
+ * of the queues to compare the two tasks which should be consistent with the
+ * dispatch path behavior.
+ */
+bool BPF_STRUCT_OPS(qmap_core_sched_before,
+ struct task_struct *a, struct task_struct *b)
+{
+ task_ctx_t *taskc_a = lookup_task_ctx(a);
+ task_ctx_t *taskc_b = lookup_task_ctx(b);
+
+ /*
+ * A task delegated to a sub-scheduler has no task_ctx here. Order such
+ * pairs by the kernel's default ordering - a running task after every
+ * waiting task, then by runnable_at.
+ */
+ if (!taskc_a || !taskc_b) {
+ if (a->on_cpu != b->on_cpu)
+ return b->on_cpu;
+ return time_before(a->scx.runnable_at, b->scx.runnable_at);
+ }
+
+ return task_qdist(a, taskc_a) < task_qdist(b, taskc_b);
+}
+
+/*
+ * sched_switch tracepoint and cpu_release handlers are no longer needed.
+ * With SCX_OPS_ALWAYS_ENQ_IMMED, wakeup_preempt_scx() reenqueues IMMED
+ * tasks when a higher-priority scheduling class takes the CPU.
+ */
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init_task, struct task_struct *p,
+ struct scx_init_task_args *args)
+{
+ struct task_ctx_stor_val *v;
+ task_ctx_t *taskc;
+
+ if (qa.inject_mode == QMAP_INJ_INIT_FAIL &&
+ !bpf_strncmp(p->comm, 6, "qmfail"))
+ return -ENOMEM;
+
+ if (p->tgid == disallow_tgid)
+ p->scx.disallow = true;
+
+ /* pop a slab entry off the free list */
+ if (qmap_spin_lock(&qa_task_lock))
+ return -EBUSY;
+ taskc = qa.task_free_head;
+ if (taskc)
+ qa.task_free_head = taskc->next_free;
+ bpf_res_spin_unlock(&qa_task_lock);
+ if (!taskc) {
+ scx_bpf_error("task_ctx slab exhausted (max_tasks=%u)", max_tasks);
+ return -ENOMEM;
+ }
+
+ taskc->next_free = NULL;
+ taskc->q_next = NULL;
+ taskc->q_prev = NULL;
+ taskc->fifo = NULL;
+ taskc->tid = p->scx.tid;
+ taskc->pid = p->pid;
+ taskc->force_local = false;
+ taskc->highpri = false;
+ taskc->core_sched_seq = 0;
+ cmask_init(&taskc->cpus_allowed, 0, scx_bpf_nr_cids());
+
+ v = bpf_task_storage_get(&task_ctx_stor, p, NULL,
+ BPF_LOCAL_STORAGE_GET_F_CREATE);
+ if (!v) {
+ /* push back to the free list */
+ if (!qmap_spin_lock(&qa_task_lock)) {
+ taskc->next_free = qa.task_free_head;
+ qa.task_free_head = taskc;
+ bpf_res_spin_unlock(&qa_task_lock);
+ }
+ return -ENOMEM;
+ }
+ v->taskc = taskc;
+ return 0;
+}
+
+void BPF_STRUCT_OPS(qmap_exit_task, struct task_struct *p,
+ struct scx_exit_task_args *args)
+{
+ struct task_ctx_stor_val *v;
+ task_ctx_t *taskc;
+
+ v = bpf_task_storage_get(&task_ctx_stor, p, NULL, 0);
+ if (!v || !v->taskc)
+ return;
+ taskc = v->taskc;
+ v->taskc = NULL;
+
+ if (qmap_spin_lock(&qa_task_lock))
+ return;
+ taskc->next_free = qa.task_free_head;
+ qa.task_free_head = taskc;
+ bpf_res_spin_unlock(&qa_task_lock);
+}
+
+void BPF_STRUCT_OPS(qmap_dump, struct scx_dump_ctx *dctx)
+{
+ task_ctx_t *taskc;
+ s32 i;
+
+ QMAP_TOUCH_ARENA();
+
+ if (suppress_dump)
+ return;
+
+ /*
+ * Walk the queue lists without locking - kfunc calls (scx_bpf_dump)
+ * aren't in the verifier's kfunc_spin_allowed() list so we can't hold
+ * a lock and dump. Best-effort; racing may print stale tids but the
+ * walk is bounded by bpf_repeat() so it always terminates.
+ */
+ bpf_for(i, 0, 5) {
+ scx_bpf_dump("QMAP FIFO[%d]:", i);
+ taskc = qa.fifos[i].head;
+ bpf_repeat(4096) {
+ if (!taskc)
+ break;
+ scx_bpf_dump(" %d:%llu", taskc->pid, taskc->tid);
+ taskc = taskc->q_next;
+ }
+ scx_bpf_dump("\n");
+ }
+}
+
+void BPF_STRUCT_OPS(qmap_dump_cid, struct scx_dump_ctx *dctx, s32 cid, bool idle)
+{
+ struct cpu_ctx __arena *cpuc = &qa.cpu_ctxs[cid];
+
+ if (suppress_dump || idle)
+ return;
+
+ scx_bpf_dump("QMAP: dsp_idx=%llu dsp_cnt=%llu avg_weight=%u cpuperf_target=%u",
+ cpuc->dsp_idx, cpuc->dsp_cnt, cpuc->avg_weight,
+ cpuc->cpuperf_target);
+}
+
+void BPF_STRUCT_OPS(qmap_dump_task, struct scx_dump_ctx *dctx, struct task_struct *p)
+{
+ struct task_ctx_stor_val *v;
+ task_ctx_t *taskc;
+
+ QMAP_TOUCH_ARENA();
+
+ if (suppress_dump)
+ return;
+ v = bpf_task_storage_get(&task_ctx_stor, p, NULL, 0);
+ if (!v || !v->taskc)
+ return;
+ taskc = v->taskc;
+
+ scx_bpf_dump("QMAP: force_local=%d core_sched_seq=%llu",
+ taskc->force_local, taskc->core_sched_seq);
+}
+
+s32 BPF_STRUCT_OPS(qmap_cpuctl_init, struct cgroup *cgrp, struct scx_cgroup_init_args *args)
+{
+ QMAP_TOUCH_ARENA();
+
+ if (print_msgs)
+ bpf_printk("CGRP INIT %llu weight=%u period=%lu quota=%ld burst=%lu",
+ cgrp->kn->id, args->weight, args->bw_period_us,
+ args->bw_quota_us, args->bw_burst_us);
+
+ if (qa.inject_mode == QMAP_INJ_CGRP_INIT_FAIL) {
+ char name[7] = {};
+
+ bpf_probe_read_kernel_str(name, sizeof(name), cgrp->kn->name);
+ if (!bpf_strncmp(name, 6, "qmfail"))
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+static void redistribute(void);
+
+void BPF_STRUCT_OPS(qmap_cpuctl_set_weight, struct cgroup *cgrp, u32 weight)
+{
+ u64 cgid = cgrp->kn->id;
+ s32 i;
+
+ QMAP_TOUCH_ARENA();
+
+ if (print_msgs)
+ bpf_printk("CGRP SET %llu weight=%u", cgid, weight);
+
+ /*
+ * Knobs belong to the parent, so this op carries the child subs'
+ * attach point weights. Adjust the matching sub's share of the cid
+ * partition. Other cgroups don't participate in the split.
+ */
+ for (i = 0; i < MAX_SUB_SCHEDS; i++) {
+ if (qa.sub_sched_ctxs[i].cgroup_id != cgid)
+ continue;
+ if (qa.sub_sched_ctxs[i].weight != weight) {
+ qa.sub_sched_ctxs[i].weight = weight;
+ redistribute();
+ }
+ break;
+ }
+}
+
+void BPF_STRUCT_OPS(qmap_cpuctl_set_bandwidth, struct cgroup *cgrp, u64 period_us,
+ u64 quota_us, u64 burst_us)
+{
+ if (print_msgs)
+ bpf_printk("CGRP SET %llu period=%lu quota=%ld burst=%lu",
+ cgrp->kn->id, period_us, quota_us, burst_us);
+}
+
+void BPF_STRUCT_OPS(qmap_cpuctl_move, struct task_struct *p, struct cgroup *from,
+ struct cgroup *to)
+{
+ if (print_msgs)
+ bpf_printk("CGRP MOVE %d %llu -> %llu",
+ p->pid, from->kn->id, to->kn->id);
+}
+
+void BPF_STRUCT_OPS(qmap_update_idle, s32 cid, bool idle)
+{
+ QMAP_TOUCH_ARENA();
+
+ /*
+ * The kernel delivers update_idle() for every cid this node holds
+ * SCX_CAP_BASE on. Track every cid's idle state regardless of
+ * delegation: the direct-dispatch pick masks idle_cids with usable_cids
+ * at selection, so a cid already idle when it returns to self needs no
+ * reseed here.
+ */
+ if (idle)
+ cmask_set(cid, &qa.idle_cids.mask);
+ else
+ cmask_clear(cid, &qa.idle_cids.mask);
+}
+
+void BPF_STRUCT_OPS(qmap_set_cmask, struct task_struct *p,
+ const struct scx_cmask *cmask_in)
+{
+ struct scx_cmask __arena *cmask = (struct scx_cmask __arena *)(long)cmask_in;
+ task_ctx_t *taskc;
+
+ taskc = lookup_task_ctx(p);
+ if (!taskc)
+ return;
+ cmask_copy(&taskc->cpus_allowed, cmask);
+}
+
+struct monitor_timer {
+ struct bpf_timer timer;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, u32);
+ __type(value, struct monitor_timer);
+} monitor_timer SEC(".maps");
+
+/*
+ * Aggregate cidperf across the first nr_online_cids cids. Post-hotplug
+ * the first-N-are-online invariant drifts, so some cap/cur values may
+ * be stale. For this demo monitor that's fine; the scheduler exits on
+ * the enable-time hotplug_seq mismatch and userspace restarts, which
+ * rebuilds the layout.
+ */
+static void monitor_cpuperf(void)
+{
+ u32 nr_online = scx_bpf_nr_online_cids();
+ u64 cap_sum = 0, cur_sum = 0, cur_min = SCX_CPUPERF_ONE, cur_max = 0;
+ u64 target_sum = 0, target_min = SCX_CPUPERF_ONE, target_max = 0;
+ s32 cid;
+
+ QMAP_TOUCH_ARENA();
+
+ bpf_for(cid, 0, nr_online) {
+ struct cpu_ctx __arena *cpuc = &qa.cpu_ctxs[cid];
+ u32 cap = scx_bpf_cidperf_cap(cid);
+ u32 cur = scx_bpf_cidperf_cur(cid);
+ u32 target;
+
+ cur_min = cur < cur_min ? cur : cur_min;
+ cur_max = cur > cur_max ? cur : cur_max;
+
+ cur_sum += (u64)cur * cap / SCX_CPUPERF_ONE;
+ cap_sum += cap;
+
+ target = cpuc->cpuperf_target;
+ target_sum += target;
+ target_min = target < target_min ? target : target_min;
+ target_max = target > target_max ? target : target_max;
+ }
+
+ if (!nr_online || !cap_sum)
+ return;
+
+ qa.cpuperf_min = cur_min;
+ qa.cpuperf_avg = cur_sum * SCX_CPUPERF_ONE / cap_sum;
+ qa.cpuperf_max = cur_max;
+
+ qa.cpuperf_target_min = target_min;
+ qa.cpuperf_target_avg = target_sum / nr_online;
+ qa.cpuperf_target_max = target_max;
+}
+
+/*
+ * Dump the currently queued tasks in the shared DSQ to demonstrate the usage of
+ * scx_bpf_dsq_nr_queued() and DSQ iterator. Raise the dispatch batch count to
+ * see meaningful dumps in the trace pipe.
+ */
+static void dump_shared_dsq(void)
+{
+ struct task_struct *p;
+ s32 nr;
+
+ if (!(nr = scx_bpf_dsq_nr_queued(SHARED_DSQ)))
+ return;
+
+ bpf_printk("Dumping %d tasks in SHARED_DSQ in reverse order", nr);
+
+ bpf_rcu_read_lock();
+ bpf_for_each(scx_dsq, p, SHARED_DSQ, SCX_DSQ_ITER_REV)
+ bpf_printk("%s[%d]", p->comm, p->pid);
+ bpf_rcu_read_unlock();
+}
+
+static int monitor_timerfn(void *map, int *key, struct bpf_timer *timer)
+{
+ bpf_rcu_read_lock();
+ scan_shared_dsq(true);
+ bpf_rcu_read_unlock();
+
+ monitor_cpuperf();
+
+ if (print_dsqs_and_events) {
+ struct scx_event_stats events;
+
+ dump_shared_dsq();
+
+ __COMPAT_scx_bpf_events(&events, sizeof(events));
+
+ bpf_printk("%35s: %lld", "SCX_EV_SELECT_CPU_FALLBACK",
+ scx_read_event(&events, SCX_EV_SELECT_CPU_FALLBACK));
+ bpf_printk("%35s: %lld", "SCX_EV_DISPATCH_LOCAL_DSQ_OFFLINE",
+ scx_read_event(&events, SCX_EV_DISPATCH_LOCAL_DSQ_OFFLINE));
+ bpf_printk("%35s: %lld", "SCX_EV_DISPATCH_KEEP_LAST",
+ scx_read_event(&events, SCX_EV_DISPATCH_KEEP_LAST));
+ bpf_printk("%35s: %lld", "SCX_EV_ENQ_SKIP_EXITING",
+ scx_read_event(&events, SCX_EV_ENQ_SKIP_EXITING));
+ bpf_printk("%35s: %lld", "SCX_EV_REFILL_SLICE_DFL",
+ scx_read_event(&events, SCX_EV_REFILL_SLICE_DFL));
+ bpf_printk("%35s: %lld", "SCX_EV_BYPASS_DURATION",
+ scx_read_event(&events, SCX_EV_BYPASS_DURATION));
+ bpf_printk("%35s: %lld", "SCX_EV_BYPASS_DISPATCH",
+ scx_read_event(&events, SCX_EV_BYPASS_DISPATCH));
+ bpf_printk("%35s: %lld", "SCX_EV_BYPASS_ACTIVATE",
+ scx_read_event(&events, SCX_EV_BYPASS_ACTIVATE));
+ }
+
+ if (bpf_timer_start(timer, ONE_SEC_IN_NS, 0))
+ scx_bpf_error("failed to re-arm stats timer");
+ return 0;
+}
+
+struct lowpri_timer {
+ struct bpf_timer timer;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, u32);
+ __type(value, struct lowpri_timer);
+} lowpri_timer SEC(".maps");
+
+/*
+ * Nice 19 tasks are put into the lowpri DSQ. Every 10ms, reenq is triggered and
+ * the tasks are transferred to SHARED_DSQ.
+ */
+static int lowpri_timerfn(void *map, int *key, struct bpf_timer *timer)
+{
+ scx_bpf_dsq_reenq(LOWPRI_DSQ, 0);
+ if (bpf_timer_start(timer, LOWPRI_INTV_NS, 0))
+ scx_bpf_error("failed to re-arm lowpri timer");
+ return 0;
+}
+
+struct round_robin_timer {
+ struct bpf_timer timer;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARRAY);
+ __uint(max_entries, 1);
+ __type(key, u32);
+ __type(value, struct round_robin_timer);
+} round_robin_timer SEC(".maps");
+
+enum part_pending_flags {
+ PART_REFRESH = BIT_U64(0),
+ PART_REDISTRIBUTE = BIT_U64(1),
+};
+
+/*
+ * Partition update synchronization. qa.part can be written from concurrent
+ * contexts. This single-runner guard admits one writer at a time without
+ * holding a lock across the grant/revoke kfuncs. part_pending coalesces
+ * refresh and repartition requests that arrive while it is held.
+ *
+ * They live in .bss, not the arena: rr_advance() runs from a bpf_timer
+ * callback, where the verifier rejects atomic ops on arena memory.
+ */
+static u64 part_busy;
+static u64 part_pending;
+
+static bool part_try_start(void)
+{
+ /* set busy, report whether it was previously clear (we acquired it) */
+ return !__sync_fetch_and_or(&part_busy, 1);
+}
+
+static void part_end(void)
+{
+ __sync_fetch_and_and(&part_busy, 0);
+}
+
+/*
+ * compute_partition() scratch.
+ *
+ * The excl-held cids are handed out in cid order: position 0..nr_excl-1 over
+ * the held cids is split into contiguous ranges, one per participant that gets
+ * at least one excl cid. Range k is owned by cp_range_owner[k] and ends at the
+ * cumulative position cp_range_end[k].
+ */
+static s32 cp_range_owner[MAX_PARTS]; /* exclusive range k: its owner id ... */
+static s32 cp_range_end[MAX_PARTS]; /* ... and the cumulative position it ends at */
+
+/* a participant in the partition: self or an attached child */
+struct participant {
+ s32 slot; /* child slot, or CID_SELF */
+ u32 weight; /* cpu.weight */
+};
+
+/**
+ * place_one - assign one excl-held cid to its owner
+ * @cid: the excl-held cid to place
+ * @n: its position among the excl-held cids, in [0, nr_excl)
+ * @total_excl: how many positions are owned exclusively (the rest are shared)
+ *
+ * Position @n below @total_excl is owned exclusively. It falls in the range
+ * whose cumulative end it is under, owned by cp_range_owner[]. A position at or
+ * above @total_excl is the rounding leftover which joins the shared pool.
+ *
+ * A separate __noinline function to help verification.
+ */
+__noinline int place_one(s32 cid, s32 n, s32 total_excl)
+{
+ s32 owner = CID_SELF, i, s;
+
+ if (cid < 0 || cid >= SCX_QMAP_MAX_CPUS || n < 0 || n >= SCX_QMAP_MAX_CPUS ||
+ total_excl < 0) {
+ scx_bpf_error("-ERANGE");
+ return 0;
+ }
+
+ if (n < total_excl) {
+ for (i = 0; i < MAX_PARTS; i++) {
+ if (n < cp_range_end[i]) {
+ owner = cp_range_owner[i];
+ break;
+ }
+ }
+ qa.part.cid_owner[cid] = owner;
+ } else {
+ s = n - total_excl;
+ if (s < 0 || s >= MAX_PARTS) {
+ scx_bpf_error("-ERANGE");
+ return 0;
+ }
+ qa.part.shared_cids[s] = cid;
+ /* time-shared: dispatch resolves the live holder via rr_pos */
+ qa.part.cid_owner[cid] = CID_SHARED;
+ }
+ return 0;
+}
+
+/**
+ * compute_partition - build the cid partition from this node's held caps
+ *
+ * Decide each cid's owner, the shared pool and the rr rotation. __noinline to
+ * help verification. See the comment at the top of the file.
+ */
+__noinline void compute_partition(void)
+{
+ s32 nr_cids = qa.nr_cids;
+ s32 nr_excl, total_excl = 0, nr_rr = 0;
+ s32 sum_w, i, cid, n = 0, share, self_w;
+ u64 cgid_snap[MAX_SUB_SCHEDS];
+ s32 w_snap[MAX_SUB_SCHEDS];
+
+ if (nr_cids > SCX_QMAP_MAX_CPUS) {
+ scx_bpf_error("-ERANGE");
+ return;
+ }
+
+ /* find out the cids we hold */
+ scx_bpf_sub_caps(0, SCX_CAP_ENQ, &qa.held_excl.mask);
+ scx_bpf_sub_caps(0, SCX_CAP_ENQ_IMMED, &qa.held_shared.mask);
+ cmask_andnot(&qa.held_shared.mask, &qa.held_excl.mask); /* held only as ENQ_IMMED */
+
+ qa.part.nr_shared = 0;
+ qa.part.nr_rr = 0;
+ qa.part.rr_pos = 0;
+
+ nr_excl = cmask_weight(&qa.held_excl.mask);
+ qa.part.nr_excl = nr_excl;
+
+ /* no excl cid: held_shared stays self-local, the rest unheld */
+ if (!nr_excl) {
+ bpf_for(cid, 0, nr_cids) {
+ if (cmask_test(cid, &qa.held_shared.mask))
+ qa.part.cid_owner[cid] = CID_SELF;
+ else
+ qa.part.cid_owner[cid] = CID_NONE;
+ }
+ return;
+ }
+
+ /*
+ * Snapshot membership and weights so the sum_w and share loops agree. A
+ * mid-compute change would otherwise wrap nr_shared negative. The self
+ * weight is fixed at the default: a cgroup's weight is its parent's
+ * knob, not the scheduler's own business.
+ */
+ self_w = 100;
+ bpf_for(i, 0, MAX_SUB_SCHEDS) {
+ cgid_snap[i] = qa.sub_sched_ctxs[i].cgroup_id;
+ w_snap[i] = cgid_snap[i] ? (qa.sub_sched_ctxs[i].weight ?: 100) : 0;
+ }
+
+ /*
+ * Participants are self plus each child. Give each a fixed range/rr
+ * slot: self at slot 0, child i at slot i+1.
+ *
+ * sum_w totals every participant's weight.
+ */
+ sum_w = self_w;
+ bpf_for(i, 0, MAX_SUB_SCHEDS) {
+ barrier_var(sum_w);
+ sum_w += w_snap[i];
+ }
+
+ /*
+ * Split [0, nr_excl) into one contiguous range per participant, each
+ * the floor of its weight share. cp_range_owner[]/cp_range_end[] record
+ * each range's owner and cumulative end, total_excl counts the
+ * exclusive slots, and the rest (nr_excl - total_excl) are shared.
+ * rr_slots[] lists every participant for the round-robin.
+ */
+ share = (u64)nr_excl * self_w / sum_w;
+ total_excl += share;
+ cp_range_owner[0] = CID_SELF;
+ cp_range_end[0] = total_excl;
+ qa.part.rr_slots[nr_rr++] = 0; /* self holds slot 0 (cgid 0 = no grant) */
+
+ bpf_for(i, 0, MAX_SUB_SCHEDS) {
+ u64 cgid = cgid_snap[i];
+ s32 w = w_snap[i];
+
+ barrier_var(total_excl);
+ share = (u64)nr_excl * w / sum_w;
+ total_excl += share;
+ cp_range_owner[i + 1] = cgid ? i : CID_NONE;
+ cp_range_end[i + 1] = total_excl;
+
+ if (cgid) {
+ barrier_var(nr_rr);
+ if (nr_rr < 0 || nr_rr >= MAX_PARTS) {
+ scx_bpf_error("-ERANGE");
+ return;
+ }
+ qa.part.rr_slots[nr_rr++] = cgid;
+ }
+ }
+
+ /* assign each cid: held-excl by position, the rest self/none */
+ bpf_for(cid, 0, nr_cids) {
+ if (cmask_test(cid, &qa.held_excl.mask)) {
+ place_one(cid, n, total_excl);
+ n++;
+ barrier_var(n);
+ } else if (cmask_test(cid, &qa.held_shared.mask)) {
+ qa.part.cid_owner[cid] = CID_SELF; /* time-share, self-local */
+ } else {
+ qa.part.cid_owner[cid] = CID_NONE; /* not held */
+ }
+ }
+
+ qa.part.nr_shared = nr_excl - total_excl;
+ qa.part.nr_rr = nr_rr;
+}
+
+/*
+ * Charge elapsed wall time to each cid's current owner. Runs under the
+ * partition guard before every ownership change and from the stats flush, so
+ * alloc_ns[] reflects the layout that was in effect. Shared-pool time is
+ * charged to the live round-robin holder.
+ */
+static __noinline void account_alloc(void)
+{
+ u64 now = bpf_ktime_get_ns();
+ s32 rr_owner = CID_SELF;
+ s32 nr_cids = qa.nr_cids;
+ u64 delta;
+ s32 cid, i;
+
+ if (nr_cids < 0 || nr_cids > SCX_QMAP_MAX_CPUS) {
+ scx_bpf_error("-ERANGE");
+ return;
+ }
+
+ /* first call starts the clock */
+ if (!qa.alloc_ts) {
+ qa.alloc_ts = now;
+ return;
+ }
+ delta = now - qa.alloc_ts;
+ qa.alloc_ts = now;
+ qa.alloc_window_ns += delta;
+
+ /* resolve the live shared-pool holder to an owner id */
+ if (qa.part.nr_shared && qa.part.nr_rr) {
+ u32 pos = qa.part.rr_pos;
+ u64 cgid = pos < MAX_PARTS ? qa.part.rr_slots[pos] : 0;
+
+ if (cgid) {
+ rr_owner = CID_NONE;
+ bpf_for(i, 0, MAX_SUB_SCHEDS)
+ if (qa.sub_sched_ctxs[i].cgroup_id == cgid)
+ rr_owner = i;
+ }
+ }
+
+ bpf_for(cid, 0, nr_cids) {
+ s32 owner = qa.part.cid_owner[cid];
+
+ if (owner == CID_SHARED)
+ owner = rr_owner;
+ if (owner >= 0 && owner < MAX_SUB_SCHEDS)
+ qa.alloc_ns[owner] += delta;
+ else if (owner == CID_SELF)
+ qa.self_alloc_ns += delta;
+ }
+}
+
+/*
+ * usable_cids = self_cids & avail_cids. The inputs have separate writers,
+ * apply_partition() and qmap_sub_ecaps_updated(), so the result is rebuilt in
+ * full under the partition guard, in scratch first so that readers never see
+ * self_cids alone.
+ */
+static void refresh_usable(void)
+{
+ cmask_copy(&qa.usable_scratch.mask, &qa.self_cids.mask);
+ cmask_and(&qa.usable_scratch.mask, &qa.avail_cids.mask);
+ cmask_copy(&qa.usable_cids.mask, &qa.usable_scratch.mask);
+}
+
+/*
+ * apply_partition - execute the plan compute_partition() built
+ *
+ * Turn the owner map into the per-child, shared and self cmasks and issue the
+ * grant/revoke kfuncs as a delta against each child's previous grant. If no
+ * excl cid, evict every child.
+ */
+__noinline void apply_partition(void)
+{
+ s32 nr_cids = qa.nr_cids;
+ s32 nr_shared = qa.part.nr_shared;
+ s32 i, cid;
+
+ if (nr_cids < 0 || nr_cids > SCX_QMAP_MAX_CPUS ||
+ nr_shared < 0 || nr_shared > MAX_PARTS) {
+ scx_bpf_error("-ERANGE");
+ return;
+ }
+
+ /* no excl cpu: run own tasks on the held shares, evict children */
+ if (!qa.part.nr_excl) {
+ cmask_copy(&qa.self_cids.mask, &qa.held_shared.mask);
+ refresh_usable();
+ bpf_for(i, 0, MAX_SUB_SCHEDS)
+ if (qa.sub_sched_ctxs[i].cgroup_id)
+ scx_bpf_sub_kill(qa.sub_sched_ctxs[i].cgroup_id,
+ "parent holds no excl cpu to distribute");
+ return;
+ }
+
+ /*
+ * Snapshot the old pool. The per-child revoke below clears ENQ_IMMED on
+ * the previously-granted pool, so a cid that left the pool (now a
+ * sibling's excl) doesn't keep a stale ENQ_IMMED on its last holder.
+ */
+ cmask_copy(&qa.prev_rr_cids.mask, &qa.rr_cids.mask);
+
+ /* turn the owner map into the rr pool, per-child excl, and self sets */
+ cmask_init(&qa.rr_cids.mask, 0, nr_cids);
+ cmask_init(&qa.self_cids.mask, 0, nr_cids);
+
+ /* snapshot each child's grant, then rebuild the new sets below */
+ bpf_for(i, 0, MAX_SUB_SCHEDS) {
+ cmask_copy(&qa.sub_sched_ctxs[i].prev_granted.mask,
+ &qa.sub_sched_ctxs[i].granted_cids.mask);
+ cmask_init(&qa.sub_sched_ctxs[i].granted_cids.mask, 0, nr_cids);
+ }
+
+ bpf_for(i, 0, nr_shared)
+ cmask_set(qa.part.shared_cids[i], &qa.rr_cids.mask);
+ bpf_for(cid, 0, nr_cids) {
+ s32 o = qa.part.cid_owner[cid];
+
+ if (cmask_test(cid, &qa.rr_cids.mask))
+ continue;
+ if (o >= 0 && o < MAX_SUB_SCHEDS)
+ cmask_set(cid, &qa.sub_sched_ctxs[o].granted_cids.mask);
+ else if (o == CID_SELF)
+ cmask_set(cid, &qa.self_cids.mask);
+ }
+ refresh_usable();
+
+ /*
+ * Apply each child's exclusive cids as a delta against its previous
+ * grant. Separately clear the previous shared grant (ENQ_IMMED on the
+ * old pool), covering cids still pooled and cids that left for a
+ * sibling's excl. The current holder is granted the new pool below.
+ */
+ bpf_for(i, 0, MAX_SUB_SCHEDS) {
+ struct sub_sched_ctx __arena *ssc = &qa.sub_sched_ctxs[i];
+ u64 cgid = ssc->cgroup_id;
+
+ if (!cgid)
+ continue;
+
+ cmask_copy(&qa.to_revoke_cids.mask, &ssc->prev_granted.mask);
+ cmask_andnot(&qa.to_revoke_cids.mask, &ssc->granted_cids.mask);
+ cmask_copy(&qa.to_grant_cids.mask, &ssc->granted_cids.mask);
+ cmask_andnot(&qa.to_grant_cids.mask, &ssc->prev_granted.mask);
+
+ scx_bpf_sub_revoke(cgid, SCX_CAP_ENQ_IMMED | SCX_CAP_PERF,
+ &qa.prev_rr_cids.mask);
+ scx_bpf_sub_revoke(cgid, SCX_CAP_ENQ | SCX_CAP_PREEMPT |
+ SCX_CAP_ENQ_IMMED | SCX_CAP_PERF,
+ &qa.to_revoke_cids.mask);
+ scx_bpf_sub_grant(cgid, SCX_CAP_ENQ | SCX_CAP_PREEMPT |
+ SCX_CAP_ENQ_IMMED | SCX_CAP_PERF,
+ &qa.to_grant_cids.mask, NULL);
+ }
+
+ /* the current holder of the shared pool gets ENQ_IMMED on all of it */
+ if (nr_shared) {
+ s32 pos = qa.part.rr_pos;
+ u64 holder_cgid;
+
+ if (pos < 0 || pos >= MAX_PARTS) {
+ scx_bpf_error("-ERANGE");
+ return;
+ }
+
+ holder_cgid = qa.part.rr_slots[pos]; /* 0 = self, nothing to grant */
+ if (holder_cgid)
+ scx_bpf_sub_grant(holder_cgid,
+ SCX_CAP_ENQ_IMMED | SCX_CAP_PERF,
+ &qa.rr_cids.mask, NULL);
+ }
+}
+
+/**
+ * execute_partition - Run pending partition updates
+ *
+ * The rr timer is the backstop if the loop reaches its iteration limit.
+ */
+static void execute_partition(void)
+{
+ u64 pending;
+ s32 i;
+
+ bpf_for(i, 0, 1024) {
+ if (!part_try_start())
+ break;
+
+ pending = __sync_fetch_and_and(&part_pending, 0);
+ if (pending & PART_REDISTRIBUTE) {
+ /* charge elapsed time before repartitioning */
+ account_alloc();
+ compute_partition();
+ apply_partition();
+ } else if (pending & PART_REFRESH) {
+ refresh_usable();
+ }
+
+ /*
+ * Requests are published before trying the guard. Releasing it
+ * before checking pending work ensures a racing request is
+ * either observed here or handled by a caller that acquires the
+ * guard.
+ */
+ part_end();
+ if (!__sync_fetch_and_or(&part_pending, 0))
+ break;
+ }
+}
+
+static void redistribute(void)
+{
+ __sync_fetch_and_or(&part_pending, PART_REDISTRIBUTE);
+ execute_partition();
+}
+
+/*
+ * Userspace pokes this (PROG_RUN) to bring alloc_ns[] current before reading
+ * it for the stats display. Skipping when the partition guard is held is
+ * fine - alloc_ts is untouched, so the elapsed time is charged next time.
+ */
+SEC("syscall")
+int flush_alloc(void *ctx)
+{
+ if (part_try_start()) {
+ account_alloc();
+ part_end();
+ execute_partition();
+ }
+ return 0;
+}
+
+/*
+ * Hand the shared pool to the next participant in the rotation. Self's turn
+ * just revokes the pool back to this sched. A child's turn grants it ENQ_IMMED
+ * on the entire pool. As only excl-held cids are time-shared, a wall-clock
+ * rotation works. Driven by the round-robin timer.
+ */
+static void rr_advance(void)
+{
+ s32 nr_shared, old_pos, new_pos;
+ u64 old_cgid, new_cgid;
+ u32 nr_rr; /* unsigned for % */
+
+ /* a redistribute holds the partition and rebuilds the pool, so skip */
+ if (!part_try_start())
+ return;
+
+ nr_rr = qa.part.nr_rr;
+ nr_shared = qa.part.nr_shared;
+
+ if (nr_shared < 0 || nr_shared > MAX_PARTS) {
+ scx_bpf_error("-ERANGE");
+ return;
+ }
+
+ if (nr_shared && nr_rr >= 2) {
+ /* close out the outgoing holder's pool time */
+ account_alloc();
+
+ old_pos = qa.part.rr_pos;
+ new_pos = (old_pos + 1) % nr_rr;
+ old_cgid = qa.part.rr_slots[old_pos];
+ new_cgid = qa.part.rr_slots[new_pos];
+ qa.part.rr_pos = new_pos;
+
+ /*
+ * Move the ENQ_IMMED cap to the next participant. The shared
+ * cids stay marked CID_SHARED. qmap_dispatch() resolves the
+ * live holder via rr_pos without the guard, so a dispatch
+ * racing this handoff may reenqueue a task once. Harmless for a
+ * time-share.
+ */
+ if (old_cgid)
+ scx_bpf_sub_revoke(old_cgid,
+ SCX_CAP_ENQ_IMMED | SCX_CAP_PERF,
+ &qa.rr_cids.mask);
+ if (new_cgid)
+ scx_bpf_sub_grant(new_cgid,
+ SCX_CAP_ENQ_IMMED | SCX_CAP_PERF,
+ &qa.rr_cids.mask, NULL);
+ }
+
+ part_end();
+
+ execute_partition();
+}
+
+/* advance the time-shared cid pool every round_robin_ns */
+static int round_robin_timerfn(void *map, int *key, struct bpf_timer *timer)
+{
+ rr_advance();
+ if (bpf_timer_start(timer, round_robin_ns, 0))
+ scx_bpf_error("failed to re-arm round-robin timer");
+ return 0;
+}
+
+/*
+ * Custom cid layout for the cid-override test. On invalid input the kfunc
+ * scx_error()s and aborts the scheduler.
+ */
+s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init_cids)
+{
+ u32 nr_cpu_ids = scx_bpf_nr_cpu_ids();
+
+ if (!cid_override_mode)
+ return 0;
+
+ /* the arena arrays are sized SCX_QMAP_MAX_CPUS */
+ if (nr_cpu_ids > SCX_QMAP_MAX_CPUS) {
+ scx_bpf_error("nr_cpu_ids=%u exceeds SCX_QMAP_MAX_CPUS=%d",
+ nr_cpu_ids, SCX_QMAP_MAX_CPUS);
+ return -EINVAL;
+ }
+
+ scx_bpf_cid_override(qa.cid_override_cpu_to_cid, nr_cpu_ids,
+ qa.cid_override_shard_start, cid_override_nr_shards);
+ return 0;
+}
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(qmap_init)
+{
+ u8 __arena *slab;
+ u32 nr_pages, key = 0, i;
+ u32 nr_cids, nr_cpu_ids;
+ struct bpf_timer *timer;
+ s32 ret;
+
+ nr_cids = scx_bpf_nr_cids();
+ nr_cpu_ids = scx_bpf_nr_cpu_ids();
+
+ if (nr_cids > SCX_QMAP_MAX_CPUS) {
+ scx_bpf_error("nr_cids=%u exceeds SCX_QMAP_MAX_CPUS=%d",
+ nr_cids, SCX_QMAP_MAX_CPUS);
+ return -EINVAL;
+ }
+ if (nr_cpu_ids > SCX_QMAP_MAX_CPUS) {
+ scx_bpf_error("nr_cpu_ids=%u exceeds SCX_QMAP_MAX_CPUS=%d",
+ nr_cpu_ids, SCX_QMAP_MAX_CPUS);
+ return -EINVAL;
+ }
+
+ /*
+ * Allocate the task_ctx slab in arena and thread the entire slab onto
+ * the free list. max_tasks is set by userspace before load. Each entry
+ * is TASK_CTX_STRIDE bytes - task_ctx's trailing cpus_allowed flex
+ * array extends into the stride tail.
+ */
+ if (!max_tasks) {
+ scx_bpf_error("max_tasks must be > 0");
+ return -EINVAL;
+ }
+
+ nr_pages = (max_tasks * TASK_CTX_STRIDE + PAGE_SIZE - 1) / PAGE_SIZE;
+ slab = bpf_arena_alloc_pages(&arena, NULL, nr_pages, NUMA_NO_NODE, 0);
+ if (!slab) {
+ scx_bpf_error("failed to allocate task_ctx slab");
+ return -ENOMEM;
+ }
+ qa.task_ctxs = (task_ctx_t *)slab;
+
+ bpf_for(i, 0, 5)
+ qa.fifos[i].idx = i;
+
+ bpf_for(i, 0, max_tasks) {
+ task_ctx_t *cur = (task_ctx_t *)(slab + i * TASK_CTX_STRIDE);
+ task_ctx_t *next = (i + 1 < max_tasks) ?
+ (task_ctx_t *)(slab + (i + 1) * TASK_CTX_STRIDE) : NULL;
+ cur->next_free = next;
+ }
+ qa.task_free_head = (task_ctx_t *)slab;
+
+ /* cache the cid count, trusted to be <= SCX_QMAP_MAX_CPUS hereafter */
+ qa.nr_cids = nr_cids;
+
+ /* cmasks are embedded in qa, so they only need initializing */
+ cmask_init(&qa.idle_cids.mask, 0, nr_cids);
+ cmask_init(&qa.rr_cids.mask, 0, nr_cids);
+ cmask_init(&qa.prev_rr_cids.mask, 0, nr_cids);
+ cmask_init(&qa.self_cids.mask, 0, nr_cids);
+ cmask_init(&qa.avail_cids.mask, 0, nr_cids);
+ cmask_init(&qa.usable_cids.mask, 0, nr_cids);
+ cmask_init(&qa.to_revoke_cids.mask, 0, nr_cids);
+ cmask_init(&qa.to_grant_cids.mask, 0, nr_cids);
+ cmask_init(&qa.usable_scratch.mask, 0, nr_cids);
+ cmask_init(&qa.held_excl.mask, 0, nr_cids);
+ cmask_init(&qa.held_shared.mask, 0, nr_cids);
+
+ scx_bpf_sub_caps(0, SCX_CAP_ENQ, &qa.held_excl.mask);
+ scx_bpf_sub_caps(0, SCX_CAP_ENQ_IMMED, &qa.held_shared.mask);
+ cmask_andnot(&qa.held_shared.mask, &qa.held_excl.mask);
+
+ bpf_for(i, 0, MAX_SUB_SCHEDS) {
+ cmask_init(&qa.sub_sched_ctxs[i].granted_cids.mask, 0, nr_cids);
+ cmask_init(&qa.sub_sched_ctxs[i].prev_granted.mask, 0, nr_cids);
+ }
+
+ /*
+ * The root starts holding every cid and gets no ecaps notifications, so
+ * its avail set is fixed here. qmap_sub_ecaps_updated() maintains the
+ * per-cid state as effective caps settle, and redistribute() rebuilds
+ * owner and self from held caps. A non-root node starts with nothing.
+ */
+ bpf_for(i, 0, nr_cids) {
+ if (!sub_cgroup_id) {
+ cmask_set(i, &qa.self_cids.mask);
+ cmask_set(i, &qa.avail_cids.mask);
+ cmask_set(i, &qa.usable_cids.mask);
+ qa.part.cid_owner[i] = CID_SELF;
+ } else {
+ qa.part.cid_owner[i] = CID_NONE;
+ }
+ }
+ qa.part.nr_shared = 0;
+
+ ret = scx_bpf_create_dsq(SHARED_DSQ, -1);
+ if (ret) {
+ scx_bpf_error("failed to create DSQ %d (%d)", SHARED_DSQ, ret);
+ return ret;
+ }
+
+ ret = scx_bpf_create_dsq(HIGHPRI_DSQ, -1);
+ if (ret) {
+ scx_bpf_error("failed to create DSQ %d (%d)", HIGHPRI_DSQ, ret);
+ return ret;
+ }
+
+ ret = scx_bpf_create_dsq(LOWPRI_DSQ, -1);
+ if (ret)
+ return ret;
+
+ timer = bpf_map_lookup_elem(&monitor_timer, &key);
+ if (!timer)
+ return -ESRCH;
+ bpf_timer_init(timer, &monitor_timer, CLOCK_MONOTONIC);
+ bpf_timer_set_callback(timer, monitor_timerfn);
+ ret = bpf_timer_start(timer, ONE_SEC_IN_NS, 0);
+ if (ret)
+ return ret;
+
+ if (__COMPAT_has_generic_reenq()) {
+ /* see lowpri_timerfn() */
+ timer = bpf_map_lookup_elem(&lowpri_timer, &key);
+ if (!timer)
+ return -ESRCH;
+ bpf_timer_init(timer, &lowpri_timer, CLOCK_MONOTONIC);
+ bpf_timer_set_callback(timer, lowpri_timerfn);
+ ret = bpf_timer_start(timer, LOWPRI_INTV_NS, 0);
+ if (ret)
+ return ret;
+ }
+
+ /* sub-sched: drive the boundary-cid round-robin from a bpf timer */
+ timer = bpf_map_lookup_elem(&round_robin_timer, &key);
+ if (!timer)
+ return -ESRCH;
+ bpf_timer_init(timer, &round_robin_timer, CLOCK_MONOTONIC);
+ bpf_timer_set_callback(timer, round_robin_timerfn);
+ ret = bpf_timer_start(timer, round_robin_ns, 0);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(qmap_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+/*
+ * Seed a new sub slot with the cgroup's current weight. The kernel delivers
+ * ops.cpuctl_set_weight() only on value-changing writes, so a weight set
+ * before the sub attached would otherwise go unnoticed.
+ */
+static u32 cgrp_cur_weight(u64 cgid)
+{
+ struct cgroup_subsys_state *css;
+ struct cgroup *cgrp;
+ u32 weight = 100;
+
+ cgrp = bpf_cgroup_from_id(cgid);
+ if (!cgrp)
+ return weight;
+
+ css = BPF_CORE_READ(cgrp, subsys[cpu_cgrp_id]);
+ if (css) {
+ struct task_group *tg = container_of(css, struct task_group, css);
+ u32 w = BPF_CORE_READ(tg, scx.weight);
+
+ if (w)
+ weight = w;
+ }
+ bpf_cgroup_release(cgrp);
+ return weight;
+}
+
+s32 BPF_STRUCT_OPS(qmap_sub_attach, struct scx_sub_attach_args *args)
+{
+ s32 i;
+
+ /* as long as there is at least one excl cpu, children can attach */
+ if (!cmask_weight(&qa.held_excl.mask))
+ return -ENOSPC;
+
+ for (i = 0; i < MAX_SUB_SCHEDS; i++) {
+ if (qa.sub_sched_ctxs[i].cgroup_id)
+ continue;
+
+ qa.sub_sched_ctxs[i].cgroup_id = args->ops->sub_cgroup_id;
+ qa.sub_sched_ctxs[i].weight = cgrp_cur_weight(args->ops->sub_cgroup_id);
+ qa.nr_sub_scheds++;
+ bpf_printk("attaching sub-sched[%d] on %s", i, args->cgroup_path);
+ redistribute();
+ return 0;
+ }
+
+ return -ENOSPC;
+}
+
+void BPF_STRUCT_OPS(qmap_sub_detach, struct scx_sub_detach_args *args)
+{
+ s32 i;
+
+ for (i = 0; i < MAX_SUB_SCHEDS; i++) {
+ if (qa.sub_sched_ctxs[i].cgroup_id != args->ops->sub_cgroup_id)
+ continue;
+
+ qa.sub_sched_ctxs[i].cgroup_id = 0;
+ qa.sub_sched_ctxs[i].weight = 100;
+ cmask_init(&qa.sub_sched_ctxs[i].granted_cids.mask, 0, qa.nr_cids);
+ qa.nr_sub_scheds--;
+ bpf_printk("detaching sub-sched[%d] on %s", i, args->cgroup_path);
+ redistribute();
+ break;
+ }
+}
+
+void BPF_STRUCT_OPS(qmap_sub_caps_updated, const struct scx_cmask *cmask, u64 caps)
+{
+ /* our held caps changed, redistribute */
+ redistribute();
+}
+
+void BPF_STRUCT_OPS(qmap_sub_ecaps_updated, s32 cid, u64 before, u64 after)
+{
+ /*
+ * Effective caps updated. Track which cids hold shared caps so a self
+ * task placed there enqueues IMMED, and which cids have ENQ_IMMED in
+ * effect at all (avail, see the header comment).
+ */
+ if (after & SCX_CAP_ENQ_IMMED) {
+ qa.cid_shared[cid] = (after & SCX_CAP_ENQ) ? 0 : 1;
+ cmask_set(cid, &qa.avail_cids.mask);
+ } else {
+ qa.cid_shared[cid] = 0;
+ cmask_clear(cid, &qa.avail_cids.mask);
+ }
+
+ __sync_fetch_and_or(&part_pending, PART_REFRESH);
+ execute_partition();
+}
+
+SCX_OPS_CID_DEFINE(qmap_ops,
+ .flags = SCX_OPS_ENQ_EXITING | SCX_OPS_TID_TO_TASK,
+ .select_cid = (void *)qmap_select_cid,
+ .enqueue = (void *)qmap_enqueue,
+ .dequeue = (void *)qmap_dequeue,
+ .dispatch = (void *)qmap_dispatch,
+ .tick = (void *)qmap_tick,
+ .core_sched_before = (void *)qmap_core_sched_before,
+ .set_cmask = (void *)qmap_set_cmask,
+ .update_idle = (void *)qmap_update_idle,
+ .init_task = (void *)qmap_init_task,
+ .exit_task = (void *)qmap_exit_task,
+ .dump = (void *)qmap_dump,
+ .dump_cid = (void *)qmap_dump_cid,
+ .dump_task = (void *)qmap_dump_task,
+ .cpuctl_init = (void *)qmap_cpuctl_init,
+ .cpuctl_set_weight = (void *)qmap_cpuctl_set_weight,
+ .cpuctl_set_bandwidth = (void *)qmap_cpuctl_set_bandwidth,
+ .cpuctl_move = (void *)qmap_cpuctl_move,
+ .sub_attach = (void *)qmap_sub_attach,
+ .sub_detach = (void *)qmap_sub_detach,
+ .sub_caps_updated = (void *)qmap_sub_caps_updated,
+ .sub_ecaps_updated = (void *)qmap_sub_ecaps_updated,
+ .init_cids = (void *)qmap_init_cids,
+ .init = (void *)qmap_init,
+ .exit = (void *)qmap_exit,
+ .timeout_ms = 5000U,
+ .name = "qmap");
diff --git a/tools/sched_ext/scx_qmap.c b/tools/sched_ext/scx_qmap.c
new file mode 100644
index 000000000..5bb5f687e
--- /dev/null
+++ b/tools/sched_ext/scx_qmap.c
@@ -0,0 +1,501 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+#include <inttypes.h>
+#include <signal.h>
+#include <libgen.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <string.h>
+#include <time.h>
+#include <bpf/bpf.h>
+#include <scx/common.h>
+#include "scx_qmap.h"
+#include "scx_qmap.bpf.skel.h"
+
+const char help_fmt[] =
+"A simple five-level FIFO queue sched_ext scheduler.\n"
+"\n"
+"It also demonstrates hierarchical sub-scheduling: a scheduler can hand some\n"
+"of its cpus to a child cgroup that runs its own scheduler. Run one qmap as\n"
+"the parent, then run another qmap on a child cgroup with -c to attach it\n"
+"beneath the parent.\n"
+"\n"
+"The policy below is deliberately simplistic and the resulting behavior can\n"
+"look odd. qmap is a demo: it exists to exercise every sub-scheduling\n"
+"primitive the kernel offers with as little code as possible, not to schedule\n"
+"well.\n"
+"\n"
+"A parent divides the full cpus it holds among itself and its children in\n"
+"proportion to cpu.weight. The cpus left over by rounding are time-shared,\n"
+"handed to each participant in turn every -R ms. A cpu a scheduler only\n"
+"holds a time-share of is never handed further down, and a parent left with\n"
+"no full cpu of its own shuts its children down.\n"
+"\n"
+"See the top-of-file comment in .bpf.c for the design.\n"
+"\n"
+"Usage: %s [-s SLICE_US] [-e COUNT] [-t COUNT] [-T COUNT] [-l COUNT] [-b COUNT]\n"
+" [-N COUNT] [-P] [-M] [-H] [-c CG_PATH] [-d PID] [-D LEN] [-S] [-p] [-I]\n"
+" [-F COUNT] [-i SEC] [-R MS] [-J MODE] [-v]\n"
+"\n"
+" -s SLICE_US Override slice duration\n"
+" -e COUNT Trigger scx_bpf_error() after COUNT enqueues\n"
+" -t COUNT Stall every COUNT'th user thread\n"
+" -T COUNT Stall every COUNT'th kernel thread\n"
+" -N COUNT Size of the task_ctx arena slab (default 16384)\n"
+" -l COUNT Trigger dispatch infinite looping after COUNT dispatches\n"
+" -b COUNT Dispatch upto COUNT tasks together\n"
+" -P Print out DSQ content and event counters to trace_pipe every second\n"
+" -M Print out debug messages to trace_pipe\n"
+" -H Boost nice -20 tasks in SHARED_DSQ, use with -b\n"
+" -c CG_PATH Cgroup path to attach as sub-scheduler, must run parent scheduler first\n"
+" -d PID Disallow a process from switching into SCHED_EXT (-1 for self)\n"
+" -D LEN Set scx_exit_info.dump buffer length\n"
+" -S Suppress qmap-specific debug dump\n"
+" -p Switch only tasks on SCHED_EXT policy instead of all\n"
+" -I Turn on SCX_OPS_ALWAYS_ENQ_IMMED\n"
+" -F COUNT IMMED stress: force every COUNT'th enqueue to a busy local DSQ (use with -I)\n"
+" -C MODE cid-override test (shuffle|bad-dup|bad-range|bad-mono)\n"
+" -i SEC Stats interval, seconds (default 5)\n"
+" -R MS Round-robin period for time-shared cpus, ms (default 200)\n"
+" -J MODE Fault injection (wrong-cid: dispatch to a cid not held,\n"
+" init-fail/cgrp-init-fail: fail init_task/cpuctl_init for\n"
+" \"qmfail*\" comms/cgroups)\n"
+" -B PPT Rescue bandwidth in parts per thousand, 0 disables (root only, default 20)\n"
+" -q US Rescue batch quantum in microseconds (root only, default 5000)\n"
+" -v Print libbpf debug messages\n"
+" -h Display this help and exit\n";
+
+static bool verbose;
+static volatile int exit_req;
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ if (level == LIBBPF_DEBUG && !verbose)
+ return 0;
+ return vfprintf(stderr, format, args);
+}
+
+static void sigint_handler(int dummy)
+{
+ exit_req = 1;
+}
+
+static void invoke_flush_alloc(struct scx_qmap *skel)
+{
+ LIBBPF_OPTS(bpf_test_run_opts, opts);
+
+ bpf_prog_test_run_opts(bpf_program__fd(skel->progs.flush_alloc), &opts);
+}
+
+/* previous counter snapshots for the per-interval hier stats */
+struct hier_prev {
+ u64 alloc_ns[MAX_SUB_SCHEDS];
+ u64 self_alloc_ns;
+ u64 alloc_window_ns;
+ u64 nr_dsps[MAX_SUB_SCHEDS];
+ u64 nr_reenq_cap;
+ u64 nr_reenq_immed;
+ u64 nr_inject_attempts;
+ u64 nr_rescue_dsp;
+};
+
+/* current wall-clock time as "HH:MM:SS" for the startup and interval headers */
+static const char *tstamp(char *buf, size_t sz)
+{
+ time_t now = time(NULL);
+
+ strftime(buf, sz, "%H:%M:%S", localtime(&now));
+ return buf;
+}
+
+/* format the cids whose cid_owner[] matches @owner as "0-3,8", "-" if none */
+static void format_cid_ranges(struct qmap_arena *qa, s32 owner, char *buf, size_t sz)
+{
+ u32 nr = qa->nr_cids, cid;
+ size_t off = 0;
+ s32 start = -1;
+
+ buf[0] = '\0';
+ for (cid = 0; cid <= nr; cid++) {
+ bool match = cid < nr && qa->part.cid_owner[cid] == owner;
+ int n;
+
+ if (match) {
+ if (start < 0)
+ start = cid;
+ continue;
+ }
+ if (start < 0)
+ continue;
+
+ if (start == (s32)cid - 1)
+ n = snprintf(buf + off, sz - off, "%s%d",
+ off ? "," : "", start);
+ else
+ n = snprintf(buf + off, sz - off, "%s%d-%d",
+ off ? "," : "", start, cid - 1);
+ if (n < 0 || (size_t)n >= sz - off) {
+ strcpy(&buf[sz - 4], "...");
+ return;
+ }
+ off += n;
+ start = -1;
+ }
+ if (!off)
+ strcpy(buf, "-");
+}
+
+/* partition summary + one row per sched: weight, cpus, dispatch rate, cids */
+static void print_hier(struct qmap_arena *qa, struct hier_prev *prev, u64 own_cgid)
+{
+ char ranges[128], who[16];
+ const char *rr = "-";
+ double secs;
+ u32 i;
+
+ /*
+ * account_alloc() bumps alloc_window_ns together with the per-owner
+ * counters, so dividing by the same window yields exact cid counts.
+ */
+ secs = (qa->alloc_window_ns - prev->alloc_window_ns) / 1e9;
+ prev->alloc_window_ns = qa->alloc_window_ns;
+
+ /* resolve the live shared-pool holder */
+ if (qa->part.nr_shared && qa->part.nr_rr) {
+ u64 cgid = qa->part.rr_slots[qa->part.rr_pos];
+
+ rr = "self";
+ if (cgid) {
+ rr = "?";
+ for (i = 0; i < MAX_SUB_SCHEDS; i++) {
+ if (qa->sub_sched_ctxs[i].cgroup_id == cgid) {
+ snprintf(who, sizeof(who), "sub%u", i);
+ rr = who;
+ break;
+ }
+ }
+ }
+ }
+
+ format_cid_ranges(qa, CID_SHARED, ranges, sizeof(ranges));
+ printf("hier : nsub=%llu excl=%u shared=%s rr=%s reenq cap/immed +%llu/+%llu inj=+%llu rescue=+%llu\n",
+ (unsigned long long)qa->nr_sub_scheds, qa->part.nr_excl, ranges, rr,
+ (unsigned long long)(qa->nr_reenq_cap - prev->nr_reenq_cap),
+ (unsigned long long)(qa->nr_reenq_immed - prev->nr_reenq_immed),
+ (unsigned long long)(qa->nr_inject_attempts - prev->nr_inject_attempts),
+ (unsigned long long)(qa->nr_rescue_dsp - prev->nr_rescue_dsp));
+ prev->nr_reenq_cap = qa->nr_reenq_cap;
+ prev->nr_reenq_immed = qa->nr_reenq_immed;
+ prev->nr_inject_attempts = qa->nr_inject_attempts;
+ prev->nr_rescue_dsp = qa->nr_rescue_dsp;
+
+ printf("hier : %-4s %10s %4s %6s %8s %s\n",
+ "", "cgroup", "w", "alloc", "disp/s", "cids");
+
+ format_cid_ranges(qa, CID_SELF, ranges, sizeof(ranges));
+ printf("hier : %-4s %10llu %4u %6.2f %8s %s\n", "self",
+ (unsigned long long)own_cgid, 100,
+ secs > 0 ? (qa->self_alloc_ns - prev->self_alloc_ns) / (secs * 1e9) : 0.0,
+ "-", ranges);
+ prev->self_alloc_ns = qa->self_alloc_ns;
+
+ for (i = 0; i < MAX_SUB_SCHEDS; i++) {
+ struct sub_sched_ctx *sc = &qa->sub_sched_ctxs[i];
+
+ if (!sc->cgroup_id)
+ continue;
+
+ snprintf(who, sizeof(who), "sub%u", i);
+ format_cid_ranges(qa, i, ranges, sizeof(ranges));
+ printf("hier : %-4s %10llu %4u %6.2f %8.1f %s\n", who,
+ (unsigned long long)sc->cgroup_id, sc->weight,
+ secs > 0 ? (qa->alloc_ns[i] - prev->alloc_ns[i]) / (secs * 1e9) : 0.0,
+ secs > 0 ? (sc->nr_dsps - prev->nr_dsps[i]) / secs : 0.0,
+ ranges);
+ prev->alloc_ns[i] = qa->alloc_ns[i];
+ prev->nr_dsps[i] = sc->nr_dsps;
+ }
+}
+
+int main(int argc, char **argv)
+{
+ struct scx_qmap *skel;
+ struct bpf_link *link;
+ struct qmap_arena *qa;
+ u32 test_error_cnt = 0;
+ u64 ecode;
+ int opt, stats_intv = 5, i, round_robin_ms = 200;
+ struct hier_prev hprev = {};
+ const char *sub_cg_path = NULL;
+ char tbuf[32];
+ u32 inject_mode = 0;
+ u64 own_cgid = 0;
+ s32 cid_override_shard_sz = 4;
+
+ libbpf_set_print(libbpf_print_fn);
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+
+ if (libbpf_num_possible_cpus() > SCX_QMAP_MAX_CPUS) {
+ fprintf(stderr,
+ "scx_qmap: %d possible CPUs exceeds compile-time cap %d; "
+ "rebuild with larger SCX_QMAP_MAX_CPUS\n",
+ libbpf_num_possible_cpus(), SCX_QMAP_MAX_CPUS);
+ return 1;
+ }
+restart:
+ optind = 1;
+ skel = SCX_OPS_CID_OPEN(qmap_ops, scx_qmap);
+
+ skel->rodata->slice_ns = __COMPAT_ENUM_OR_ZERO("scx_public_consts", "SCX_SLICE_DFL");
+ skel->rodata->max_tasks = 16384;
+
+ while ((opt = getopt(argc, argv,
+ "s:e:t:T:l:b:N:PMHc:d:D:SpIF:C:i:R:J:B:q:vh")) != -1) {
+ switch (opt) {
+ case 's':
+ skel->rodata->slice_ns = strtoull(optarg, NULL, 0) * 1000;
+ break;
+ case 'e':
+ test_error_cnt = strtoul(optarg, NULL, 0);
+ break;
+ case 't':
+ skel->rodata->stall_user_nth = strtoul(optarg, NULL, 0);
+ break;
+ case 'T':
+ skel->rodata->stall_kernel_nth = strtoul(optarg, NULL, 0);
+ break;
+ case 'l':
+ skel->rodata->dsp_inf_loop_after = strtoul(optarg, NULL, 0);
+ break;
+ case 'b':
+ skel->rodata->dsp_batch = strtoul(optarg, NULL, 0);
+ break;
+ case 'N':
+ skel->rodata->max_tasks = strtoul(optarg, NULL, 0);
+ break;
+ case 'P':
+ skel->rodata->print_dsqs_and_events = true;
+ break;
+ case 'M':
+ skel->rodata->print_msgs = true;
+ break;
+ case 'H':
+ skel->rodata->highpri_boosting = true;
+ break;
+ case 'c': {
+ struct stat st;
+ if (stat(optarg, &st) < 0) {
+ perror("stat");
+ return 1;
+ }
+ skel->struct_ops.qmap_ops->sub_cgroup_id = st.st_ino;
+ skel->rodata->sub_cgroup_id = st.st_ino;
+ own_cgid = st.st_ino;
+ sub_cg_path = optarg;
+ break;
+ }
+ case 'd':
+ skel->rodata->disallow_tgid = strtol(optarg, NULL, 0);
+ if (skel->rodata->disallow_tgid < 0)
+ skel->rodata->disallow_tgid = getpid();
+ break;
+ case 'D':
+ skel->struct_ops.qmap_ops->exit_dump_len = strtoul(optarg, NULL, 0);
+ break;
+ case 'S':
+ skel->rodata->suppress_dump = true;
+ break;
+ case 'p':
+ skel->struct_ops.qmap_ops->flags |= SCX_OPS_SWITCH_PARTIAL;
+ break;
+ case 'I':
+ skel->struct_ops.qmap_ops->flags |= SCX_OPS_ALWAYS_ENQ_IMMED;
+ break;
+ case 'F':
+ skel->rodata->immed_stress_nth = strtoul(optarg, NULL, 0);
+ break;
+ case 'C': {
+ u32 nr_cpus = libbpf_num_possible_cpus();
+ u32 mode;
+
+ if (!strcmp(optarg, "shuffle"))
+ mode = QMAP_CID_OVR_SHUFFLE;
+ else if (!strcmp(optarg, "bad-dup"))
+ mode = QMAP_CID_OVR_BAD_DUP;
+ else if (!strcmp(optarg, "bad-range"))
+ mode = QMAP_CID_OVR_BAD_RANGE;
+ else if (!strcmp(optarg, "bad-mono"))
+ mode = QMAP_CID_OVR_BAD_MONO;
+ else {
+ fprintf(stderr, "unknown cid-override mode '%s'\n", optarg);
+ return 1;
+ }
+ skel->rodata->cid_override_mode = mode;
+ cid_override_shard_sz = 4;
+
+ /*
+ * bad-mono needs >= 3 shards to build a 0-based but
+ * non-monotonic shard_start. Shrink the shard size so
+ * the test runs on any machine with >= 3 cpus.
+ */
+ if (mode == QMAP_CID_OVR_BAD_MONO) {
+ if (nr_cpus < 3) {
+ fprintf(stderr, "bad-mono needs >= 3 cpus (have %u)\n",
+ nr_cpus);
+ return 1;
+ }
+ cid_override_shard_sz = nr_cpus / 3;
+ }
+
+ /* shards of shard_sz each */
+ skel->rodata->cid_override_nr_shards =
+ (nr_cpus + cid_override_shard_sz - 1) / cid_override_shard_sz;
+ break;
+ }
+ case 'i':
+ stats_intv = atoi(optarg);
+ if (stats_intv < 1)
+ stats_intv = 1;
+ break;
+ case 'R':
+ round_robin_ms = atoi(optarg);
+ if (round_robin_ms < 10)
+ round_robin_ms = 10;
+ break;
+ case 'J':
+ if (!strcmp(optarg, "wrong-cid"))
+ inject_mode = QMAP_INJ_WRONG_CID;
+ else if (!strcmp(optarg, "init-fail"))
+ inject_mode = QMAP_INJ_INIT_FAIL;
+ else if (!strcmp(optarg, "cgrp-init-fail"))
+ inject_mode = QMAP_INJ_CGRP_INIT_FAIL;
+ else
+ inject_mode = strtoul(optarg, NULL, 0);
+ break;
+ case 'B': {
+ u32 ppt = strtoul(optarg, NULL, 0);
+
+ if (!ppt)
+ ppt = __COMPAT_ENUM_OR_ZERO("scx_consts", "SCX_RESCUE_DISABLE");
+ skel->struct_ops.qmap_ops->rescue_bandwidth_ppt = ppt;
+ break;
+ }
+ case 'q':
+ skel->struct_ops.qmap_ops->rescue_quantum_us = strtoul(optarg, NULL, 0);
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ fprintf(stderr, help_fmt, basename(argv[0]));
+ return opt != 'h';
+ }
+ }
+
+ skel->rodata->round_robin_ns = (u64)round_robin_ms * 1000000;
+
+ SCX_OPS_LOAD(skel, qmap_ops, scx_qmap, uei);
+
+ qa = &skel->arena->qa;
+
+ /*
+ * The cid-override arrays live in the arena, which is mmapped at load.
+ * Populate them before qmap_init_cids() consumes them at attach.
+ */
+ if (skel->rodata->cid_override_mode) {
+ u32 mode = skel->rodata->cid_override_mode;
+ u32 nr_cpus = libbpf_num_possible_cpus();
+ u32 i;
+
+ /* shuffle: reversed cpu_to_cid; others: identity */
+ for (i = 0; i < nr_cpus; i++) {
+ if (mode == QMAP_CID_OVR_SHUFFLE)
+ qa->cid_override_cpu_to_cid[i] = nr_cpus - 1 - i;
+ else
+ qa->cid_override_cpu_to_cid[i] = i;
+ }
+ if (mode == QMAP_CID_OVR_BAD_DUP && nr_cpus >= 2)
+ qa->cid_override_cpu_to_cid[1] = 0;
+ if (mode == QMAP_CID_OVR_BAD_RANGE)
+ qa->cid_override_cpu_to_cid[0] = (s32)nr_cpus;
+
+ for (i = 0; i < skel->rodata->cid_override_nr_shards; i++)
+ qa->cid_override_shard_start[i] = i * cid_override_shard_sz;
+
+ if (mode == QMAP_CID_OVR_BAD_MONO) {
+ /* swap [1] and [2] to break monotonicity */
+ s32 tmp = qa->cid_override_shard_start[1];
+ qa->cid_override_shard_start[1] = qa->cid_override_shard_start[2];
+ qa->cid_override_shard_start[2] = tmp;
+ }
+ }
+
+ link = SCX_OPS_ATTACH(skel, qmap_ops, scx_qmap);
+
+ qa->test_error_cnt = test_error_cnt;
+ qa->inject_mode = inject_mode;
+
+ if (sub_cg_path)
+ printf("%s scx_qmap started: sub-scheduler on %s, stats every %ds\n",
+ tstamp(tbuf, sizeof(tbuf)), sub_cg_path, stats_intv);
+ else
+ printf("%s scx_qmap started: root scheduler, stats every %ds\n",
+ tstamp(tbuf, sizeof(tbuf)), stats_intv);
+ fflush(stdout);
+
+ while (!exit_req && !UEI_EXITED(skel, uei)) {
+ long nr_enqueued = qa->nr_enqueued;
+ long nr_dispatched = qa->nr_dispatched;
+
+ printf("---- %s ----\n",
+ tstamp(tbuf, sizeof(tbuf)));
+ printf("stats : enq=%lu dsp=%lu delta=%ld reenq/cid0=%llu/%llu deq=%llu core=%llu enq_ddsp=%llu\n",
+ nr_enqueued, nr_dispatched, nr_enqueued - nr_dispatched,
+ (unsigned long long)qa->nr_reenqueued,
+ (unsigned long long)qa->nr_reenqueued_cid0,
+ (unsigned long long)qa->nr_dequeued,
+ (unsigned long long)qa->nr_core_sched_execed,
+ (unsigned long long)qa->nr_ddsp_from_enq);
+ printf(" exp_local=%llu exp_remote=%llu exp_timer=%llu exp_lost=%llu\n",
+ (unsigned long long)qa->nr_expedited_local,
+ (unsigned long long)qa->nr_expedited_remote,
+ (unsigned long long)qa->nr_expedited_from_timer,
+ (unsigned long long)qa->nr_expedited_lost);
+ if (__COMPAT_has_ksym("scx_bpf_cidperf_cur"))
+ printf("cpuperf: cur min/avg/max=%u/%u/%u target min/avg/max=%u/%u/%u\n",
+ qa->cpuperf_min,
+ qa->cpuperf_avg,
+ qa->cpuperf_max,
+ qa->cpuperf_target_min,
+ qa->cpuperf_target_avg,
+ qa->cpuperf_target_max);
+
+ invoke_flush_alloc(skel);
+ print_hier(qa, &hprev, own_cgid);
+ fflush(stdout);
+
+ for (i = 0; i < stats_intv && !exit_req && !UEI_EXITED(skel, uei); i++)
+ sleep(1);
+ }
+
+ bpf_link__destroy(link);
+ ecode = UEI_REPORT(skel, uei);
+ scx_qmap__destroy(skel);
+
+ if (!exit_req && UEI_ECODE_RESTART(ecode))
+ goto restart;
+ return 0;
+}
diff --git a/tools/sched_ext/scx_qmap.h b/tools/sched_ext/scx_qmap.h
new file mode 100644
index 000000000..e95fffcf7
--- /dev/null
+++ b/tools/sched_ext/scx_qmap.h
@@ -0,0 +1,189 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Shared definitions between scx_qmap.bpf.c and scx_qmap.c.
+ *
+ * The scheduler keeps all state in a single BPF arena map. struct
+ * qmap_arena is the one object that lives at the base of the arena and is
+ * mmap'd into userspace so the loader can read counters directly.
+ *
+ * Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2026 Tejun Heo <tj@kernel.org>
+ */
+#ifndef __SCX_QMAP_H
+#define __SCX_QMAP_H
+
+#ifdef __BPF__
+#include <scx/bpf_arena_common.bpf.h>
+#else
+#include <linux/types.h>
+#include <scx/bpf_arena_common.h>
+#endif
+
+#define MAX_SUB_SCHEDS 8
+#define MAX_PARTS (MAX_SUB_SCHEDS + 1) /* participants: children + self */
+
+/*
+ * cpu_ctxs[] is sized to a fixed cap so the layout is shared between BPF and
+ * userspace. Keep this in sync with NR_CPUS used by the BPF side.
+ */
+#define SCX_QMAP_MAX_CPUS 1024
+
+/*
+ * An owner id identifies who holds a cid: a child slot in [0, MAX_SUB_SCHEDS),
+ * CID_SELF for this node, CID_NONE for a cid not currently held, or CID_SHARED
+ * for a cid in the round-robin pool (its live holder is rr_slots[rr_pos]). Used
+ * by the partition's cid_owner[].
+ */
+#define CID_SELF (-1)
+#define CID_NONE (-2)
+#define CID_SHARED (-3)
+
+/* -C cid-override test modes. Selects cid_override_mode in scx_qmap.bpf.c. */
+enum qmap_cid_override {
+ QMAP_CID_OVR_OFF = 0, /* disabled */
+ QMAP_CID_OVR_SHUFFLE = 1, /* valid reversed cpu->cid mapping */
+ QMAP_CID_OVR_BAD_DUP = 2, /* invalid: duplicate cid assignment */
+ QMAP_CID_OVR_BAD_RANGE = 3, /* invalid: out-of-range cid */
+ QMAP_CID_OVR_BAD_MONO = 4, /* invalid: non-monotonic shard_start */
+};
+
+struct cpu_ctx {
+ u64 dsp_idx; /* dispatch index */
+ u64 dsp_cnt; /* remaining count */
+ u32 avg_weight;
+ u32 cpuperf_target;
+};
+
+struct qmap_fifo {
+ struct task_ctx __arena *head;
+ struct task_ctx __arena *tail;
+ s32 idx;
+};
+
+/* -J fault-injection modes. Selects inject_mode in struct qmap_arena. */
+enum qmap_inject {
+ QMAP_INJ_OFF = 0,
+ QMAP_INJ_WRONG_CID = 1, /* dispatch to a cid we don't hold */
+ QMAP_INJ_INIT_FAIL = 2, /* fail init_task for "qmfail*" comms */
+ QMAP_INJ_CGRP_INIT_FAIL = 3, /* fail cpuctl_init for "qmfail*" cgroups */
+};
+
+/*
+ * scx_cmask's are embedded in struct qmap_arena with inline backing storage.
+ * The bpf side uses &field.mask with the normal cmask_* helpers. Userspace
+ * doesn't have access to the type definition and sees same-sized opaque words.
+ * _Static_assert()'s in .bpf.c ensure that they are in sync.
+ */
+#define QMAP_CMASK_WORDS (((SCX_QMAP_MAX_CPUS) + 63) / 64 + 1)
+struct qmap_cmask {
+#ifdef __BPF__
+ union {
+ struct scx_cmask mask;
+ u64 words[QMAP_CMASK_WORDS + 2];
+ };
+#else
+ u64 words[QMAP_CMASK_WORDS + 2];
+#endif
+};
+
+/* Opaque to userspace; defined in scx_qmap.bpf.c. */
+struct task_ctx;
+
+/* per-direct-child state for the sub-scheduler */
+struct sub_sched_ctx {
+ u64 cgroup_id;
+ u32 weight; /* cpu.weight, seeded at attach, then set_weight */
+ u64 nr_dsps;
+ struct qmap_cmask granted_cids; /* cids granted excl to this child */
+ struct qmap_cmask prev_granted; /* last grant, for delta calculation */
+};
+
+/*
+ * compute_partition() builds the following from this node's held caps, and
+ * apply_partition()/rr_advance() execute it. Userspace only reads for the
+ * hierarchy display.
+ */
+struct qmap_partition {
+ u32 nr_excl; /* number of excl-held (delegatable) cids */
+ s32 cid_owner[SCX_QMAP_MAX_CPUS]; /* per cid: owner id, or CID_NONE */
+ s32 shared_cids[MAX_PARTS]; /* the round-robin cid pool */
+ u32 nr_shared; /* number of shared_cids entries */
+ u64 rr_slots[MAX_PARTS]; /* rotation order: holder cgroup_id, 0 = self */
+ u32 nr_rr; /* number of rr_slots entries */
+ u32 rr_pos; /* current rotation index */
+};
+
+struct qmap_arena {
+ /* userspace-visible stats */
+ u64 nr_enqueued, nr_dispatched, nr_reenqueued, nr_reenqueued_cid0;
+ u64 nr_dequeued, nr_ddsp_from_enq;
+ u64 nr_core_sched_execed;
+ u64 nr_expedited_local, nr_expedited_remote;
+ u64 nr_expedited_lost, nr_expedited_from_timer;
+ u64 nr_highpri_queued;
+ u32 test_error_cnt;
+ u32 cpuperf_min, cpuperf_avg, cpuperf_max;
+ u32 cpuperf_target_min, cpuperf_target_avg, cpuperf_target_max;
+
+ /* kernel-side runtime state */
+ u64 core_sched_head_seqs[5];
+ u64 core_sched_tail_seqs[5];
+
+ struct cpu_ctx cpu_ctxs[SCX_QMAP_MAX_CPUS];
+
+ /* cid-override test input, populated by the loader before attach */
+ __s32 cid_override_cpu_to_cid[SCX_QMAP_MAX_CPUS];
+ __s32 cid_override_shard_start[SCX_QMAP_MAX_CPUS];
+
+ /* task_ctx slab; allocated and threaded by qmap_init() */
+ struct task_ctx __arena *task_ctxs;
+ struct task_ctx __arena *task_free_head;
+
+ /* five priority FIFOs, each a doubly-linked list through task_ctx */
+ struct qmap_fifo fifos[5];
+
+ /*
+ * Hierarchical sub-scheduling state. See the design comment at the top
+ * of scx_qmap.bpf.c.
+ */
+ u32 nr_cids; /* cid count, cached at init */
+
+ /* bpf-owned partition: read by userspace for display */
+ struct qmap_partition part;
+
+ struct sub_sched_ctx sub_sched_ctxs[MAX_SUB_SCHEDS]; /* per-child context */
+ u64 nr_sub_scheds; /* number of attached children */
+
+ /* bpf-internal per-cid state */
+ u8 cid_shared[SCX_QMAP_MAX_CPUS]; /* per cid: 1 if held shared (ENQ_IMMED-only) */
+
+ /* allocated cid-time, charged per owner by account_alloc() */
+ u64 alloc_ns[MAX_SUB_SCHEDS]; /* per child slot */
+ u64 self_alloc_ns;
+ u64 alloc_ts; /* last accounting timestamp */
+ u64 alloc_window_ns; /* total accounted time, the alloc denominator */
+
+ /* bpf-internal cmasks (embedded, see struct qmap_cmask) */
+ struct qmap_cmask self_cids; /* cids this node runs its own tasks on */
+ struct qmap_cmask avail_cids; /* cids with caps in effect on the cpu */
+ struct qmap_cmask usable_cids; /* self_cids & avail_cids, placeable right now */
+ struct qmap_cmask idle_cids; /* idle state of all cids regardless of delegation */
+ struct qmap_cmask rr_cids; /* the shared pool, as a mask for grant/revoke */
+
+ /* scratch cmasks */
+ struct qmap_cmask to_revoke_cids; /* delta cids to revoke */
+ struct qmap_cmask to_grant_cids; /* delta cids to grant */
+ struct qmap_cmask usable_scratch; /* refresh_usable() build area */
+ struct qmap_cmask prev_rr_cids; /* previous shared pool, to clear stale grants */
+ struct qmap_cmask held_excl; /* cids held excl (ENQ): delegatable */
+ struct qmap_cmask held_shared; /* cids held shared (ENQ_IMMED only): self-local */
+
+ /* bpf -> userspace: stats */
+ u64 nr_reenq_cap; /* SCX_TASK_REENQ_CAP bounces */
+ u64 nr_reenq_immed; /* SCX_TASK_REENQ_IMMED bounces */
+ u64 nr_inject_attempts; /* fault-injection: dispatches to an unheld cid */
+ u64 nr_rescue_dsp; /* SCX_ENQ_RESCUE dispatch attempts */
+ u32 inject_mode; /* fault-injection mode (QMAP_INJ_*) */
+};
+
+#endif /* __SCX_QMAP_H */
diff --git a/tools/sched_ext/scx_sdt.bpf.c b/tools/sched_ext/scx_sdt.bpf.c
new file mode 100644
index 000000000..a1e33e6c4
--- /dev/null
+++ b/tools/sched_ext/scx_sdt.bpf.c
@@ -0,0 +1,717 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Arena-based task data scheduler. This is a variation of scx_simple
+ * that uses a combined allocator and indexing structure to organize
+ * task data. Task context allocation is done when a task enters the
+ * scheduler, while freeing is done when it exits. Task contexts are
+ * retrieved from task-local storage, pointing to the allocated memory.
+ *
+ * The main purpose of this scheduler is to demostrate arena memory
+ * management.
+ *
+ * Copyright (c) 2024-2025 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2024-2025 Emil Tsalapatis <etsal@meta.com>
+ * Copyright (c) 2024-2025 Tejun Heo <tj@kernel.org>
+ *
+ */
+#include <scx/common.bpf.h>
+#include <scx/bpf_arena_common.bpf.h>
+
+#include "scx_sdt.h"
+
+char _license[] SEC("license") = "GPL";
+
+UEI_DEFINE(uei);
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+#if defined(__TARGET_ARCH_arm64) || defined(__aarch64__)
+ __uint(max_entries, 1 << 16); /* number of pages */
+ __ulong(map_extra, (1ull << 32)); /* start of mmap() region */
+#else
+ __uint(max_entries, 1 << 20); /* number of pages */
+ __ulong(map_extra, (1ull << 44)); /* start of mmap() region */
+#endif
+} arena __weak SEC(".maps");
+
+#define SHARED_DSQ 0
+
+#define DEFINE_SDT_STAT(metric) \
+static inline void \
+stat_inc_##metric(struct scx_stats __arena *stats) \
+{ \
+ cast_kern(stats); \
+ stats->metric += 1; \
+} \
+__u64 stat_##metric; \
+
+DEFINE_SDT_STAT(enqueue);
+DEFINE_SDT_STAT(init);
+DEFINE_SDT_STAT(exit);
+DEFINE_SDT_STAT(select_idle_cpu);
+DEFINE_SDT_STAT(select_busy_cpu);
+
+/*
+ * Necessary for cond_break/can_loop's semantics. According to kernel commit
+ * 011832b, the loop counter variable must be seen as imprecise and bounded
+ * by the verifier. Initializing it from a constant (e.g., i = 0;), then,
+ * makes it precise and prevents may_goto from helping with converging the
+ * loop. For these loops we must initialize the loop counter from a variable
+ * whose value the verifier cannot reason about when checking the program, so
+ * that the loop counter's value is imprecise.
+ */
+static __u64 zero = 0;
+
+/*
+ * XXX Hack to get the verifier to find the arena for sdt_exit_task.
+ * As of 6.12-rc5, The verifier associates arenas with programs by
+ * checking LD.IMM instruction operands for an arena and populating
+ * the program state with the first instance it finds. This requires
+ * accessing our global arena variable, but scx methods do not necessarily
+ * do so while still using pointers from that arena. Insert a bpf_printk
+ * statement that triggers at most once to generate an LD.IMM instruction
+ * to access the arena and help the verifier.
+ */
+static volatile bool scx_arena_verify_once;
+
+__hidden void scx_arena_subprog_init(void)
+{
+ if (scx_arena_verify_once)
+ return;
+
+ bpf_printk("%s: arena pointer %p", __func__, &arena);
+ scx_arena_verify_once = true;
+}
+
+
+private(LOCK) struct bpf_spin_lock alloc_lock;
+private(POOL_LOCK) struct bpf_spin_lock alloc_pool_lock;
+
+/* allocation pools */
+struct sdt_pool desc_pool;
+struct sdt_pool chunk_pool;
+
+/* Protected by alloc_lock. */
+struct scx_alloc_stats alloc_stats;
+
+
+/* Allocate element from the pool. Must be called with a then pool lock held. */
+static
+void __arena *scx_alloc_from_pool(struct sdt_pool *pool)
+{
+ __u64 elem_size, max_elems;
+ void __arena *slab;
+ void __arena *ptr;
+
+ elem_size = pool->elem_size;
+ max_elems = pool->max_elems;
+
+ /* If the chunk is spent, get a new one. */
+ if (pool->idx >= max_elems) {
+ slab = bpf_arena_alloc_pages(&arena, NULL,
+ div_round_up(max_elems * elem_size, PAGE_SIZE), NUMA_NO_NODE, 0);
+ if (!slab)
+ return NULL;
+
+ pool->slab = slab;
+ pool->idx = 0;
+ }
+
+ ptr = (void __arena *)((__u64) pool->slab + elem_size * pool->idx);
+ pool->idx += 1;
+
+ return ptr;
+}
+
+/* Alloc desc and associated chunk. Called with the allocator spinlock held. */
+static sdt_desc_t *scx_alloc_chunk(void)
+{
+ struct sdt_chunk __arena *chunk;
+ sdt_desc_t *desc;
+ sdt_desc_t *out;
+
+ chunk = scx_alloc_from_pool(&chunk_pool);
+ if (!chunk)
+ return NULL;
+
+ desc = scx_alloc_from_pool(&desc_pool);
+ if (!desc) {
+ /*
+ * Effectively frees the previous chunk allocation.
+ * Index cannot be 0, so decrementing is always
+ * valid.
+ */
+ chunk_pool.idx -= 1;
+ return NULL;
+ }
+
+ out = desc;
+
+ desc->nr_free = SDT_TASK_ENTS_PER_CHUNK;
+ desc->chunk = chunk;
+
+ alloc_stats.chunk_allocs += 1;
+
+ return out;
+}
+
+static int pool_set_size(struct sdt_pool *pool, __u64 data_size, __u64 nr_pages)
+{
+ if (unlikely(data_size % 8))
+ return -EINVAL;
+
+ if (unlikely(nr_pages == 0))
+ return -EINVAL;
+
+ pool->elem_size = data_size;
+ pool->max_elems = (PAGE_SIZE * nr_pages) / pool->elem_size;
+ /* Populate the pool slab on the first allocation. */
+ pool->idx = pool->max_elems;
+
+ return 0;
+}
+
+/* Initialize both the base pool allocators and the root chunk of the index. */
+__hidden int
+scx_alloc_init(struct scx_allocator *alloc, __u64 data_size)
+{
+ size_t min_chunk_size;
+ int ret;
+
+ _Static_assert(sizeof(struct sdt_chunk) <= PAGE_SIZE,
+ "chunk size must fit into a page");
+
+ ret = pool_set_size(&chunk_pool, sizeof(struct sdt_chunk), 1);
+ if (ret != 0)
+ return ret;
+
+ ret = pool_set_size(&desc_pool, sizeof(struct sdt_desc), 1);
+ if (ret != 0)
+ return ret;
+
+ /* Wrap data into a descriptor and word align. */
+ data_size += sizeof(struct sdt_data);
+ data_size = round_up(data_size, 8);
+
+ /*
+ * Ensure we allocate large enough chunks from the arena to avoid excessive
+ * internal fragmentation when turning chunks it into structs.
+ */
+ min_chunk_size = div_round_up(SDT_TASK_MIN_ELEM_PER_ALLOC * data_size, PAGE_SIZE);
+ ret = pool_set_size(&alloc->pool, data_size, min_chunk_size);
+ if (ret != 0)
+ return ret;
+
+ bpf_spin_lock(&alloc_lock);
+ alloc->root = scx_alloc_chunk();
+ bpf_spin_unlock(&alloc_lock);
+ if (!alloc->root)
+ return -ENOMEM;
+
+ return 0;
+}
+
+static
+int set_idx_state(sdt_desc_t *desc, __u64 pos, bool state)
+{
+ __u64 __arena *allocated = desc->allocated;
+ __u64 bit;
+
+ if (unlikely(pos >= SDT_TASK_ENTS_PER_CHUNK))
+ return -EINVAL;
+
+ bit = (__u64)1 << (pos % 64);
+
+ if (state)
+ allocated[pos / 64] |= bit;
+ else
+ allocated[pos / 64] &= ~bit;
+
+ return 0;
+}
+
+static __noinline
+int mark_nodes_avail(sdt_desc_t *lv_desc[SDT_TASK_LEVELS], __u64 lv_pos[SDT_TASK_LEVELS])
+{
+ sdt_desc_t *desc;
+ __u64 u, level;
+ int ret;
+
+ for (u = zero; u < SDT_TASK_LEVELS && can_loop; u++) {
+ level = SDT_TASK_LEVELS - 1 - u;
+
+ /* Only propagate upwards if we are the parent's only free chunk. */
+ desc = lv_desc[level];
+
+ ret = set_idx_state(desc, lv_pos[level], false);
+ if (unlikely(ret != 0))
+ return ret;
+
+ desc->nr_free += 1;
+ if (desc->nr_free > 1)
+ return 0;
+ }
+
+ return 0;
+}
+
+/*
+ * Free the allocated struct with the given index. Called with the
+ * allocator lock taken.
+ */
+__hidden
+int scx_alloc_free_idx(struct scx_allocator *alloc, __u64 idx)
+{
+ const __u64 mask = (1 << SDT_TASK_ENTS_PER_PAGE_SHIFT) - 1;
+ sdt_desc_t *lv_desc[SDT_TASK_LEVELS];
+ sdt_desc_t * __arena *desc_children;
+ struct sdt_chunk __arena *chunk;
+ sdt_desc_t *desc;
+ struct sdt_data __arena *data;
+ __u64 level, shift, pos;
+ __u64 lv_pos[SDT_TASK_LEVELS];
+ int ret;
+ int i;
+
+ if (!alloc)
+ return 0;
+
+ desc = alloc->root;
+ if (unlikely(!desc))
+ return -EINVAL;
+
+ /* To appease the verifier. */
+ for (level = zero; level < SDT_TASK_LEVELS && can_loop; level++) {
+ lv_desc[level] = NULL;
+ lv_pos[level] = 0;
+ }
+
+ /* Find the leaf node containing the index. */
+ for (level = zero; level < SDT_TASK_LEVELS && can_loop; level++) {
+ shift = (SDT_TASK_LEVELS - 1 - level) * SDT_TASK_ENTS_PER_PAGE_SHIFT;
+ pos = (idx >> shift) & mask;
+
+ lv_desc[level] = desc;
+ lv_pos[level] = pos;
+
+ if (level == SDT_TASK_LEVELS - 1)
+ break;
+
+ chunk = desc->chunk;
+
+ desc_children = (sdt_desc_t * __arena *)chunk->descs;
+ desc = desc_children[pos];
+
+ if (unlikely(!desc))
+ return -EINVAL;
+ }
+
+ chunk = desc->chunk;
+
+ pos = idx & mask;
+ data = chunk->data[pos];
+ if (likely(data)) {
+ *data = (struct sdt_data) {
+ .tid.genn = data->tid.genn + 1,
+ };
+
+ /* Zero out one word at a time. */
+ for (i = zero; i < (alloc->pool.elem_size - sizeof(struct sdt_data)) / 8
+ && can_loop; i++) {
+ data->payload[i] = 0;
+ }
+ }
+
+ ret = mark_nodes_avail(lv_desc, lv_pos);
+ if (unlikely(ret != 0))
+ return ret;
+
+ alloc_stats.active_allocs -= 1;
+ alloc_stats.free_ops += 1;
+
+ return 0;
+}
+
+static inline
+int ffs(__u64 word)
+{
+ unsigned int num = 0;
+
+ if ((word & 0xffffffff) == 0) {
+ num += 32;
+ word >>= 32;
+ }
+
+ if ((word & 0xffff) == 0) {
+ num += 16;
+ word >>= 16;
+ }
+
+ if ((word & 0xff) == 0) {
+ num += 8;
+ word >>= 8;
+ }
+
+ if ((word & 0xf) == 0) {
+ num += 4;
+ word >>= 4;
+ }
+
+ if ((word & 0x3) == 0) {
+ num += 2;
+ word >>= 2;
+ }
+
+ if ((word & 0x1) == 0) {
+ num += 1;
+ word >>= 1;
+ }
+
+ return num;
+}
+
+
+/* find the first empty slot */
+__hidden
+__u64 chunk_find_empty(sdt_desc_t __arg_arena *desc)
+{
+ __u64 freeslots;
+ __u64 i;
+
+ for (i = 0; i < SDT_TASK_CHUNK_BITMAP_U64S; i++) {
+ freeslots = ~desc->allocated[i];
+ if (freeslots == (__u64)0)
+ continue;
+
+ return (i * 64) + ffs(freeslots);
+ }
+
+ return SDT_TASK_ENTS_PER_CHUNK;
+}
+
+/*
+ * Find and return an available idx on the allocator.
+ * Called with the task spinlock held.
+ */
+static sdt_desc_t * desc_find_empty(sdt_desc_t *desc, __u64 *idxp)
+{
+ sdt_desc_t *lv_desc[SDT_TASK_LEVELS];
+ sdt_desc_t * __arena *desc_children;
+ struct sdt_chunk __arena *chunk;
+ sdt_desc_t *tmp;
+ __u64 lv_pos[SDT_TASK_LEVELS];
+ __u64 u, pos, level;
+ __u64 idx = 0;
+ int ret;
+
+ for (level = zero; level < SDT_TASK_LEVELS && can_loop; level++) {
+ pos = chunk_find_empty(desc);
+
+ /* If we error out, something has gone very wrong. */
+ if (unlikely(pos > SDT_TASK_ENTS_PER_CHUNK))
+ return NULL;
+
+ if (pos == SDT_TASK_ENTS_PER_CHUNK)
+ return NULL;
+
+ idx <<= SDT_TASK_ENTS_PER_PAGE_SHIFT;
+ idx |= pos;
+
+ /* Log the levels to complete allocation. */
+ lv_desc[level] = desc;
+ lv_pos[level] = pos;
+
+ /* The rest of the loop is for internal node traversal. */
+ if (level == SDT_TASK_LEVELS - 1)
+ break;
+
+ /* Allocate an internal node if necessary. */
+ chunk = desc->chunk;
+ desc_children = (sdt_desc_t * __arena *)chunk->descs;
+
+ desc = desc_children[pos];
+ if (!desc) {
+ desc = scx_alloc_chunk();
+ if (!desc)
+ return NULL;
+
+ desc_children[pos] = desc;
+ }
+ }
+
+ /*
+ * Finding the descriptor along with any internal node
+ * allocations was successful. Update all levels with
+ * the new allocation.
+ */
+ bpf_for(u, 0, SDT_TASK_LEVELS) {
+ level = SDT_TASK_LEVELS - 1 - u;
+ tmp = lv_desc[level];
+
+ ret = set_idx_state(tmp, lv_pos[level], true);
+ if (ret != 0)
+ break;
+
+ tmp->nr_free -= 1;
+ if (tmp->nr_free > 0)
+ break;
+ }
+
+ *idxp = idx;
+
+ return desc;
+}
+
+__hidden
+void __arena *scx_alloc(struct scx_allocator *alloc)
+{
+ struct sdt_data __arena *data = NULL;
+ struct sdt_chunk __arena *chunk;
+ sdt_desc_t *desc;
+ __u64 idx, pos;
+
+ if (!alloc)
+ return NULL;
+
+ bpf_spin_lock(&alloc_lock);
+
+ /* We unlock if we encounter an error in the function. */
+ desc = desc_find_empty(alloc->root, &idx);
+ if (unlikely(desc == NULL)) {
+ bpf_spin_unlock(&alloc_lock);
+ return NULL;
+ }
+
+ chunk = desc->chunk;
+
+ /* Populate the leaf node if necessary. */
+ pos = idx & (SDT_TASK_ENTS_PER_CHUNK - 1);
+ data = chunk->data[pos];
+ if (!data) {
+ data = scx_alloc_from_pool(&alloc->pool);
+ if (!data) {
+ scx_alloc_free_idx(alloc, idx);
+ bpf_spin_unlock(&alloc_lock);
+ return NULL;
+ }
+ }
+
+ chunk->data[pos] = data;
+
+ /* The data counts as a chunk */
+ alloc_stats.data_allocs += 1;
+ alloc_stats.alloc_ops += 1;
+ alloc_stats.active_allocs += 1;
+
+ data->tid.idx = idx;
+
+ bpf_spin_unlock(&alloc_lock);
+
+ return data;
+}
+
+/*
+ * Task BPF map entry recording the task's assigned ID and pointing to the data
+ * area allocated in arena.
+ */
+struct scx_task_map_val {
+ union sdt_id tid;
+ __u64 tptr;
+ struct sdt_data __arena *data;
+};
+
+struct {
+ __uint(type, BPF_MAP_TYPE_TASK_STORAGE);
+ __uint(map_flags, BPF_F_NO_PREALLOC);
+ __type(key, int);
+ __type(value, struct scx_task_map_val);
+} scx_task_map SEC(".maps");
+
+static struct scx_allocator scx_task_allocator;
+
+__hidden
+void __arena *scx_task_alloc(struct task_struct *p)
+{
+ struct sdt_data __arena *data = NULL;
+ struct scx_task_map_val *mval;
+
+ mval = bpf_task_storage_get(&scx_task_map, p, 0,
+ BPF_LOCAL_STORAGE_GET_F_CREATE);
+ if (!mval)
+ return NULL;
+
+ data = scx_alloc(&scx_task_allocator);
+ if (unlikely(!data))
+ return NULL;
+
+ mval->tid = data->tid;
+ mval->tptr = (__u64) p;
+ mval->data = data;
+
+ return (void __arena *)data->payload;
+}
+
+__hidden
+int scx_task_init(__u64 data_size)
+{
+ return scx_alloc_init(&scx_task_allocator, data_size);
+}
+
+__hidden
+void __arena *scx_task_data(struct task_struct *p)
+{
+ struct sdt_data __arena *data;
+ struct scx_task_map_val *mval;
+
+ scx_arena_subprog_init();
+
+ mval = bpf_task_storage_get(&scx_task_map, p, 0, 0);
+ if (!mval)
+ return NULL;
+
+ data = mval->data;
+
+ return (void __arena *)data->payload;
+}
+
+__hidden
+void scx_task_free(struct task_struct *p)
+{
+ struct scx_task_map_val *mval;
+
+ scx_arena_subprog_init();
+
+ mval = bpf_task_storage_get(&scx_task_map, p, 0, 0);
+ if (!mval)
+ return;
+
+ bpf_spin_lock(&alloc_lock);
+ scx_alloc_free_idx(&scx_task_allocator, mval->tid.idx);
+ bpf_spin_unlock(&alloc_lock);
+
+ bpf_task_storage_delete(&scx_task_map, p);
+}
+
+static inline void
+scx_stat_global_update(struct scx_stats __arena *stats)
+{
+ cast_kern(stats);
+ __sync_fetch_and_add(&stat_enqueue, stats->enqueue);
+ __sync_fetch_and_add(&stat_init, stats->init);
+ __sync_fetch_and_add(&stat_exit, stats->exit);
+ __sync_fetch_and_add(&stat_select_idle_cpu, stats->select_idle_cpu);
+ __sync_fetch_and_add(&stat_select_busy_cpu, stats->select_busy_cpu);
+}
+
+s32 BPF_STRUCT_OPS(sdt_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags)
+{
+ struct scx_stats __arena *stats;
+ bool is_idle = false;
+ s32 cpu;
+
+ stats = scx_task_data(p);
+ if (!stats) {
+ scx_bpf_error("%s: no stats for pid %d", __func__, p->pid);
+ return 0;
+ }
+
+ cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle);
+ if (is_idle) {
+ stat_inc_select_idle_cpu(stats);
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
+ } else {
+ stat_inc_select_busy_cpu(stats);
+ }
+
+ return cpu;
+}
+
+void BPF_STRUCT_OPS(sdt_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ struct scx_stats __arena *stats;
+
+ stats = scx_task_data(p);
+ if (!stats) {
+ scx_bpf_error("%s: no stats for pid %d", __func__, p->pid);
+ return;
+ }
+
+ stat_inc_enqueue(stats);
+
+ scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, enq_flags);
+}
+
+void BPF_STRUCT_OPS(sdt_dispatch, s32 cpu, struct task_struct *prev)
+{
+ scx_bpf_dsq_move_to_local(SHARED_DSQ, 0);
+}
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(sdt_init_task, struct task_struct *p,
+ struct scx_init_task_args *args)
+{
+ struct scx_stats __arena *stats;
+
+ stats = scx_task_alloc(p);
+ if (!stats) {
+ scx_bpf_error("arena allocator out of memory");
+ return -ENOMEM;
+ }
+
+ stats->pid = p->pid;
+
+ stat_inc_init(stats);
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(sdt_exit_task, struct task_struct *p,
+ struct scx_exit_task_args *args)
+{
+ struct scx_stats __arena *stats;
+
+ stats = scx_task_data(p);
+ if (!stats) {
+ scx_bpf_error("%s: no stats for pid %d", __func__, p->pid);
+ return;
+ }
+
+ stat_inc_exit(stats);
+ scx_stat_global_update(stats);
+
+ scx_task_free(p);
+}
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(sdt_init)
+{
+ int ret;
+
+ ret = scx_task_init(sizeof(struct scx_stats));
+ if (ret < 0) {
+ scx_bpf_error("%s: failed with %d", __func__, ret);
+ return ret;
+ }
+
+ ret = scx_bpf_create_dsq(SHARED_DSQ, -1);
+ if (ret) {
+ scx_bpf_error("failed to create DSQ %d (%d)", SHARED_DSQ, ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(sdt_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+SCX_OPS_DEFINE(sdt_ops,
+ .select_cpu = (void *)sdt_select_cpu,
+ .enqueue = (void *)sdt_enqueue,
+ .dispatch = (void *)sdt_dispatch,
+ .init_task = (void *)sdt_init_task,
+ .exit_task = (void *)sdt_exit_task,
+ .init = (void *)sdt_init,
+ .exit = (void *)sdt_exit,
+ .name = "sdt");
diff --git a/tools/sched_ext/scx_sdt.c b/tools/sched_ext/scx_sdt.c
new file mode 100644
index 000000000..2f93a00de
--- /dev/null
+++ b/tools/sched_ext/scx_sdt.c
@@ -0,0 +1,102 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2024 Emil Tsalapatis <etsal@meta.com>
+ * Copyright (c) 2024 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+#include <libgen.h>
+#include <bpf/bpf.h>
+#include <scx/common.h>
+
+#include "scx_sdt.h"
+#include "scx_sdt.bpf.skel.h"
+
+const char help_fmt[] =
+"A simple arena-based sched_ext scheduler.\n"
+"\n"
+"Modified version of scx_simple that demonstrates arena-based data structures.\n"
+"\n"
+"Usage: %s [-v]\n"
+"\n"
+" -v Print libbpf debug messages\n"
+" -h Display this help and exit\n";
+
+static bool verbose;
+static volatile int exit_req;
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ if (level == LIBBPF_DEBUG && !verbose)
+ return 0;
+ return vfprintf(stderr, format, args);
+}
+
+static void sigint_handler(int sig)
+{
+ exit_req = 1;
+}
+
+int main(int argc, char **argv)
+{
+ struct scx_sdt *skel;
+ struct bpf_link *link;
+ __s32 opt;
+ __u64 ecode;
+
+ libbpf_set_print(libbpf_print_fn);
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+restart:
+ optind = 1;
+ skel = SCX_OPS_OPEN(sdt_ops, scx_sdt);
+
+ while ((opt = getopt(argc, argv, "vh")) != -1) {
+ switch (opt) {
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ fprintf(stderr, help_fmt, basename(argv[0]));
+ return opt != 'h';
+ }
+ }
+
+ SCX_OPS_LOAD(skel, sdt_ops, scx_sdt, uei);
+ link = SCX_OPS_ATTACH(skel, sdt_ops, scx_sdt);
+
+ while (!exit_req && !UEI_EXITED(skel, uei)) {
+ printf("====SCHEDULING STATS====\n");
+ printf("enqueues=%llu\t", skel->bss->stat_enqueue);
+ printf("inits=%llu\t", skel->bss->stat_init);
+ printf("exits=%llu\t", skel->bss->stat_exit);
+ printf("\n");
+
+ printf("select_idle_cpu=%llu\t", skel->bss->stat_select_idle_cpu);
+ printf("select_busy_cpu=%llu\t", skel->bss->stat_select_busy_cpu);
+ printf("\n");
+
+ printf("====ALLOCATION STATS====\n");
+ printf("chunk allocs=%llu\t", skel->bss->alloc_stats.chunk_allocs);
+ printf("data_allocs=%llu\n", skel->bss->alloc_stats.data_allocs);
+ printf("alloc_ops=%llu\t", skel->bss->alloc_stats.alloc_ops);
+ printf("free_ops=%llu\t", skel->bss->alloc_stats.free_ops);
+ printf("active_allocs=%llu\t", skel->bss->alloc_stats.active_allocs);
+ printf("arena_pages_used=%llu\t", skel->bss->alloc_stats.arena_pages_used);
+ printf("\n\n");
+
+ fflush(stdout);
+ sleep(1);
+ }
+
+ bpf_link__destroy(link);
+ ecode = UEI_REPORT(skel, uei);
+ scx_sdt__destroy(skel);
+
+ if (!exit_req && UEI_ECODE_RESTART(ecode))
+ goto restart;
+ return 0;
+}
diff --git a/tools/sched_ext/scx_sdt.h b/tools/sched_ext/scx_sdt.h
new file mode 100644
index 000000000..67982ce9b
--- /dev/null
+++ b/tools/sched_ext/scx_sdt.h
@@ -0,0 +1,113 @@
+/*
+ * SPDX-License-Identifier: GPL-2.0
+ * Copyright (c) 2025 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2025 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2025 Emil Tsalapatis <etsal@meta.com>
+ */
+#pragma once
+
+#ifndef __BPF__
+#define __arena
+#endif /* __BPF__ */
+
+struct scx_alloc_stats {
+ __u64 chunk_allocs;
+ __u64 data_allocs;
+ __u64 alloc_ops;
+ __u64 free_ops;
+ __u64 active_allocs;
+ __u64 arena_pages_used;
+};
+
+struct sdt_pool {
+ void __arena *slab;
+ __u64 elem_size;
+ __u64 max_elems;
+ __u64 idx;
+};
+
+#ifndef div_round_up
+#define div_round_up(a, b) (((a) + (b) - 1) / (b))
+#endif
+
+#ifndef round_up
+#define round_up(a, b) (div_round_up((a), (b)) * (b))
+#endif
+
+typedef struct sdt_desc __arena sdt_desc_t;
+
+enum sdt_consts {
+ SDT_TASK_ENTS_PER_PAGE_SHIFT = 9,
+ SDT_TASK_LEVELS = 3,
+ SDT_TASK_ENTS_PER_CHUNK = 1 << SDT_TASK_ENTS_PER_PAGE_SHIFT,
+ SDT_TASK_CHUNK_BITMAP_U64S = div_round_up(SDT_TASK_ENTS_PER_CHUNK, 64),
+ SDT_TASK_MIN_ELEM_PER_ALLOC = 8,
+};
+
+union sdt_id {
+ __s64 val;
+ struct {
+ __s32 idx; /* index in the radix tree */
+ __s32 genn; /* ++'d on recycle so that it forms unique'ish 64bit ID */
+ };
+};
+
+struct sdt_chunk;
+
+/*
+ * Each index page is described by the following descriptor which carries the
+ * bitmap. This way the actual index can host power-of-two numbers of entries
+ * which makes indexing cheaper.
+ */
+struct sdt_desc {
+ __u64 allocated[SDT_TASK_CHUNK_BITMAP_U64S];
+ __u64 nr_free;
+ struct sdt_chunk __arena *chunk;
+};
+
+/*
+ * Leaf node containing per-task data.
+ */
+struct sdt_data {
+ union sdt_id tid;
+ __u64 payload[];
+};
+
+/*
+ * Intermediate node pointing to another intermediate node or leaf node.
+ */
+struct sdt_chunk {
+ union {
+ sdt_desc_t * descs[SDT_TASK_ENTS_PER_CHUNK];
+ struct sdt_data __arena *data[SDT_TASK_ENTS_PER_CHUNK];
+ };
+};
+
+struct scx_allocator {
+ struct sdt_pool pool;
+ sdt_desc_t *root;
+};
+
+struct scx_stats {
+ int seq;
+ pid_t pid;
+ __u64 enqueue;
+ __u64 exit;
+ __u64 init;
+ __u64 select_busy_cpu;
+ __u64 select_idle_cpu;
+};
+
+#ifdef __BPF__
+
+void __arena *scx_task_data(struct task_struct *p);
+int scx_task_init(__u64 data_size);
+void __arena *scx_task_alloc(struct task_struct *p);
+void scx_task_free(struct task_struct *p);
+void scx_arena_subprog_init(void);
+
+int scx_alloc_init(struct scx_allocator *alloc, __u64 data_size);
+u64 scx_alloc_internal(struct scx_allocator *alloc);
+int scx_alloc_free_idx(struct scx_allocator *alloc, __u64 idx);
+
+#endif /* __BPF__ */
diff --git a/tools/sched_ext/scx_show_state.py b/tools/sched_ext/scx_show_state.py
new file mode 100644
index 000000000..446d82807
--- /dev/null
+++ b/tools/sched_ext/scx_show_state.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env drgn
+#
+# Copyright (C) 2024 Tejun Heo <tj@kernel.org>
+# Copyright (C) 2024 Meta Platforms, Inc. and affiliates.
+
+desc = """
+This is a drgn script to show the current sched_ext state.
+For more info on drgn, visit https://github.com/osandov/drgn.
+"""
+
+import drgn
+import sys
+
+def err(s):
+ print(s, file=sys.stderr, flush=True)
+ sys.exit(1)
+
+def read_int(name):
+ return int(prog[name].value_())
+
+def read_atomic(name):
+ return prog[name].counter.value_()
+
+def read_static_key(name):
+ return prog[name].key.enabled.counter.value_()
+
+def state_str(state):
+ return prog['scx_enable_state_str'][state].string_().decode()
+
+def read_root_ops_name():
+ if root:
+ return root.ops.name.string_().decode()
+ return ''
+
+def read_root_field(name, default):
+ if root:
+ return getattr(root, name).value_()
+ return default
+
+root = prog['scx_root']
+enable_state = read_atomic("scx_enable_state_var")
+
+print(f'ops : {read_root_ops_name()}')
+print(f'enabled : {read_static_key("__scx_enabled")}')
+print(f'switching_all : {read_int("scx_switching_all")}')
+print(f'switched_all : {read_static_key("__scx_switched_all")}')
+print(f'enable_state : {state_str(enable_state)} ({enable_state})')
+print(f'aborting : {read_root_field("aborting", False)}')
+print(f'bypass_depth : {read_root_field("bypass_depth", 0)}')
+print(f'nr_rejected : {read_atomic("scx_nr_rejected")}')
+print(f'enable_seq : {read_atomic("scx_enable_seq")}')
diff --git a/tools/sched_ext/scx_simple.bpf.c b/tools/sched_ext/scx_simple.bpf.c
new file mode 100644
index 000000000..cc40552b2
--- /dev/null
+++ b/tools/sched_ext/scx_simple.bpf.c
@@ -0,0 +1,161 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * A simple scheduler.
+ *
+ * By default, it operates as a simple global weighted vtime scheduler and can
+ * be switched to FIFO scheduling. It also demonstrates the following niceties.
+ *
+ * - Statistics tracking how many tasks are queued to local and global dsq's.
+ * - Termination notification for userspace.
+ *
+ * While very simple, this scheduler should work reasonably well on CPUs with a
+ * uniform L3 cache topology. While preemption is not implemented, the fact that
+ * the scheduling queue is shared across all CPUs means that whatever is at the
+ * front of the queue is likely to be executed fairly quickly given enough
+ * number of CPUs. The FIFO scheduling mode may be beneficial to some workloads
+ * but comes with the usual problems with FIFO scheduling where saturating
+ * threads can easily drown out interactive ones.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <scx/common.bpf.h>
+
+char _license[] SEC("license") = "GPL";
+
+const volatile bool fifo_sched;
+
+static u64 vtime_now;
+UEI_DEFINE(uei);
+
+/*
+ * Built-in DSQs such as SCX_DSQ_GLOBAL cannot be used as priority queues
+ * (meaning, cannot be dispatched to with scx_bpf_dsq_insert_vtime()). We
+ * therefore create a separate DSQ with ID 0 that we dispatch to and consume
+ * from. If scx_simple only supported global FIFO scheduling, then we could just
+ * use SCX_DSQ_GLOBAL.
+ */
+#define SHARED_DSQ 0
+
+struct {
+ __uint(type, BPF_MAP_TYPE_PERCPU_ARRAY);
+ __uint(key_size, sizeof(u32));
+ __uint(value_size, sizeof(u64));
+ __uint(max_entries, 2); /* [local, global] */
+} stats SEC(".maps");
+
+static void stat_inc(u32 idx)
+{
+ u64 *cnt_p = bpf_map_lookup_elem(&stats, &idx);
+ if (cnt_p)
+ (*cnt_p)++;
+}
+
+s32 BPF_STRUCT_OPS(simple_select_cpu, struct task_struct *p, s32 prev_cpu, u64 wake_flags)
+{
+ bool is_idle = false;
+ s32 cpu;
+
+ cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &is_idle);
+ if (is_idle) {
+ stat_inc(0); /* count local queueing */
+ scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
+ }
+
+ return cpu;
+}
+
+void BPF_STRUCT_OPS(simple_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ stat_inc(1); /* count global queueing */
+
+ if (fifo_sched) {
+ scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, enq_flags);
+ } else {
+ u64 vtime = p->scx.dsq_vtime;
+
+ /*
+ * Limit the amount of budget that an idling task can accumulate
+ * to one slice.
+ */
+ if (time_before(vtime, vtime_now - SCX_SLICE_DFL))
+ vtime = vtime_now - SCX_SLICE_DFL;
+
+ scx_bpf_dsq_insert_vtime(p, SHARED_DSQ, SCX_SLICE_DFL, vtime,
+ enq_flags);
+ }
+}
+
+void BPF_STRUCT_OPS(simple_dispatch, s32 cpu, struct task_struct *prev)
+{
+ scx_bpf_dsq_move_to_local(SHARED_DSQ, 0);
+}
+
+void BPF_STRUCT_OPS(simple_running, struct task_struct *p)
+{
+ if (fifo_sched)
+ return;
+
+ /*
+ * Global vtime always progresses forward as tasks start executing. The
+ * test and update can be performed concurrently from multiple CPUs and
+ * thus racy. Any error should be contained and temporary. Let's just
+ * live with it.
+ */
+ if (time_before(vtime_now, p->scx.dsq_vtime))
+ vtime_now = p->scx.dsq_vtime;
+}
+
+void BPF_STRUCT_OPS(simple_stopping, struct task_struct *p, bool runnable)
+{
+ if (fifo_sched)
+ return;
+
+ /*
+ * Scale the execution time by the inverse of the weight and charge.
+ *
+ * Note that the default yield implementation yields by setting
+ * @p->scx.slice to zero and the following would treat the yielding task
+ * as if it has consumed all its slice. If this penalizes yielding tasks
+ * too much, determine the execution time by taking explicit timestamps
+ * instead of depending on @p->scx.slice.
+ */
+ u64 delta = scale_by_task_weight_inverse(p, SCX_SLICE_DFL - p->scx.slice);
+
+ scx_bpf_task_set_dsq_vtime(p, p->scx.dsq_vtime + delta);
+}
+
+void BPF_STRUCT_OPS(simple_enable, struct task_struct *p)
+{
+ scx_bpf_task_set_dsq_vtime(p, vtime_now);
+}
+
+s32 BPF_STRUCT_OPS_SLEEPABLE(simple_init)
+{
+ int ret;
+
+ ret = scx_bpf_create_dsq(SHARED_DSQ, -1);
+ if (ret) {
+ scx_bpf_error("failed to create DSQ %d (%d)", SHARED_DSQ, ret);
+ return ret;
+ }
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(simple_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+SCX_OPS_DEFINE(simple_ops,
+ .select_cpu = (void *)simple_select_cpu,
+ .enqueue = (void *)simple_enqueue,
+ .dispatch = (void *)simple_dispatch,
+ .running = (void *)simple_running,
+ .stopping = (void *)simple_stopping,
+ .enable = (void *)simple_enable,
+ .init = (void *)simple_init,
+ .exit = (void *)simple_exit,
+ .name = "simple");
diff --git a/tools/sched_ext/scx_simple.c b/tools/sched_ext/scx_simple.c
new file mode 100644
index 000000000..b7589a83f
--- /dev/null
+++ b/tools/sched_ext/scx_simple.c
@@ -0,0 +1,110 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <stdio.h>
+#include <unistd.h>
+#include <signal.h>
+#include <assert.h>
+#include <libgen.h>
+#include <bpf/bpf.h>
+#include <scx/common.h>
+#include "scx_simple.bpf.skel.h"
+
+const char help_fmt[] =
+"A simple sched_ext scheduler.\n"
+"\n"
+"See the top-level comment in .bpf.c for more details.\n"
+"\n"
+"Usage: %s [-f] [-v]\n"
+"\n"
+" -f Use FIFO scheduling instead of weighted vtime scheduling\n"
+" -v Print libbpf debug messages\n"
+" -h Display this help and exit\n";
+
+static bool verbose;
+static volatile int exit_req;
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ if (level == LIBBPF_DEBUG && !verbose)
+ return 0;
+ return vfprintf(stderr, format, args);
+}
+
+static void sigint_handler(int simple)
+{
+ exit_req = 1;
+}
+
+static void read_stats(struct scx_simple *skel, __u64 *stats)
+{
+ int nr_cpus = libbpf_num_possible_cpus();
+ assert(nr_cpus > 0);
+ __u64 cnts[2][nr_cpus];
+ __u32 idx;
+
+ memset(stats, 0, sizeof(stats[0]) * 2);
+
+ for (idx = 0; idx < 2; idx++) {
+ int ret, cpu;
+
+ ret = bpf_map_lookup_elem(bpf_map__fd(skel->maps.stats),
+ &idx, cnts[idx]);
+ if (ret < 0)
+ continue;
+ for (cpu = 0; cpu < nr_cpus; cpu++)
+ stats[idx] += cnts[idx][cpu];
+ }
+}
+
+int main(int argc, char **argv)
+{
+ struct scx_simple *skel;
+ struct bpf_link *link;
+ __s32 opt;
+ __u64 ecode;
+
+ libbpf_set_print(libbpf_print_fn);
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+restart:
+ optind = 1;
+ skel = SCX_OPS_OPEN(simple_ops, scx_simple);
+
+ while ((opt = getopt(argc, argv, "fvh")) != -1) {
+ switch (opt) {
+ case 'f':
+ skel->rodata->fifo_sched = true;
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ fprintf(stderr, help_fmt, basename(argv[0]));
+ return opt != 'h';
+ }
+ }
+
+ SCX_OPS_LOAD(skel, simple_ops, scx_simple, uei);
+ link = SCX_OPS_ATTACH(skel, simple_ops, scx_simple);
+
+ while (!exit_req && !UEI_EXITED(skel, uei)) {
+ __u64 stats[2];
+
+ read_stats(skel, stats);
+ printf("local=%llu global=%llu\n", stats[0], stats[1]);
+ fflush(stdout);
+ sleep(1);
+ }
+
+ bpf_link__destroy(link);
+ ecode = UEI_REPORT(skel, uei);
+ scx_simple__destroy(skel);
+
+ if (!exit_req && UEI_ECODE_RESTART(ecode))
+ goto restart;
+ return 0;
+}
diff --git a/tools/sched_ext/scx_userland.bpf.c b/tools/sched_ext/scx_userland.bpf.c
new file mode 100644
index 000000000..f29862b89
--- /dev/null
+++ b/tools/sched_ext/scx_userland.bpf.c
@@ -0,0 +1,344 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * A minimal userland scheduler.
+ *
+ * In terms of scheduling, this provides two different types of behaviors:
+ * 1. A global FIFO scheduling order for _any_ tasks that have CPU affinity.
+ * All such tasks are direct-dispatched from the kernel, and are never
+ * enqueued in user space.
+ * 2. A primitive vruntime scheduler that is implemented in user space, for all
+ * other tasks.
+ *
+ * Some parts of this example user space scheduler could be implemented more
+ * efficiently using more complex and sophisticated data structures. For
+ * example, rather than using BPF_MAP_TYPE_QUEUE's,
+ * BPF_MAP_TYPE_{USER_}RINGBUF's could be used for exchanging messages between
+ * user space and kernel space. Similarly, we use a simple vruntime-sorted list
+ * in user space, but an rbtree could be used instead.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <scx/common.bpf.h>
+#include "scx_userland.h"
+
+/*
+ * Maximum amount of tasks enqueued/dispatched between kernel and user-space.
+ */
+#define MAX_ENQUEUED_TASKS 4096
+
+char _license[] SEC("license") = "GPL";
+
+const volatile s32 usersched_pid;
+
+/* !0 for veristat, set during init */
+const volatile u32 num_possible_cpus = 64;
+
+/* Stats that are printed by user space. */
+u64 nr_failed_enqueues, nr_kernel_enqueues, nr_user_enqueues;
+
+/*
+ * Number of tasks that are queued for scheduling.
+ *
+ * This number is incremented by the BPF component when a task is queued to the
+ * user-space scheduler and it must be decremented by the user-space scheduler
+ * when a task is consumed.
+ */
+volatile u64 nr_queued;
+
+/*
+ * Number of tasks that are waiting for scheduling.
+ *
+ * This number must be updated by the user-space scheduler to keep track if
+ * there is still some scheduling work to do.
+ */
+volatile u64 nr_scheduled;
+
+UEI_DEFINE(uei);
+
+/*
+ * The map containing tasks that are enqueued in user space from the kernel.
+ *
+ * This map is drained by the user space scheduler.
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_QUEUE);
+ __uint(max_entries, MAX_ENQUEUED_TASKS);
+ __type(value, struct scx_userland_enqueued_task);
+} enqueued SEC(".maps");
+
+/*
+ * The map containing tasks that are dispatched to the kernel from user space.
+ *
+ * Drained by the kernel in userland_dispatch().
+ */
+struct {
+ __uint(type, BPF_MAP_TYPE_QUEUE);
+ __uint(max_entries, MAX_ENQUEUED_TASKS);
+ __type(value, s32);
+} dispatched SEC(".maps");
+
+/* Per-task scheduling context */
+struct task_ctx {
+ bool force_local; /* Dispatch directly to local DSQ */
+};
+
+/* Map that contains task-local storage. */
+struct {
+ __uint(type, BPF_MAP_TYPE_TASK_STORAGE);
+ __uint(map_flags, BPF_F_NO_PREALLOC);
+ __type(key, int);
+ __type(value, struct task_ctx);
+} task_ctx_stor SEC(".maps");
+
+/*
+ * Flag used to wake-up the user-space scheduler.
+ */
+static volatile u32 usersched_needed;
+
+/*
+ * Set user-space scheduler wake-up flag (equivalent to an atomic release
+ * operation).
+ */
+static void set_usersched_needed(void)
+{
+ __sync_fetch_and_or(&usersched_needed, 1);
+}
+
+/*
+ * Check and clear user-space scheduler wake-up flag (equivalent to an atomic
+ * acquire operation).
+ */
+static bool test_and_clear_usersched_needed(void)
+{
+ return __sync_fetch_and_and(&usersched_needed, 0) == 1;
+}
+
+static bool is_usersched_task(const struct task_struct *p)
+{
+ return p->pid == usersched_pid;
+}
+
+static bool keep_in_kernel(const struct task_struct *p)
+{
+ return p->nr_cpus_allowed < num_possible_cpus;
+}
+
+static struct task_struct *usersched_task(void)
+{
+ struct task_struct *p;
+
+ p = bpf_task_from_pid(usersched_pid);
+ /*
+ * Should never happen -- the usersched task should always be managed
+ * by sched_ext.
+ */
+ if (!p)
+ scx_bpf_error("Failed to find usersched task %d", usersched_pid);
+
+ return p;
+}
+
+s32 BPF_STRUCT_OPS(userland_select_cpu, struct task_struct *p,
+ s32 prev_cpu, u64 wake_flags)
+{
+ if (keep_in_kernel(p)) {
+ s32 cpu;
+ struct task_ctx *tctx;
+
+ tctx = bpf_task_storage_get(&task_ctx_stor, p, 0, 0);
+ if (!tctx) {
+ scx_bpf_error("Failed to look up task-local storage for %s", p->comm);
+ return -ESRCH;
+ }
+
+ if (p->nr_cpus_allowed == 1 ||
+ scx_bpf_test_and_clear_cpu_idle(prev_cpu)) {
+ tctx->force_local = true;
+ return prev_cpu;
+ }
+
+ cpu = scx_bpf_pick_idle_cpu(p->cpus_ptr, 0);
+ if (cpu >= 0) {
+ tctx->force_local = true;
+ return cpu;
+ }
+ }
+
+ return prev_cpu;
+}
+
+static void dispatch_user_scheduler(void)
+{
+ struct task_struct *p;
+
+ p = usersched_task();
+ if (p) {
+ scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0);
+ bpf_task_release(p);
+ }
+}
+
+static void enqueue_task_in_user_space(struct task_struct *p, u64 enq_flags)
+{
+ struct scx_userland_enqueued_task task = {};
+
+ task.pid = p->pid;
+ task.sum_exec_runtime = p->se.sum_exec_runtime;
+ task.weight = p->scx.weight;
+
+ if (bpf_map_push_elem(&enqueued, &task, 0)) {
+ /*
+ * If we fail to enqueue the task in user space, put it
+ * directly on the global DSQ.
+ */
+ __sync_fetch_and_add(&nr_failed_enqueues, 1);
+ scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
+ } else {
+ __sync_fetch_and_add(&nr_user_enqueues, 1);
+ set_usersched_needed();
+ }
+}
+
+void BPF_STRUCT_OPS(userland_enqueue, struct task_struct *p, u64 enq_flags)
+{
+ if (keep_in_kernel(p)) {
+ u64 dsq_id = SCX_DSQ_GLOBAL;
+ struct task_ctx *tctx;
+
+ tctx = bpf_task_storage_get(&task_ctx_stor, p, 0, 0);
+ if (!tctx) {
+ scx_bpf_error("Failed to lookup task ctx for %s", p->comm);
+ return;
+ }
+
+ if (tctx->force_local)
+ dsq_id = SCX_DSQ_LOCAL;
+ tctx->force_local = false;
+ scx_bpf_dsq_insert(p, dsq_id, SCX_SLICE_DFL, enq_flags);
+ __sync_fetch_and_add(&nr_kernel_enqueues, 1);
+ return;
+ } else if (!is_usersched_task(p)) {
+ enqueue_task_in_user_space(p, enq_flags);
+ }
+}
+
+void BPF_STRUCT_OPS(userland_dispatch, s32 cpu, struct task_struct *prev)
+{
+ if (test_and_clear_usersched_needed())
+ dispatch_user_scheduler();
+
+ bpf_repeat(MAX_ENQUEUED_TASKS) {
+ s32 pid;
+ struct task_struct *p;
+
+ if (bpf_map_pop_elem(&dispatched, &pid))
+ break;
+
+ /*
+ * The task could have exited by the time we get around to
+ * dispatching it. Treat this as a normal occurrence, and simply
+ * move onto the next iteration.
+ */
+ p = bpf_task_from_pid(pid);
+ if (!p)
+ continue;
+
+ scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0);
+ bpf_task_release(p);
+ }
+}
+
+/*
+ * A CPU is about to change its idle state. If the CPU is going idle, ensure
+ * that the user-space scheduler has a chance to run if there is any remaining
+ * work to do.
+ */
+void BPF_STRUCT_OPS(userland_update_idle, s32 cpu, bool idle)
+{
+ /*
+ * Don't do anything if we exit from and idle state, a CPU owner will
+ * be assigned in .running().
+ */
+ if (!idle)
+ return;
+ /*
+ * A CPU is now available, notify the user-space scheduler that tasks
+ * can be dispatched, if there is at least one task waiting to be
+ * scheduled, either queued (accounted in nr_queued) or scheduled
+ * (accounted in nr_scheduled).
+ *
+ * NOTE: nr_queued is incremented by the BPF component, more exactly in
+ * enqueue(), when a task is sent to the user-space scheduler, then
+ * the scheduler drains the queued tasks (updating nr_queued) and adds
+ * them to its internal data structures / state; at this point tasks
+ * become "scheduled" and the user-space scheduler will take care of
+ * updating nr_scheduled accordingly; lastly tasks will be dispatched
+ * and the user-space scheduler will update nr_scheduled again.
+ *
+ * Checking both counters allows to determine if there is still some
+ * pending work to do for the scheduler: new tasks have been queued
+ * since last check, or there are still tasks "queued" or "scheduled"
+ * since the previous user-space scheduler run. If the counters are
+ * both zero it is pointless to wake-up the scheduler (even if a CPU
+ * becomes idle), because there is nothing to do.
+ *
+ * Keep in mind that update_idle() doesn't run concurrently with the
+ * user-space scheduler (that is single-threaded): this function is
+ * naturally serialized with the user-space scheduler code, therefore
+ * this check here is also safe from a concurrency perspective.
+ */
+ if (nr_queued || nr_scheduled) {
+ /*
+ * Kick the CPU to make it immediately ready to accept
+ * dispatched tasks.
+ */
+ set_usersched_needed();
+ scx_bpf_kick_cpu(cpu, 0);
+ }
+}
+
+s32 BPF_STRUCT_OPS(userland_init_task, struct task_struct *p,
+ struct scx_init_task_args *args)
+{
+ if (bpf_task_storage_get(&task_ctx_stor, p, 0,
+ BPF_LOCAL_STORAGE_GET_F_CREATE))
+ return 0;
+ else
+ return -ENOMEM;
+}
+
+s32 BPF_STRUCT_OPS(userland_init)
+{
+ if (num_possible_cpus == 0) {
+ scx_bpf_error("User scheduler # CPUs uninitialized (%d)",
+ num_possible_cpus);
+ return -EINVAL;
+ }
+
+ if (usersched_pid <= 0) {
+ scx_bpf_error("User scheduler pid uninitialized (%d)",
+ usersched_pid);
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+void BPF_STRUCT_OPS(userland_exit, struct scx_exit_info *ei)
+{
+ UEI_RECORD(uei, ei);
+}
+
+SCX_OPS_DEFINE(userland_ops,
+ .select_cpu = (void *)userland_select_cpu,
+ .enqueue = (void *)userland_enqueue,
+ .dispatch = (void *)userland_dispatch,
+ .update_idle = (void *)userland_update_idle,
+ .init_task = (void *)userland_init_task,
+ .init = (void *)userland_init,
+ .exit = (void *)userland_exit,
+ .flags = SCX_OPS_ENQ_LAST |
+ SCX_OPS_KEEP_BUILTIN_IDLE,
+ .name = "userland");
diff --git a/tools/sched_ext/scx_userland.c b/tools/sched_ext/scx_userland.c
new file mode 100644
index 000000000..b16b6db5f
--- /dev/null
+++ b/tools/sched_ext/scx_userland.c
@@ -0,0 +1,447 @@
+/* SPDX-License-Identifier: GPL-2.0 */
+/*
+ * A demo sched_ext user space scheduler which provides vruntime semantics
+ * using a simple ordered-list implementation.
+ *
+ * Each CPU in the system resides in a single, global domain. This precludes
+ * the need to do any load balancing between domains. The scheduler could
+ * easily be extended to support multiple domains, with load balancing
+ * happening in user space.
+ *
+ * Any task which has any CPU affinity is scheduled entirely in BPF. This
+ * program only schedules tasks which may run on any CPU.
+ *
+ * Copyright (c) 2022 Meta Platforms, Inc. and affiliates.
+ * Copyright (c) 2022 Tejun Heo <tj@kernel.org>
+ * Copyright (c) 2022 David Vernet <dvernet@meta.com>
+ */
+#include <stdio.h>
+#include <unistd.h>
+#include <sched.h>
+#include <signal.h>
+#include <assert.h>
+#include <libgen.h>
+#include <pthread.h>
+#include <bpf/bpf.h>
+#include <sys/mman.h>
+#include <sys/queue.h>
+#include <sys/syscall.h>
+
+#include <scx/common.h>
+#include "scx_userland.h"
+#include "scx_userland.bpf.skel.h"
+
+const char help_fmt[] =
+"A minimal userland sched_ext scheduler.\n"
+"\n"
+"See the top-level comment in .bpf.c for more details.\n"
+"\n"
+"Try to reduce `sysctl kernel.pid_max` if this program triggers OOMs.\n"
+"\n"
+"Usage: %s [-b BATCH] [-v]\n"
+"\n"
+" -b BATCH The number of tasks to batch when dispatching (default: 8)\n"
+" -v Print libbpf debug messages\n"
+" -h Display this help and exit\n";
+
+/* Defined in UAPI */
+#define SCHED_EXT 7
+
+/* Number of tasks to batch when dispatching to user space. */
+static __u32 batch_size = 8;
+
+static bool verbose;
+static volatile int exit_req;
+static volatile int stats_stop;
+static int enqueued_fd, dispatched_fd;
+
+static pthread_t stats_printer;
+static struct scx_userland *skel;
+static struct bpf_link *ops_link;
+
+/* Stats collected in user space. */
+static __u64 nr_vruntime_enqueues, nr_vruntime_dispatches, nr_vruntime_failed;
+
+/* Number of tasks currently enqueued. */
+static __u64 nr_curr_enqueued;
+
+/* The data structure containing tasks that are enqueued in user space. */
+struct enqueued_task {
+ LIST_ENTRY(enqueued_task) entries;
+ __u64 sum_exec_runtime;
+ double vruntime;
+};
+
+/*
+ * Use a vruntime-sorted list to store tasks. This could easily be extended to
+ * a more optimal data structure, such as an rbtree as is done in CFS. We
+ * currently elect to use a sorted list to simplify the example for
+ * illustrative purposes.
+ */
+LIST_HEAD(listhead, enqueued_task);
+
+/*
+ * A vruntime-sorted list of tasks. The head of the list contains the task with
+ * the lowest vruntime. That is, the task that has the "highest" claim to be
+ * scheduled.
+ */
+static struct listhead vruntime_head = LIST_HEAD_INITIALIZER(vruntime_head);
+
+/*
+ * The main array of tasks. The array is allocated all at once during
+ * initialization, based on /proc/sys/kernel/pid_max, to avoid having to
+ * dynamically allocate memory on the enqueue path, which could cause a
+ * deadlock. A more substantive user space scheduler could e.g. provide a hook
+ * for newly enabled tasks that are passed to the scheduler from the
+ * .prep_enable() callback to allows the scheduler to allocate on safe paths.
+ */
+struct enqueued_task *tasks;
+static int pid_max;
+
+static double min_vruntime;
+
+static int libbpf_print_fn(enum libbpf_print_level level, const char *format, va_list args)
+{
+ if (level == LIBBPF_DEBUG && !verbose)
+ return 0;
+ return vfprintf(stderr, format, args);
+}
+
+static void sigint_handler(int userland)
+{
+ exit_req = 1;
+}
+
+static int get_pid_max(void)
+{
+ FILE *fp;
+ int pid_max;
+
+ fp = fopen("/proc/sys/kernel/pid_max", "r");
+ if (fp == NULL) {
+ fprintf(stderr, "Error opening /proc/sys/kernel/pid_max\n");
+ return -1;
+ }
+ if (fscanf(fp, "%d", &pid_max) != 1) {
+ fprintf(stderr, "Error reading from /proc/sys/kernel/pid_max\n");
+ fclose(fp);
+ return -1;
+ }
+ fclose(fp);
+
+ return pid_max;
+}
+
+static int init_tasks(void)
+{
+ pid_max = get_pid_max();
+ if (pid_max < 0)
+ return pid_max;
+
+ tasks = calloc(pid_max, sizeof(*tasks));
+ if (!tasks) {
+ fprintf(stderr, "Error allocating tasks array\n");
+ return -ENOMEM;
+ }
+
+ return 0;
+}
+
+static __u32 task_pid(const struct enqueued_task *task)
+{
+ return ((uintptr_t)task - (uintptr_t)tasks) / sizeof(*task);
+}
+
+static int dispatch_task(__s32 pid)
+{
+ int err;
+
+ err = bpf_map_update_elem(dispatched_fd, NULL, &pid, 0);
+ if (err) {
+ __atomic_add_fetch(&nr_vruntime_failed, 1, __ATOMIC_RELAXED);
+ } else {
+ __atomic_add_fetch(&nr_vruntime_dispatches, 1, __ATOMIC_RELAXED);
+ }
+
+ return err;
+}
+
+static struct enqueued_task *get_enqueued_task(__s32 pid)
+{
+ if (pid >= pid_max)
+ return NULL;
+
+ return &tasks[pid];
+}
+
+static double calc_vruntime_delta(__u64 weight, __u64 delta)
+{
+ double weight_f = (double)weight / 100.0;
+ double delta_f = (double)delta;
+
+ return delta_f / weight_f;
+}
+
+static void update_enqueued(struct enqueued_task *enqueued, const struct scx_userland_enqueued_task *bpf_task)
+{
+ __u64 delta;
+
+ delta = bpf_task->sum_exec_runtime - enqueued->sum_exec_runtime;
+
+ enqueued->vruntime += calc_vruntime_delta(bpf_task->weight, delta);
+ if (min_vruntime > enqueued->vruntime)
+ enqueued->vruntime = min_vruntime;
+ enqueued->sum_exec_runtime = bpf_task->sum_exec_runtime;
+}
+
+static int vruntime_enqueue(const struct scx_userland_enqueued_task *bpf_task)
+{
+ struct enqueued_task *curr, *enqueued, *prev;
+
+ curr = get_enqueued_task(bpf_task->pid);
+ if (!curr)
+ return ENOENT;
+
+ update_enqueued(curr, bpf_task);
+ __atomic_add_fetch(&nr_vruntime_enqueues, 1, __ATOMIC_RELAXED);
+ __atomic_add_fetch(&nr_curr_enqueued, 1, __ATOMIC_RELAXED);
+
+ /*
+ * Enqueue the task in a vruntime-sorted list. A more optimal data
+ * structure such as an rbtree could easily be used as well. We elect
+ * to use a list here simply because it's less code, and thus the
+ * example is less convoluted and better serves to illustrate what a
+ * user space scheduler could look like.
+ */
+
+ if (LIST_EMPTY(&vruntime_head)) {
+ LIST_INSERT_HEAD(&vruntime_head, curr, entries);
+ return 0;
+ }
+
+ LIST_FOREACH(enqueued, &vruntime_head, entries) {
+ if (curr->vruntime <= enqueued->vruntime) {
+ LIST_INSERT_BEFORE(enqueued, curr, entries);
+ return 0;
+ }
+ prev = enqueued;
+ }
+
+ LIST_INSERT_AFTER(prev, curr, entries);
+
+ return 0;
+}
+
+static void drain_enqueued_map(void)
+{
+ while (1) {
+ struct scx_userland_enqueued_task task;
+ int err;
+
+ if (bpf_map_lookup_and_delete_elem(enqueued_fd, NULL, &task)) {
+ skel->bss->nr_queued = 0;
+ skel->bss->nr_scheduled = nr_curr_enqueued;
+ return;
+ }
+
+ err = vruntime_enqueue(&task);
+ if (err) {
+ fprintf(stderr, "Failed to enqueue task %d: %s\n",
+ task.pid, strerror(err));
+ exit_req = 1;
+ return;
+ }
+ }
+}
+
+static void dispatch_batch(void)
+{
+ __u32 i;
+
+ for (i = 0; i < batch_size; i++) {
+ struct enqueued_task *task;
+ int err;
+ __s32 pid;
+
+ task = LIST_FIRST(&vruntime_head);
+ if (!task)
+ break;
+
+ min_vruntime = task->vruntime;
+ pid = task_pid(task);
+ LIST_REMOVE(task, entries);
+ err = dispatch_task(pid);
+ if (err) {
+ /*
+ * If we fail to dispatch, put the task back to the
+ * vruntime_head list and stop dispatching additional
+ * tasks in this batch.
+ */
+ LIST_INSERT_HEAD(&vruntime_head, task, entries);
+ break;
+ }
+ __atomic_sub_fetch(&nr_curr_enqueued, 1, __ATOMIC_RELAXED);
+ }
+ skel->bss->nr_scheduled = __atomic_load_n(&nr_curr_enqueued, __ATOMIC_RELAXED);
+}
+
+static void *run_stats_printer(void *arg)
+{
+ while (!stats_stop) {
+ __u64 nr_failed_enqueues, nr_kernel_enqueues, nr_user_enqueues, total;
+
+ nr_failed_enqueues = skel->bss->nr_failed_enqueues;
+ nr_kernel_enqueues = skel->bss->nr_kernel_enqueues;
+ nr_user_enqueues = skel->bss->nr_user_enqueues;
+ total = nr_failed_enqueues + nr_kernel_enqueues + nr_user_enqueues;
+
+ printf("o-----------------------o\n");
+ printf("| BPF ENQUEUES |\n");
+ printf("|-----------------------|\n");
+ printf("| kern: %10llu |\n", nr_kernel_enqueues);
+ printf("| user: %10llu |\n", nr_user_enqueues);
+ printf("| failed: %10llu |\n", nr_failed_enqueues);
+ printf("| -------------------- |\n");
+ printf("| total: %10llu |\n", total);
+ printf("| |\n");
+ printf("|-----------------------|\n");
+ printf("| VRUNTIME / USER |\n");
+ printf("|-----------------------|\n");
+ printf("| enq: %10llu |\n", __atomic_load_n(&nr_vruntime_enqueues, __ATOMIC_RELAXED));
+ printf("| disp: %10llu |\n", __atomic_load_n(&nr_vruntime_dispatches, __ATOMIC_RELAXED));
+ printf("| failed: %10llu |\n", __atomic_load_n(&nr_vruntime_failed, __ATOMIC_RELAXED));
+ printf("o-----------------------o\n");
+ printf("\n\n");
+ fflush(stdout);
+ sleep(1);
+ }
+
+ return NULL;
+}
+
+static int spawn_stats_thread(void)
+{
+ return pthread_create(&stats_printer, NULL, run_stats_printer, NULL);
+}
+
+static void pre_bootstrap(int argc, char **argv)
+{
+ int err;
+ __s32 opt;
+ struct sched_param sched_param = {
+ .sched_priority = sched_get_priority_max(SCHED_EXT),
+ };
+
+ err = init_tasks();
+ if (err)
+ exit(err);
+
+ libbpf_set_print(libbpf_print_fn);
+ signal(SIGINT, sigint_handler);
+ signal(SIGTERM, sigint_handler);
+
+ /*
+ * Enforce that the user scheduler task is managed by sched_ext. The
+ * task eagerly drains the list of enqueued tasks in its main work
+ * loop, and then yields the CPU. The BPF scheduler only schedules the
+ * user space scheduler task when at least one other task in the system
+ * needs to be scheduled.
+ */
+ err = syscall(__NR_sched_setscheduler, getpid(), SCHED_EXT, &sched_param);
+ SCX_BUG_ON(err, "Failed to set scheduler to SCHED_EXT");
+
+ while ((opt = getopt(argc, argv, "b:vh")) != -1) {
+ switch (opt) {
+ case 'b':
+ batch_size = strtoul(optarg, NULL, 0);
+ break;
+ case 'v':
+ verbose = true;
+ break;
+ default:
+ fprintf(stderr, help_fmt, basename(argv[0]));
+ exit(opt != 'h');
+ }
+ }
+
+ /*
+ * It's not always safe to allocate in a user space scheduler, as an
+ * enqueued task could hold a lock that we require in order to be able
+ * to allocate.
+ */
+ err = mlockall(MCL_CURRENT | MCL_FUTURE);
+ SCX_BUG_ON(err, "Failed to prefault and lock address space");
+}
+
+static void bootstrap(char *comm)
+{
+ stats_stop = 0;
+ min_vruntime = 0.0;
+ __atomic_store_n(&nr_vruntime_enqueues, 0, __ATOMIC_RELAXED);
+ __atomic_store_n(&nr_vruntime_dispatches, 0, __ATOMIC_RELAXED);
+ __atomic_store_n(&nr_vruntime_failed, 0, __ATOMIC_RELAXED);
+ __atomic_store_n(&nr_curr_enqueued, 0, __ATOMIC_RELAXED);
+ memset(tasks, 0, pid_max * sizeof(*tasks));
+ LIST_INIT(&vruntime_head);
+
+ skel = SCX_OPS_OPEN(userland_ops, scx_userland);
+
+ skel->rodata->num_possible_cpus = libbpf_num_possible_cpus();
+ assert(skel->rodata->num_possible_cpus > 0);
+ skel->rodata->usersched_pid = getpid();
+ assert(skel->rodata->usersched_pid > 0);
+
+ SCX_OPS_LOAD(skel, userland_ops, scx_userland, uei);
+
+ enqueued_fd = bpf_map__fd(skel->maps.enqueued);
+ dispatched_fd = bpf_map__fd(skel->maps.dispatched);
+ assert(enqueued_fd > 0);
+ assert(dispatched_fd > 0);
+
+ SCX_BUG_ON(spawn_stats_thread(), "Failed to spawn stats thread");
+
+ ops_link = SCX_OPS_ATTACH(skel, userland_ops, scx_userland);
+}
+
+static void sched_main_loop(void)
+{
+ while (!exit_req && !UEI_EXITED(skel, uei)) {
+ /*
+ * Perform the following work in the main user space scheduler
+ * loop:
+ *
+ * 1. Drain all tasks from the enqueued map, and enqueue them
+ * to the vruntime sorted list.
+ *
+ * 2. Dispatch a batch of tasks from the vruntime sorted list
+ * down to the kernel.
+ *
+ * 3. Yield the CPU back to the system. The BPF scheduler will
+ * reschedule the user space scheduler once another task has
+ * been enqueued to user space.
+ */
+ drain_enqueued_map();
+ dispatch_batch();
+ sched_yield();
+ }
+}
+
+int main(int argc, char **argv)
+{
+ __u64 ecode;
+
+ pre_bootstrap(argc, argv);
+restart:
+ bootstrap(argv[0]);
+ sched_main_loop();
+
+ stats_stop = 1;
+ bpf_link__destroy(ops_link);
+ pthread_join(stats_printer, NULL);
+ ecode = UEI_REPORT(skel, uei);
+ scx_userland__destroy(skel);
+
+ if (!exit_req && UEI_ECODE_RESTART(ecode))
+ goto restart;
+ return 0;
+}
diff --git a/tools/sched_ext/scx_userland.h b/tools/sched_ext/scx_userland.h
new file mode 100644
index 000000000..684fb2dd5
--- /dev/null
+++ b/tools/sched_ext/scx_userland.h
@@ -0,0 +1,17 @@
+// SPDX-License-Identifier: GPL-2.0
+/* Copyright (c) 2022 Meta, Inc */
+
+#ifndef __SCX_USERLAND_COMMON_H
+#define __SCX_USERLAND_COMMON_H
+
+/*
+ * An instance of a task that has been enqueued by the kernel for consumption
+ * by a user space global scheduler thread.
+ */
+struct scx_userland_enqueued_task {
+ __s32 pid;
+ u64 sum_exec_runtime;
+ u64 weight;
+};
+
+#endif // __SCX_USERLAND_COMMON_H