Resguardo inicial

This commit is contained in:
lastdragon
2026-09-06 12:53:21 -06:00
parent 662efe3c2a
commit ad2088d178
94853 changed files with 43200404 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
tools/
build/
+61
View File
@@ -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
+261
View File
@@ -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:
+263
View File
@@ -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.
@@ -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.
*/
@@ -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;
@@ -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)
{
}
@@ -0,0 +1,700 @@
/* 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;
}
/**
* 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 */
File diff suppressed because it is too large Load Diff
@@ -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 */
@@ -0,0 +1,461 @@
/* 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(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) ? \
scx_bpf_dsq_move_to_local___v2((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 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.
*
* Preserve the following macro until v6.21.
*/
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))
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.2: scx_bpf_cid_override() for explicit cpu->cid mapping. Ignore if
* missing.
*/
void scx_bpf_cid_override___compat(const s32 *cpu_to_cid, u32 cpu_to_cid__sz) __ksym __weak;
static inline void scx_bpf_cid_override(const s32 *cpu_to_cid, u32 cpu_to_cid__sz)
{
if (bpf_ksym_exists(scx_bpf_cid_override___compat))
return scx_bpf_cid_override___compat(cpu_to_cid, cpu_to_cid__sz);
}
/**
* __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.
*
* Keep this helper available until v6.20 for compatibility.
*/
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;
}
/*
* 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_scx_bpf_reenqueue_local_from_anywhere())
scx_bpf_reenqueue_local___v2___compat();
else
scx_bpf_reenqueue_local___v1();
}
/*
* v6.20: 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, const struct bpf_prog_aux *aux__prog) __ksym __weak;
static inline bool __COMPAT_has_generic_reenq(void)
{
return bpf_ksym_exists(scx_bpf_dsq_reenq___compat);
}
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, NULL);
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 */
@@ -0,0 +1,281 @@
/* 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 <stdlib.h>
#include <unistd.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()");
}
}
static inline bool __COMPAT_read_enum(const char *type, const char *name, u64 *v)
{
const struct btf_type *t;
const char *n;
s32 tid;
int 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_INFO_VLEN(t->info); 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 = e[i].val;
return true;
}
}
} else if (btf_is_enum64(t)) {
struct btf_enum64 *e = btf_enum64(t);
for (i = 0; i < BTF_INFO_VLEN(t->info); 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;
int 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_INFO_VLEN(t->info); 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 (this macro). 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
*/
#define SCX_OPS_OPEN(__ops_name, __scx_name) ({ \
struct __scx_name *__skel; \
\
SCX_BUG_ON(!__COMPAT_struct_has_field("sched_ext_ops", "dump"), \
"sched_ext_ops.dump() missing, kernel too old?"); \
\
__skel = __scx_name##__open(); \
SCX_BUG_ON(!__skel, "Could not open " #__scx_name); \
__skel->struct_ops.__ops_name->hotplug_seq = scx_hotplug_seq(); \
SCX_ENUM_INIT(__skel); \
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; \
} \
__skel; \
})
/*
* 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 */
@@ -0,0 +1,158 @@
/*
* 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_DSP_DFL_MAX_BATCH
#define HAVE_SCX_DSP_MAX_LOOPS
#define HAVE_SCX_WATCHDOG_MAX_TIMEOUT
#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_LOCAL_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_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_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_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_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_STATE_SHIFT
#define HAVE_SCX_TASK_STATE_BITS
#define HAVE_SCX_TASK_STATE_MASK
#define HAVE_SCX_TASK_NONE
#define HAVE_SCX_TASK_INIT
#define HAVE_SCX_TASK_READY
#define HAVE_SCX_TASK_ENABLED
#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_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_ERROR
#define HAVE_SCX_EXIT_ERROR_BPF
#define HAVE_SCX_EXIT_ERROR_STALL
#define HAVE_SCX_KF_UNLOCKED
#define HAVE_SCX_KF_CPU_RELEASE
#define HAVE_SCX_KF_DISPATCH
#define HAVE_SCX_KF_ENQUEUE
#define HAVE_SCX_KF_SELECT_CPU
#define HAVE_SCX_KF_REST
#define HAVE___SCX_KF_RQ_LOCKED
#define HAVE___SCX_KF_TERMINAL
#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_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_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_BAL_KEEP
#define HAVE_SCX_RQ_CLK_VALID
#define HAVE_SCX_RQ_BAL_CB_PENDING
#define HAVE_SCX_RQ_IN_WAKEUP
#define HAVE_SCX_RQ_IN_BALANCE
#define HAVE_SCX_SCHED_PCPU_BYPASSING
#define HAVE_SCX_TG_ONLINE
#define HAVE_SCX_TG_INITED
#define HAVE_SCX_WAKE_FORK
#define HAVE_SCX_WAKE_TTWU
#define HAVE_SCX_WAKE_SYNC
#endif /* __ENUM_DEFS_AUTOGEN_H__ */
@@ -0,0 +1,140 @@
/*
* 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_BAL_KEEP __weak;
#define SCX_RQ_BAL_KEEP __SCX_RQ_BAL_KEEP
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_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_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
@@ -0,0 +1,53 @@
/*
* 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_BAL_KEEP); \
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_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_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)
@@ -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 */
@@ -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 */
@@ -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 */
@@ -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 */
@@ -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 */
@@ -0,0 +1,384 @@
/* 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;
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);
}
bpf_timer_start(timer, TIMER_INTERVAL_NS, BPF_F_TIMER_CPU_PIN);
__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");
+126
View File
@@ -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 (UEI_ECODE_RESTART(ecode))
goto restart;
return 0;
}
@@ -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");
+105
View File
@@ -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;
__u32 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 (UEI_ECODE_RESTART(ecode))
goto restart;
return 0;
}
@@ -0,0 +1,966 @@
/* 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_sub(&cgc->cvtime_delta, cgc->cvtime_delta);
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);
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);
}
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...
*/
bpf_spin_lock(&cgv_tree_lock);
__sync_fetch_and_add(&cgc->cvtime_delta,
(cpuc->cur_at + cgrp_slice_ns - now) *
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 = time_delta(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");
+239
View File
@@ -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;
cnts = calloc(skel->rodata->nr_cpus, sizeof(__u64));
if (!cnts)
return;
memset(stats, 0, sizeof(stats[0]) * FCG_NR_STATS);
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 (UEI_ECODE_RESTART(ecode))
goto restart;
return 0;
}
+51
View File
@@ -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 */
+610
View File
@@ -0,0 +1,610 @@
/* 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 implements pair_cpu_release() and
* pair_cpu_acquire() callbacks which are invoked by the core scheduler when
* the scheduler loses and gains control of the CPU respectively.
*
* In pair_cpu_release(), 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 is re-acquired in pair_cpu_acquire(), we unmark the preemption
* in the pair_ctx, and send another resched IPI to the pair CPU to re-enable
* pair scheduling.
*
* 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"
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;
}
__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;
}
}
void BPF_STRUCT_OPS(pair_cpu_acquire, s32 cpu, struct scx_cpu_acquire_args *args)
{
int ret;
u32 in_pair_mask;
struct pair_ctx *pairc;
bool kick_pair;
ret = lookup_pairc_and_mask(cpu, &pairc, &in_pair_mask);
if (ret)
return;
bpf_spin_lock(&pairc->lock);
pairc->preempted_mask &= ~in_pair_mask;
/* Kick the pair CPU, unless it was also preempted. */
kick_pair = !pairc->preempted_mask;
bpf_spin_unlock(&pairc->lock);
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);
}
}
}
void BPF_STRUCT_OPS(pair_cpu_release, s32 cpu, struct scx_cpu_release_args *args)
{
int ret;
u32 in_pair_mask;
struct pair_ctx *pairc;
bool kick_pair;
ret = lookup_pairc_and_mask(cpu, &pairc, &in_pair_mask);
if (ret)
return;
bpf_spin_lock(&pairc->lock);
pairc->preempted_mask |= in_pair_mask;
pairc->active_mask &= ~in_pair_mask;
/* Kick the pair CPU if it's still running. */
kick_pair = pairc->active_mask;
pairc->draining = true;
bpf_spin_unlock(&pairc->lock);
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 | SCX_KICK_WAIT);
}
}
__sync_fetch_and_add(&nr_preemptions, 1);
}
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,
.cpu_acquire = (void *)pair_cpu_acquire,
.cpu_release = (void *)pair_cpu_release,
.cgroup_init = (void *)pair_cgroup_init,
.cgroup_exit = (void *)pair_cgroup_exit,
.exit = (void *)pair_exit,
.name = "pair");
+196
View File
@@ -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 (UEI_ECODE_RESTART(ecode))
goto restart;
return 0;
}
+9
View File
@@ -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 */
File diff suppressed because it is too large Load Diff
+233
View File
@@ -0,0 +1,233 @@
/* 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 <stdlib.h>
#include <unistd.h>
#include <inttypes.h>
#include <signal.h>
#include <libgen.h>
#include <sys/mman.h>
#include <sys/stat.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"
"See the top-level comment in .bpf.c for more details.\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] [-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)\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_qmap *skel;
struct bpf_link *link;
struct qmap_arena *qa;
__u32 test_error_cnt = 0;
__u64 ecode;
int opt;
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_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: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;
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->rodata->always_enq_immed = true;
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, i;
if (!strcmp(optarg, "shuffle"))
mode = 1;
else if (!strcmp(optarg, "bad-dup"))
mode = 2;
else if (!strcmp(optarg, "bad-range"))
mode = 3;
else {
fprintf(stderr, "unknown cid-override mode '%s'\n", optarg);
return 1;
}
skel->rodata->cid_override_mode = mode;
/* shuffle: reversed cpu_to_cid, bad-dup: dup cid 0, bad-range: identity */
for (i = 0; i < nr_cpus; i++) {
if (mode == 1)
skel->bss->cid_override_cpu_to_cid[i] = nr_cpus - 1 - i;
else
skel->bss->cid_override_cpu_to_cid[i] = i;
}
if (mode == 2 && nr_cpus >= 2)
skel->bss->cid_override_cpu_to_cid[1] = 0;
if (mode == 3)
skel->bss->cid_override_cpu_to_cid[0] = (s32)nr_cpus;
break;
}
case 'v':
verbose = true;
break;
default:
fprintf(stderr, help_fmt, basename(argv[0]));
return opt != 'h';
}
}
SCX_OPS_LOAD(skel, qmap_ops, scx_qmap, uei);
link = SCX_OPS_ATTACH(skel, qmap_ops, scx_qmap);
qa = &skel->arena->qa;
qa->test_error_cnt = test_error_cnt;
while (!exit_req && !UEI_EXITED(skel, uei)) {
long nr_enqueued = qa->nr_enqueued;
long nr_dispatched = qa->nr_dispatched;
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,
qa->nr_reenqueued, qa->nr_reenqueued_cid0,
qa->nr_dequeued,
qa->nr_core_sched_execed,
qa->nr_ddsp_from_enq);
printf(" exp_local=%llu exp_remote=%llu exp_timer=%llu exp_lost=%llu\n",
qa->nr_expedited_local,
qa->nr_expedited_remote,
qa->nr_expedited_from_timer,
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);
fflush(stdout);
sleep(1);
}
bpf_link__destroy(link);
ecode = UEI_REPORT(skel, uei);
scx_qmap__destroy(skel);
if (UEI_ECODE_RESTART(ecode))
goto restart;
return 0;
}
+73
View File
@@ -0,0 +1,73 @@
/* 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
/*
* 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
struct cpu_ctx {
__u64 dsp_idx; /* dispatch index */
__u64 dsp_cnt; /* remaining count */
__u32 avg_weight;
__u32 cpuperf_target;
};
/* Opaque to userspace; defined in scx_qmap.bpf.c. */
struct task_ctx;
struct qmap_fifo {
struct task_ctx __arena *head;
struct task_ctx __arena *tail;
__s32 idx;
};
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 sub_sched_cgroup_ids[MAX_SUB_SCHEDS];
__u64 core_sched_head_seqs[5];
__u64 core_sched_tail_seqs[5];
struct cpu_ctx cpu_ctxs[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];
};
#endif /* __SCX_QMAP_H */
+717
View File
@@ -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");
+102
View File
@@ -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;
__u32 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 (UEI_ECODE_RESTART(ecode))
goto restart;
return 0;
}
+113
View File
@@ -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__ */
@@ -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")}')
@@ -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");
+110
View File
@@ -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;
__u32 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 (UEI_ECODE_RESTART(ecode))
goto restart;
return 0;
}
@@ -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");
+446
View File
@@ -0,0 +1,446 @@
/* 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 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 (!exit_req) {
__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;
__u32 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)
{
exit_req = 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) {
/*
* 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();
exit_req = 1;
bpf_link__destroy(ops_link);
pthread_join(stats_printer, NULL);
ecode = UEI_REPORT(skel, uei);
scx_userland__destroy(skel);
if (UEI_ECODE_RESTART(ecode))
goto restart;
return 0;
}
@@ -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