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
@@ -0,0 +1,6 @@
*
!*.c
!*.h
!Makefile
!.gitignore
!config
@@ -0,0 +1,222 @@
# 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
TEST_GEN_PROGS := runner
# override lib.mk's default rules
OVERRIDE_TARGETS := 1
include ../lib.mk
CURDIR := $(abspath .)
REPOROOT := $(abspath ../../../..)
TOOLSDIR := $(REPOROOT)/tools
LIBDIR := $(TOOLSDIR)/lib
BPFDIR := $(LIBDIR)/bpf
TOOLSINCDIR := $(TOOLSDIR)/include
BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool
APIDIR := $(TOOLSINCDIR)/uapi
GENDIR := $(REPOROOT)/include/generated
GENHDR := $(GENDIR)/autoconf.h
SCXTOOLSDIR := $(TOOLSDIR)/sched_ext
SCXTOOLSINCDIR := $(TOOLSDIR)/sched_ext/include
OUTPUT_DIR := $(OUTPUT)/build
OBJ_DIR := $(OUTPUT_DIR)/obj
INCLUDE_DIR := $(OUTPUT_DIR)/include
BPFOBJ_DIR := $(OBJ_DIR)/libbpf
SCXOBJ_DIR := $(OBJ_DIR)/sched_ext
BPFOBJ := $(BPFOBJ_DIR)/libbpf.a
LIBBPF_OUTPUT := $(OBJ_DIR)/libbpf/libbpf.a
DEFAULT_BPFTOOL := $(OUTPUT_DIR)/host/sbin/bpftool
HOST_OBJ_DIR := $(OBJ_DIR)/host/bpftool
HOST_LIBBPF_OUTPUT := $(OBJ_DIR)/host/libbpf/
HOST_LIBBPF_DESTDIR := $(OUTPUT_DIR)/host/
HOST_DESTDIR := $(OUTPUT_DIR)/host/
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 -I$(SCXTOOLSINCDIR)
# Silence some warnings when compiled with clang
ifneq ($(LLVM),)
CFLAGS += -Wno-unused-command-line-argument
endif
LDFLAGS = -lelf -lz -lpthread -lzstd
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) $(2) -v -E - </dev/null 2>&1 \
| sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \
$(shell $(1) $(2) -dM -E - </dev/null | grep '__riscv_xlen ' | awk '{printf("-D__riscv_xlen=%d -D__BITS_PER_LONG=%d", $$3, $$3)}')
endef
ifneq ($(CROSS_COMPILE),)
CLANG_TARGET_ARCH = --target=$(notdir $(CROSS_COMPILE:%-=%))
endif
CLANG_SYS_INCLUDES = $(call get_sys_includes,$(CLANG),$(CLANG_TARGET_ARCH))
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$(SCXTOOLSINCDIR) \
-I$(REPOROOT)/include \
$(CLANG_SYS_INCLUDES) \
-Wall -Wno-compare-distinct-pointer-types \
-Wno-incompatible-function-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 $(OBJ_DIR)/libbpf \
$(OBJ_DIR)/bpftool $(OBJ_DIR)/resolve_btfids \
$(HOST_OBJ_DIR) $(INCLUDE_DIR) $(SCXOBJ_DIR))
$(MAKE_DIRS):
$(call msg,MKDIR,,$@)
$(Q)mkdir -p $@
$(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile) \
$(APIDIR)/linux/bpf.h \
| $(OBJ_DIR)/libbpf
$(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) OUTPUT=$(OBJ_DIR)/libbpf/ \
ARCH=$(ARCH) CC="$(CC)" CROSS_COMPILE=$(CROSS_COMPILE) \
EXTRA_CFLAGS='-g -O0 -fPIC' \
DESTDIR=$(OUTPUT_DIR) prefix= all install_headers
$(DEFAULT_BPFTOOL): $(wildcard $(BPFTOOLDIR)/*.[ch] $(BPFTOOLDIR)/Makefile) \
$(LIBBPF_OUTPUT) | $(HOST_OBJ_DIR)
$(Q)$(MAKE) $(submake_extras) -C $(BPFTOOLDIR) \
ARCH= CROSS_COMPILE= CC=$(HOSTCC) LD=$(HOSTLD) \
EXTRA_CFLAGS='-g -O0' \
OUTPUT=$(HOST_OBJ_DIR)/ \
LIBBPF_OUTPUT=$(HOST_LIBBPF_OUTPUT) \
LIBBPF_DESTDIR=$(HOST_LIBBPF_DESTDIR) \
prefix= DESTDIR=$(HOST_DESTDIR) 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 | $(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) | $(INCLUDE_DIR)
$(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)
################
# C schedulers #
################
override define CLEAN
rm -rf $(OUTPUT_DIR)
rm -f $(TEST_GEN_PROGS)
endef
# Every testcase takes all of the BPF progs are dependencies by default. This
# allows testcases to load any BPF scheduler, which is useful for testcases
# that don't need their own prog to run their test.
all_test_bpfprogs := $(foreach prog,$(wildcard *.bpf.c),$(INCLUDE_DIR)/$(patsubst %.c,%.skel.h,$(prog)))
auto-test-targets := \
create_dsq \
dequeue \
enq_last_no_enq_fails \
ddsp_bogus_dsq_fail \
ddsp_vtimelocal_fail \
dsp_local_on \
enq_select_cpu \
exit \
hotplug \
init_enable_count \
maximal \
maybe_null \
minimal \
non_scx_kfunc_deny \
nohz_tick \
numa \
allowed_cpus \
peek_dsq \
prog_run \
reload_loop \
select_cpu_dfl \
select_cpu_dfl_nodispatch \
select_cpu_dispatch \
select_cpu_dispatch_bad_dsq \
select_cpu_dispatch_dbl_dsp \
select_cpu_vtime \
rt_stall \
test_example \
total_bw \
cyclic_kick_wait \
testcase-targets := $(addsuffix .o,$(addprefix $(SCXOBJ_DIR)/,$(auto-test-targets)))
$(SCXOBJ_DIR)/runner.o: runner.c | $(SCXOBJ_DIR) $(BPFOBJ)
$(CC) $(CFLAGS) -c $< -o $@
# Create all of the test targets object files, whose testcase objects will be
# registered into the runner in ELF constructors.
#
# Note that we must do double expansion here in order to support conditionally
# compiling BPF object files only if one is present, as the wildcard Make
# function doesn't support using implicit rules otherwise.
$(testcase-targets): $(SCXOBJ_DIR)/%.o: %.c $(SCXOBJ_DIR)/runner.o $(all_test_bpfprogs) | $(SCXOBJ_DIR)
$(eval test=$(patsubst %.o,%.c,$(notdir $@)))
$(CC) $(CFLAGS) -c $< -o $@
$(SCXOBJ_DIR)/util.o: util.c | $(SCXOBJ_DIR)
$(CC) $(CFLAGS) -c $< -o $@
$(OUTPUT)/runner: $(SCXOBJ_DIR)/runner.o $(SCXOBJ_DIR)/util.o $(BPFOBJ) $(testcase-targets)
@echo "$(testcase-targets)"
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
.DEFAULT_GOAL := all
.DELETE_ON_ERROR:
.SECONDARY:
@@ -0,0 +1,144 @@
// SPDX-License-Identifier: GPL-2.0
/*
* A scheduler that validates the behavior of scx_bpf_select_cpu_and() by
* selecting idle CPUs strictly within a subset of allowed CPUs.
*
* Copyright (c) 2025 Andrea Righi <arighi@nvidia.com>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
private(PREF_CPUS) struct bpf_cpumask __kptr * allowed_cpumask;
static void
validate_idle_cpu(const struct task_struct *p, const struct cpumask *allowed, s32 cpu)
{
if (scx_bpf_test_and_clear_cpu_idle(cpu))
scx_bpf_error("CPU %d should be marked as busy", cpu);
if (bpf_cpumask_subset(allowed, p->cpus_ptr) &&
!bpf_cpumask_test_cpu(cpu, allowed))
scx_bpf_error("CPU %d not in the allowed domain for %d (%s)",
cpu, p->pid, p->comm);
}
s32 BPF_STRUCT_OPS(allowed_cpus_select_cpu,
struct task_struct *p, s32 prev_cpu, u64 wake_flags)
{
const struct cpumask *allowed;
s32 cpu;
allowed = cast_mask(allowed_cpumask);
if (!allowed) {
scx_bpf_error("allowed domain not initialized");
return -EINVAL;
}
/*
* Select an idle CPU strictly within the allowed domain.
*/
cpu = scx_bpf_select_cpu_and(p, prev_cpu, wake_flags, allowed, 0);
if (cpu >= 0) {
validate_idle_cpu(p, allowed, cpu);
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
return cpu;
}
return prev_cpu;
}
void BPF_STRUCT_OPS(allowed_cpus_enqueue, struct task_struct *p, u64 enq_flags)
{
const struct cpumask *allowed;
s32 prev_cpu = scx_bpf_task_cpu(p), cpu;
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0);
allowed = cast_mask(allowed_cpumask);
if (!allowed) {
scx_bpf_error("allowed domain not initialized");
return;
}
/*
* Use scx_bpf_select_cpu_and() to proactively kick an idle CPU
* within @allowed_cpumask, usable by @p.
*/
cpu = scx_bpf_select_cpu_and(p, prev_cpu, 0, allowed, 0);
if (cpu >= 0) {
validate_idle_cpu(p, allowed, cpu);
scx_bpf_kick_cpu(cpu, SCX_KICK_IDLE);
}
}
s32 BPF_STRUCT_OPS_SLEEPABLE(allowed_cpus_init)
{
struct bpf_cpumask *mask;
mask = bpf_cpumask_create();
if (!mask)
return -ENOMEM;
mask = bpf_kptr_xchg(&allowed_cpumask, mask);
if (mask)
bpf_cpumask_release(mask);
bpf_rcu_read_lock();
/*
* Assign the first online CPU to the allowed domain.
*/
mask = allowed_cpumask;
if (mask) {
const struct cpumask *online = scx_bpf_get_online_cpumask();
bpf_cpumask_set_cpu(bpf_cpumask_first(online), mask);
scx_bpf_put_cpumask(online);
}
bpf_rcu_read_unlock();
return 0;
}
void BPF_STRUCT_OPS(allowed_cpus_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
struct task_cpu_arg {
pid_t pid;
};
SEC("syscall")
int select_cpu_from_user(struct task_cpu_arg *input)
{
struct task_struct *p;
int cpu;
p = bpf_task_from_pid(input->pid);
if (!p)
return -EINVAL;
bpf_rcu_read_lock();
cpu = scx_bpf_select_cpu_and(p, bpf_get_smp_processor_id(), 0, p->cpus_ptr, 0);
bpf_rcu_read_unlock();
bpf_task_release(p);
return cpu;
}
SEC(".struct_ops.link")
struct sched_ext_ops allowed_cpus_ops = {
.select_cpu = (void *)allowed_cpus_select_cpu,
.enqueue = (void *)allowed_cpus_enqueue,
.init = (void *)allowed_cpus_init,
.exit = (void *)allowed_cpus_exit,
.name = "allowed_cpus",
};
@@ -0,0 +1,84 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025 Andrea Righi <arighi@nvidia.com>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "allowed_cpus.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct allowed_cpus *skel;
skel = allowed_cpus__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(allowed_cpus__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static int test_select_cpu_from_user(const struct allowed_cpus *skel)
{
int fd, ret;
__u64 args[1];
LIBBPF_OPTS(bpf_test_run_opts, attr,
.ctx_in = args,
.ctx_size_in = sizeof(args),
);
args[0] = getpid();
fd = bpf_program__fd(skel->progs.select_cpu_from_user);
if (fd < 0)
return fd;
ret = bpf_prog_test_run_opts(fd, &attr);
if (ret < 0)
return ret;
fprintf(stderr, "%s: CPU %d\n", __func__, attr.retval);
return 0;
}
static enum scx_test_status run(void *ctx)
{
struct allowed_cpus *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.allowed_cpus_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
/* Pick an idle CPU from user-space */
SCX_FAIL_IF(test_select_cpu_from_user(skel), "Failed to pick idle CPU");
/* Just sleeping is fine, plenty of scheduling events happening */
sleep(1);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_NONE));
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct allowed_cpus *skel = ctx;
allowed_cpus__destroy(skel);
}
struct scx_test allowed_cpus = {
.name = "allowed_cpus",
.description = "Verify scx_bpf_select_cpu_and()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&allowed_cpus)
@@ -0,0 +1,8 @@
CONFIG_SCHED_CLASS_EXT=y
CONFIG_CGROUPS=y
CONFIG_CGROUP_SCHED=y
CONFIG_EXT_GROUP_SCHED=y
CONFIG_BPF=y
CONFIG_BPF_SYSCALL=y
CONFIG_DEBUG_INFO=y
CONFIG_DEBUG_INFO_BTF=y
@@ -0,0 +1,58 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Create and destroy DSQs in a loop.
*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
void BPF_STRUCT_OPS(create_dsq_exit_task, struct task_struct *p,
struct scx_exit_task_args *args)
{
scx_bpf_destroy_dsq(p->pid);
}
s32 BPF_STRUCT_OPS_SLEEPABLE(create_dsq_init_task, struct task_struct *p,
struct scx_init_task_args *args)
{
s32 err;
err = scx_bpf_create_dsq(p->pid, -1);
if (err)
scx_bpf_error("Failed to create DSQ for %s[%d]",
p->comm, p->pid);
return err;
}
s32 BPF_STRUCT_OPS_SLEEPABLE(create_dsq_init)
{
u32 i;
s32 err;
bpf_for(i, 0, 1024) {
err = scx_bpf_create_dsq(i, -1);
if (err) {
scx_bpf_error("Failed to create DSQ %d", i);
return 0;
}
}
bpf_for(i, 0, 1024) {
scx_bpf_destroy_dsq(i);
}
return 0;
}
SEC(".struct_ops.link")
struct sched_ext_ops create_dsq_ops = {
.init_task = (void *) create_dsq_init_task,
.exit_task = (void *) create_dsq_exit_task,
.init = (void *) create_dsq_init,
.name = "create_dsq",
};
@@ -0,0 +1,57 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "create_dsq.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct create_dsq *skel;
skel = create_dsq__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(create_dsq__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct create_dsq *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.create_dsq_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
return SCX_TEST_FAIL;
}
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct create_dsq *skel = ctx;
create_dsq__destroy(skel);
}
struct scx_test create_dsq = {
.name = "create_dsq",
.description = "Create and destroy a dsq in a loop",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&create_dsq)
@@ -0,0 +1,68 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Stress concurrent SCX_KICK_WAIT calls to reproduce wait-cycle deadlock.
*
* Three CPUs are designated from userspace. Every enqueue from one of the
* three CPUs kicks the next CPU in the ring with SCX_KICK_WAIT, creating a
* persistent A -> B -> C -> A wait cycle pressure.
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
const volatile s32 test_cpu_a;
const volatile s32 test_cpu_b;
const volatile s32 test_cpu_c;
u64 nr_enqueues;
u64 nr_wait_kicks;
UEI_DEFINE(uei);
static s32 target_cpu(s32 cpu)
{
if (cpu == test_cpu_a)
return test_cpu_b;
if (cpu == test_cpu_b)
return test_cpu_c;
if (cpu == test_cpu_c)
return test_cpu_a;
return -1;
}
void BPF_STRUCT_OPS(cyclic_kick_wait_enqueue, struct task_struct *p,
u64 enq_flags)
{
s32 this_cpu = bpf_get_smp_processor_id();
s32 tgt;
__sync_fetch_and_add(&nr_enqueues, 1);
if (p->flags & PF_KTHREAD) {
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_INF,
enq_flags | SCX_ENQ_PREEMPT);
return;
}
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
tgt = target_cpu(this_cpu);
if (tgt < 0 || tgt == this_cpu)
return;
__sync_fetch_and_add(&nr_wait_kicks, 1);
scx_bpf_kick_cpu(tgt, SCX_KICK_WAIT);
}
void BPF_STRUCT_OPS(cyclic_kick_wait_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops cyclic_kick_wait_ops = {
.enqueue = cyclic_kick_wait_enqueue,
.exit = cyclic_kick_wait_exit,
.name = "cyclic_kick_wait",
.timeout_ms = 1000U,
};
@@ -0,0 +1,194 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Test SCX_KICK_WAIT forward progress under cyclic wait pressure.
*
* SCX_KICK_WAIT busy-waits until the target CPU enters the scheduling path.
* If multiple CPUs form a wait cycle (A waits for B, B waits for C, C waits
* for A), all CPUs deadlock unless the implementation breaks the cycle.
*
* This test creates that scenario: three CPUs are arranged in a ring. The BPF
* scheduler's ops.enqueue() kicks the next CPU in the ring with SCX_KICK_WAIT
* on every enqueue. Userspace pins 4 worker threads per CPU that loop calling
* sched_yield(), generating a steady stream of enqueues and thus sustained
* A->B->C->A kick_wait cycle pressure. The test passes if the system remains
* responsive for 5 seconds without the scheduler being killed by the watchdog.
*/
#define _GNU_SOURCE
#include <bpf/bpf.h>
#include <errno.h>
#include <pthread.h>
#include <sched.h>
#include <scx/common.h>
#include <stdint.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
#include "scx_test.h"
#include "cyclic_kick_wait.bpf.skel.h"
#define WORKERS_PER_CPU 4
#define NR_TEST_CPUS 3
#define NR_WORKERS (NR_TEST_CPUS * WORKERS_PER_CPU)
struct worker_ctx {
pthread_t tid;
int cpu;
volatile bool stop;
volatile __u64 iters;
bool started;
};
static void *worker_fn(void *arg)
{
struct worker_ctx *worker = arg;
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(worker->cpu, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask))
return (void *)(uintptr_t)errno;
while (!worker->stop) {
sched_yield();
worker->iters++;
}
return NULL;
}
static int join_worker(struct worker_ctx *worker)
{
void *ret;
struct timespec ts;
int err;
if (!worker->started)
return 0;
if (clock_gettime(CLOCK_REALTIME, &ts))
return -errno;
ts.tv_sec += 2;
err = pthread_timedjoin_np(worker->tid, &ret, &ts);
if (err == ETIMEDOUT)
pthread_detach(worker->tid);
if (err)
return -err;
if ((uintptr_t)ret)
return -(int)(uintptr_t)ret;
return 0;
}
static enum scx_test_status setup(void **ctx)
{
struct cyclic_kick_wait *skel;
skel = cyclic_kick_wait__open();
SCX_FAIL_IF(!skel, "Failed to open skel");
SCX_ENUM_INIT(skel);
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct cyclic_kick_wait *skel = ctx;
struct worker_ctx workers[NR_WORKERS] = {};
struct bpf_link *link = NULL;
enum scx_test_status status = SCX_TEST_PASS;
int test_cpus[NR_TEST_CPUS];
int nr_cpus = 0;
cpu_set_t mask;
int ret, i;
if (sched_getaffinity(0, sizeof(mask), &mask)) {
SCX_ERR("Failed to get affinity (%d)", errno);
return SCX_TEST_FAIL;
}
for (i = 0; i < CPU_SETSIZE; i++) {
if (CPU_ISSET(i, &mask))
test_cpus[nr_cpus++] = i;
if (nr_cpus == NR_TEST_CPUS)
break;
}
if (nr_cpus < NR_TEST_CPUS)
return SCX_TEST_SKIP;
skel->rodata->test_cpu_a = test_cpus[0];
skel->rodata->test_cpu_b = test_cpus[1];
skel->rodata->test_cpu_c = test_cpus[2];
if (cyclic_kick_wait__load(skel)) {
SCX_ERR("Failed to load skel");
return SCX_TEST_FAIL;
}
link = bpf_map__attach_struct_ops(skel->maps.cyclic_kick_wait_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
return SCX_TEST_FAIL;
}
for (i = 0; i < NR_WORKERS; i++)
workers[i].cpu = test_cpus[i / WORKERS_PER_CPU];
for (i = 0; i < NR_WORKERS; i++) {
ret = pthread_create(&workers[i].tid, NULL, worker_fn, &workers[i]);
if (ret) {
SCX_ERR("Failed to create worker thread %d (%d)", i, ret);
status = SCX_TEST_FAIL;
goto out;
}
workers[i].started = true;
}
sleep(5);
if (skel->data->uei.kind != EXIT_KIND(SCX_EXIT_NONE)) {
SCX_ERR("Scheduler exited unexpectedly (kind=%llu code=%lld)",
(unsigned long long)skel->data->uei.kind,
(long long)skel->data->uei.exit_code);
status = SCX_TEST_FAIL;
}
out:
for (i = 0; i < NR_WORKERS; i++)
workers[i].stop = true;
for (i = 0; i < NR_WORKERS; i++) {
ret = join_worker(&workers[i]);
if (ret && status == SCX_TEST_PASS) {
SCX_ERR("Failed to join worker thread %d (%d)", i, ret);
status = SCX_TEST_FAIL;
}
}
if (link)
bpf_link__destroy(link);
return status;
}
static void cleanup(void *ctx)
{
struct cyclic_kick_wait *skel = ctx;
cyclic_kick_wait__destroy(skel);
}
struct scx_test cyclic_kick_wait = {
.name = "cyclic_kick_wait",
.description = "Verify SCX_KICK_WAIT forward progress under a 3-CPU wait cycle",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&cyclic_kick_wait)
@@ -0,0 +1,42 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
* Copyright (c) 2024 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
s32 BPF_STRUCT_OPS(ddsp_bogus_dsq_fail_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
s32 cpu = scx_bpf_pick_idle_cpu(p->cpus_ptr, 0);
if (cpu >= 0) {
/*
* If we dispatch to a bogus DSQ that will fall back to the
* builtin global DSQ, we fail gracefully.
*/
scx_bpf_dsq_insert_vtime(p, 0xcafef00d, SCX_SLICE_DFL,
p->scx.dsq_vtime, 0);
return cpu;
}
return prev_cpu;
}
void BPF_STRUCT_OPS(ddsp_bogus_dsq_fail_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops ddsp_bogus_dsq_fail_ops = {
.select_cpu = (void *) ddsp_bogus_dsq_fail_select_cpu,
.exit = (void *) ddsp_bogus_dsq_fail_exit,
.name = "ddsp_bogus_dsq_fail",
.timeout_ms = 1000U,
};
@@ -0,0 +1,60 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
* Copyright (c) 2024 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "ddsp_bogus_dsq_fail.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct ddsp_bogus_dsq_fail *skel;
skel = ddsp_bogus_dsq_fail__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(ddsp_bogus_dsq_fail__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct ddsp_bogus_dsq_fail *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.ddsp_bogus_dsq_fail_ops);
SCX_FAIL_IF(!link, "Failed to attach struct_ops");
sleep(1);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_ERROR));
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct ddsp_bogus_dsq_fail *skel = ctx;
ddsp_bogus_dsq_fail__destroy(skel);
}
struct scx_test ddsp_bogus_dsq_fail = {
.name = "ddsp_bogus_dsq_fail",
.description = "Verify we gracefully fail, and fall back to using a "
"built-in DSQ, if we do a direct dispatch to an invalid"
" DSQ in ops.select_cpu()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&ddsp_bogus_dsq_fail)
@@ -0,0 +1,39 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
* Copyright (c) 2024 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
s32 BPF_STRUCT_OPS(ddsp_vtimelocal_fail_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
s32 cpu = scx_bpf_pick_idle_cpu(p->cpus_ptr, 0);
if (cpu >= 0) {
/* Shouldn't be allowed to vtime dispatch to a builtin DSQ. */
scx_bpf_dsq_insert_vtime(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL,
p->scx.dsq_vtime, 0);
return cpu;
}
return prev_cpu;
}
void BPF_STRUCT_OPS(ddsp_vtimelocal_fail_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops ddsp_vtimelocal_fail_ops = {
.select_cpu = (void *) ddsp_vtimelocal_fail_select_cpu,
.exit = (void *) ddsp_vtimelocal_fail_exit,
.name = "ddsp_vtimelocal_fail",
.timeout_ms = 1000U,
};
@@ -0,0 +1,59 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
* Copyright (c) 2024 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <unistd.h>
#include "ddsp_vtimelocal_fail.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct ddsp_vtimelocal_fail *skel;
skel = ddsp_vtimelocal_fail__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(ddsp_vtimelocal_fail__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct ddsp_vtimelocal_fail *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.ddsp_vtimelocal_fail_ops);
SCX_FAIL_IF(!link, "Failed to attach struct_ops");
sleep(1);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_ERROR));
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct ddsp_vtimelocal_fail *skel = ctx;
ddsp_vtimelocal_fail__destroy(skel);
}
struct scx_test ddsp_vtimelocal_fail = {
.name = "ddsp_vtimelocal_fail",
.description = "Verify we gracefully fail, and fall back to using a "
"built-in DSQ, if we do a direct vtime dispatch to a "
"built-in DSQ from DSQ in ops.select_cpu()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&ddsp_vtimelocal_fail)
@@ -0,0 +1,389 @@
// SPDX-License-Identifier: GPL-2.0
/*
* A scheduler that validates ops.dequeue() is called correctly:
* - Tasks dispatched to terminal DSQs (local, global) bypass the BPF
* scheduler entirely: no ops.dequeue() should be called
* - Tasks dispatched to user DSQs from ops.enqueue() enter BPF custody:
* ops.dequeue() must be called when they leave custody
* - Every ops.enqueue() dispatch to non-terminal DSQs is followed by
* exactly one ops.dequeue() (validate 1:1 pairing and state machine)
*
* Copyright (c) 2026 NVIDIA Corporation.
*/
#include <scx/common.bpf.h>
#define SHARED_DSQ 0
/*
* BPF internal queue.
*
* Tasks are stored here and consumed from ops.dispatch(), validating that
* tasks on BPF internal structures still get ops.dequeue() when they
* leave.
*/
struct {
__uint(type, BPF_MAP_TYPE_QUEUE);
__uint(max_entries, 32768);
__type(value, s32);
} global_queue SEC(".maps");
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
/*
* Counters to track the lifecycle of tasks:
* - enqueue_cnt: Number of times ops.enqueue() was called
* - dequeue_cnt: Number of times ops.dequeue() was called (any type)
* - dispatch_dequeue_cnt: Number of regular dispatch dequeues (no flag)
* - change_dequeue_cnt: Number of property change dequeues
* - bpf_queue_full: Number of times the BPF internal queue was full
*/
u64 enqueue_cnt, dequeue_cnt, dispatch_dequeue_cnt, change_dequeue_cnt, bpf_queue_full;
/*
* Test scenarios:
* 0) Dispatch to local DSQ from ops.select_cpu() (terminal DSQ, bypasses BPF
* scheduler, no dequeue callbacks)
* 1) Dispatch to global DSQ from ops.select_cpu() (terminal DSQ, bypasses BPF
* scheduler, no dequeue callbacks)
* 2) Dispatch to shared user DSQ from ops.select_cpu() (enters BPF scheduler,
* dequeue callbacks expected)
* 3) Dispatch to local DSQ from ops.enqueue() (terminal DSQ, bypasses BPF
* scheduler, no dequeue callbacks)
* 4) Dispatch to global DSQ from ops.enqueue() (terminal DSQ, bypasses BPF
* scheduler, no dequeue callbacks)
* 5) Dispatch to shared user DSQ from ops.enqueue() (enters BPF scheduler,
* dequeue callbacks expected)
* 6) BPF internal queue from ops.enqueue(): store task PIDs in ops.enqueue(),
* consume in ops.dispatch() and dispatch to local DSQ (validates dequeue
* for tasks stored in internal BPF data structures)
*/
u32 test_scenario;
/*
* Per-task state to track lifecycle and validate workflow semantics.
* State transitions:
* NONE -> ENQUEUED (on enqueue)
* NONE -> DISPATCHED (on direct dispatch to terminal DSQ)
* ENQUEUED -> DISPATCHED (on dispatch dequeue)
* DISPATCHED -> NONE (on property change dequeue or re-enqueue)
* ENQUEUED -> NONE (on property change dequeue before dispatch)
*/
enum task_state {
TASK_NONE = 0,
TASK_ENQUEUED,
TASK_DISPATCHED,
};
struct task_ctx {
enum task_state state; /* Current state in the workflow */
u64 enqueue_seq; /* Sequence number for debugging */
};
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");
static struct task_ctx *try_lookup_task_ctx(struct task_struct *p)
{
return bpf_task_storage_get(&task_ctx_stor, p, 0, 0);
}
s32 BPF_STRUCT_OPS(dequeue_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
struct task_ctx *tctx;
tctx = try_lookup_task_ctx(p);
if (!tctx)
return prev_cpu;
switch (test_scenario) {
case 0:
/*
* Direct dispatch to the local DSQ.
*
* Task bypasses BPF scheduler entirely: no enqueue
* tracking, no ops.dequeue() callbacks.
*/
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, 0);
tctx->state = TASK_DISPATCHED;
break;
case 1:
/*
* Direct dispatch to the global DSQ.
*
* Task bypasses BPF scheduler entirely: no enqueue
* tracking, no ops.dequeue() callbacks.
*/
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0);
tctx->state = TASK_DISPATCHED;
break;
case 2:
/*
* Dispatch to a shared user DSQ.
*
* Task enters BPF scheduler management: track
* enqueue/dequeue lifecycle and validate state
* transitions.
*/
if (tctx->state == TASK_ENQUEUED)
scx_bpf_error("%d (%s): enqueue while in ENQUEUED state seq=%llu",
p->pid, p->comm, tctx->enqueue_seq);
scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, 0);
__sync_fetch_and_add(&enqueue_cnt, 1);
tctx->state = TASK_ENQUEUED;
tctx->enqueue_seq++;
break;
}
return prev_cpu;
}
void BPF_STRUCT_OPS(dequeue_enqueue, struct task_struct *p, u64 enq_flags)
{
struct task_ctx *tctx;
s32 pid = p->pid;
tctx = try_lookup_task_ctx(p);
if (!tctx)
return;
switch (test_scenario) {
case 3:
/*
* Direct dispatch to the local DSQ.
*
* Task bypasses BPF scheduler entirely: no enqueue
* tracking, no ops.dequeue() callbacks.
*/
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL, SCX_SLICE_DFL, enq_flags);
tctx->state = TASK_DISPATCHED;
break;
case 4:
/*
* Direct dispatch to the global DSQ.
*
* Task bypasses BPF scheduler entirely: no enqueue
* tracking, no ops.dequeue() callbacks.
*/
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
tctx->state = TASK_DISPATCHED;
break;
case 5:
/*
* Dispatch to shared user DSQ.
*
* Task enters BPF scheduler management: track
* enqueue/dequeue lifecycle and validate state
* transitions.
*/
if (tctx->state == TASK_ENQUEUED)
scx_bpf_error("%d (%s): enqueue while in ENQUEUED state seq=%llu",
p->pid, p->comm, tctx->enqueue_seq);
scx_bpf_dsq_insert(p, SHARED_DSQ, SCX_SLICE_DFL, enq_flags);
__sync_fetch_and_add(&enqueue_cnt, 1);
tctx->state = TASK_ENQUEUED;
tctx->enqueue_seq++;
break;
case 6:
/*
* Store task in BPF internal queue.
*
* Task enters BPF scheduler management: track
* enqueue/dequeue lifecycle and validate state
* transitions.
*/
if (tctx->state == TASK_ENQUEUED)
scx_bpf_error("%d (%s): enqueue while in ENQUEUED state seq=%llu",
p->pid, p->comm, tctx->enqueue_seq);
if (bpf_map_push_elem(&global_queue, &pid, 0)) {
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
__sync_fetch_and_add(&bpf_queue_full, 1);
tctx->state = TASK_DISPATCHED;
} else {
__sync_fetch_and_add(&enqueue_cnt, 1);
tctx->state = TASK_ENQUEUED;
tctx->enqueue_seq++;
}
break;
default:
/* For all other scenarios, dispatch to the global DSQ */
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
tctx->state = TASK_DISPATCHED;
break;
}
scx_bpf_kick_cpu(scx_bpf_task_cpu(p), SCX_KICK_IDLE);
}
void BPF_STRUCT_OPS(dequeue_dequeue, struct task_struct *p, u64 deq_flags)
{
struct task_ctx *tctx;
__sync_fetch_and_add(&dequeue_cnt, 1);
tctx = try_lookup_task_ctx(p);
if (!tctx)
return;
/*
* For scenarios 0, 1, 3, and 4 (terminal DSQs: local and global),
* ops.dequeue() should never be called because tasks bypass the
* BPF scheduler entirely. If we get here, it's a kernel bug.
*/
if (test_scenario == 0 || test_scenario == 3) {
scx_bpf_error("%d (%s): dequeue called for local DSQ scenario",
p->pid, p->comm);
return;
}
if (test_scenario == 1 || test_scenario == 4) {
scx_bpf_error("%d (%s): dequeue called for global DSQ scenario",
p->pid, p->comm);
return;
}
if (deq_flags & SCX_DEQ_SCHED_CHANGE) {
/*
* Property change interrupting the workflow. Valid from
* both ENQUEUED and DISPATCHED states. Transitions task
* back to NONE state.
*/
__sync_fetch_and_add(&change_dequeue_cnt, 1);
/* Validate state transition */
if (tctx->state != TASK_ENQUEUED && tctx->state != TASK_DISPATCHED)
scx_bpf_error("%d (%s): invalid property change dequeue state=%d seq=%llu",
p->pid, p->comm, tctx->state, tctx->enqueue_seq);
/*
* Transition back to NONE: task outside scheduler control.
*
* Scenario 6: dispatch() checks tctx->state after popping a
* PID, if the task is in state NONE, it was dequeued by
* property change and must not be dispatched (this
* prevents "target CPU not allowed").
*/
tctx->state = TASK_NONE;
} else {
/*
* Regular dispatch dequeue: kernel is moving the task from
* BPF custody to a terminal DSQ. Normally we come from
* ENQUEUED state. We can also see TASK_NONE if the task
* was dequeued by property change (SCX_DEQ_SCHED_CHANGE)
* while it was already on a DSQ (dispatched but not yet
* consumed); in that case we just leave state as NONE.
*/
__sync_fetch_and_add(&dispatch_dequeue_cnt, 1);
/*
* Must be ENQUEUED (normal path) or NONE (already dequeued
* by property change while on a DSQ).
*/
if (tctx->state != TASK_ENQUEUED && tctx->state != TASK_NONE)
scx_bpf_error("%d (%s): dispatch dequeue from state %d seq=%llu",
p->pid, p->comm, tctx->state, tctx->enqueue_seq);
if (tctx->state == TASK_ENQUEUED)
tctx->state = TASK_DISPATCHED;
/* NONE: leave as-is, task was already property-change dequeued */
}
}
void BPF_STRUCT_OPS(dequeue_dispatch, s32 cpu, struct task_struct *prev)
{
if (test_scenario == 6) {
struct task_ctx *tctx;
struct task_struct *p;
s32 pid;
if (bpf_map_pop_elem(&global_queue, &pid))
return;
p = bpf_task_from_pid(pid);
if (!p)
return;
/*
* If the task was dequeued by property change
* (ops.dequeue() set tctx->state = TASK_NONE), skip
* dispatch.
*/
tctx = try_lookup_task_ctx(p);
if (!tctx || tctx->state == TASK_NONE) {
bpf_task_release(p);
return;
}
/*
* Dispatch to this CPU's local DSQ if allowed, otherwise
* fallback to the global DSQ.
*/
if (bpf_cpumask_test_cpu(cpu, p->cpus_ptr))
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cpu, SCX_SLICE_DFL, 0);
else
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0);
bpf_task_release(p);
} else {
scx_bpf_dsq_move_to_local(SHARED_DSQ, 0);
}
}
s32 BPF_STRUCT_OPS(dequeue_init_task, struct task_struct *p,
struct scx_init_task_args *args)
{
struct task_ctx *tctx;
tctx = bpf_task_storage_get(&task_ctx_stor, p, 0,
BPF_LOCAL_STORAGE_GET_F_CREATE);
if (!tctx)
return -ENOMEM;
return 0;
}
s32 BPF_STRUCT_OPS_SLEEPABLE(dequeue_init)
{
s32 ret;
ret = scx_bpf_create_dsq(SHARED_DSQ, -1);
if (ret)
return ret;
return 0;
}
void BPF_STRUCT_OPS(dequeue_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops dequeue_ops = {
.select_cpu = (void *)dequeue_select_cpu,
.enqueue = (void *)dequeue_enqueue,
.dequeue = (void *)dequeue_dequeue,
.dispatch = (void *)dequeue_dispatch,
.init_task = (void *)dequeue_init_task,
.init = (void *)dequeue_init,
.exit = (void *)dequeue_exit,
.flags = SCX_OPS_ENQ_LAST,
.name = "dequeue_test",
};
@@ -0,0 +1,275 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025 NVIDIA Corporation.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <time.h>
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <sched.h>
#include <pthread.h>
#include "scx_test.h"
#include "dequeue.bpf.skel.h"
#define NUM_WORKERS 8
#define AFFINITY_HAMMER_MS 500
/*
* Worker function that creates enqueue/dequeue events via CPU work and
* sleep.
*/
static void worker_fn(int id)
{
int i;
volatile int sum = 0;
for (i = 0; i < 1000; i++) {
volatile int j;
/* Do some work to trigger scheduling events */
for (j = 0; j < 10000; j++)
sum += j;
asm volatile("" : : "r"(sum));
/* Sleep to trigger dequeue */
usleep(1000 + (id * 100));
}
exit(0);
}
/*
* This thread changes workers' affinity from outside so that some changes
* hit tasks while they are still in the scheduler's queue and trigger
* property-change dequeues.
*/
static void *affinity_hammer_fn(void *arg)
{
pid_t *pids = arg;
cpu_set_t cpuset;
int i = 0, n = NUM_WORKERS;
struct timespec start, now;
clock_gettime(CLOCK_MONOTONIC, &start);
while (1) {
int w = i % n;
int cpu = (i / n) % 4;
CPU_ZERO(&cpuset);
CPU_SET(cpu, &cpuset);
sched_setaffinity(pids[w], sizeof(cpuset), &cpuset);
i++;
/* Check elapsed time every 256 iterations to limit gettime cost */
if ((i & 255) == 0) {
long long elapsed_ms;
clock_gettime(CLOCK_MONOTONIC, &now);
elapsed_ms = (now.tv_sec - start.tv_sec) * 1000LL +
(now.tv_nsec - start.tv_nsec) / 1000000;
if (elapsed_ms >= AFFINITY_HAMMER_MS)
break;
}
}
return NULL;
}
static enum scx_test_status run_scenario(struct dequeue *skel, u32 scenario,
const char *scenario_name)
{
struct bpf_link *link;
pid_t pids[NUM_WORKERS];
pthread_t hammer;
int i, status;
u64 enq_start, deq_start,
dispatch_deq_start, change_deq_start, bpf_queue_full_start;
u64 enq_delta, deq_delta,
dispatch_deq_delta, change_deq_delta, bpf_queue_full_delta;
/* Set the test scenario */
skel->bss->test_scenario = scenario;
/* Record starting counts */
enq_start = skel->bss->enqueue_cnt;
deq_start = skel->bss->dequeue_cnt;
dispatch_deq_start = skel->bss->dispatch_dequeue_cnt;
change_deq_start = skel->bss->change_dequeue_cnt;
bpf_queue_full_start = skel->bss->bpf_queue_full;
link = bpf_map__attach_struct_ops(skel->maps.dequeue_ops);
SCX_FAIL_IF(!link, "Failed to attach struct_ops for scenario %s", scenario_name);
/* Fork worker processes to generate enqueue/dequeue events */
for (i = 0; i < NUM_WORKERS; i++) {
pids[i] = fork();
SCX_FAIL_IF(pids[i] < 0, "Failed to fork worker %d", i);
if (pids[i] == 0) {
worker_fn(i);
/* Should not reach here */
exit(1);
}
}
/*
* Run an "affinity hammer" so that some property changes hit tasks
* while they are still in BPF custody (e.g., in user DSQ or BPF
* queue), triggering SCX_DEQ_SCHED_CHANGE dequeues.
*/
SCX_FAIL_IF(pthread_create(&hammer, NULL, affinity_hammer_fn, pids) != 0,
"Failed to create affinity hammer thread");
pthread_join(hammer, NULL);
/* Wait for all workers to complete */
for (i = 0; i < NUM_WORKERS; i++) {
SCX_FAIL_IF(waitpid(pids[i], &status, 0) != pids[i],
"Failed to wait for worker %d", i);
SCX_FAIL_IF(status != 0, "Worker %d exited with status %d", i, status);
}
bpf_link__destroy(link);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_UNREG));
/* Calculate deltas */
enq_delta = skel->bss->enqueue_cnt - enq_start;
deq_delta = skel->bss->dequeue_cnt - deq_start;
dispatch_deq_delta = skel->bss->dispatch_dequeue_cnt - dispatch_deq_start;
change_deq_delta = skel->bss->change_dequeue_cnt - change_deq_start;
bpf_queue_full_delta = skel->bss->bpf_queue_full - bpf_queue_full_start;
printf("%s:\n", scenario_name);
printf(" enqueues: %lu\n", (unsigned long)enq_delta);
printf(" dequeues: %lu (dispatch: %lu, property_change: %lu)\n",
(unsigned long)deq_delta,
(unsigned long)dispatch_deq_delta,
(unsigned long)change_deq_delta);
printf(" BPF queue full: %lu\n", (unsigned long)bpf_queue_full_delta);
/*
* Validate enqueue/dequeue lifecycle tracking.
*
* For scenarios 0, 1, 3, 4 (local and global DSQs from
* ops.select_cpu() and ops.enqueue()), both enqueues and dequeues
* should be 0 because tasks bypass the BPF scheduler entirely:
* tasks never enter BPF scheduler's custody.
*
* For scenarios 2, 5, 6 (user DSQ or BPF internal queue) we expect
* both enqueues and dequeues.
*
* The BPF code does strict state machine validation with
* scx_bpf_error() to ensure the workflow semantics are correct.
*
* If we reach this point without errors, the semantics are
* validated correctly.
*/
if (scenario == 0 || scenario == 1 ||
scenario == 3 || scenario == 4) {
/* Tasks bypass BPF scheduler completely */
SCX_EQ(enq_delta, 0);
SCX_EQ(deq_delta, 0);
SCX_EQ(dispatch_deq_delta, 0);
SCX_EQ(change_deq_delta, 0);
} else {
/*
* User DSQ from ops.enqueue() or ops.select_cpu(): tasks
* enter BPF scheduler's custody.
*
* Also validate 1:1 enqueue/dequeue pairing.
*/
SCX_GT(enq_delta, 0);
SCX_GT(deq_delta, 0);
SCX_EQ(enq_delta, deq_delta);
}
return SCX_TEST_PASS;
}
static enum scx_test_status setup(void **ctx)
{
struct dequeue *skel;
skel = dequeue__open();
SCX_FAIL_IF(!skel, "Failed to open skel");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(dequeue__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct dequeue *skel = ctx;
enum scx_test_status status;
status = run_scenario(skel, 0, "Scenario 0: Local DSQ from ops.select_cpu()");
if (status != SCX_TEST_PASS)
return status;
status = run_scenario(skel, 1, "Scenario 1: Global DSQ from ops.select_cpu()");
if (status != SCX_TEST_PASS)
return status;
status = run_scenario(skel, 2, "Scenario 2: User DSQ from ops.select_cpu()");
if (status != SCX_TEST_PASS)
return status;
status = run_scenario(skel, 3, "Scenario 3: Local DSQ from ops.enqueue()");
if (status != SCX_TEST_PASS)
return status;
status = run_scenario(skel, 4, "Scenario 4: Global DSQ from ops.enqueue()");
if (status != SCX_TEST_PASS)
return status;
status = run_scenario(skel, 5, "Scenario 5: User DSQ from ops.enqueue()");
if (status != SCX_TEST_PASS)
return status;
status = run_scenario(skel, 6, "Scenario 6: BPF queue from ops.enqueue()");
if (status != SCX_TEST_PASS)
return status;
printf("\n=== Summary ===\n");
printf("Total enqueues: %lu\n", (unsigned long)skel->bss->enqueue_cnt);
printf("Total dequeues: %lu\n", (unsigned long)skel->bss->dequeue_cnt);
printf(" Dispatch dequeues: %lu (no flag, normal workflow)\n",
(unsigned long)skel->bss->dispatch_dequeue_cnt);
printf(" Property change dequeues: %lu (SCX_DEQ_SCHED_CHANGE flag)\n",
(unsigned long)skel->bss->change_dequeue_cnt);
printf(" BPF queue full: %lu\n",
(unsigned long)skel->bss->bpf_queue_full);
printf("\nAll scenarios passed - no state machine violations detected\n");
printf("-> Validated: Local DSQ dispatch bypasses BPF scheduler\n");
printf("-> Validated: Global DSQ dispatch bypasses BPF scheduler\n");
printf("-> Validated: User DSQ dispatch triggers ops.dequeue() callbacks\n");
printf("-> Validated: Dispatch dequeues have no flags (normal workflow)\n");
printf("-> Validated: Property change dequeues have SCX_DEQ_SCHED_CHANGE flag\n");
printf("-> Validated: No duplicate enqueues or invalid state transitions\n");
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct dequeue *skel = ctx;
dequeue__destroy(skel);
}
struct scx_test dequeue_test = {
.name = "dequeue",
.description = "Verify ops.dequeue() semantics",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&dequeue_test)
@@ -0,0 +1,68 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
const volatile s32 nr_cpus;
UEI_DEFINE(uei);
struct {
__uint(type, BPF_MAP_TYPE_QUEUE);
__uint(max_entries, 8192);
__type(value, s32);
} queue SEC(".maps");
s32 BPF_STRUCT_OPS(dsp_local_on_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
return prev_cpu;
}
void BPF_STRUCT_OPS(dsp_local_on_enqueue, struct task_struct *p,
u64 enq_flags)
{
s32 pid = p->pid;
if (bpf_map_push_elem(&queue, &pid, 0))
scx_bpf_error("Failed to enqueue %s[%d]", p->comm, p->pid);
}
void BPF_STRUCT_OPS(dsp_local_on_dispatch, s32 cpu, struct task_struct *prev)
{
s32 pid, target;
struct task_struct *p;
if (bpf_map_pop_elem(&queue, &pid))
return;
p = bpf_task_from_pid(pid);
if (!p)
return;
if (p->nr_cpus_allowed == nr_cpus && !is_migration_disabled(p))
target = bpf_get_prandom_u32() % nr_cpus;
else
target = scx_bpf_task_cpu(p);
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | target, SCX_SLICE_DFL, 0);
bpf_task_release(p);
}
void BPF_STRUCT_OPS(dsp_local_on_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops dsp_local_on_ops = {
.select_cpu = (void *) dsp_local_on_select_cpu,
.enqueue = (void *) dsp_local_on_enqueue,
.dispatch = (void *) dsp_local_on_dispatch,
.exit = (void *) dsp_local_on_exit,
.name = "dsp_local_on",
.timeout_ms = 1000U,
};
@@ -0,0 +1,60 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <unistd.h>
#include "dsp_local_on.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct dsp_local_on *skel;
skel = dsp_local_on__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
skel->rodata->nr_cpus = libbpf_num_possible_cpus();
SCX_FAIL_IF(dsp_local_on__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct dsp_local_on *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.dsp_local_on_ops);
SCX_FAIL_IF(!link, "Failed to attach struct_ops");
/* Just sleeping is fine, plenty of scheduling events happening */
sleep(1);
bpf_link__destroy(link);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_UNREG));
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct dsp_local_on *skel = ctx;
dsp_local_on__destroy(skel);
}
struct scx_test dsp_local_on = {
.name = "dsp_local_on",
.description = "Verify we can directly dispatch tasks to a local DSQs "
"from ops.dispatch()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&dsp_local_on)
@@ -0,0 +1,29 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that validates the behavior of direct dispatching with a default
* select_cpu implementation.
*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
u32 exit_kind;
void BPF_STRUCT_OPS_SLEEPABLE(enq_last_no_enq_fails_exit, struct scx_exit_info *info)
{
exit_kind = info->kind;
}
SEC(".struct_ops.link")
struct sched_ext_ops enq_last_no_enq_fails_ops = {
.name = "enq_last_no_enq_fails",
/* Need to define ops.enqueue() with SCX_OPS_ENQ_LAST */
.flags = SCX_OPS_ENQ_LAST,
.exit = (void *) enq_last_no_enq_fails_exit,
.timeout_ms = 1000U,
};
@@ -0,0 +1,64 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "enq_last_no_enq_fails.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct enq_last_no_enq_fails *skel;
skel = enq_last_no_enq_fails__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(enq_last_no_enq_fails__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct enq_last_no_enq_fails *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.enq_last_no_enq_fails_ops);
if (!link) {
SCX_ERR("Incorrectly failed at attaching scheduler");
return SCX_TEST_FAIL;
}
if (!skel->bss->exit_kind) {
SCX_ERR("Incorrectly stayed loaded");
return SCX_TEST_FAIL;
}
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct enq_last_no_enq_fails *skel = ctx;
enq_last_no_enq_fails__destroy(skel);
}
struct scx_test enq_last_no_enq_fails = {
.name = "enq_last_no_enq_fails",
.description = "Verify we eject a scheduler if we specify "
"the SCX_OPS_ENQ_LAST flag without defining "
"ops.enqueue()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&enq_last_no_enq_fails)
@@ -0,0 +1,74 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
s32 BPF_STRUCT_OPS(enq_select_cpu_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
/* Bounce all tasks to ops.enqueue() */
return prev_cpu;
}
void BPF_STRUCT_OPS(enq_select_cpu_enqueue, struct task_struct *p,
u64 enq_flags)
{
s32 cpu, prev_cpu = scx_bpf_task_cpu(p);
bool found = false;
cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, 0, &found);
if (found) {
scx_bpf_dsq_insert(p, SCX_DSQ_LOCAL_ON | cpu, SCX_SLICE_DFL, enq_flags);
return;
}
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
}
void BPF_STRUCT_OPS(enq_select_cpu_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
struct task_cpu_arg {
pid_t pid;
};
SEC("syscall")
int select_cpu_from_user(struct task_cpu_arg *input)
{
struct task_struct *p;
bool found = false;
s32 cpu;
p = bpf_task_from_pid(input->pid);
if (!p)
return -EINVAL;
bpf_rcu_read_lock();
cpu = scx_bpf_select_cpu_dfl(p, bpf_get_smp_processor_id(), 0, &found);
if (!found)
cpu = -EBUSY;
bpf_rcu_read_unlock();
bpf_task_release(p);
return cpu;
}
SEC(".struct_ops.link")
struct sched_ext_ops enq_select_cpu_ops = {
.select_cpu = (void *)enq_select_cpu_select_cpu,
.enqueue = (void *)enq_select_cpu_enqueue,
.exit = (void *)enq_select_cpu_exit,
.name = "enq_select_cpu",
.timeout_ms = 1000U,
};
@@ -0,0 +1,88 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "enq_select_cpu.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct enq_select_cpu *skel;
skel = enq_select_cpu__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(enq_select_cpu__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static int test_select_cpu_from_user(const struct enq_select_cpu *skel)
{
int fd, ret;
__u64 args[1];
LIBBPF_OPTS(bpf_test_run_opts, attr,
.ctx_in = args,
.ctx_size_in = sizeof(args),
);
args[0] = getpid();
fd = bpf_program__fd(skel->progs.select_cpu_from_user);
if (fd < 0)
return fd;
ret = bpf_prog_test_run_opts(fd, &attr);
if (ret < 0)
return ret;
fprintf(stderr, "%s: CPU %d\n", __func__, attr.retval);
return 0;
}
static enum scx_test_status run(void *ctx)
{
struct enq_select_cpu *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.enq_select_cpu_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
return SCX_TEST_FAIL;
}
/* Pick an idle CPU from user-space */
SCX_FAIL_IF(test_select_cpu_from_user(skel), "Failed to pick idle CPU");
sleep(1);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_NONE));
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct enq_select_cpu *skel = ctx;
enq_select_cpu__destroy(skel);
}
struct scx_test enq_select_cpu = {
.name = "enq_select_cpu",
.description = "Verify scx_bpf_select_cpu_dfl() from multiple contexts",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&enq_select_cpu)
@@ -0,0 +1,86 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
#include "exit_test.h"
const volatile int exit_point;
UEI_DEFINE(uei);
#define EXIT_CLEANLY() scx_bpf_exit(exit_point, "%d", exit_point)
#define DSQ_ID 0
s32 BPF_STRUCT_OPS(exit_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
bool found;
if (exit_point == EXIT_SELECT_CPU)
EXIT_CLEANLY();
return scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags, &found);
}
void BPF_STRUCT_OPS(exit_enqueue, struct task_struct *p, u64 enq_flags)
{
if (exit_point == EXIT_ENQUEUE)
EXIT_CLEANLY();
scx_bpf_dsq_insert(p, DSQ_ID, SCX_SLICE_DFL, enq_flags);
}
void BPF_STRUCT_OPS(exit_dispatch, s32 cpu, struct task_struct *p)
{
if (exit_point == EXIT_DISPATCH)
EXIT_CLEANLY();
scx_bpf_dsq_move_to_local(DSQ_ID, 0);
}
void BPF_STRUCT_OPS(exit_enable, struct task_struct *p)
{
if (exit_point == EXIT_ENABLE)
EXIT_CLEANLY();
}
s32 BPF_STRUCT_OPS(exit_init_task, struct task_struct *p,
struct scx_init_task_args *args)
{
if (exit_point == EXIT_INIT_TASK)
EXIT_CLEANLY();
return 0;
}
void BPF_STRUCT_OPS(exit_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
s32 BPF_STRUCT_OPS_SLEEPABLE(exit_init)
{
if (exit_point == EXIT_INIT)
EXIT_CLEANLY();
return scx_bpf_create_dsq(DSQ_ID, -1);
}
SEC(".struct_ops.link")
struct sched_ext_ops exit_ops = {
.select_cpu = (void *) exit_select_cpu,
.enqueue = (void *) exit_enqueue,
.dispatch = (void *) exit_dispatch,
.init_task = (void *) exit_init_task,
.enable = (void *) exit_enable,
.exit = (void *) exit_exit,
.init = (void *) exit_init,
.name = "exit",
.timeout_ms = 1000U,
};
@@ -0,0 +1,64 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <bpf/bpf.h>
#include <sched.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "exit.bpf.skel.h"
#include "scx_test.h"
#include "exit_test.h"
static enum scx_test_status run(void *ctx)
{
enum exit_test_case tc;
for (tc = 0; tc < NUM_EXITS; tc++) {
struct exit *skel;
struct bpf_link *link;
char buf[16];
/*
* On single-CPU systems, ops.select_cpu() is never
* invoked, so skip this test to avoid getting stuck
* indefinitely.
*/
if (tc == EXIT_SELECT_CPU && libbpf_num_possible_cpus() == 1)
continue;
skel = exit__open();
SCX_ENUM_INIT(skel);
skel->rodata->exit_point = tc;
SCX_FAIL_IF(exit__load(skel), "Failed to load skel");
link = bpf_map__attach_struct_ops(skel->maps.exit_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
exit__destroy(skel);
return SCX_TEST_FAIL;
}
/* Assumes uei.kind is written last */
while (skel->data->uei.kind == EXIT_KIND(SCX_EXIT_NONE))
sched_yield();
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_UNREG_BPF));
SCX_EQ(skel->data->uei.exit_code, tc);
sprintf(buf, "%d", tc);
SCX_ASSERT(!strcmp(skel->data->uei.msg, buf));
bpf_link__destroy(link);
exit__destroy(skel);
}
return SCX_TEST_PASS;
}
struct scx_test exit_test = {
.name = "exit",
.description = "Verify we can cleanly exit a scheduler in multiple places",
.run = run,
};
REGISTER_SCX_TEST(&exit_test)
@@ -0,0 +1,20 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#ifndef __EXIT_TEST_H__
#define __EXIT_TEST_H__
enum exit_test_case {
EXIT_SELECT_CPU,
EXIT_ENQUEUE,
EXIT_DISPATCH,
EXIT_ENABLE,
EXIT_INIT_TASK,
EXIT_INIT,
NUM_EXITS,
};
#endif // __EXIT_TEST_H__
@@ -0,0 +1,61 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
#include "hotplug_test.h"
UEI_DEFINE(uei);
void BPF_STRUCT_OPS(hotplug_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
static void exit_from_hotplug(s32 cpu, bool onlining)
{
/*
* Ignored, just used to verify that we can invoke blocking kfuncs
* from the hotplug path.
*/
scx_bpf_create_dsq(0, -1);
s64 code = SCX_ECODE_ACT_RESTART | HOTPLUG_EXIT_RSN;
if (onlining)
code |= HOTPLUG_ONLINING;
scx_bpf_exit(code, "hotplug event detected (%d going %s)", cpu,
onlining ? "online" : "offline");
}
void BPF_STRUCT_OPS_SLEEPABLE(hotplug_cpu_online, s32 cpu)
{
exit_from_hotplug(cpu, true);
}
void BPF_STRUCT_OPS_SLEEPABLE(hotplug_cpu_offline, s32 cpu)
{
exit_from_hotplug(cpu, false);
}
SEC(".struct_ops.link")
struct sched_ext_ops hotplug_cb_ops = {
.cpu_online = (void *) hotplug_cpu_online,
.cpu_offline = (void *) hotplug_cpu_offline,
.exit = (void *) hotplug_exit,
.name = "hotplug_cbs",
.timeout_ms = 1000U,
};
SEC(".struct_ops.link")
struct sched_ext_ops hotplug_nocb_ops = {
.exit = (void *) hotplug_exit,
.name = "hotplug_nocbs",
.timeout_ms = 1000U,
};
@@ -0,0 +1,169 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <bpf/bpf.h>
#include <sched.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "hotplug_test.h"
#include "hotplug.bpf.skel.h"
#include "scx_test.h"
#include "util.h"
const char *online_path = "/sys/devices/system/cpu/cpu1/online";
static bool is_cpu_online(void)
{
return file_read_long(online_path) > 0;
}
static void toggle_online_status(bool online)
{
long val = online ? 1 : 0;
int ret;
ret = file_write_long(online_path, val);
if (ret != 0)
fprintf(stderr, "Failed to bring CPU %s (%s)",
online ? "online" : "offline", strerror(errno));
}
static enum scx_test_status setup(void **ctx)
{
if (!is_cpu_online())
return SCX_TEST_SKIP;
return SCX_TEST_PASS;
}
static enum scx_test_status test_hotplug(bool onlining, bool cbs_defined)
{
struct hotplug *skel;
struct bpf_link *link;
long kind, code;
SCX_ASSERT(is_cpu_online());
skel = hotplug__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(hotplug__load(skel), "Failed to load skel");
/* Testing the offline -> online path, so go offline before starting */
if (onlining)
toggle_online_status(0);
if (cbs_defined) {
kind = SCX_KIND_VAL(SCX_EXIT_UNREG_BPF);
code = SCX_ECODE_VAL(SCX_ECODE_ACT_RESTART) | HOTPLUG_EXIT_RSN;
if (onlining)
code |= HOTPLUG_ONLINING;
} else {
kind = SCX_KIND_VAL(SCX_EXIT_UNREG_KERN);
code = SCX_ECODE_VAL(SCX_ECODE_ACT_RESTART) |
SCX_ECODE_VAL(SCX_ECODE_RSN_HOTPLUG);
}
if (cbs_defined)
link = bpf_map__attach_struct_ops(skel->maps.hotplug_cb_ops);
else
link = bpf_map__attach_struct_ops(skel->maps.hotplug_nocb_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
hotplug__destroy(skel);
return SCX_TEST_FAIL;
}
toggle_online_status(onlining ? 1 : 0);
while (!UEI_EXITED(skel, uei))
sched_yield();
SCX_EQ(skel->data->uei.kind, kind);
SCX_EQ(UEI_REPORT(skel, uei), code);
if (!onlining)
toggle_online_status(1);
bpf_link__destroy(link);
hotplug__destroy(skel);
return SCX_TEST_PASS;
}
static enum scx_test_status test_hotplug_attach(void)
{
struct hotplug *skel;
struct bpf_link *link;
enum scx_test_status status = SCX_TEST_PASS;
long kind, code;
SCX_ASSERT(is_cpu_online());
SCX_ASSERT(scx_hotplug_seq() > 0);
skel = SCX_OPS_OPEN(hotplug_nocb_ops, hotplug);
SCX_ASSERT(skel);
SCX_OPS_LOAD(skel, hotplug_nocb_ops, hotplug, uei);
/*
* Take the CPU offline to increment the global hotplug seq, which
* should cause attach to fail due to us setting the hotplug seq above
*/
toggle_online_status(0);
link = bpf_map__attach_struct_ops(skel->maps.hotplug_nocb_ops);
toggle_online_status(1);
SCX_ASSERT(link);
while (!UEI_EXITED(skel, uei))
sched_yield();
kind = SCX_KIND_VAL(SCX_EXIT_UNREG_KERN);
code = SCX_ECODE_VAL(SCX_ECODE_ACT_RESTART) |
SCX_ECODE_VAL(SCX_ECODE_RSN_HOTPLUG);
SCX_EQ(skel->data->uei.kind, kind);
SCX_EQ(UEI_REPORT(skel, uei), code);
bpf_link__destroy(link);
hotplug__destroy(skel);
return status;
}
static enum scx_test_status run(void *ctx)
{
#define HP_TEST(__onlining, __cbs_defined) ({ \
if (test_hotplug(__onlining, __cbs_defined) != SCX_TEST_PASS) \
return SCX_TEST_FAIL; \
})
HP_TEST(true, true);
HP_TEST(false, true);
HP_TEST(true, false);
HP_TEST(false, false);
#undef HP_TEST
return test_hotplug_attach();
}
static void cleanup(void *ctx)
{
toggle_online_status(1);
}
struct scx_test hotplug_test = {
.name = "hotplug",
.description = "Verify hotplug behavior",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&hotplug_test)
@@ -0,0 +1,15 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#ifndef __HOTPLUG_TEST_H__
#define __HOTPLUG_TEST_H__
enum hotplug_test_flags {
HOTPLUG_EXIT_RSN = 1LLU << 0,
HOTPLUG_ONLINING = 1LLU << 1,
};
#endif // # __HOTPLUG_TEST_H__
@@ -0,0 +1,53 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that verifies that we do proper counting of init, enable, etc
* callbacks.
*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
u64 init_task_cnt, exit_task_cnt, enable_cnt, disable_cnt;
u64 init_fork_cnt, init_transition_cnt;
s32 BPF_STRUCT_OPS_SLEEPABLE(cnt_init_task, struct task_struct *p,
struct scx_init_task_args *args)
{
__sync_fetch_and_add(&init_task_cnt, 1);
if (args->fork)
__sync_fetch_and_add(&init_fork_cnt, 1);
else
__sync_fetch_and_add(&init_transition_cnt, 1);
return 0;
}
void BPF_STRUCT_OPS(cnt_exit_task, struct task_struct *p)
{
__sync_fetch_and_add(&exit_task_cnt, 1);
}
void BPF_STRUCT_OPS(cnt_enable, struct task_struct *p)
{
__sync_fetch_and_add(&enable_cnt, 1);
}
void BPF_STRUCT_OPS(cnt_disable, struct task_struct *p)
{
__sync_fetch_and_add(&disable_cnt, 1);
}
SEC(".struct_ops.link")
struct sched_ext_ops init_enable_count_ops = {
.init_task = (void *) cnt_init_task,
.exit_task = (void *) cnt_exit_task,
.enable = (void *) cnt_enable,
.disable = (void *) cnt_disable,
.name = "init_enable_count",
};
@@ -0,0 +1,170 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <sched.h>
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include "scx_test.h"
#include "init_enable_count.bpf.skel.h"
#define SCHED_EXT 7
static enum scx_test_status run_test(bool global)
{
struct init_enable_count *skel;
struct bpf_link *link;
const u32 num_children = 5, num_pre_forks = 1024;
int ret, i, status;
struct sched_param param = {};
pid_t pids[num_pre_forks];
int pipe_fds[2];
SCX_FAIL_IF(pipe(pipe_fds) < 0, "Failed to create pipe");
skel = init_enable_count__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
if (!global)
skel->struct_ops.init_enable_count_ops->flags |= SCX_OPS_SWITCH_PARTIAL;
SCX_FAIL_IF(init_enable_count__load(skel), "Failed to load skel");
/*
* Fork a bunch of children before we attach the scheduler so that we
* ensure (at least in practical terms) that there are more tasks that
* transition from SCHED_OTHER -> SCHED_EXT than there are tasks that
* take the fork() path either below or in other processes.
*
* All children will block on read() on the pipe until the parent closes
* the write end after attaching the scheduler, which signals all of
* them to exit simultaneously. Auto-reap so we don't have to wait on
* them.
*/
signal(SIGCHLD, SIG_IGN);
for (i = 0; i < num_pre_forks; i++) {
pid_t pid = fork();
SCX_FAIL_IF(pid < 0, "Failed to fork child");
if (pid == 0) {
char buf;
close(pipe_fds[1]);
if (read(pipe_fds[0], &buf, 1) < 0)
exit(1);
close(pipe_fds[0]);
exit(0);
}
}
close(pipe_fds[0]);
link = bpf_map__attach_struct_ops(skel->maps.init_enable_count_ops);
SCX_FAIL_IF(!link, "Failed to attach struct_ops");
/* Signal all pre-forked children to exit. */
close(pipe_fds[1]);
signal(SIGCHLD, SIG_DFL);
bpf_link__destroy(link);
SCX_GE(skel->bss->init_task_cnt, num_pre_forks);
SCX_GE(skel->bss->exit_task_cnt, num_pre_forks);
link = bpf_map__attach_struct_ops(skel->maps.init_enable_count_ops);
SCX_FAIL_IF(!link, "Failed to attach struct_ops");
/* SCHED_EXT children */
for (i = 0; i < num_children; i++) {
pids[i] = fork();
SCX_FAIL_IF(pids[i] < 0, "Failed to fork child");
if (pids[i] == 0) {
ret = sched_setscheduler(0, SCHED_EXT, &param);
SCX_BUG_ON(ret, "Failed to set sched to sched_ext");
/*
* Reset to SCHED_OTHER for half of them. Counts for
* everything should still be the same regardless, as
* ops.disable() is invoked even if a task is still on
* SCHED_EXT before it exits.
*/
if (i % 2 == 0) {
ret = sched_setscheduler(0, SCHED_OTHER, &param);
SCX_BUG_ON(ret, "Failed to reset sched to normal");
}
exit(0);
}
}
for (i = 0; i < num_children; i++) {
SCX_FAIL_IF(waitpid(pids[i], &status, 0) != pids[i],
"Failed to wait for SCX child\n");
SCX_FAIL_IF(status != 0, "SCX child %d exited with status %d\n", i,
status);
}
/* SCHED_OTHER children */
for (i = 0; i < num_children; i++) {
pids[i] = fork();
if (pids[i] == 0)
exit(0);
}
for (i = 0; i < num_children; i++) {
SCX_FAIL_IF(waitpid(pids[i], &status, 0) != pids[i],
"Failed to wait for normal child\n");
SCX_FAIL_IF(status != 0, "Normal child %d exited with status %d\n", i,
status);
}
bpf_link__destroy(link);
SCX_GE(skel->bss->init_task_cnt, 2 * num_children);
SCX_GE(skel->bss->exit_task_cnt, 2 * num_children);
if (global) {
SCX_GE(skel->bss->enable_cnt, 2 * num_children);
SCX_GE(skel->bss->disable_cnt, 2 * num_children);
} else {
SCX_EQ(skel->bss->enable_cnt, num_children);
SCX_EQ(skel->bss->disable_cnt, num_children);
}
/*
* We forked a ton of tasks before we attached the scheduler above, so
* this should be fine. Technically it could be flaky if a ton of forks
* are happening at the same time in other processes, but that should
* be exceedingly unlikely.
*/
SCX_GT(skel->bss->init_transition_cnt, skel->bss->init_fork_cnt);
SCX_GE(skel->bss->init_fork_cnt, 2 * num_children);
init_enable_count__destroy(skel);
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
enum scx_test_status status;
status = run_test(true);
if (status != SCX_TEST_PASS)
return status;
return run_test(false);
}
struct scx_test init_enable_count = {
.name = "init_enable_count",
.description = "Verify we correctly count the occurrences of init, "
"enable, etc callbacks.",
.run = run,
};
REGISTER_SCX_TEST(&init_enable_count)
@@ -0,0 +1,168 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler with every callback defined.
*
* This scheduler defines every callback.
*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
#define DSQ_ID 0
s32 BPF_STRUCT_OPS(maximal_select_cpu, struct task_struct *p, s32 prev_cpu,
u64 wake_flags)
{
return prev_cpu;
}
void BPF_STRUCT_OPS(maximal_enqueue, struct task_struct *p, u64 enq_flags)
{
scx_bpf_dsq_insert(p, DSQ_ID, SCX_SLICE_DFL, enq_flags);
}
void BPF_STRUCT_OPS(maximal_dequeue, struct task_struct *p, u64 deq_flags)
{}
void BPF_STRUCT_OPS(maximal_dispatch, s32 cpu, struct task_struct *prev)
{
scx_bpf_dsq_move_to_local(DSQ_ID, 0);
}
void BPF_STRUCT_OPS(maximal_runnable, struct task_struct *p, u64 enq_flags)
{}
void BPF_STRUCT_OPS(maximal_running, struct task_struct *p)
{}
void BPF_STRUCT_OPS(maximal_stopping, struct task_struct *p, bool runnable)
{}
void BPF_STRUCT_OPS(maximal_quiescent, struct task_struct *p, u64 deq_flags)
{}
bool BPF_STRUCT_OPS(maximal_yield, struct task_struct *from,
struct task_struct *to)
{
return false;
}
bool BPF_STRUCT_OPS(maximal_core_sched_before, struct task_struct *a,
struct task_struct *b)
{
return false;
}
void BPF_STRUCT_OPS(maximal_set_weight, struct task_struct *p, u32 weight)
{}
void BPF_STRUCT_OPS(maximal_set_cpumask, struct task_struct *p,
const struct cpumask *cpumask)
{}
void BPF_STRUCT_OPS(maximal_update_idle, s32 cpu, bool idle)
{}
SEC("tp_btf/sched_switch")
int BPF_PROG(maximal_sched_switch, bool preempt, struct task_struct *prev,
struct task_struct *next, unsigned int prev_state)
{
return 0;
}
void BPF_STRUCT_OPS(maximal_cpu_online, s32 cpu)
{}
void BPF_STRUCT_OPS(maximal_cpu_offline, s32 cpu)
{}
s32 BPF_STRUCT_OPS(maximal_init_task, struct task_struct *p,
struct scx_init_task_args *args)
{
return 0;
}
void BPF_STRUCT_OPS(maximal_enable, struct task_struct *p)
{}
void BPF_STRUCT_OPS(maximal_exit_task, struct task_struct *p,
struct scx_exit_task_args *args)
{}
void BPF_STRUCT_OPS(maximal_disable, struct task_struct *p)
{}
s32 BPF_STRUCT_OPS(maximal_cgroup_init, struct cgroup *cgrp,
struct scx_cgroup_init_args *args)
{
return 0;
}
void BPF_STRUCT_OPS(maximal_cgroup_exit, struct cgroup *cgrp)
{}
s32 BPF_STRUCT_OPS(maximal_cgroup_prep_move, struct task_struct *p,
struct cgroup *from, struct cgroup *to)
{
return 0;
}
void BPF_STRUCT_OPS(maximal_cgroup_move, struct task_struct *p,
struct cgroup *from, struct cgroup *to)
{}
void BPF_STRUCT_OPS(maximal_cgroup_cancel_move, struct task_struct *p,
struct cgroup *from, struct cgroup *to)
{}
void BPF_STRUCT_OPS(maximal_cgroup_set_weight, struct cgroup *cgrp, u32 weight)
{}
void BPF_STRUCT_OPS(maximal_cgroup_set_bandwidth, struct cgroup *cgrp,
u64 period_us, u64 quota_us, u64 burst_us)
{}
s32 BPF_STRUCT_OPS_SLEEPABLE(maximal_init)
{
return scx_bpf_create_dsq(DSQ_ID, -1);
}
void BPF_STRUCT_OPS(maximal_exit, struct scx_exit_info *info)
{}
SEC(".struct_ops.link")
struct sched_ext_ops maximal_ops = {
.select_cpu = (void *) maximal_select_cpu,
.enqueue = (void *) maximal_enqueue,
.dequeue = (void *) maximal_dequeue,
.dispatch = (void *) maximal_dispatch,
.runnable = (void *) maximal_runnable,
.running = (void *) maximal_running,
.stopping = (void *) maximal_stopping,
.quiescent = (void *) maximal_quiescent,
.yield = (void *) maximal_yield,
.core_sched_before = (void *) maximal_core_sched_before,
.set_weight = (void *) maximal_set_weight,
.set_cpumask = (void *) maximal_set_cpumask,
.update_idle = (void *) maximal_update_idle,
.cpu_online = (void *) maximal_cpu_online,
.cpu_offline = (void *) maximal_cpu_offline,
.init_task = (void *) maximal_init_task,
.enable = (void *) maximal_enable,
.exit_task = (void *) maximal_exit_task,
.disable = (void *) maximal_disable,
.cgroup_init = (void *) maximal_cgroup_init,
.cgroup_exit = (void *) maximal_cgroup_exit,
.cgroup_prep_move = (void *) maximal_cgroup_prep_move,
.cgroup_move = (void *) maximal_cgroup_move,
.cgroup_cancel_move = (void *) maximal_cgroup_cancel_move,
.cgroup_set_weight = (void *) maximal_cgroup_set_weight,
.cgroup_set_bandwidth = (void *) maximal_cgroup_set_bandwidth,
.init = (void *) maximal_init,
.exit = (void *) maximal_exit,
.name = "maximal",
};
@@ -0,0 +1,57 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "maximal.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct maximal *skel;
skel = maximal__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(maximal__load(skel), "Failed to load skel");
bpf_map__set_autoattach(skel->maps.maximal_ops, false);
SCX_FAIL_IF(maximal__attach(skel), "Failed to attach skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct maximal *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.maximal_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct maximal *skel = ctx;
maximal__destroy(skel);
}
struct scx_test maximal = {
.name = "maximal",
.description = "Verify we can load a scheduler with every callback defined",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&maximal)
@@ -0,0 +1,36 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
u64 vtime_test;
void BPF_STRUCT_OPS(maybe_null_running, struct task_struct *p)
{}
void BPF_STRUCT_OPS(maybe_null_success_dispatch, s32 cpu, struct task_struct *p)
{
if (p != NULL)
vtime_test = p->scx.dsq_vtime;
}
bool BPF_STRUCT_OPS(maybe_null_success_yield, struct task_struct *from,
struct task_struct *to)
{
if (to)
bpf_printk("Yielding to %s[%d]", to->comm, to->pid);
return false;
}
SEC(".struct_ops.link")
struct sched_ext_ops maybe_null_success = {
.dispatch = (void *) maybe_null_success_dispatch,
.yield = (void *) maybe_null_success_yield,
.enable = (void *) maybe_null_running,
.name = "minimal",
};
@@ -0,0 +1,49 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "maybe_null.bpf.skel.h"
#include "maybe_null_fail_dsp.bpf.skel.h"
#include "maybe_null_fail_yld.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status run(void *ctx)
{
struct maybe_null *skel;
struct maybe_null_fail_dsp *fail_dsp;
struct maybe_null_fail_yld *fail_yld;
skel = maybe_null__open_and_load();
if (!skel) {
SCX_ERR("Failed to open and load maybe_null skel");
return SCX_TEST_FAIL;
}
maybe_null__destroy(skel);
fail_dsp = maybe_null_fail_dsp__open_and_load();
if (fail_dsp) {
maybe_null_fail_dsp__destroy(fail_dsp);
SCX_ERR("Should failed to open and load maybe_null_fail_dsp skel");
return SCX_TEST_FAIL;
}
fail_yld = maybe_null_fail_yld__open_and_load();
if (fail_yld) {
maybe_null_fail_yld__destroy(fail_yld);
SCX_ERR("Should failed to open and load maybe_null_fail_yld skel");
return SCX_TEST_FAIL;
}
return SCX_TEST_PASS;
}
struct scx_test maybe_null = {
.name = "maybe_null",
.description = "Verify if PTR_MAYBE_NULL works for .dispatch",
.run = run,
};
REGISTER_SCX_TEST(&maybe_null)
@@ -0,0 +1,25 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
u64 vtime_test;
void BPF_STRUCT_OPS(maybe_null_running, struct task_struct *p)
{}
void BPF_STRUCT_OPS(maybe_null_fail_dispatch, s32 cpu, struct task_struct *p)
{
vtime_test = p->scx.dsq_vtime;
}
SEC(".struct_ops.link")
struct sched_ext_ops maybe_null_fail = {
.dispatch = (void *) maybe_null_fail_dispatch,
.enable = (void *) maybe_null_running,
.name = "maybe_null_fail_dispatch",
};
@@ -0,0 +1,28 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
u64 vtime_test;
void BPF_STRUCT_OPS(maybe_null_running, struct task_struct *p)
{}
bool BPF_STRUCT_OPS(maybe_null_fail_yield, struct task_struct *from,
struct task_struct *to)
{
bpf_printk("Yielding to %s[%d]", to->comm, to->pid);
return false;
}
SEC(".struct_ops.link")
struct sched_ext_ops maybe_null_fail = {
.yield = (void *) maybe_null_fail_yield,
.enable = (void *) maybe_null_running,
.name = "maybe_null_fail_yield",
};
@@ -0,0 +1,21 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A completely minimal scheduler.
*
* This scheduler defines the absolute minimal set of struct sched_ext_ops
* fields: its name. It should _not_ fail to be loaded, and can be used to
* exercise the default scheduling paths in ext.c.
*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
SEC(".struct_ops.link")
struct sched_ext_ops minimal_ops = {
.name = "minimal",
};
@@ -0,0 +1,58 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "minimal.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct minimal *skel;
skel = minimal__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(minimal__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct minimal *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.minimal_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
return SCX_TEST_FAIL;
}
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct minimal *skel = ctx;
minimal__destroy(skel);
}
struct scx_test minimal = {
.name = "minimal",
.description = "Verify we can load a fully minimal scheduler",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&minimal)
@@ -0,0 +1,65 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
*
* Exercise tick dependency transitions between infinite and finite slices.
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
const volatile s32 test_cpu;
bool finite_phase;
u64 nr_inf_running;
u64 nr_finite_running;
u64 nr_finite_ticks;
UEI_DEFINE(uei);
s32 BPF_STRUCT_OPS(nohz_tick_select_cpu, struct task_struct *p, s32 prev_cpu,
u64 wake_flags)
{
return prev_cpu;
}
void BPF_STRUCT_OPS(nohz_tick_enqueue, struct task_struct *p, u64 enq_flags)
{
u64 slice = finite_phase ? 1000000ULL : SCX_SLICE_INF;
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, slice, enq_flags);
if (enq_flags & SCX_ENQ_LAST)
scx_bpf_kick_cpu(test_cpu, SCX_KICK_IDLE);
}
void BPF_STRUCT_OPS(nohz_tick_running, struct task_struct *p)
{
if (bpf_get_smp_processor_id() != test_cpu)
return;
if (finite_phase)
__sync_fetch_and_add(&nr_finite_running, 1);
else
__sync_fetch_and_add(&nr_inf_running, 1);
}
void BPF_STRUCT_OPS(nohz_tick_tick, struct task_struct *p)
{
if (bpf_get_smp_processor_id() == test_cpu && finite_phase)
__sync_fetch_and_add(&nr_finite_ticks, 1);
}
void BPF_STRUCT_OPS(nohz_tick_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops nohz_tick_ops = {
.select_cpu = (void *)nohz_tick_select_cpu,
.enqueue = (void *)nohz_tick_enqueue,
.running = (void *)nohz_tick_running,
.tick = (void *)nohz_tick_tick,
.exit = (void *)nohz_tick_exit,
.name = "nohz_tick",
.timeout_ms = 1000U,
};
@@ -0,0 +1,347 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
*
* Validate that a finite-slice EXT task restarts the scheduler tick when it
* follows an infinite-slice EXT task and an idle interval on a NOHZ_FULL CPU.
*/
#define _GNU_SOURCE
#include <bpf/bpf.h>
#include <errno.h>
#include <sched.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <sys/wait.h>
#include <unistd.h>
#include <scx/common.h>
#include "nohz_tick.bpf.skel.h"
#include "scx_test.h"
#ifndef SCHED_EXT
#define SCHED_EXT 7
#endif
#define MIN_FINITE_TICKS 3
#define PHASE_TIMEOUT_MS 1000
struct nohz_tick_ctx {
struct nohz_tick *skel;
cpu_set_t original_mask;
int test_cpu;
};
static int first_allowed_cpu(const cpu_set_t *mask, int first, int last)
{
int cpu;
for (cpu = first; cpu <= last && cpu < CPU_SETSIZE; cpu++)
if (CPU_ISSET(cpu, mask))
return cpu;
return -1;
}
static int find_nohz_full_cpu(const cpu_set_t *allowed)
{
char buf[4096], *cur, *end;
FILE *file;
file = fopen("/sys/devices/system/cpu/nohz_full", "r");
if (!file)
return -1;
if (!fgets(buf, sizeof(buf), file)) {
fclose(file);
return -1;
}
fclose(file);
cur = buf;
while (*cur) {
long first, last;
int cpu;
while (*cur == ' ' || *cur == '\t' || *cur == ',')
cur++;
if (*cur < '0' || *cur > '9')
break;
errno = 0;
first = strtol(cur, &end, 10);
if (errno || end == cur || first < 0 || first >= CPU_SETSIZE)
return -1;
cur = end;
last = first;
if (*cur == '-') {
cur++;
errno = 0;
last = strtol(cur, &end, 10);
if (errno || end == cur || last < first)
return -1;
cur = end;
}
cpu = first_allowed_cpu(allowed, first, last);
if (cpu >= 0)
return cpu;
}
return -1;
}
static pid_t start_worker(int cpu)
{
struct sched_param param = {};
cpu_set_t mask;
pid_t parent;
pid_t pid;
parent = getpid();
pid = fork();
if (pid != 0)
return pid;
if (prctl(PR_SET_PDEATHSIG, SIGKILL) || getppid() != parent)
_exit(1);
/*
* Become EXT before touching the target so it stays idle until wakeup.
*/
if (sched_setscheduler(0, SCHED_EXT, &param))
_exit(1);
CPU_ZERO(&mask);
CPU_SET(cpu, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask))
_exit(1);
for (;;)
asm volatile("" ::: "memory");
}
static void stop_worker(pid_t pid)
{
if (pid <= 0)
return;
kill(pid, SIGKILL);
waitpid(pid, NULL, 0);
}
static int pause_worker(pid_t pid)
{
int status;
if (kill(pid, SIGSTOP))
return -errno;
if (waitpid(pid, &status, WUNTRACED) != pid)
return -errno;
if (!WIFSTOPPED(status))
return -ECHILD;
return 0;
}
static bool wait_for_counter(const u64 *counter, u64 value, int timeout_ms)
{
int elapsed;
for (elapsed = 0; elapsed < timeout_ms; elapsed++) {
if (__atomic_load_n(counter, __ATOMIC_RELAXED) >= value)
return true;
usleep(1000);
}
return false;
}
static enum scx_test_status setup(void **ctx_ptr)
{
struct nohz_tick_ctx *ctx;
cpu_set_t controller_mask;
int cpu;
ctx = calloc(1, sizeof(*ctx));
SCX_FAIL_IF(!ctx, "Failed to allocate context");
if (sched_getaffinity(0, sizeof(ctx->original_mask),
&ctx->original_mask)) {
free(ctx);
SCX_FAIL("Failed to get affinity (%d)", errno);
}
cpu = find_nohz_full_cpu(&ctx->original_mask);
if (cpu < 0) {
fprintf(stderr, "SKIP: no allowed NOHZ_FULL CPU\n");
free(ctx);
return SCX_TEST_SKIP;
}
controller_mask = ctx->original_mask;
CPU_CLR(cpu, &controller_mask);
if (CPU_COUNT(&controller_mask) == 0) {
fprintf(stderr, "SKIP: no housekeeping CPU available\n");
free(ctx);
return SCX_TEST_SKIP;
}
ctx->test_cpu = cpu;
ctx->skel = nohz_tick__open();
if (!ctx->skel) {
free(ctx);
SCX_FAIL("Failed to open skeleton");
}
SCX_ENUM_INIT(ctx->skel);
ctx->skel->rodata->test_cpu = cpu;
ctx->skel->struct_ops.nohz_tick_ops->flags |= SCX_OPS_SWITCH_PARTIAL |
SCX_OPS_ENQ_LAST;
if (nohz_tick__load(ctx->skel)) {
nohz_tick__destroy(ctx->skel);
free(ctx);
SCX_FAIL("Failed to load skeleton");
}
if (sched_setaffinity(0, sizeof(controller_mask), &controller_mask)) {
nohz_tick__destroy(ctx->skel);
free(ctx);
SCX_FAIL("Failed to move controller off CPU %d (%d)", cpu, errno);
}
*ctx_ptr = ctx;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx_ptr)
{
struct nohz_tick_ctx *ctx = ctx_ptr;
struct nohz_tick *skel = ctx->skel;
struct bpf_link *link = NULL;
enum scx_test_status status = SCX_TEST_FAIL;
pid_t finite_worker = -1;
pid_t inf_worker = -1;
u64 finite_running;
u64 finite_ticks;
int ret;
link = bpf_map__attach_struct_ops(skel->maps.nohz_tick_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
goto out;
}
/*
* Establish SCX_RQ_CAN_STOP_TICK with an infinite-slice task.
*/
inf_worker = start_worker(ctx->test_cpu);
if (inf_worker < 0) {
SCX_ERR("Failed to start infinite-slice worker (%d)", errno);
goto out;
}
if (!wait_for_counter(&skel->bss->nr_inf_running, 1,
PHASE_TIMEOUT_MS)) {
SCX_ERR("Infinite-slice worker was not scheduled");
goto out;
}
/* Block without exiting so the rq retains the infinite-slice state. */
ret = pause_worker(inf_worker);
if (ret) {
SCX_ERR("Failed to stop infinite-slice worker (%d)", ret);
goto out;
}
/* Let the target enter idle with its tick stopped. */
usleep(100000);
/*
* The next EXT task receives a finite slice and must restart the tick.
*/
__atomic_store_n(&skel->bss->finite_phase, true, __ATOMIC_RELEASE);
finite_worker = start_worker(ctx->test_cpu);
if (finite_worker < 0) {
SCX_ERR("Failed to start finite-slice worker (%d)", errno);
goto out;
}
if (!wait_for_counter(&skel->bss->nr_finite_running, 1,
PHASE_TIMEOUT_MS)) {
SCX_ERR("Finite-slice worker was not scheduled");
goto out;
}
if (!wait_for_counter(&skel->bss->nr_finite_ticks, MIN_FINITE_TICKS,
PHASE_TIMEOUT_MS)) {
SCX_ERR("Finite-slice worker received only %llu scheduler ticks",
(unsigned long long)skel->bss->nr_finite_ticks);
goto out;
}
stop_worker(finite_worker);
finite_worker = -1;
/*
* Leave the CPU idle after a finite-slice task. The next finite-slice
* task must restart the tick even though the slice type is unchanged.
*/
usleep(100000);
finite_running = __atomic_load_n(&skel->bss->nr_finite_running,
__ATOMIC_RELAXED);
finite_ticks = __atomic_load_n(&skel->bss->nr_finite_ticks,
__ATOMIC_RELAXED);
finite_worker = start_worker(ctx->test_cpu);
if (finite_worker < 0) {
SCX_ERR("Failed to start second finite-slice worker (%d)", errno);
goto out;
}
if (!wait_for_counter(&skel->bss->nr_finite_running,
finite_running + 1, PHASE_TIMEOUT_MS)) {
SCX_ERR("Second finite-slice worker was not scheduled");
goto out;
}
if (!wait_for_counter(&skel->bss->nr_finite_ticks,
finite_ticks + MIN_FINITE_TICKS,
PHASE_TIMEOUT_MS)) {
SCX_ERR("Second finite-slice worker received only %llu scheduler ticks",
(unsigned long long)(skel->bss->nr_finite_ticks -
finite_ticks));
goto out;
}
if (skel->data->uei.kind != EXIT_KIND(SCX_EXIT_NONE)) {
SCX_ERR("Scheduler exited unexpectedly (kind=%llu code=%lld)",
(unsigned long long)skel->data->uei.kind,
(long long)skel->data->uei.exit_code);
goto out;
}
fprintf(stderr, "CPU %d received %llu finite-slice ticks\n",
ctx->test_cpu,
(unsigned long long)skel->bss->nr_finite_ticks);
status = SCX_TEST_PASS;
out:
stop_worker(finite_worker);
stop_worker(inf_worker);
if (link)
bpf_link__destroy(link);
return status;
}
static void cleanup(void *ctx_ptr)
{
struct nohz_tick_ctx *ctx = ctx_ptr;
sched_setaffinity(0, sizeof(ctx->original_mask), &ctx->original_mask);
nohz_tick__destroy(ctx->skel);
free(ctx);
}
struct scx_test nohz_tick = {
.name = "nohz_tick",
.description = "Verify finite EXT slices restart the NOHZ_FULL tick",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&nohz_tick)
@@ -0,0 +1,39 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Verify that context-sensitive SCX kfuncs (even "unlocked" ones) are
* restricted to only SCX struct_ops programs. Non-SCX struct_ops programs,
* such as TCP congestion control programs, should be rejected by the BPF
* verifier when attempting to call these kfuncs.
*
* Copyright (C) 2026 Ching-Chun (Jim) Huang <jserv@ccns.ncku.edu.tw>
* Copyright (C) 2026 Cheng-Yang Chou <yphbchou0911@gmail.com>
*/
#include <scx/common.bpf.h>
SEC("struct_ops/ssthresh")
__u32 BPF_PROG(tcp_ca_ssthresh, struct sock *sk)
{
/*
* This call should be rejected by the verifier because this is a
* TCP congestion control program (non-SCX struct_ops).
*/
scx_bpf_kick_cpu(0, 0);
return 2;
}
SEC("struct_ops/cong_avoid")
void BPF_PROG(tcp_ca_cong_avoid, struct sock *sk, __u32 ack, __u32 acked) {}
SEC("struct_ops/undo_cwnd")
__u32 BPF_PROG(tcp_ca_undo_cwnd, struct sock *sk) { return 2; }
SEC(".struct_ops")
struct tcp_congestion_ops tcp_non_scx_ca = {
.ssthresh = (void *)tcp_ca_ssthresh,
.cong_avoid = (void *)tcp_ca_cong_avoid,
.undo_cwnd = (void *)tcp_ca_undo_cwnd,
.name = "tcp_kfunc_deny",
};
char _license[] SEC("license") = "GPL";
@@ -0,0 +1,47 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Verify that context-sensitive SCX kfuncs (even "unlocked" ones) are
* restricted to only SCX struct_ops programs. Non-SCX struct_ops programs,
* such as TCP congestion control programs, should be rejected by the BPF
* verifier when attempting to call these kfuncs.
*
* Copyright (C) 2026 Ching-Chun (Jim) Huang <jserv@ccns.ncku.edu.tw>
* Copyright (C) 2026 Cheng-Yang Chou <yphbchou0911@gmail.com>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include "non_scx_kfunc_deny.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status run(void *ctx)
{
struct non_scx_kfunc_deny *skel;
int err;
skel = non_scx_kfunc_deny__open();
if (!skel) {
SCX_ERR("Failed to open skel");
return SCX_TEST_FAIL;
}
err = non_scx_kfunc_deny__load(skel);
non_scx_kfunc_deny__destroy(skel);
if (err == 0) {
SCX_ERR("non-SCX BPF program loaded when it should have been rejected");
return SCX_TEST_FAIL;
}
return SCX_TEST_PASS;
}
struct scx_test non_scx_kfunc_deny = {
.name = "non_scx_kfunc_deny",
.description = "Verify that non-SCX struct_ops programs cannot call SCX kfuncs",
.run = run,
};
REGISTER_SCX_TEST(&non_scx_kfunc_deny)
@@ -0,0 +1,111 @@
// SPDX-License-Identifier: GPL-2.0
/*
* A scheduler that validates the behavior of the NUMA-aware
* functionalities.
*
* The scheduler creates a separate DSQ for each NUMA node, ensuring tasks
* are exclusively processed by CPUs within their respective nodes. Idle
* CPUs are selected only within the same node, so task migration can only
* occurs between CPUs belonging to the same node.
*
* Copyright (c) 2025 Andrea Righi <arighi@nvidia.com>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
const volatile unsigned int __COMPAT_SCX_PICK_IDLE_IN_NODE;
static bool is_cpu_idle(s32 cpu, int node)
{
const struct cpumask *idle_cpumask;
bool idle;
idle_cpumask = __COMPAT_scx_bpf_get_idle_cpumask_node(node);
idle = bpf_cpumask_test_cpu(cpu, idle_cpumask);
scx_bpf_put_cpumask(idle_cpumask);
return idle;
}
s32 BPF_STRUCT_OPS(numa_select_cpu,
struct task_struct *p, s32 prev_cpu, u64 wake_flags)
{
s32 task_cpu = scx_bpf_task_cpu(p);
int node = __COMPAT_scx_bpf_cpu_node(task_cpu);
s32 cpu;
/*
* We could just use __COMPAT_scx_bpf_pick_any_cpu_node() here,
* since it already tries to pick an idle CPU within the node
* first, but let's use both functions for better testing coverage.
*/
cpu = __COMPAT_scx_bpf_pick_idle_cpu_node(p->cpus_ptr, node,
__COMPAT_SCX_PICK_IDLE_IN_NODE);
if (cpu < 0)
cpu = __COMPAT_scx_bpf_pick_any_cpu_node(p->cpus_ptr, node,
__COMPAT_SCX_PICK_IDLE_IN_NODE);
/*
* @task_cpu may be outside of p->cpus_ptr if @p's affinity
* changed while it was sleeping. This means it's possible for
* p->cpus_ptr to not include any CPUs from @node.
* If we failed to find a cpu in @node, check if @task_cpu
* is outside of p->cpus_ptr and just return @prev_cpu if it is.
*/
if (cpu < 0 && !bpf_cpumask_test_cpu(task_cpu, p->cpus_ptr))
return prev_cpu;
if (is_cpu_idle(cpu, node))
scx_bpf_error("CPU %d should be marked as busy", cpu);
if (__COMPAT_scx_bpf_cpu_node(cpu) != node)
scx_bpf_error("CPU %d should be in node %d", cpu, node);
return cpu;
}
void BPF_STRUCT_OPS(numa_enqueue, struct task_struct *p, u64 enq_flags)
{
int node = __COMPAT_scx_bpf_cpu_node(scx_bpf_task_cpu(p));
scx_bpf_dsq_insert(p, node, SCX_SLICE_DFL, enq_flags);
}
void BPF_STRUCT_OPS(numa_dispatch, s32 cpu, struct task_struct *prev)
{
int node = __COMPAT_scx_bpf_cpu_node(cpu);
scx_bpf_dsq_move_to_local(node, 0);
}
s32 BPF_STRUCT_OPS_SLEEPABLE(numa_init)
{
int node, err;
bpf_for(node, 0, __COMPAT_scx_bpf_nr_node_ids()) {
err = scx_bpf_create_dsq(node, node);
if (err)
return err;
}
return 0;
}
void BPF_STRUCT_OPS(numa_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops numa_ops = {
.select_cpu = (void *)numa_select_cpu,
.enqueue = (void *)numa_enqueue,
.dispatch = (void *)numa_dispatch,
.init = (void *)numa_init,
.exit = (void *)numa_exit,
.name = "numa",
};
@@ -0,0 +1,59 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025 Andrea Righi <arighi@nvidia.com>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "numa.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct numa *skel;
skel = numa__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
skel->rodata->__COMPAT_SCX_PICK_IDLE_IN_NODE = SCX_PICK_IDLE_IN_NODE;
skel->struct_ops.numa_ops->flags = SCX_OPS_BUILTIN_IDLE_PER_NODE;
SCX_FAIL_IF(numa__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct numa *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.numa_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
/* Just sleeping is fine, plenty of scheduling events happening */
sleep(1);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_NONE));
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct numa *skel = ctx;
numa__destroy(skel);
}
struct scx_test numa = {
.name = "numa",
.description = "Verify NUMA-aware functionalities",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&numa)
@@ -0,0 +1,251 @@
// SPDX-License-Identifier: GPL-2.0
/*
* A BPF program for testing DSQ operations and peek in particular.
*
* Copyright (c) 2025 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu>
*/
#include <scx/common.bpf.h>
#include <scx/compat.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei); /* Error handling */
#define MAX_SAMPLES 100
#define MAX_CPUS 512
#define DSQ_POOL_SIZE 8
int max_samples = MAX_SAMPLES;
int max_cpus = MAX_CPUS;
int dsq_pool_size = DSQ_POOL_SIZE;
/* Global variables to store test results */
int dsq_peek_result1 = -1;
long dsq_inserted_pid = -1;
int insert_test_cpu = -1; /* Set to the cpu that performs the test */
long dsq_peek_result2 = -1;
long dsq_peek_result2_pid = -1;
long dsq_peek_result2_expected = -1;
int test_dsq_id = 1234; /* Use a simple ID like create_dsq example */
int real_dsq_id = 1235; /* DSQ for normal operation */
int enqueue_count = -1;
int dispatch_count = -1;
bool debug_ksym_exists;
/* DSQ pool for stress testing */
int dsq_pool_base_id = 2000;
int phase1_complete = -1;
long total_peek_attempts = -1;
long successful_peeks = -1;
/* BPF map for sharing peek results with userspace */
struct {
__uint(type, BPF_MAP_TYPE_ARRAY);
__uint(max_entries, MAX_SAMPLES);
__type(key, u32);
__type(value, long);
} peek_results SEC(".maps");
static int get_random_dsq_id(void)
{
u64 time = bpf_ktime_get_ns();
return dsq_pool_base_id + (time % DSQ_POOL_SIZE);
}
static void record_peek_result(long pid)
{
u32 slot_key;
long *slot_pid_ptr;
u32 ix;
if (pid <= 0)
return;
/* Find an empty slot or one with the same PID */
bpf_for(ix, 0, 10) {
slot_key = ((u64)pid + ix) % MAX_SAMPLES;
slot_pid_ptr = bpf_map_lookup_elem(&peek_results, &slot_key);
if (!slot_pid_ptr)
continue;
if (*slot_pid_ptr == -1 || *slot_pid_ptr == pid) {
*slot_pid_ptr = pid;
break;
}
}
}
/* Scan all DSQs in the pool and try to move a task to local */
static int scan_dsq_pool(void)
{
struct task_struct *task;
int moved = 0;
int i;
bpf_for(i, 0, DSQ_POOL_SIZE) {
int dsq_id = dsq_pool_base_id + i;
total_peek_attempts++;
task = __COMPAT_scx_bpf_dsq_peek(dsq_id);
if (task) {
successful_peeks++;
record_peek_result(task->pid);
/* Try to move this task to local */
if (!moved && scx_bpf_dsq_move_to_local(dsq_id, 0)) {
moved = 1;
break;
}
}
}
return moved;
}
/* Struct_ops scheduler for testing DSQ peek operations */
void BPF_STRUCT_OPS(peek_dsq_enqueue, struct task_struct *p, u64 enq_flags)
{
struct task_struct *peek_result;
int last_insert_test_cpu, cpu;
enqueue_count++;
cpu = bpf_get_smp_processor_id();
last_insert_test_cpu = __sync_val_compare_and_swap(&insert_test_cpu, -1, cpu);
/* Phase 1: Simple insert-then-peek test (only on first task) */
if (last_insert_test_cpu == -1) {
bpf_printk("peek_dsq_enqueue beginning phase 1 peek test on cpu %d", cpu);
/* Test 1: Peek empty DSQ - should return NULL */
peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id);
dsq_peek_result1 = (long)peek_result; /* Should be 0 (NULL) */
/* Test 2: Insert task into test DSQ for testing in dispatch callback */
dsq_inserted_pid = p->pid;
scx_bpf_dsq_insert(p, test_dsq_id, 0, enq_flags);
dsq_peek_result2_expected = (long)p; /* Expected the task we just inserted */
} else if (!phase1_complete) {
/* Still in phase 1, use real DSQ */
scx_bpf_dsq_insert(p, real_dsq_id, 0, enq_flags);
} else {
/* Phase 2: Random DSQ insertion for stress testing */
int random_dsq_id = get_random_dsq_id();
scx_bpf_dsq_insert(p, random_dsq_id, 0, enq_flags);
}
}
void BPF_STRUCT_OPS(peek_dsq_dispatch, s32 cpu, struct task_struct *prev)
{
dispatch_count++;
/* Phase 1: Complete the simple peek test if we inserted a task but
* haven't tested peek yet
*/
if (insert_test_cpu == cpu && dsq_peek_result2 == -1) {
struct task_struct *peek_result;
bpf_printk("peek_dsq_dispatch completing phase 1 peek test on cpu %d", cpu);
/* Test 3: Peek DSQ after insert - should return the task we inserted */
peek_result = __COMPAT_scx_bpf_dsq_peek(test_dsq_id);
/* Store the PID of the peeked task for comparison */
dsq_peek_result2 = (long)peek_result;
dsq_peek_result2_pid = peek_result ? peek_result->pid : -1;
/* Now consume the task since we've peeked at it */
scx_bpf_dsq_move_to_local(test_dsq_id, 0);
/* Mark phase 1 as complete */
phase1_complete = 1;
bpf_printk("Phase 1 complete, starting phase 2 stress testing");
} else if (!phase1_complete) {
/* Still in phase 1, use real DSQ */
scx_bpf_dsq_move_to_local(real_dsq_id, 0);
} else {
/* Phase 2: Scan all DSQs in the pool and try to move a task */
if (!scan_dsq_pool()) {
/* No tasks found in DSQ pool, fall back to real DSQ */
scx_bpf_dsq_move_to_local(real_dsq_id, 0);
}
}
}
s32 BPF_STRUCT_OPS_SLEEPABLE(peek_dsq_init)
{
s32 err;
int i;
/* Always set debug values so we can see which version we're using */
debug_ksym_exists = bpf_ksym_exists(scx_bpf_dsq_peek) ? 1 : 0;
/* Initialize state first */
insert_test_cpu = -1;
enqueue_count = 0;
dispatch_count = 0;
phase1_complete = 0;
total_peek_attempts = 0;
successful_peeks = 0;
/* Create the test and real DSQs */
err = scx_bpf_create_dsq(test_dsq_id, -1);
if (err) {
scx_bpf_error("Failed to create DSQ %d: %d", test_dsq_id, err);
return err;
}
err = scx_bpf_create_dsq(real_dsq_id, -1);
if (err) {
scx_bpf_error("Failed to create DSQ %d: %d", real_dsq_id, err);
return err;
}
/* Create the DSQ pool for stress testing */
bpf_for(i, 0, DSQ_POOL_SIZE) {
int dsq_id = dsq_pool_base_id + i;
err = scx_bpf_create_dsq(dsq_id, -1);
if (err) {
scx_bpf_error("Failed to create DSQ pool entry %d: %d", dsq_id, err);
return err;
}
}
/* Initialize the peek results map */
bpf_for(i, 0, MAX_SAMPLES) {
u32 key = i;
long pid = -1;
bpf_map_update_elem(&peek_results, &key, &pid, BPF_ANY);
}
return 0;
}
void BPF_STRUCT_OPS(peek_dsq_exit, struct scx_exit_info *ei)
{
int i;
/* Destroy the primary DSQs */
scx_bpf_destroy_dsq(test_dsq_id);
scx_bpf_destroy_dsq(real_dsq_id);
/* Destroy the DSQ pool */
bpf_for(i, 0, DSQ_POOL_SIZE) {
int dsq_id = dsq_pool_base_id + i;
scx_bpf_destroy_dsq(dsq_id);
}
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops peek_dsq_ops = {
.enqueue = (void *)peek_dsq_enqueue,
.dispatch = (void *)peek_dsq_dispatch,
.init = (void *)peek_dsq_init,
.exit = (void *)peek_dsq_exit,
.name = "peek_dsq",
};
@@ -0,0 +1,224 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Test for DSQ operations including create, destroy, and peek operations.
*
* Copyright (c) 2025 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2025 Ryan Newton <ryan.newton@alum.mit.edu>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include <pthread.h>
#include <string.h>
#include <sched.h>
#include "peek_dsq.bpf.skel.h"
#include "scx_test.h"
#define NUM_WORKERS 4
static bool workload_running = true;
static pthread_t workload_threads[NUM_WORKERS];
/**
* Background workload thread that sleeps and wakes rapidly to exercise
* the scheduler's enqueue operations and ensure DSQ operations get tested.
*/
static void *workload_thread_fn(void *arg)
{
while (workload_running) {
/* Sleep for a very short time to trigger scheduler activity */
usleep(1000); /* 1ms sleep */
/* Yield to ensure we go through the scheduler */
sched_yield();
}
return NULL;
}
static enum scx_test_status setup(void **ctx)
{
struct peek_dsq *skel;
skel = peek_dsq__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(peek_dsq__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static int print_observed_pids(struct bpf_map *map, int max_samples, const char *dsq_name)
{
long count = 0;
printf("Observed %s DSQ peek pids:\n", dsq_name);
for (int i = 0; i < max_samples; i++) {
long pid;
int err;
err = bpf_map_lookup_elem(bpf_map__fd(map), &i, &pid);
if (err == 0) {
if (pid == 0) {
printf(" Sample %d: NULL peek\n", i);
} else if (pid > 0) {
printf(" Sample %d: pid %ld\n", i, pid);
count++;
}
} else {
printf(" Sample %d: error reading pid (err=%d)\n", i, err);
}
}
printf("Observed ~%ld pids in the %s DSQ(s)\n", count, dsq_name);
return count;
}
static enum scx_test_status run(void *ctx)
{
struct peek_dsq *skel = ctx;
bool failed = false;
int seconds = 3;
int err;
/* Enable the scheduler to test DSQ operations */
printf("Enabling scheduler to test DSQ insert operations...\n");
struct bpf_link *link =
bpf_map__attach_struct_ops(skel->maps.peek_dsq_ops);
if (!link) {
SCX_ERR("Failed to attach struct_ops");
return SCX_TEST_FAIL;
}
printf("Starting %d background workload threads...\n", NUM_WORKERS);
workload_running = true;
for (int i = 0; i < NUM_WORKERS; i++) {
err = pthread_create(&workload_threads[i], NULL, workload_thread_fn, NULL);
if (err) {
SCX_ERR("Failed to create workload thread %d: %s", i, strerror(err));
/* Stop already created threads */
workload_running = false;
for (int j = 0; j < i; j++)
pthread_join(workload_threads[j], NULL);
bpf_link__destroy(link);
return SCX_TEST_FAIL;
}
}
printf("Waiting for enqueue events.\n");
sleep(seconds);
while (skel->data->enqueue_count <= 0) {
printf(".");
fflush(stdout);
sleep(1);
seconds++;
if (seconds >= 30) {
printf("\n\u2717 Timeout waiting for enqueue events\n");
/* Stop workload threads and cleanup */
workload_running = false;
for (int i = 0; i < NUM_WORKERS; i++)
pthread_join(workload_threads[i], NULL);
bpf_link__destroy(link);
return SCX_TEST_FAIL;
}
}
workload_running = false;
for (int i = 0; i < NUM_WORKERS; i++) {
err = pthread_join(workload_threads[i], NULL);
if (err) {
SCX_ERR("Failed to join workload thread %d: %s", i, strerror(err));
bpf_link__destroy(link);
return SCX_TEST_FAIL;
}
}
printf("Background workload threads stopped.\n");
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_NONE));
/* Detach the scheduler */
bpf_link__destroy(link);
printf("Enqueue/dispatch count over %d seconds: %d / %d\n", seconds,
skel->data->enqueue_count, skel->data->dispatch_count);
printf("Debug: ksym_exists=%d\n",
skel->bss->debug_ksym_exists);
/* Check DSQ insert result */
printf("DSQ insert test done on cpu: %d\n", skel->data->insert_test_cpu);
if (skel->data->insert_test_cpu != -1)
printf("\u2713 DSQ insert succeeded !\n");
else {
printf("\u2717 DSQ insert failed or not attempted\n");
failed = true;
}
/* Check DSQ peek results */
printf(" DSQ peek result 1 (before insert): %d\n",
skel->data->dsq_peek_result1);
if (skel->data->dsq_peek_result1 == 0)
printf("\u2713 DSQ peek verification success: peek returned NULL!\n");
else {
printf("\u2717 DSQ peek verification failed\n");
failed = true;
}
printf(" DSQ peek result 2 (after insert): %ld\n",
skel->data->dsq_peek_result2);
printf(" DSQ peek result 2, expected: %ld\n",
skel->data->dsq_peek_result2_expected);
if (skel->data->dsq_peek_result2 ==
skel->data->dsq_peek_result2_expected)
printf("\u2713 DSQ peek verification success: peek returned the inserted task!\n");
else {
printf("\u2717 DSQ peek verification failed\n");
failed = true;
}
printf(" Inserted test task -> pid: %ld\n", skel->data->dsq_inserted_pid);
printf(" DSQ peek result 2 -> pid: %ld\n", skel->data->dsq_peek_result2_pid);
int pid_count;
pid_count = print_observed_pids(skel->maps.peek_results,
skel->data->max_samples, "DSQ pool");
printf("Total non-null peek observations: %ld out of %ld\n",
skel->data->successful_peeks, skel->data->total_peek_attempts);
if (skel->bss->debug_ksym_exists && pid_count == 0) {
printf("\u2717 DSQ pool test failed: no successful peeks in native mode\n");
failed = true;
}
if (skel->bss->debug_ksym_exists && pid_count > 0)
printf("\u2713 DSQ pool test success: observed successful peeks in native mode\n");
if (failed)
return SCX_TEST_FAIL;
else
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct peek_dsq *skel = ctx;
if (workload_running) {
workload_running = false;
for (int i = 0; i < NUM_WORKERS; i++)
pthread_join(workload_threads[i], NULL);
}
peek_dsq__destroy(skel);
}
struct scx_test peek_dsq = {
.name = "peek_dsq",
.description =
"Test DSQ create/destroy operations and future peek functionality",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&peek_dsq)
@@ -0,0 +1,33 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that validates that we can invoke sched_ext kfuncs in
* BPF_PROG_TYPE_SYSCALL programs.
*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <scx/common.bpf.h>
UEI_DEFINE(uei);
char _license[] SEC("license") = "GPL";
SEC("syscall")
int BPF_PROG(prog_run_syscall)
{
scx_bpf_create_dsq(0, -1);
scx_bpf_exit(0xdeadbeef, "Exited from PROG_RUN");
return 0;
}
void BPF_STRUCT_OPS(prog_run_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops prog_run_ops = {
.exit = (void *) prog_run_exit,
.name = "prog_run",
};
@@ -0,0 +1,78 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <bpf/bpf.h>
#include <sched.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "prog_run.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct prog_run *skel;
skel = prog_run__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(prog_run__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct prog_run *skel = ctx;
struct bpf_link *link;
int prog_fd, err = 0;
prog_fd = bpf_program__fd(skel->progs.prog_run_syscall);
if (prog_fd < 0) {
SCX_ERR("Failed to get BPF_PROG_RUN prog");
return SCX_TEST_FAIL;
}
LIBBPF_OPTS(bpf_test_run_opts, topts);
link = bpf_map__attach_struct_ops(skel->maps.prog_run_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
close(prog_fd);
return SCX_TEST_FAIL;
}
err = bpf_prog_test_run_opts(prog_fd, &topts);
SCX_EQ(err, 0);
/* Assumes uei.kind is written last */
while (skel->data->uei.kind == EXIT_KIND(SCX_EXIT_NONE))
sched_yield();
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_UNREG_BPF));
SCX_EQ(skel->data->uei.exit_code, 0xdeadbeef);
close(prog_fd);
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct prog_run *skel = ctx;
prog_run__destroy(skel);
}
struct scx_test prog_run = {
.name = "prog_run",
.description = "Verify we can call into a scheduler with BPF_PROG_RUN, and invoke kfuncs",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&prog_run)
@@ -0,0 +1,77 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <bpf/bpf.h>
#include <pthread.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "maximal.bpf.skel.h"
#include "scx_test.h"
static struct maximal *skel;
static pthread_t threads[2];
bool force_exit = false;
static enum scx_test_status setup(void **ctx)
{
skel = maximal__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(maximal__load(skel), "Failed to load skel");
bpf_map__set_autoattach(skel->maps.maximal_ops, false);
SCX_FAIL_IF(maximal__attach(skel), "Failed to attach skel");
return SCX_TEST_PASS;
}
static void *do_reload_loop(void *arg)
{
u32 i;
for (i = 0; i < 1024 && !force_exit; i++) {
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.maximal_ops);
if (link)
bpf_link__destroy(link);
}
return NULL;
}
static enum scx_test_status run(void *ctx)
{
int err;
void *ret;
err = pthread_create(&threads[0], NULL, do_reload_loop, NULL);
SCX_FAIL_IF(err, "Failed to create thread 0");
err = pthread_create(&threads[1], NULL, do_reload_loop, NULL);
SCX_FAIL_IF(err, "Failed to create thread 1");
SCX_FAIL_IF(pthread_join(threads[0], &ret), "thread 0 failed");
SCX_FAIL_IF(pthread_join(threads[1], &ret), "thread 1 failed");
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
force_exit = true;
maximal__destroy(skel);
}
struct scx_test reload_loop = {
.name = "reload_loop",
.description = "Stress test loading and unloading schedulers repeatedly in a tight loop",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&reload_loop)
@@ -0,0 +1,23 @@
// SPDX-License-Identifier: GPL-2.0
/*
* A scheduler that verified if RT tasks can stall SCHED_EXT tasks.
*
* Copyright (c) 2025 NVIDIA Corporation.
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
void BPF_STRUCT_OPS(rt_stall_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops rt_stall_ops = {
.exit = (void *)rt_stall_exit,
.name = "rt_stall",
};
@@ -0,0 +1,293 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (c) 2025 NVIDIA Corporation.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sched.h>
#include <sys/prctl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <linux/sched.h>
#include <signal.h>
#include <bpf/bpf.h>
#include <scx/common.h>
#include "rt_stall.bpf.skel.h"
#include "scx_test.h"
#include "../kselftest.h"
#define CORE_ID 0 /* CPU to pin tasks to */
#define RUN_TIME 5 /* How long to run the test in seconds */
/* Signal the parent that setup is complete by writing to a pipe */
static void signal_ready(int fd)
{
char c = 1;
if (write(fd, &c, 1) != 1) {
perror("write to ready pipe");
exit(EXIT_FAILURE);
}
close(fd);
}
/* Wait for a child to signal readiness via a pipe */
static void wait_ready(int fd)
{
char c;
if (read(fd, &c, 1) != 1) {
perror("read from ready pipe");
exit(EXIT_FAILURE);
}
close(fd);
}
/* Simple busy-wait function for test tasks */
static void process_func(void)
{
while (1) {
/* Busy wait */
for (volatile unsigned long i = 0; i < 10000000UL; i++)
;
}
}
/* Set CPU affinity to a specific core */
static void set_affinity(int cpu)
{
cpu_set_t mask;
CPU_ZERO(&mask);
CPU_SET(cpu, &mask);
if (sched_setaffinity(0, sizeof(mask), &mask) != 0) {
perror("sched_setaffinity");
exit(EXIT_FAILURE);
}
}
/* Set task scheduling policy and priority */
static void set_sched(int policy, int priority)
{
struct sched_param param;
param.sched_priority = priority;
if (sched_setscheduler(0, policy, &param) != 0) {
perror("sched_setscheduler");
exit(EXIT_FAILURE);
}
}
/* Get process runtime from /proc/<pid>/stat */
static float get_process_runtime(int pid)
{
char path[256];
FILE *file;
long utime, stime;
int fields;
snprintf(path, sizeof(path), "/proc/%d/stat", pid);
file = fopen(path, "r");
if (file == NULL) {
perror("Failed to open stat file");
return -1;
}
/* Skip the first 13 fields and read the 14th and 15th */
fields = fscanf(file,
"%*d %*s %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu",
&utime, &stime);
fclose(file);
if (fields != 2) {
fprintf(stderr, "Failed to read stat file\n");
return -1;
}
/* Calculate the total time spent in the process */
long total_time = utime + stime;
long ticks_per_second = sysconf(_SC_CLK_TCK);
float runtime_seconds = total_time * 1.0 / ticks_per_second;
return runtime_seconds;
}
static enum scx_test_status setup(void **ctx)
{
struct rt_stall *skel;
if (!__COMPAT_struct_has_field("rq", "ext_server")) {
fprintf(stderr, "SKIP: ext DL server not supported\n");
return SCX_TEST_SKIP;
}
skel = rt_stall__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(rt_stall__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static bool sched_stress_test(bool is_ext)
{
/*
* We're expecting the EXT task to get around 5% of CPU time when
* competing with the RT task (small 1% fluctuations are expected).
*
* However, the EXT task should get at least 4% of the CPU to prove
* that the EXT deadline server is working correctly. A percentage
* less than 4% indicates a bug where RT tasks can potentially
* stall SCHED_EXT tasks, causing the test to fail.
*/
const float expected_min_ratio = 0.04; /* 4% */
const char *class_str = is_ext ? "EXT" : "FAIR";
float ext_runtime, rt_runtime, actual_ratio;
int ext_pid, rt_pid;
int ext_ready[2], rt_ready[2];
ksft_print_header();
ksft_set_plan(1);
if (pipe(ext_ready) || pipe(rt_ready)) {
perror("pipe");
ksft_exit_fail();
}
/* Create and set up a EXT task */
ext_pid = fork();
if (ext_pid == 0) {
close(ext_ready[0]);
close(rt_ready[0]);
close(rt_ready[1]);
set_affinity(CORE_ID);
signal_ready(ext_ready[1]);
process_func();
exit(0);
} else if (ext_pid < 0) {
perror("fork task");
ksft_exit_fail();
}
/* Create an RT task */
rt_pid = fork();
if (rt_pid == 0) {
close(ext_ready[0]);
close(ext_ready[1]);
close(rt_ready[0]);
set_affinity(CORE_ID);
set_sched(SCHED_FIFO, 50);
signal_ready(rt_ready[1]);
process_func();
exit(0);
} else if (rt_pid < 0) {
perror("fork for RT task");
ksft_exit_fail();
}
/*
* Wait for both children to complete their setup (affinity and
* scheduling policy) before starting the measurement window.
* This prevents flaky failures caused by the RT child's setup
* time eating into the measurement period.
*/
close(ext_ready[1]);
close(rt_ready[1]);
wait_ready(ext_ready[0]);
wait_ready(rt_ready[0]);
/* Let the processes run for the specified time */
sleep(RUN_TIME);
/* Get runtime for the EXT task */
ext_runtime = get_process_runtime(ext_pid);
if (ext_runtime == -1)
ksft_exit_fail_msg("Error getting runtime for %s task (PID %d)\n",
class_str, ext_pid);
ksft_print_msg("Runtime of %s task (PID %d) is %f seconds\n",
class_str, ext_pid, ext_runtime);
/* Get runtime for the RT task */
rt_runtime = get_process_runtime(rt_pid);
if (rt_runtime == -1)
ksft_exit_fail_msg("Error getting runtime for RT task (PID %d)\n", rt_pid);
ksft_print_msg("Runtime of RT task (PID %d) is %f seconds\n", rt_pid, rt_runtime);
/* Kill the processes */
kill(ext_pid, SIGKILL);
kill(rt_pid, SIGKILL);
waitpid(ext_pid, NULL, 0);
waitpid(rt_pid, NULL, 0);
/* Verify that the scx task got enough runtime */
actual_ratio = ext_runtime / (ext_runtime + rt_runtime);
ksft_print_msg("%s task got %.2f%% of total runtime\n",
class_str, actual_ratio * 100);
if (actual_ratio >= expected_min_ratio) {
ksft_test_result_pass("PASS: %s task got more than %.2f%% of runtime\n",
class_str, expected_min_ratio * 100);
return true;
}
ksft_test_result_fail("FAIL: %s task got less than %.2f%% of runtime\n",
class_str, expected_min_ratio * 100);
return false;
}
static enum scx_test_status run(void *ctx)
{
struct rt_stall *skel = ctx;
struct bpf_link *link = NULL;
bool res;
int i;
/*
* Test if the dl_server is working both with and without the
* sched_ext scheduler attached.
*
* This ensures all the scenarios are covered:
* - fair_server stop -> ext_server start
* - ext_server stop -> fair_server stop
*/
for (i = 0; i < 4; i++) {
bool is_ext = i % 2;
if (is_ext) {
memset(&skel->data->uei, 0, sizeof(skel->data->uei));
link = bpf_map__attach_struct_ops(skel->maps.rt_stall_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
}
res = sched_stress_test(is_ext);
if (is_ext) {
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_NONE));
bpf_link__destroy(link);
}
if (!res)
ksft_exit_fail();
}
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct rt_stall *skel = ctx;
rt_stall__destroy(skel);
}
struct scx_test rt_stall = {
.name = "rt_stall",
.description = "Verify that RT tasks cannot stall SCHED_EXT tasks",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&rt_stall)
@@ -0,0 +1,255 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
* Copyright (c) 2024 Tejun Heo <tj@kernel.org>
*/
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <libgen.h>
#include <bpf/bpf.h>
#include "scx_test.h"
const char help_fmt[] =
"The runner for sched_ext tests.\n"
"\n"
"The runner is statically linked against all testcases, and runs them all serially.\n"
"It's required for the testcases to be serial, as only a single host-wide sched_ext\n"
"scheduler may be loaded at any given time."
"\n"
"Usage: %s [-t TEST] [-s] [-l] [-q]\n"
"\n"
" -t TEST Only run tests whose name includes this string\n"
" -s Include print output for skipped tests\n"
" -l List all available tests\n"
" -q Don't print the test descriptions during run\n"
" -h Display this help and exit\n";
static volatile int exit_req;
static bool quiet, print_skipped, list;
#define MAX_SCX_TESTS 2048
static struct scx_test __scx_tests[MAX_SCX_TESTS];
static unsigned __scx_num_tests = 0;
static void sigint_handler(int simple)
{
exit_req = 1;
}
static void print_test_preamble(const struct scx_test *test, bool quiet)
{
printf("===== START =====\n");
printf("TEST: %s\n", test->name);
if (!quiet)
printf("DESCRIPTION: %s\n", test->description);
printf("OUTPUT:\n");
/*
* The tests may fork with the preamble buffered
* in the children's stdout. Flush before the test
* to avoid printing the message multiple times.
*/
fflush(stdout);
fflush(stderr);
}
static const char *status_to_result(enum scx_test_status status)
{
switch (status) {
case SCX_TEST_PASS:
case SCX_TEST_SKIP:
return "ok";
case SCX_TEST_FAIL:
return "not ok";
default:
return "<UNKNOWN>";
}
}
static void print_test_result(const struct scx_test *test,
enum scx_test_status status,
unsigned int testnum)
{
const char *result = status_to_result(status);
const char *directive = status == SCX_TEST_SKIP ? "SKIP " : "";
printf("%s %u %s # %s\n", result, testnum, test->name, directive);
printf("===== END =====\n");
}
static bool should_skip_test(const struct scx_test *test, const char * filter)
{
return !strstr(test->name, filter);
}
static enum scx_test_status run_test(const struct scx_test *test)
{
enum scx_test_status status;
void *context = NULL;
if (test->setup) {
status = test->setup(&context);
if (status != SCX_TEST_PASS)
return status;
}
status = test->run(context);
if (test->cleanup)
test->cleanup(context);
return status;
}
static bool test_valid(const struct scx_test *test)
{
if (!test) {
fprintf(stderr, "NULL test detected\n");
return false;
}
if (!test->name) {
fprintf(stderr,
"Test with no name found. Must specify test name.\n");
return false;
}
if (!test->description) {
fprintf(stderr, "Test %s requires description.\n", test->name);
return false;
}
if (!test->run) {
fprintf(stderr, "Test %s has no run() callback\n", test->name);
return false;
}
return true;
}
int main(int argc, char **argv)
{
const char *filter = NULL;
const char *failed_tests[MAX_SCX_TESTS];
const char *skipped_tests[MAX_SCX_TESTS];
unsigned testnum = 0, i;
unsigned passed = 0, skipped = 0, failed = 0;
int opt;
signal(SIGINT, sigint_handler);
signal(SIGTERM, sigint_handler);
libbpf_set_strict_mode(LIBBPF_STRICT_ALL);
while ((opt = getopt(argc, argv, "qslt:h")) != -1) {
switch (opt) {
case 'q':
quiet = true;
break;
case 's':
print_skipped = true;
break;
case 'l':
list = true;
break;
case 't':
filter = optarg;
break;
default:
fprintf(stderr, help_fmt, basename(argv[0]));
return opt != 'h';
}
}
if (optind < argc) {
fprintf(stderr, "Unexpected argument '%s'. Use -t to filter tests.\n",
argv[optind]);
return 1;
}
if (filter) {
for (i = 0; i < __scx_num_tests; i++) {
if (!should_skip_test(&__scx_tests[i], filter))
break;
}
if (i == __scx_num_tests) {
fprintf(stderr, "No tests matched filter '%s'\n", filter);
fprintf(stderr, "Available tests (use -l to list):\n");
for (i = 0; i < __scx_num_tests; i++)
fprintf(stderr, " %s\n", __scx_tests[i].name);
return 1;
}
}
for (i = 0; i < __scx_num_tests; i++) {
enum scx_test_status status;
struct scx_test *test = &__scx_tests[i];
if (exit_req)
break;
if (list) {
printf("%s\n", test->name);
if (i == (__scx_num_tests - 1))
return 0;
continue;
}
if (filter && should_skip_test(test, filter)) {
/*
* Printing the skipped tests and their preambles can
* add a lot of noise to the runner output. Printing
* this is only really useful for CI, so let's skip it
* by default.
*/
if (print_skipped) {
print_test_preamble(test, quiet);
print_test_result(test, SCX_TEST_SKIP, ++testnum);
}
continue;
}
print_test_preamble(test, quiet);
status = run_test(test);
print_test_result(test, status, ++testnum);
switch (status) {
case SCX_TEST_PASS:
passed++;
break;
case SCX_TEST_SKIP:
skipped_tests[skipped++] = test->name;
break;
case SCX_TEST_FAIL:
failed_tests[failed++] = test->name;
break;
}
}
printf("\n\n=============================\n\n");
printf("RESULTS:\n\n");
printf("PASSED: %u\n", passed);
printf("SKIPPED: %u\n", skipped);
printf("FAILED: %u\n", failed);
if (skipped > 0) {
printf("\nSkipped tests:\n");
for (i = 0; i < skipped; i++)
printf(" - %s\n", skipped_tests[i]);
}
if (failed > 0) {
printf("\nFailed tests:\n");
for (i = 0; i < failed; i++)
printf(" - %s\n", failed_tests[i]);
}
return failed > 0 ? 1 : 0;
}
void scx_test_register(struct scx_test *test)
{
SCX_BUG_ON(!test_valid(test), "Invalid test found");
SCX_BUG_ON(__scx_num_tests >= MAX_SCX_TESTS, "Maximum tests exceeded");
__scx_tests[__scx_num_tests++] = *test;
}
@@ -0,0 +1,131 @@
/* 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 __SCX_TEST_H__
#define __SCX_TEST_H__
#include <errno.h>
#include <scx/common.h>
#include <scx/compat.h>
enum scx_test_status {
SCX_TEST_PASS = 0,
SCX_TEST_SKIP,
SCX_TEST_FAIL,
};
#define EXIT_KIND(__ent) __COMPAT_ENUM_OR_ZERO("scx_exit_kind", #__ent)
struct scx_test {
/**
* name - The name of the testcase.
*/
const char *name;
/**
* description - A description of your testcase: what it tests and is
* meant to validate.
*/
const char *description;
/*
* setup - Setup the test.
* @ctx: A pointer to a context object that will be passed to run and
* cleanup.
*
* An optional callback that allows a testcase to perform setup for its
* run. A test may return SCX_TEST_SKIP to skip the run.
*/
enum scx_test_status (*setup)(void **ctx);
/*
* run - Run the test.
* @ctx: Context set in the setup() callback. If @ctx was not set in
* setup(), it is NULL.
*
* The main test. Callers should return one of:
*
* - SCX_TEST_PASS: Test passed
* - SCX_TEST_SKIP: Test should be skipped
* - SCX_TEST_FAIL: Test failed
*
* This callback must be defined.
*/
enum scx_test_status (*run)(void *ctx);
/*
* cleanup - Perform cleanup following the test
* @ctx: Context set in the setup() callback. If @ctx was not set in
* setup(), it is NULL.
*
* An optional callback that allows a test to perform cleanup after
* being run. This callback is run even if the run() callback returns
* SCX_TEST_SKIP or SCX_TEST_FAIL. It is not run if setup() returns
* SCX_TEST_SKIP or SCX_TEST_FAIL.
*/
void (*cleanup)(void *ctx);
};
void scx_test_register(struct scx_test *test);
#define REGISTER_SCX_TEST(__test) \
__attribute__((constructor)) \
static void ___scxregister##__LINE__(void) \
{ \
scx_test_register(__test); \
}
#define SCX_ERR(__fmt, ...) \
do { \
fprintf(stderr, "ERR: %s:%d\n", __FILE__, __LINE__); \
fprintf(stderr, __fmt"\n", ##__VA_ARGS__); \
} while (0)
#define SCX_FAIL(__fmt, ...) \
do { \
SCX_ERR(__fmt, ##__VA_ARGS__); \
return SCX_TEST_FAIL; \
} while (0)
#define SCX_FAIL_IF(__cond, __fmt, ...) \
do { \
if (__cond) \
SCX_FAIL(__fmt, ##__VA_ARGS__); \
} while (0)
#define SCX_GT(_x, _y) SCX_FAIL_IF((_x) <= (_y), "Expected %s > %s (%lu > %lu)", \
#_x, #_y, (u64)(_x), (u64)(_y))
#define SCX_GE(_x, _y) SCX_FAIL_IF((_x) < (_y), "Expected %s >= %s (%lu >= %lu)", \
#_x, #_y, (u64)(_x), (u64)(_y))
#define SCX_LT(_x, _y) SCX_FAIL_IF((_x) >= (_y), "Expected %s < %s (%lu < %lu)", \
#_x, #_y, (u64)(_x), (u64)(_y))
#define SCX_LE(_x, _y) SCX_FAIL_IF((_x) > (_y), "Expected %s <= %s (%lu <= %lu)", \
#_x, #_y, (u64)(_x), (u64)(_y))
#define SCX_EQ(_x, _y) SCX_FAIL_IF((_x) != (_y), "Expected %s == %s (%lu == %lu)", \
#_x, #_y, (u64)(_x), (u64)(_y))
#define SCX_ASSERT(_x) SCX_FAIL_IF(!(_x), "Expected %s to be true (%lu)", \
#_x, (u64)(_x))
#define SCX_ECODE_VAL(__ecode) ({ \
u64 __val = 0; \
bool __found = false; \
\
__found = __COMPAT_read_enum("scx_exit_code", #__ecode, &__val); \
SCX_ASSERT(__found); \
(s64)__val; \
})
#define SCX_KIND_VAL(__kind) ({ \
u64 __val = 0; \
bool __found = false; \
\
__found = __COMPAT_read_enum("scx_exit_kind", #__kind, &__val); \
SCX_ASSERT(__found); \
__val; \
})
#endif // # __SCX_TEST_H__
@@ -0,0 +1,40 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that validates the behavior of direct dispatching with a default
* select_cpu implementation.
*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
bool saw_local = false;
static bool task_is_test(const struct task_struct *p)
{
return !bpf_strncmp(p->comm, 9, "select_cpu");
}
void BPF_STRUCT_OPS(select_cpu_dfl_enqueue, struct task_struct *p,
u64 enq_flags)
{
const struct cpumask *idle_mask = scx_bpf_get_idle_cpumask();
if (task_is_test(p) &&
bpf_cpumask_test_cpu(scx_bpf_task_cpu(p), idle_mask)) {
saw_local = true;
}
scx_bpf_put_idle_cpumask(idle_mask);
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, enq_flags);
}
SEC(".struct_ops.link")
struct sched_ext_ops select_cpu_dfl_ops = {
.enqueue = (void *) select_cpu_dfl_enqueue,
.name = "select_cpu_dfl",
};
@@ -0,0 +1,97 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include "select_cpu_dfl.bpf.skel.h"
#include "scx_test.h"
#define NUM_CHILDREN 1028
struct select_cpu_dfl_ctx {
struct select_cpu_dfl *skel;
struct bpf_link *link;
};
static enum scx_test_status setup(void **ctx)
{
struct select_cpu_dfl_ctx *tctx;
tctx = malloc(sizeof(*tctx));
SCX_FAIL_IF(!tctx, "Failed to allocate test context");
tctx->link = NULL;
tctx->skel = select_cpu_dfl__open();
if (!tctx->skel) {
free(tctx);
SCX_FAIL("Failed to open");
}
SCX_ENUM_INIT(tctx->skel);
if (select_cpu_dfl__load(tctx->skel)) {
select_cpu_dfl__destroy(tctx->skel);
free(tctx);
SCX_FAIL("Failed to load skel");
}
*ctx = tctx;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct select_cpu_dfl_ctx *tctx = ctx;
pid_t pids[NUM_CHILDREN];
int i, status, nforked = 0;
tctx->link = bpf_map__attach_struct_ops(tctx->skel->maps.select_cpu_dfl_ops);
SCX_FAIL_IF(!tctx->link, "Failed to attach scheduler");
for (i = 0; i < NUM_CHILDREN; i++) {
pids[i] = fork();
if (pids[i] == 0) {
sleep(1);
exit(0);
}
if (pids[i] > 0)
nforked++;
}
for (i = 0; i < NUM_CHILDREN; i++) {
if (pids[i] <= 0)
continue;
SCX_EQ(waitpid(pids[i], &status, 0), pids[i]);
SCX_EQ(status, 0);
}
SCX_GT(nforked, 0);
SCX_ASSERT(!tctx->skel->bss->saw_local);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct select_cpu_dfl_ctx *tctx = ctx;
if (tctx->link)
bpf_link__destroy(tctx->link);
select_cpu_dfl__destroy(tctx->skel);
free(tctx);
}
struct scx_test select_cpu_dfl = {
.name = "select_cpu_dfl",
.description = "Verify the default ops.select_cpu() dispatches tasks "
"when idles cores are found, and skips ops.enqueue()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&select_cpu_dfl)
@@ -0,0 +1,89 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that validates the behavior of direct dispatching with a default
* select_cpu implementation, and with the SCX_OPS_ENQ_DFL_NO_DISPATCH ops flag
* specified.
*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
bool saw_local = false;
/* Per-task scheduling context */
struct task_ctx {
bool force_local; /* CPU changed by ops.select_cpu() */
};
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");
/* Manually specify the signature until the kfunc is added to the scx repo. */
s32 scx_bpf_select_cpu_dfl(struct task_struct *p, s32 prev_cpu, u64 wake_flags,
bool *found) __ksym;
s32 BPF_STRUCT_OPS(select_cpu_dfl_nodispatch_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
struct task_ctx *tctx;
s32 cpu;
tctx = bpf_task_storage_get(&task_ctx_stor, p, 0, 0);
if (!tctx) {
scx_bpf_error("task_ctx lookup failed");
return -ESRCH;
}
cpu = scx_bpf_select_cpu_dfl(p, prev_cpu, wake_flags,
&tctx->force_local);
return cpu;
}
void BPF_STRUCT_OPS(select_cpu_dfl_nodispatch_enqueue, struct task_struct *p,
u64 enq_flags)
{
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("task_ctx lookup failed");
return;
}
if (tctx->force_local) {
dsq_id = SCX_DSQ_LOCAL;
tctx->force_local = false;
saw_local = true;
}
scx_bpf_dsq_insert(p, dsq_id, SCX_SLICE_DFL, enq_flags);
}
s32 BPF_STRUCT_OPS(select_cpu_dfl_nodispatch_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;
}
SEC(".struct_ops.link")
struct sched_ext_ops select_cpu_dfl_nodispatch_ops = {
.select_cpu = (void *) select_cpu_dfl_nodispatch_select_cpu,
.enqueue = (void *) select_cpu_dfl_nodispatch_enqueue,
.init_task = (void *) select_cpu_dfl_nodispatch_init_task,
.name = "select_cpu_dfl_nodispatch",
};
@@ -0,0 +1,75 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "select_cpu_dfl_nodispatch.bpf.skel.h"
#include "scx_test.h"
#define NUM_CHILDREN 1028
static enum scx_test_status setup(void **ctx)
{
struct select_cpu_dfl_nodispatch *skel;
skel = select_cpu_dfl_nodispatch__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(select_cpu_dfl_nodispatch__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct select_cpu_dfl_nodispatch *skel = ctx;
struct bpf_link *link;
pid_t pids[NUM_CHILDREN];
int i, status;
link = bpf_map__attach_struct_ops(skel->maps.select_cpu_dfl_nodispatch_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
for (i = 0; i < NUM_CHILDREN; i++) {
pids[i] = fork();
if (pids[i] == 0) {
sleep(1);
exit(0);
}
}
for (i = 0; i < NUM_CHILDREN; i++) {
SCX_EQ(waitpid(pids[i], &status, 0), pids[i]);
SCX_EQ(status, 0);
}
SCX_ASSERT(skel->bss->saw_local);
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct select_cpu_dfl_nodispatch *skel = ctx;
select_cpu_dfl_nodispatch__destroy(skel);
}
struct scx_test select_cpu_dfl_nodispatch = {
.name = "select_cpu_dfl_nodispatch",
.description = "Verify behavior of scx_bpf_select_cpu_dfl() in "
"ops.select_cpu()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&select_cpu_dfl_nodispatch)
@@ -0,0 +1,41 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that validates the behavior of direct dispatching with a default
* select_cpu implementation.
*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
s32 BPF_STRUCT_OPS(select_cpu_dispatch_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
u64 dsq_id = SCX_DSQ_LOCAL;
s32 cpu = prev_cpu;
if (scx_bpf_test_and_clear_cpu_idle(cpu))
goto dispatch;
cpu = scx_bpf_pick_idle_cpu(p->cpus_ptr, 0);
if (cpu >= 0)
goto dispatch;
dsq_id = SCX_DSQ_GLOBAL;
cpu = prev_cpu;
dispatch:
scx_bpf_dsq_insert(p, dsq_id, SCX_SLICE_DFL, 0);
return cpu;
}
SEC(".struct_ops.link")
struct sched_ext_ops select_cpu_dispatch_ops = {
.select_cpu = (void *) select_cpu_dispatch_select_cpu,
.name = "select_cpu_dispatch",
.timeout_ms = 1000U,
};
@@ -0,0 +1,73 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "select_cpu_dispatch.bpf.skel.h"
#include "scx_test.h"
#define NUM_CHILDREN 1028
static enum scx_test_status setup(void **ctx)
{
struct select_cpu_dispatch *skel;
skel = select_cpu_dispatch__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(select_cpu_dispatch__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct select_cpu_dispatch *skel = ctx;
struct bpf_link *link;
pid_t pids[NUM_CHILDREN];
int i, status;
link = bpf_map__attach_struct_ops(skel->maps.select_cpu_dispatch_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
for (i = 0; i < NUM_CHILDREN; i++) {
pids[i] = fork();
if (pids[i] == 0) {
sleep(1);
exit(0);
}
}
for (i = 0; i < NUM_CHILDREN; i++) {
SCX_EQ(waitpid(pids[i], &status, 0), pids[i]);
SCX_EQ(status, 0);
}
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct select_cpu_dispatch *skel = ctx;
select_cpu_dispatch__destroy(skel);
}
struct scx_test select_cpu_dispatch = {
.name = "select_cpu_dispatch",
.description = "Test direct dispatching to built-in DSQs from "
"ops.select_cpu()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&select_cpu_dispatch)
@@ -0,0 +1,37 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that validates the behavior of direct dispatching with a default
* select_cpu implementation.
*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
s32 BPF_STRUCT_OPS(select_cpu_dispatch_bad_dsq_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
/* Dispatching to a random DSQ should fail. */
scx_bpf_dsq_insert(p, 0xcafef00d, SCX_SLICE_DFL, 0);
return prev_cpu;
}
void BPF_STRUCT_OPS(select_cpu_dispatch_bad_dsq_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops select_cpu_dispatch_bad_dsq_ops = {
.select_cpu = (void *) select_cpu_dispatch_bad_dsq_select_cpu,
.exit = (void *) select_cpu_dispatch_bad_dsq_exit,
.name = "select_cpu_dispatch_bad_dsq",
.timeout_ms = 1000U,
};
@@ -0,0 +1,59 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "select_cpu_dispatch_bad_dsq.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct select_cpu_dispatch_bad_dsq *skel;
skel = select_cpu_dispatch_bad_dsq__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(select_cpu_dispatch_bad_dsq__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct select_cpu_dispatch_bad_dsq *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.select_cpu_dispatch_bad_dsq_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
sleep(1);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_ERROR));
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct select_cpu_dispatch_bad_dsq *skel = ctx;
select_cpu_dispatch_bad_dsq__destroy(skel);
}
struct scx_test select_cpu_dispatch_bad_dsq = {
.name = "select_cpu_dispatch_bad_dsq",
.description = "Verify graceful failure if we direct-dispatch to a "
"bogus DSQ in ops.select_cpu()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&select_cpu_dispatch_bad_dsq)
@@ -0,0 +1,38 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that validates the behavior of direct dispatching with a default
* select_cpu implementation.
*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
UEI_DEFINE(uei);
s32 BPF_STRUCT_OPS(select_cpu_dispatch_dbl_dsp_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
/* Dispatching twice in a row is disallowed. */
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0);
scx_bpf_dsq_insert(p, SCX_DSQ_GLOBAL, SCX_SLICE_DFL, 0);
return prev_cpu;
}
void BPF_STRUCT_OPS(select_cpu_dispatch_dbl_dsp_exit, struct scx_exit_info *ei)
{
UEI_RECORD(uei, ei);
}
SEC(".struct_ops.link")
struct sched_ext_ops select_cpu_dispatch_dbl_dsp_ops = {
.select_cpu = (void *) select_cpu_dispatch_dbl_dsp_select_cpu,
.exit = (void *) select_cpu_dispatch_dbl_dsp_exit,
.name = "select_cpu_dispatch_dbl_dsp",
.timeout_ms = 1000U,
};
@@ -0,0 +1,59 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2023 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2023 David Vernet <dvernet@meta.com>
* Copyright (c) 2023 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "select_cpu_dispatch_dbl_dsp.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct select_cpu_dispatch_dbl_dsp *skel;
skel = select_cpu_dispatch_dbl_dsp__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(select_cpu_dispatch_dbl_dsp__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct select_cpu_dispatch_dbl_dsp *skel = ctx;
struct bpf_link *link;
link = bpf_map__attach_struct_ops(skel->maps.select_cpu_dispatch_dbl_dsp_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
sleep(1);
SCX_EQ(skel->data->uei.kind, EXIT_KIND(SCX_EXIT_ERROR));
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct select_cpu_dispatch_dbl_dsp *skel = ctx;
select_cpu_dispatch_dbl_dsp__destroy(skel);
}
struct scx_test select_cpu_dispatch_dbl_dsp = {
.name = "select_cpu_dispatch_dbl_dsp",
.description = "Verify graceful failure if we dispatch twice to a "
"DSQ in ops.select_cpu()",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&select_cpu_dispatch_dbl_dsp)
@@ -0,0 +1,94 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* A scheduler that validates that enqueue flags are properly stored and
* applied at dispatch time when a task is directly dispatched from
* ops.select_cpu(). We validate this by using scx_bpf_dsq_insert_vtime(),
* and making the test a very basic vtime scheduler.
*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
* Copyright (c) 2024 Tejun Heo <tj@kernel.org>
*/
#include <scx/common.bpf.h>
char _license[] SEC("license") = "GPL";
volatile bool consumed;
static u64 vtime_now;
#define VTIME_DSQ 0
static inline bool vtime_before(u64 a, u64 b)
{
return (s64)(a - b) < 0;
}
static inline u64 task_vtime(const struct task_struct *p)
{
u64 vtime = p->scx.dsq_vtime;
if (vtime_before(vtime, vtime_now - SCX_SLICE_DFL))
return vtime_now - SCX_SLICE_DFL;
else
return vtime;
}
s32 BPF_STRUCT_OPS(select_cpu_vtime_select_cpu, struct task_struct *p,
s32 prev_cpu, u64 wake_flags)
{
s32 cpu;
cpu = scx_bpf_pick_idle_cpu(p->cpus_ptr, 0);
if (cpu >= 0)
goto ddsp;
cpu = prev_cpu;
scx_bpf_test_and_clear_cpu_idle(cpu);
ddsp:
scx_bpf_dsq_insert_vtime(p, VTIME_DSQ, SCX_SLICE_DFL, task_vtime(p), 0);
return cpu;
}
void BPF_STRUCT_OPS(select_cpu_vtime_dispatch, s32 cpu, struct task_struct *p)
{
if (scx_bpf_dsq_move_to_local(VTIME_DSQ, 0))
consumed = true;
}
void BPF_STRUCT_OPS(select_cpu_vtime_running, struct task_struct *p)
{
if (vtime_before(vtime_now, p->scx.dsq_vtime))
vtime_now = p->scx.dsq_vtime;
}
void BPF_STRUCT_OPS(select_cpu_vtime_stopping, struct task_struct *p,
bool runnable)
{
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(select_cpu_vtime_enable, struct task_struct *p)
{
scx_bpf_task_set_dsq_vtime(p, vtime_now);
}
s32 BPF_STRUCT_OPS_SLEEPABLE(select_cpu_vtime_init)
{
return scx_bpf_create_dsq(VTIME_DSQ, -1);
}
SEC(".struct_ops.link")
struct sched_ext_ops select_cpu_vtime_ops = {
.select_cpu = (void *) select_cpu_vtime_select_cpu,
.dispatch = (void *) select_cpu_vtime_dispatch,
.running = (void *) select_cpu_vtime_running,
.stopping = (void *) select_cpu_vtime_stopping,
.enable = (void *) select_cpu_vtime_enable,
.init = (void *) select_cpu_vtime_init,
.name = "select_cpu_vtime",
.timeout_ms = 1000U,
};
@@ -0,0 +1,62 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
* Copyright (c) 2024 Tejun Heo <tj@kernel.org>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include <sys/wait.h>
#include <unistd.h>
#include "select_cpu_vtime.bpf.skel.h"
#include "scx_test.h"
static enum scx_test_status setup(void **ctx)
{
struct select_cpu_vtime *skel;
skel = select_cpu_vtime__open();
SCX_FAIL_IF(!skel, "Failed to open");
SCX_ENUM_INIT(skel);
SCX_FAIL_IF(select_cpu_vtime__load(skel), "Failed to load skel");
*ctx = skel;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct select_cpu_vtime *skel = ctx;
struct bpf_link *link;
SCX_ASSERT(!skel->bss->consumed);
link = bpf_map__attach_struct_ops(skel->maps.select_cpu_vtime_ops);
SCX_FAIL_IF(!link, "Failed to attach scheduler");
sleep(1);
SCX_ASSERT(skel->bss->consumed);
bpf_link__destroy(link);
return SCX_TEST_PASS;
}
static void cleanup(void *ctx)
{
struct select_cpu_vtime *skel = ctx;
select_cpu_vtime__destroy(skel);
}
struct scx_test select_cpu_vtime = {
.name = "select_cpu_vtime",
.description = "Test doing direct vtime-dispatching from "
"ops.select_cpu(), to a non-built-in DSQ",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&select_cpu_vtime)
@@ -0,0 +1,49 @@
/* 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>
*/
#include <bpf/bpf.h>
#include <scx/common.h>
#include "scx_test.h"
static bool setup_called = false;
static bool run_called = false;
static bool cleanup_called = false;
static int context = 10;
static enum scx_test_status setup(void **ctx)
{
setup_called = true;
*ctx = &context;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
int *arg = ctx;
SCX_ASSERT(setup_called);
SCX_ASSERT(!run_called && !cleanup_called);
SCX_EQ(*arg, context);
run_called = true;
return SCX_TEST_PASS;
}
static void cleanup (void *ctx)
{
SCX_BUG_ON(!run_called || cleanup_called, "Wrong callbacks invoked");
}
struct scx_test example = {
.name = "example",
.description = "Validate the basic function of the test suite itself",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&example)
@@ -0,0 +1,480 @@
// SPDX-License-Identifier: GPL-2.0
/*
* Test to verify that total_bw value remains consistent across all CPUs
* in different BPF program states.
*
* Copyright (C) 2025 NVIDIA Corporation.
*/
#include <bpf/bpf.h>
#include <errno.h>
#include <pthread.h>
#include <scx/common.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include "minimal.bpf.skel.h"
#include "scx_test.h"
#define MAX_CPUS 512
#define STRESS_DURATION_SEC 5
struct total_bw_ctx {
struct minimal *skel;
long baseline_bw[MAX_CPUS];
int nr_cpus;
};
static void *cpu_stress_thread(void *arg)
{
volatile int i;
time_t end_time = time(NULL) + STRESS_DURATION_SEC;
while (time(NULL) < end_time)
for (i = 0; i < 1000000; i++)
;
return NULL;
}
/*
* The first enqueue on a CPU causes the DL server to start, for that
* reason run stressor threads in the hopes it schedules on all CPUs.
*/
static int run_cpu_stress(int nr_cpus)
{
pthread_t *threads;
int i, ret = 0;
threads = calloc(nr_cpus, sizeof(pthread_t));
if (!threads)
return -ENOMEM;
/* Create threads to run on each CPU */
for (i = 0; i < nr_cpus; i++) {
if (pthread_create(&threads[i], NULL, cpu_stress_thread, NULL)) {
ret = -errno;
fprintf(stderr, "Failed to create thread %d: %s\n", i, strerror(-ret));
break;
}
}
/* Wait for all threads to complete */
for (i = 0; i < nr_cpus; i++) {
if (threads[i])
pthread_join(threads[i], NULL);
}
free(threads);
return ret;
}
static int read_total_bw_values(long *bw_values, int max_cpus)
{
FILE *fp;
char line[256];
int cpu_count = 0;
fp = fopen("/sys/kernel/debug/sched/debug", "r");
if (!fp) {
SCX_ERR("Failed to open debug file");
return -1;
}
while (fgets(line, sizeof(line), fp)) {
char *bw_str = strstr(line, "total_bw");
if (bw_str) {
bw_str = strchr(bw_str, ':');
if (bw_str) {
/* Only store up to max_cpus values */
if (cpu_count < max_cpus)
bw_values[cpu_count] = atol(bw_str + 1);
cpu_count++;
}
}
}
fclose(fp);
return cpu_count;
}
/*
* Read a per-CPU dl_server param (runtime or period) from debugfs.
* Returns the value in nanoseconds, or -1 on failure.
*/
static long read_server_param(const char *server, const char *param, int cpu)
{
char path[128];
long value = -1;
FILE *fp;
snprintf(path, sizeof(path),
"/sys/kernel/debug/sched/%s_server/cpu%d/%s",
server, cpu, param);
fp = fopen(path, "r");
if (!fp)
return -1;
if (fscanf(fp, "%ld", &value) != 1)
value = -1;
fclose(fp);
return value;
}
/*
* Write a per-CPU dl_server param to debugfs. Returns 0 on success.
*/
static int write_server_param(const char *server, const char *param,
int cpu, long value)
{
char path[128];
FILE *fp;
int ret = 0;
snprintf(path, sizeof(path),
"/sys/kernel/debug/sched/%s_server/cpu%d/%s",
server, cpu, param);
fp = fopen(path, "w");
if (!fp)
return -1;
if (fprintf(fp, "%ld", value) < 0)
ret = -1;
if (fclose(fp) != 0)
ret = -1;
return ret;
}
static int read_fair_runtime_all(int nr_cpus, long *runtimes)
{
int i;
for (i = 0; i < nr_cpus; i++) {
runtimes[i] = read_server_param("fair", "runtime", i);
if (runtimes[i] <= 0)
return -1;
}
return 0;
}
static int write_fair_runtime_all(int nr_cpus, long value)
{
int i;
for (i = 0; i < nr_cpus; i++) {
if (write_server_param("fair", "runtime", i, value) < 0) {
SCX_ERR("Failed to write fair_server runtime on CPU %d", i);
return -1;
}
}
return 0;
}
/*
* Restore per-CPU fair_server runtimes.
*/
static int restore_fair_runtime_all(int nr_cpus, const long *runtimes)
{
int ret = 0;
int i;
for (i = 0; i < nr_cpus; i++) {
if (write_server_param("fair", "runtime", i, runtimes[i]) < 0) {
SCX_ERR("Failed to restore fair_server runtime on CPU %d", i);
ret = -1;
}
}
return ret;
}
static bool verify_total_bw_consistency(long *bw_values, int count)
{
int i;
long first_value;
if (count <= 0)
return false;
first_value = bw_values[0];
for (i = 1; i < count; i++) {
if (bw_values[i] != first_value) {
SCX_ERR("Inconsistent total_bw: CPU0=%ld, CPU%d=%ld",
first_value, i, bw_values[i]);
return false;
}
}
return true;
}
static int fetch_verify_total_bw(long *bw_values, int nr_cpus)
{
int attempts = 0;
int max_attempts = 10;
int count;
/*
* The first enqueue on a CPU causes the DL server to start, for that
* reason run stressor threads in the hopes it schedules on all CPUs.
*/
if (run_cpu_stress(nr_cpus) < 0) {
SCX_ERR("Failed to run CPU stress");
return -1;
}
/* Try multiple times to get stable values */
while (attempts < max_attempts) {
count = read_total_bw_values(bw_values, nr_cpus);
fprintf(stderr, "Read %d total_bw values (testing %d CPUs)\n", count, nr_cpus);
/* If system has more CPUs than we're testing, that's OK */
if (count < nr_cpus) {
SCX_ERR("Expected at least %d CPUs, got %d", nr_cpus, count);
attempts++;
sleep(1);
continue;
}
/* Only verify the CPUs we're testing */
if (verify_total_bw_consistency(bw_values, nr_cpus)) {
fprintf(stderr, "Values are consistent: %ld\n", bw_values[0]);
return 0;
}
attempts++;
sleep(1);
}
return -1;
}
static enum scx_test_status setup(void **ctx)
{
struct total_bw_ctx *test_ctx;
if (access("/sys/kernel/debug/sched/debug", R_OK) != 0) {
fprintf(stderr, "Skipping test: debugfs sched/debug not accessible\n");
return SCX_TEST_SKIP;
}
test_ctx = calloc(1, sizeof(*test_ctx));
if (!test_ctx)
return SCX_TEST_FAIL;
test_ctx->nr_cpus = sysconf(_SC_NPROCESSORS_ONLN);
if (test_ctx->nr_cpus <= 0) {
free(test_ctx);
return SCX_TEST_FAIL;
}
/* If system has more CPUs than MAX_CPUS, just test the first MAX_CPUS */
if (test_ctx->nr_cpus > MAX_CPUS)
test_ctx->nr_cpus = MAX_CPUS;
/* Test scenario 1: BPF program not loaded */
/* Read and verify baseline total_bw before loading BPF program */
fprintf(stderr, "BPF prog initially not loaded, reading total_bw values\n");
if (fetch_verify_total_bw(test_ctx->baseline_bw, test_ctx->nr_cpus) < 0) {
SCX_ERR("Failed to get stable baseline values");
free(test_ctx);
return SCX_TEST_FAIL;
}
/* Load the BPF skeleton */
test_ctx->skel = minimal__open();
if (!test_ctx->skel) {
free(test_ctx);
return SCX_TEST_FAIL;
}
SCX_ENUM_INIT(test_ctx->skel);
if (minimal__load(test_ctx->skel)) {
minimal__destroy(test_ctx->skel);
free(test_ctx);
return SCX_TEST_FAIL;
}
*ctx = test_ctx;
return SCX_TEST_PASS;
}
static enum scx_test_status run(void *ctx)
{
struct total_bw_ctx *test_ctx = ctx;
struct bpf_link *link;
long loaded_bw[MAX_CPUS];
long unloaded_bw[MAX_CPUS];
long doubled_bw[MAX_CPUS];
long original_runtime[MAX_CPUS], doubled_runtime;
enum scx_test_status ret;
int i;
/* Test scenario 2: BPF program loaded */
link = bpf_map__attach_struct_ops(test_ctx->skel->maps.minimal_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler");
return SCX_TEST_FAIL;
}
fprintf(stderr, "BPF program loaded, reading total_bw values\n");
if (fetch_verify_total_bw(loaded_bw, test_ctx->nr_cpus) < 0) {
SCX_ERR("Failed to get stable values with BPF loaded");
bpf_link__destroy(link);
return SCX_TEST_FAIL;
}
bpf_link__destroy(link);
/* Test scenario 3: BPF program unloaded */
fprintf(stderr, "BPF program unloaded, reading total_bw values\n");
if (fetch_verify_total_bw(unloaded_bw, test_ctx->nr_cpus) < 0) {
SCX_ERR("Failed to get stable values after BPF unload");
return SCX_TEST_FAIL;
}
/* Verify all three scenarios have the same total_bw values */
for (i = 0; i < test_ctx->nr_cpus; i++) {
if (test_ctx->baseline_bw[i] != loaded_bw[i]) {
SCX_ERR("CPU%d: baseline_bw=%ld != loaded_bw=%ld",
i, test_ctx->baseline_bw[i], loaded_bw[i]);
return SCX_TEST_FAIL;
}
if (test_ctx->baseline_bw[i] != unloaded_bw[i]) {
SCX_ERR("CPU%d: baseline_bw=%ld != unloaded_bw=%ld",
i, test_ctx->baseline_bw[i], unloaded_bw[i]);
return SCX_TEST_FAIL;
}
}
fprintf(stderr, "All total_bw values are consistent across all scenarios\n");
/*
* Validate auto-register/unregister of dl_server bandwidth reservations.
*
* Doubling fair_server's runtime doubles its bw contribution. With a
* full-mode BPF scheduler (minimal_ops), the kernel should detach
* fair_server and attach ext_server, dropping total_bw back to its
* pre-customization (default ext_server-only) value. On unload, the
* fair_server reservation should come back with its customized runtime
* preserved, so total_bw doubles again.
*/
if (read_fair_runtime_all(test_ctx->nr_cpus, original_runtime) < 0) {
fprintf(stderr, "Skipping attach/detach validation: debugfs not accessible\n");
return SCX_TEST_PASS;
}
doubled_runtime = original_runtime[0] * 2;
fprintf(stderr,
"Setting fair_server runtime to %ld ns on all CPUs (orig %ld)\n",
doubled_runtime, original_runtime[0]);
if (write_fair_runtime_all(test_ctx->nr_cpus, doubled_runtime) < 0) {
ret = SCX_TEST_FAIL;
goto restore;
}
if (fetch_verify_total_bw(doubled_bw, test_ctx->nr_cpus) < 0) {
SCX_ERR("Failed to get stable values after doubling fair runtime");
ret = SCX_TEST_FAIL;
goto restore;
}
/*
* After doubling the runtime, fair_server's bw contribution must grow.
* We don't assert exactly 2x, because the kernel's to_ratio() truncates
* the value, so 2 * to_ratio(period, runtime) and
* to_ratio(period, 2 * runtime) can differ.
*/
for (i = 0; i < test_ctx->nr_cpus; i++) {
if (doubled_bw[i] <= test_ctx->baseline_bw[i]) {
SCX_ERR("CPU%d: fair did not increase total_bw (baseline=%ld, doubled=%ld)",
i, test_ctx->baseline_bw[i], doubled_bw[i]);
ret = SCX_TEST_FAIL;
goto restore;
}
}
link = bpf_map__attach_struct_ops(test_ctx->skel->maps.minimal_ops);
if (!link) {
SCX_ERR("Failed to attach scheduler for detach test");
ret = SCX_TEST_FAIL;
goto restore;
}
if (fetch_verify_total_bw(loaded_bw, test_ctx->nr_cpus) < 0) {
SCX_ERR("Failed to get stable values with BPF loaded (detach test)");
bpf_link__destroy(link);
ret = SCX_TEST_FAIL;
goto restore;
}
/*
* In full mode the customized fair_server is detached and ext_server is
* attached at its default runtime, total_bw must match baseline.
*/
for (i = 0; i < test_ctx->nr_cpus; i++) {
if (loaded_bw[i] != test_ctx->baseline_bw[i]) {
SCX_ERR("CPU%d: expected bw %ld (fair detached, ext default), got %ld",
i, test_ctx->baseline_bw[i], loaded_bw[i]);
bpf_link__destroy(link);
ret = SCX_TEST_FAIL;
goto restore;
}
}
bpf_link__destroy(link);
if (fetch_verify_total_bw(unloaded_bw, test_ctx->nr_cpus) < 0) {
SCX_ERR("Failed to get stable values after BPF unload (detach test)");
ret = SCX_TEST_FAIL;
goto restore;
}
/*
* After unload, fair_server is re-attached with its preserved 2x
* runtime, so total_bw should return to the doubled value.
*/
for (i = 0; i < test_ctx->nr_cpus; i++) {
if (unloaded_bw[i] != doubled_bw[i]) {
SCX_ERR("CPU%d: BPF unloaded: expected %ld (fair restored at 2x), got %ld",
i, doubled_bw[i], unloaded_bw[i]);
ret = SCX_TEST_FAIL;
goto restore;
}
}
fprintf(stderr,
"dl_server attach/detach with customized fair runtime verified\n");
ret = SCX_TEST_PASS;
restore:
if (restore_fair_runtime_all(test_ctx->nr_cpus, original_runtime) < 0)
SCX_ERR("Failed to fully restore per-CPU fair_server runtimes");
return ret;
}
static void cleanup(void *ctx)
{
struct total_bw_ctx *test_ctx = ctx;
if (test_ctx) {
if (test_ctx->skel)
minimal__destroy(test_ctx->skel);
free(test_ctx);
}
}
struct scx_test total_bw = {
.name = "total_bw",
.description = "Verify total_bw consistency across BPF program states",
.setup = setup,
.run = run,
.cleanup = cleanup,
};
REGISTER_SCX_TEST(&total_bw)
@@ -0,0 +1,71 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <dvernet@meta.com>
*/
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Returns read len on success, or -errno on failure. */
static ssize_t read_text(const char *path, char *buf, size_t max_len)
{
ssize_t len;
int fd;
fd = open(path, O_RDONLY);
if (fd < 0)
return -errno;
len = read(fd, buf, max_len - 1);
if (len >= 0)
buf[len] = 0;
close(fd);
return len < 0 ? -errno : len;
}
/* Returns written len on success, or -errno on failure. */
static ssize_t write_text(const char *path, char *buf, ssize_t len)
{
int fd;
ssize_t written;
fd = open(path, O_WRONLY | O_APPEND);
if (fd < 0)
return -errno;
written = write(fd, buf, len);
close(fd);
return written < 0 ? -errno : written;
}
long file_read_long(const char *path)
{
char buf[128];
if (read_text(path, buf, sizeof(buf)) <= 0)
return -1;
return atol(buf);
}
int file_write_long(const char *path, long val)
{
char buf[64];
int ret;
ret = sprintf(buf, "%ld", val);
if (ret < 0)
return ret;
if (write_text(path, buf, ret) <= 0)
return -1;
return 0;
}
@@ -0,0 +1,13 @@
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (c) 2024 Meta Platforms, Inc. and affiliates.
* Copyright (c) 2024 David Vernet <void@manifault.com>
*/
#ifndef __SCX_TEST_UTIL_H__
#define __SCX_TEST_UTIL_H__
long file_read_long(const char *path);
int file_write_long(const char *path, long val);
#endif // __SCX_TEST_UTIL_H__