The libbpf + bpftool + vmlinux.h + BPF-object + skeleton build tool-chain is currently duplicated across tools/testing/selftests/{bpf,sched_ext, hid}/, each carrying ~100-140 lines of near-identical Makefile. As more subsystems grow BPF-based selftests, the duplication scales poorly. This patch adds tools/testing/selftests/lib.bpf.mk, a single includable fragment that provides the whole chain end-to-end. To use: set BPF_SRCS and OVERRIDE_TARGETS := 1 before including ../lib.mk (so lib.mk's default link rule is suppressed), then include ../lib.bpf.mk and list $(BPF_SKELS) and $(BPFOBJ) as prerequisites of the test binary, e.g.,: BPF_SRCS := progs/foo.bpf.c TEST_GEN_PROGS := foo_test OVERRIDE_TARGETS := 1 include ../lib.mk include ../lib.bpf.mk $(OUTPUT)/foo_test: foo_test.c $(BPF_SKELS) $(BPFOBJ) $(call bpf_link,$@,$<) This eases adding BPF-based selftests in other directories, such as cgroup. Suggested-by: Shakeel Butt Suggested-by: Eduard Zingerman Suggested-by: Mykola Lysenko Assisted-by: Claude:claude-opus-5 Signed-off-by: Ziyang Men --- tools/testing/selftests/lib.bpf.mk | 321 +++++++++++++++++++++++++++++ 1 file changed, 321 insertions(+) create mode 100644 tools/testing/selftests/lib.bpf.mk diff --git a/tools/testing/selftests/lib.bpf.mk b/tools/testing/selftests/lib.bpf.mk new file mode 100644 index 000000000000..9238cbf1de06 --- /dev/null +++ b/tools/testing/selftests/lib.bpf.mk @@ -0,0 +1,321 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Build BPF programs and skeletons for selftests. +# +# Use it from a test Makefile like this: +# +# BPF_SRCS := foo.bpf.c bar.bpf.c +# TEST_GEN_PROGS := foo_test +# OVERRIDE_TARGETS := 1 # set before lib.mk +# include ../lib.mk +# include ../lib.bpf.mk +# +# $(OUTPUT)/foo_test: foo_test.c $(BPF_SKELS) $(BPFOBJ) +# $(call bpf_link,$@,$<) +# +# Depend on $(BPFOBJ) to relink the test when libbpf.a changes. +# +# Options to set before including lib.bpf.mk: +# BPF_PROG_EXT - source ending; default .bpf.c. Use .c for progs/foo.c. +# BPF_EXTRA_HDRS - more headers needed by the BPF objects. +# BPF_EXTRA_CFLAGS - more flags for compiling BPF programs. +# BPF_SKEL_EXT - skeleton header ending; default .skel.h. Must end in +# skel.h. +# BPF_GEN_SUBSKEL - also create a subskeleton header when set. +# BPF_OBJ_DIR - folder for BPF objects; default $(OUTPUT). +# BPF_SKEL_DIR - folder for skeleton headers; default $(OUTPUT). +# +# Set output directories that use $(OUTPUT) after including lib.mk. +# +# BPF_CFLAGS may be replaced after this include. Generated names use the source +# stem without its directory. +# +# Include lib.mk first. This file defines the BPF build variables and bpf_link, +# extends all, CFLAGS and EXTRA_CLEAN, and enables .DELETE_ON_ERROR. + +include $(top_srcdir)/tools/scripts/Makefile.arch # ARCH / SRCARCH / HOSTARCH + +# Remove partial files written directly to $@. +.DELETE_ON_ERROR: + +# Match the libbpf and bpftool host compiler to LLVM=. +ifneq ($(LLVM),) +HOSTCC ?= $(LLVM_PREFIX)clang$(LLVM_SUFFIX) +HOSTLD ?= $(LLVM_PREFIX)ld.lld$(LLVM_SUFFIX) +else +HOSTCC ?= gcc +HOSTLD ?= ld +endif +CLANG ?= clang + +ifneq ($(V),1) +lib_bpf_submake_extras := feature_display=0 +endif + +# Match selftests/bpf debug and release optimization. +OPT_FLAGS ?= $(if $(RELEASE),-O2,-O0) + +# ---- files and tools ------------------------------------------------------ +# Use one full path. Different forms of the same -I path can change BTF order. +lib_bpf_tools_dir := $(abspath $(top_srcdir)/tools) +lib_bpf_dir := $(lib_bpf_tools_dir)/lib/bpf +lib_bpf_tools_include_dir := $(lib_bpf_tools_dir)/include +lib_bpf_bpftool_dir := $(lib_bpf_tools_dir)/bpf/bpftool +lib_bpf_api_dir := $(lib_bpf_tools_include_dir)/uapi +lib_bpf_sources := $(wildcard $(lib_bpf_dir)/*.[ch] \ + $(lib_bpf_dir)/Makefile) +lib_bpf_header_sources := $(filter %.h,$(lib_bpf_sources)) \ + $(lib_bpf_api_dir)/linux/bpf.h +# Keep these names for callers which add their own BPF flags or dependencies. +BPFDIR := $(lib_bpf_dir) +APIDIR := $(lib_bpf_api_dir) + +# Keep private build files under $(OUTPUT) for both in-tree and O= builds. +lib_bpf_scratch_dir := $(OUTPUT)/tools +lib_bpf_build_dir := $(lib_bpf_scratch_dir)/build +lib_bpf_include_dir := $(lib_bpf_scratch_dir)/include +# INCLUDE_DIR is also used by callers that replace BPF_CFLAGS. +INCLUDE_DIR := $(lib_bpf_include_dir) +BPFOBJ := $(lib_bpf_build_dir)/libbpf/libbpf.a + +# Cross builds use a separate native libbpf for bpftool. +ifneq ($(CROSS_COMPILE)$(filter-out $(HOSTARCH),$(SRCARCH)),) +lib_bpf_host_build_dir := $(lib_bpf_build_dir)/host +lib_bpf_host_scratch_dir := $(OUTPUT)/host-tools +else +lib_bpf_host_build_dir := $(lib_bpf_build_dir) +lib_bpf_host_scratch_dir := $(lib_bpf_scratch_dir) +endif +lib_bpf_host_obj := $(lib_bpf_host_build_dir)/libbpf/libbpf.a +DEFAULT_BPFTOOL := $(lib_bpf_host_scratch_dir)/sbin/bpftool +BPFTOOL ?= $(DEFAULT_BPFTOOL) + +# Reuse target USERCFLAGS only when bpftool shares the target libbpf. +ifeq ($(BPFOBJ),$(lib_bpf_host_obj)) +lib_bpf_host_user_cflags := $(USERCFLAGS) +endif + +# ---- find vmlinux BTF ----------------------------------------------------- +VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux) \ + $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux) \ + $(top_srcdir)/vmlinux \ + /sys/kernel/btf/vmlinux \ + /boot/vmlinux-$(shell uname -r) +VMLINUX_BTF ?= $(abspath $(firstword $(wildcard $(VMLINUX_BTF_PATHS)))) +# Delay missing-vmlinux errors so "make clean" still works. +lib_bpf_vmlinux_deps := $(if $(VMLINUX_H),$(VMLINUX_H),$(VMLINUX_BTF) $(BPFTOOL)) + +# ---- compiler flags ------------------------------------------------------- +# Find the normal system headers that Clang omits with --target=bpf. Put them +# last with -idirafter so they cannot replace project headers. +define lib_bpf_get_sys_includes +$(shell $(1) $(2) -v -E - &1 \ + | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \ +$(shell $(1) $(2) -dM -E - &1 | \ + grep -q 'v3' && echo v3 || echo v2) + +# Accept anonymous struct and union members in vmlinux.h. +BPF_CFLAGS = -g -Wall -Werror -D__TARGET_ARCH_$(SRCARCH) $(MENDIAN) \ + -I$(INCLUDE_DIR) -I$(APIDIR) \ + -std=gnu11 \ + -fno-strict-aliasing \ + -fms-extensions -Wno-microsoft-anon-tag \ + -Wno-compare-distinct-pointer-types \ + $(CLANG_SYS_INCLUDES) $(BPF_EXTRA_CFLAGS) + +# $1 = source, $2 = object. -MMD -MP tracks non-system headers. +define lib_bpf_build_rule + $(call msg,CLNG-BPF,,$2) + $(Q)$(CLANG) $(BPF_CFLAGS) -O2 --target=bpf -mcpu=$(lib_bpf_clang_cpu) \ + -MMD -MP -c $1 -o $2 +endef + +# ---- output folders ------------------------------------------------------- +BPF_OBJ_DIR ?= $(OUTPUT) +BPF_SKEL_DIR ?= $(OUTPUT) +# Reject empty directories before cleanup globs can reach the filesystem root. +ifeq ($(strip $(BPF_OBJ_DIR)),) +$(error lib.bpf.mk: BPF_OBJ_DIR is empty; set it after "include ../lib.mk") +endif +ifeq ($(strip $(BPF_SKEL_DIR)),) +$(error lib.bpf.mk: BPF_SKEL_DIR is empty; set it after "include ../lib.mk") +endif + +# ---- build folders -------------------------------------------------------- +lib_bpf_make_dirs := $(sort $(lib_bpf_build_dir)/libbpf \ + $(lib_bpf_host_build_dir)/libbpf \ + $(lib_bpf_host_build_dir)/bpftool \ + $(lib_bpf_include_dir) \ + $(filter-out $(OUTPUT),$(BPF_OBJ_DIR) $(BPF_SKEL_DIR))) +$(lib_bpf_make_dirs): + $(call msg,MKDIR,,$@) + $(Q)mkdir -p $@ + +# ---- target libbpf -------------------------------------------------------- +# Pass unexported toolchain settings and build PIC for PIE test binaries. +# libbpf consumes USERCFLAGS through EXTRA_CFLAGS. +$(BPFOBJ): $(lib_bpf_sources) $(lib_bpf_api_dir)/linux/bpf.h \ + | $(lib_bpf_build_dir)/libbpf + $(Q)$(MAKE) $(lib_bpf_submake_extras) -C $(lib_bpf_dir) \ + OUTPUT=$(lib_bpf_build_dir)/libbpf/ \ + ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) CC="$(CC)" \ + EXTRA_CFLAGS='-g $(OPT_FLAGS) -fPIC $(EXTRA_CFLAGS) $(USERCFLAGS)' \ + DESTDIR=$(lib_bpf_scratch_dir) prefix= all install_headers + +# ---- host libbpf, only when the target differs ---------------------------- +ifneq ($(BPFOBJ),$(lib_bpf_host_obj)) +$(lib_bpf_host_obj): $(lib_bpf_sources) $(lib_bpf_api_dir)/linux/bpf.h \ + | $(lib_bpf_host_build_dir)/libbpf + $(Q)$(MAKE) $(lib_bpf_submake_extras) -C $(lib_bpf_dir) \ + ARCH= CROSS_COMPILE= \ + OUTPUT=$(lib_bpf_host_build_dir)/libbpf/ \ + CC="$(HOSTCC)" LD="$(HOSTLD)" \ + EXTRA_CFLAGS='-g $(OPT_FLAGS) $(EXTRA_CFLAGS)' \ + DESTDIR=$(lib_bpf_host_scratch_dir) prefix= all install_headers +endif + +# ---- host bpftool --------------------------------------------------------- +$(DEFAULT_BPFTOOL): $(wildcard $(lib_bpf_bpftool_dir)/*.[ch] \ + $(lib_bpf_bpftool_dir)/Makefile) \ + $(lib_bpf_host_obj) | $(lib_bpf_host_build_dir)/bpftool + $(Q)$(MAKE) $(lib_bpf_submake_extras) -C $(lib_bpf_bpftool_dir) \ + ARCH= CROSS_COMPILE= CC="$(HOSTCC)" LD="$(HOSTLD)" \ + EXTRA_CFLAGS='-g $(OPT_FLAGS) $(EXTRA_CFLAGS) $(lib_bpf_host_user_cflags)' \ + EXTRA_LDFLAGS='$(EXTRA_LDFLAGS)' \ + OUTPUT=$(lib_bpf_host_build_dir)/bpftool/ \ + LIBBPF_OUTPUT=$(lib_bpf_host_build_dir)/libbpf/ \ + LIBBPF_DESTDIR=$(lib_bpf_host_scratch_dir)/ \ + prefix= DESTDIR=$(lib_bpf_host_scratch_dir)/ install-bin + +# ---- build vmlinux.h ------------------------------------------------------ +lib_bpf_vmlinux_h := $(INCLUDE_DIR)/vmlinux.h +lib_bpf_vmlinux_stamp := $(INCLUDE_DIR)/vmlinux.h.stamp + +# Preserve vmlinux.h's timestamp when its contents do not change. +ifeq ($(wildcard $(lib_bpf_vmlinux_h)),) +# Regenerate if the header is missing but its stamp remains. +.PHONY: $(lib_bpf_vmlinux_stamp) +endif + +# Use a normal edge so dependents see a changed header in the same make run. +$(lib_bpf_vmlinux_h): $(lib_bpf_vmlinux_stamp) ; + +$(lib_bpf_vmlinux_stamp): $(lib_bpf_vmlinux_deps) | $(INCLUDE_DIR) +ifeq ($(VMLINUX_H),) + $(call msg,GEN,,$(lib_bpf_vmlinux_h)) + $(Q)test -n "$(VMLINUX_BTF)" || { \ + echo "lib.bpf.mk: no vmlinux at any of \"$(VMLINUX_BTF_PATHS)\"" >&2; \ + exit 1; } + $(Q)$(BPFTOOL) btf dump file $(VMLINUX_BTF) format c > $@.tmp +else + $(call msg,CP,,$(lib_bpf_vmlinux_h)) + $(Q)cp "$(VMLINUX_H)" $@.tmp +endif + $(Q)cmp -s $@.tmp $(lib_bpf_vmlinux_h) || mv $@.tmp $(lib_bpf_vmlinux_h) + $(Q)rm -f $@.tmp + $(Q)touch $@ + +# ---- BPF objects and skeletons -------------------------------------------- +BPF_PROG_EXT ?= .bpf.c +# Each source must end with BPF_PROG_EXT so Make can remove that ending. +lib_bpf_bad_srcs := $(filter-out %$(BPF_PROG_EXT),$(BPF_SRCS)) +ifneq ($(lib_bpf_bad_srcs),) +$(error lib.bpf.mk: BPF_SRCS entries must end in $(BPF_PROG_EXT): $(lib_bpf_bad_srcs)) +endif +lib_bpf_stems := $(patsubst %$(BPF_PROG_EXT),%,$(notdir $(BPF_SRCS))) +# The stem becomes the skeleton's C name; reject dots and hyphens. +lib_bpf_bad_stems := $(strip $(foreach s,$(lib_bpf_stems), \ + $(if $(findstring .,$(s))$(findstring -,$(s)),$(s)))) +ifneq ($(lib_bpf_bad_stems),) +$(error lib.bpf.mk: BPF_SRCS basenames must not contain '.' or '-': $(lib_bpf_bad_stems)) +endif +# Output names omit directories, so reject duplicate stems. +ifneq ($(words $(lib_bpf_stems)),$(words $(sort $(lib_bpf_stems)))) +$(error lib.bpf.mk: BPF_SRCS has colliding basenames: $(BPF_SRCS)) +endif +BPF_SKEL_EXT ?= .skel.h +lib_bpf_subskel_ext := $(patsubst %skel.h,%subskel.h,$(BPF_SKEL_EXT)) +# Keep skeleton suffixes distinct and cleanup globs narrow. +ifeq ($(lib_bpf_subskel_ext),$(BPF_SKEL_EXT)) +$(error lib.bpf.mk: BPF_SKEL_EXT must end in skel.h: $(BPF_SKEL_EXT)) +endif +BPF_OBJS := $(addprefix $(BPF_OBJ_DIR)/,$(addsuffix .bpf.o,$(lib_bpf_stems))) +BPF_SKELS := $(addprefix $(BPF_SKEL_DIR)/,$(addsuffix $(BPF_SKEL_EXT),$(lib_bpf_stems))) +ifneq ($(BPF_GEN_SUBSKEL),) +BPF_SUBSKELS := $(addprefix $(BPF_SKEL_DIR)/,$(addsuffix $(lib_bpf_subskel_ext),$(lib_bpf_stems))) +endif + +# Use per-source rules to avoid changing normal .c lookup. +# Track source headers before BPFOBJ installs their updated copies. +define lib_bpf_obj_rule +$(BPF_OBJ_DIR)/$(patsubst %$(BPF_PROG_EXT),%,$(notdir $(1))).bpf.o: $(1) \ + $(BPF_EXTRA_HDRS) $(lib_bpf_header_sources) \ + $(INCLUDE_DIR)/vmlinux.h | $(BPF_OBJ_DIR) $(BPFOBJ) + $$(call lib_bpf_build_rule,$$<,$$@) +endef +$(foreach src,$(BPF_SRCS),$(eval $(call lib_bpf_obj_rule,$(src)))) + +# Generate both headers together so either missing target rebuilds the pair. +lib_bpf_skel_targets := $(BPF_SKEL_DIR)/%$(BPF_SKEL_EXT) +ifneq ($(BPF_GEN_SUBSKEL),) +lib_bpf_skel_targets += $(BPF_SKEL_DIR)/%$(lib_bpf_subskel_ext) +endif + +# Link three times and require the final two objects to match. +$(lib_bpf_skel_targets): $(BPF_OBJ_DIR)/%.bpf.o $(BPFTOOL) | $(BPF_SKEL_DIR) + $(call msg,GEN-SKEL,,$(BPF_SKEL_DIR)/$*$(BPF_SKEL_EXT)) + $(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 $* > $(BPF_SKEL_DIR)/$*$(BPF_SKEL_EXT) +ifneq ($(BPF_GEN_SUBSKEL),) + $(Q)$(BPFTOOL) gen subskeleton $(<:.o=.linked3.o) name $* > $(BPF_SKEL_DIR)/$*$(lib_bpf_subskel_ext) +endif + $(Q)rm -f $(<:.o=.linked1.o) $(<:.o=.linked2.o) $(<:.o=.linked3.o) + +# Read the header dependencies written by -MMD. +-include $(BPF_OBJS:.o=.d) + +# ---- values for the test Makefile ----------------------------------------- +# Add the installed libbpf/vmlinux.h directory and the skeleton directory. +CFLAGS += -I$(INCLUDE_DIR) -I$(BPF_SKEL_DIR) + +# Add target zstd when found by the target pkg-config. +PKG_CONFIG ?= $(CROSS_COMPILE)pkg-config +BPF_LDLIBS := $(BPFOBJ) -lelf -lz +ifneq ($(shell $(PKG_CONFIG) --exists libzstd 2>/dev/null && echo y),) +BPF_LDLIBS += -lzstd +endif + +# Add skeletons after lib.mk defines all. +all: $(BPF_SKELS) $(BPF_SUBSKELS) + +# The skeleton already embeds each BPF object in the test binary. To install the +# objects separately, add TEST_GEN_FILES += $(BPF_OBJS). + +# $1 = binary, $2 = test source. Use the same compile and link flags as lib.mk. +define bpf_link + $(call msg,BINARY,,$1) + $(Q)$(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH) $2 \ + $(BPF_LDLIBS) $(LDLIBS) -o $1 +endef + +EXTRA_CLEAN += $(sort $(lib_bpf_scratch_dir) $(lib_bpf_host_scratch_dir)) \ + $(addprefix $(BPF_OBJ_DIR)/,*.bpf.o *.bpf.d *.linked*.o) \ + $(addprefix $(BPF_SKEL_DIR)/,*$(BPF_SKEL_EXT) *$(lib_bpf_subskel_ext)) -- 2.53.0-Meta Add test_memcg_stat_cross_cpu, which verifies that flushing a memcg subtree produces the same statistics through the BPF memcg kfuncs and cgroupfs. The existing cgroup_iter_memcg test only checks that selected values are non-zero. That can pass even when aggregation is incomplete, and it does not exercise a hierarchy with charges spread over multiple CPUs. Build a multi-level cgroup tree, charge each leaf from several CPUs, flush the subtree once from a sleepable cgroup iterator, then compare every reported value with memory.stat. Also check that the root value equals the sum of its leaves. Probe all required kfuncs and sleepable iterator support before running. Bound CPU fanout and aggregate memory use so large systems skip cases that would exceed the limit instead of exhausting resources. Suggested-by: Shakeel Butt Assisted-by: Claude:claude-opus-5 Signed-off-by: Ziyang Men --- tools/testing/selftests/cgroup/.gitignore | 8 + tools/testing/selftests/cgroup/Makefile | 46 ++ tools/testing/selftests/cgroup/config | 5 + .../selftests/cgroup/lib/cgroup_util.c | 82 ++ .../cgroup/lib/include/cgroup_util.h | 3 + .../cgroup/memcg_stat_cross_cpu.bpf.c | 101 +++ .../selftests/cgroup/memcg_stat_cross_cpu.h | 18 + .../cgroup/test_memcg_stat_cross_cpu.c | 755 ++++++++++++++++++ .../selftests/cgroup/test_memcontrol.c | 29 +- 9 files changed, 1022 insertions(+), 25 deletions(-) create mode 100644 tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c create mode 100644 tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h create mode 100644 tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore index 952e4448bf07..561a3e891b98 100644 --- a/tools/testing/selftests/cgroup/.gitignore +++ b/tools/testing/selftests/cgroup/.gitignore @@ -6,7 +6,15 @@ test_freezer test_hugetlb_memcg test_kill test_kmem +test_memcg_stat_cross_cpu test_memcontrol test_pids test_zswap wait_inotify +# Artifacts generated by lib.bpf.mk +/tools +/host-tools +*.bpf.o +*.bpf.d +*.linked*.o +*.skel.h diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile index e01584c2189a..cb999bc2c875 100644 --- a/tools/testing/selftests/cgroup/Makefile +++ b/tools/testing/selftests/cgroup/Makefile @@ -20,9 +20,46 @@ TEST_GEN_PROGS += test_zswap LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h +# Resolve Clang early so the optional BPF test can be selected. +ifneq ($(filter %/,$(LLVM)),) +CLANG ?= $(LLVM)clang +else +CLANG ?= clang$(filter -%,$(LLVM)) +endif +READELF ?= $(CROSS_COMPILE)readelf +VMLINUX_BTF_PATHS ?= $(if $(O),$(O)/vmlinux) \ + $(if $(KBUILD_OUTPUT),$(KBUILD_OUTPUT)/vmlinux) \ + ../../../../vmlinux \ + /sys/kernel/btf/vmlinux \ + /boot/vmlinux-$(shell uname -r) +# Ignore ELF images without .BTF; raw sysfs BTF needs no check. +VMLINUX_BTF ?= $(abspath $(firstword $(foreach v,$(wildcard $(VMLINUX_BTF_PATHS)),\ + $(if $(filter /sys/kernel/btf/%,$(v)),$(v),\ + $(if $(shell $(READELF) -S "$(v)" 2>/dev/null | grep -F .BTF),$(v)))))) +# Accept either a supplied header or usable BTF. +HAVE_BPF := $(and $(or $(VMLINUX_H),$(VMLINUX_BTF)), \ + $(shell command -v $(CLANG) 2>/dev/null)) + +ifneq ($(HAVE_BPF),) +TEST_GEN_PROGS += test_memcg_stat_cross_cpu +BPF_SRCS := memcg_stat_cross_cpu.bpf.c +# Replace lib.mk's link rule below. +OVERRIDE_TARGETS := 1 +endif + include ../lib.mk include lib/libcgroup.mk +ifneq ($(HAVE_BPF),) +include ../lib.bpf.mk + +# Restore lib.mk's rule for non-BPF tests. +LOCAL_HDRS += $(selfdir)/kselftest_harness.h $(selfdir)/kselftest.h +$(OUTPUT)/%: %.c $(LOCAL_HDRS) + $(call msg,CC,,$@) + $(Q)$(LINK.c) $(filter-out $(LOCAL_HDRS),$^) $(LDLIBS) -o $@ +endif + $(OUTPUT)/test_core: $(LIBCGROUP_O) $(OUTPUT)/test_cpu: $(LIBCGROUP_O) $(OUTPUT)/test_cpuset: $(LIBCGROUP_O) @@ -33,3 +70,12 @@ $(OUTPUT)/test_kmem: $(LIBCGROUP_O) $(OUTPUT)/test_memcontrol: $(LIBCGROUP_O) $(OUTPUT)/test_pids: $(LIBCGROUP_O) $(OUTPUT)/test_zswap: $(LIBCGROUP_O) + +ifneq ($(HAVE_BPF),) +# Keep in-tree UAPI headers private to the BPF test. +$(OUTPUT)/test_memcg_stat_cross_cpu: private CFLAGS += -I$(top_srcdir)/tools/include/uapi +# Link cgroup_util and the skeleton against the in-tree libbpf. +$(OUTPUT)/test_memcg_stat_cross_cpu: test_memcg_stat_cross_cpu.c \ + $(BPF_SKELS) $(LIBCGROUP_O) $(BPFOBJ) + $(call bpf_link,$@,$< $(LIBCGROUP_O)) +endif diff --git a/tools/testing/selftests/cgroup/config b/tools/testing/selftests/cgroup/config index 39f979690dd3..1f585ed9a596 100644 --- a/tools/testing/selftests/cgroup/config +++ b/tools/testing/selftests/cgroup/config @@ -4,3 +4,8 @@ CONFIG_CGROUP_FREEZER=y CONFIG_CGROUP_SCHED=y CONFIG_MEMCG=y CONFIG_PAGE_COUNTER=y +CONFIG_BPF=y +CONFIG_BPF_SYSCALL=y +CONFIG_BPF_EVENTS=y +CONFIG_DEBUG_INFO_BTF=y +CONFIG_DEBUG_INFO_DWARF4=y diff --git a/tools/testing/selftests/cgroup/lib/cgroup_util.c b/tools/testing/selftests/cgroup/lib/cgroup_util.c index 2596c12cd864..ca12d794c092 100644 --- a/tools/testing/selftests/cgroup/lib/cgroup_util.c +++ b/tools/testing/selftests/cgroup/lib/cgroup_util.c @@ -54,6 +54,88 @@ ssize_t write_text(const char *path, char *buf, ssize_t len) return len < 0 ? -errno : len; } +/** + * cg_get_id - return a cgroup's kernfs ID + * @cgroup: absolute cgroup path + * + * This is cgrp->kn->id, not st_ino. + * + * Return: ID or 0 on failure. + */ +unsigned long long cg_get_id(const char *cgroup) +{ + union { + unsigned long long id; + unsigned char raw[8]; + } handle; + struct file_handle *fhp, *fhp2; + int mount_id, fhsize, err; + unsigned long long ret = 0; + + fhsize = sizeof(*fhp); + fhp = calloc(1, fhsize); + if (!fhp) + return 0; + + /* The failed probe reports the cgroupfs handle size. */ + err = name_to_handle_at(AT_FDCWD, cgroup, fhp, &mount_id, 0); + if (err >= 0 || fhp->handle_bytes != 8) + goto out; + + fhsize = sizeof(*fhp) + fhp->handle_bytes; + fhp2 = realloc(fhp, fhsize); + if (!fhp2) + goto out; + fhp = fhp2; + + if (name_to_handle_at(AT_FDCWD, cgroup, fhp, &mount_id, 0) < 0) + goto out; + + memcpy(handle.raw, fhp->f_handle, 8); + ret = handle.id; +out: + free(fhp); + return ret; +} + +/** + * cg_touch_pages - fault every page in a region + * @buf: start of the region + * @size: length of the region in bytes + * + * Pages are charged to the caller's cgroup on its current CPU. + */ +void cg_touch_pages(char *buf, size_t size) +{ + long page_size = sysconf(_SC_PAGESIZE); + char *ptr; + + if (page_size <= 0) + page_size = BUF_SIZE; + + for (ptr = buf; ptr < buf + size; ptr += page_size) + *ptr = 0; +} + +/** + * cg_alloc_anon - allocate and fault anonymous memory + * @size: bytes to allocate + * + * Return: Region to free(), or NULL. + */ +char *cg_alloc_anon(size_t size) +{ + char *buf = malloc(size); + + if (!buf) { + fprintf(stderr, "malloc() failed\n"); + return NULL; + } + + cg_touch_pages(buf, size); + return buf; +} + char *cg_name(const char *root, const char *name) { size_t len = strlen(root) + strlen(name) + 2; diff --git a/tools/testing/selftests/cgroup/lib/include/cgroup_util.h b/tools/testing/selftests/cgroup/lib/include/cgroup_util.h index 8ebb2b4d4ec0..640778018780 100644 --- a/tools/testing/selftests/cgroup/lib/include/cgroup_util.h +++ b/tools/testing/selftests/cgroup/lib/include/cgroup_util.h @@ -54,6 +54,9 @@ extern ssize_t write_text(const char *path, char *buf, ssize_t len); extern int cg_find_controller_root(char *root, size_t len, const char *controller); extern int cg_find_unified_root(char *root, size_t len, bool *nsdelegate); extern char *cg_name(const char *root, const char *name); +extern unsigned long long cg_get_id(const char *cgroup); +extern void cg_touch_pages(char *buf, size_t size); +extern char *cg_alloc_anon(size_t size); extern char *cg_name_indexed(const char *root, const char *name, int index); extern char *cg_control(const char *cgroup, const char *control); extern int cg_create(const char *cgroup); diff --git a/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c new file mode 100644 index 000000000000..fff3b9256aa3 --- /dev/null +++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.bpf.c @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ +#define BPF_NO_KFUNC_PROTOTYPES +#include +#include +#include +#include "memcg_stat_cross_cpu.h" + +char _license[] SEC("license") = "GPL"; + +/* Ensure the upper-bound checks also reject negative BPF arguments. */ +_Static_assert((enum vm_event_item)-1 >= NR_VM_EVENT_ITEMS, + "negative vm_event_item must fail the bounds check"); +_Static_assert((enum memcg_memory_event)-1 >= MEMCG_NR_MEMORY_EVENTS, + "negative memcg_memory_event must fail the bounds check"); + +/* Declare kfuncs that may be absent from the build-time vmlinux.h. */ +struct mem_cgroup *bpf_get_mem_cgroup(struct cgroup_subsys_state *css) __ksym; +void bpf_put_mem_cgroup(struct mem_cgroup *memcg) __ksym; +void bpf_mem_cgroup_flush_stats(struct mem_cgroup *memcg) __ksym; +unsigned long bpf_mem_cgroup_page_state(struct mem_cgroup *memcg, int idx) __ksym; +unsigned long bpf_mem_cgroup_vm_events(struct mem_cgroup *memcg, + enum vm_event_item event) __ksym; + +/* Userspace resizes this cgroup-ID map before load. */ +struct { + __uint(type, BPF_MAP_TYPE_HASH); + __uint(max_entries, 1); + __type(key, __u64); + __type(value, struct memcg_stat_snapshot); +} results SEC(".maps"); + +/* Isolate capability failures from verifier failures in the test program. */ +SEC("iter.s/cgroup") +int memcg_kfuncs_probe(struct bpf_iter__cgroup *ctx) +{ + struct cgroup *cgrp = ctx->cgroup; + struct mem_cgroup *memcg; + + if (!cgrp) + return 0; + + memcg = bpf_get_mem_cgroup(&cgrp->self); + if (!memcg) + return 0; + + bpf_mem_cgroup_flush_stats(memcg); + bpf_mem_cgroup_page_state(memcg, 0); + bpf_mem_cgroup_vm_events(memcg, 0); + bpf_put_mem_cgroup(memcg); + return 0; +} + +/* Sleepable iterator: flush the root once, then snapshot each cgroup. */ +SEC("iter.s/cgroup") +int cgroup_memcg_stat_cross_cpu(struct bpf_iter__cgroup *ctx) +{ + struct cgroup *cgrp = ctx->cgroup; + struct memcg_stat_snapshot snap = {}; + struct cgroup_subsys_state *css; + struct mem_cgroup *memcg; + int idx_anon, idx_file, idx_shmem, idx_fmapped, idx_pgfault; + __u64 cg_id; + + /* Ignore the final post-processing call. */ + if (!cgrp) + return 0; + + css = &cgrp->self; + memcg = bpf_get_mem_cgroup(css); + if (!memcg) + return 0; + + /* DESCENDANTS_PRE visits the subtree root first. */ + if (ctx->meta->seq_num == 0) + bpf_mem_cgroup_flush_stats(memcg); + + cg_id = BPF_CORE_READ(cgrp, kn, id); + snap.cgroup_id = cg_id; + + idx_anon = bpf_core_enum_value(enum node_stat_item, NR_ANON_MAPPED); + idx_file = bpf_core_enum_value(enum node_stat_item, NR_FILE_PAGES); + idx_shmem = bpf_core_enum_value(enum node_stat_item, NR_SHMEM); + idx_fmapped = bpf_core_enum_value(enum node_stat_item, NR_FILE_MAPPED); + idx_pgfault = bpf_core_enum_value(enum vm_event_item, PGFAULT); + + snap.anon = bpf_mem_cgroup_page_state(memcg, idx_anon); + snap.file = bpf_mem_cgroup_page_state(memcg, idx_file); + snap.shmem = bpf_mem_cgroup_page_state(memcg, idx_shmem); + snap.file_mapped = bpf_mem_cgroup_page_state(memcg, idx_fmapped); + snap.pgfault = bpf_mem_cgroup_vm_events(memcg, idx_pgfault); + + /* Read page counters from the trusted pointer. */ + snap.usage_pages = BPF_CORE_READ(memcg, memory.usage.counter); + snap.max_pages = BPF_CORE_READ(memcg, memory.max); + + bpf_map_update_elem(&results, &cg_id, &snap, BPF_ANY); + + bpf_put_mem_cgroup(memcg); + return 0; +} diff --git a/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h new file mode 100644 index 000000000000..f75e341f8cb8 --- /dev/null +++ b/tools/testing/selftests/cgroup/memcg_stat_cross_cpu.h @@ -0,0 +1,18 @@ +/* SPDX-License-Identifier: GPL-2.0 */ +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ +#ifndef __MEMCG_STAT_CROSS_CPU_H +#define __MEMCG_STAT_CROSS_CPU_H + +/* Fixed-width snapshot shared by BPF and userspace. */ +struct memcg_stat_snapshot { + __u64 cgroup_id; + __u64 anon; /* NR_ANON_MAPPED, bytes */ + __u64 file; /* NR_FILE_PAGES, bytes */ + __u64 shmem; /* NR_SHMEM, bytes */ + __u64 file_mapped; /* NR_FILE_MAPPED, bytes */ + __u64 pgfault; /* PGFAULT, count */ + __u64 usage_pages; /* page_counter memory.usage, in PAGES */ + __u64 max_pages; /* page_counter memory.max, in PAGES */ +}; + +#endif /* __MEMCG_STAT_CROSS_CPU_H */ diff --git a/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c b/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c new file mode 100644 index 000000000000..96e4a4a958ed --- /dev/null +++ b/tools/testing/selftests/cgroup/test_memcg_stat_cross_cpu.c @@ -0,0 +1,755 @@ +// SPDX-License-Identifier: GPL-2.0 +/* Copyright (c) 2026 Meta Platforms, Inc. and affiliates. */ + +/* + * Compare memcg BPF kfuncs with memory.stat over a charged subtree. Chargers + * spread anonymous memory across CPUs and hold it. Read BPF first so its flush + * is not consumed by memory.stat, then require two file samples to bracket each + * BPF value. Each leaf must cover its charge; root anon must equal the leaf sum. + */ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "kselftest.h" +#include "cgroup_util.h" +#include "memcg_stat_cross_cpu.h" +#include "memcg_stat_cross_cpu.skel.h" + +#define SUBTREE_NAME "mcg_xcpu" + +#define MEMCG_CHARGE_BATCH 64 + +#define FLUSH_MARGIN 2 + +/* Bound total charge on large systems. */ +#define MAX_TOTAL_CHARGE (256UL << 20) + +/* Bound the CPUs used by each leaf. */ +#define MAX_CPUS_PER_LEAF 16 + +#define CHARGE_WAIT_RETRIES 500 + +static char root[PATH_MAX]; +static char *subtree_root; +static long page_size; + +/* cgroupfs values in bytes. */ +struct file_snap { + long anon, file, shmem, file_mapped, pgfault; + long current; +}; + +struct cg_node { + char path[PATH_MAX]; + unsigned long long id; + bool is_leaf; /* holds a charge of its own */ + long previous_current; /* previous memory.current poll */ + struct memcg_stat_snapshot bpf; /* read through the kfuncs */ + struct file_snap file[2]; /* post-BPF cgroupfs samples */ +}; + +static struct cg_node *nodes; /* DFS order: a parent precedes its children */ +static int n_nodes; +static int n_leaves; + +/* ---- CPUs this test may run on ----------------------------------------- */ + +static cpu_set_t allowed_cpus; +static int n_cpu; +static long n_online_cpu; + +static int nth_cpu(int n) +{ + int i, seen = 0; + + for (i = 0; i < CPU_SETSIZE; i++) { + if (!CPU_ISSET(i, &allowed_cpus)) + continue; + if (seen++ == n % n_cpu) + return i; + } + return -1; +} + +static int pin_cpu(int cpu) +{ + cpu_set_t set; + + if (cpu < 0) + return -1; + + CPU_ZERO(&set); + CPU_SET(cpu, &set); + return sched_setaffinity(0, sizeof(set), &set); +} + +/* ---- tree construction -------------------------------------------------- */ + +static int add_node(const char *path, bool is_leaf) +{ + struct cg_node *n = &nodes[n_nodes]; + + if (cg_create(path)) + return -1; + + strncpy(n->path, path, sizeof(n->path) - 1); + n->id = cg_get_id(path); + n->is_leaf = is_leaf; + n->previous_current = -1; + if (is_leaf) + n_leaves++; + n_nodes++; + return 0; +} + +/* Build @levels below @path; only leaves are charged. */ +static int build_children(const char *path, int fanout, int levels) +{ + char child[PATH_MAX]; + int i; + + if (levels == 0) + return 0; + + /* Give child cgroups a memcg. */ + if (cg_write(path, "cgroup.subtree_control", "+memory")) + return -1; + + for (i = 0; i < fanout; i++) { + snprintf(child, sizeof(child), "%s/c%d", path, i); + if (add_node(child, levels == 1)) + return -1; + if (build_children(child, fanout, levels - 1)) + return -1; + } + return 0; +} + +static size_t tree_capacity(int fanout, int depth) +{ + size_t total = 1, level = 1; + int d; + + for (d = 0; d < depth; d++) { + level *= fanout; + total += level; + } + return total; +} + +static int build_tree(int fanout, int depth, int *root_fd) +{ + n_nodes = 0; + n_leaves = 0; + nodes = calloc(tree_capacity(fanout, depth), sizeof(*nodes)); + if (!nodes) + return -1; + + if (add_node(subtree_root, depth == 0)) + return -1; + + *root_fd = open(subtree_root, O_RDONLY); + if (*root_fd < 0) + return -1; + + return build_children(subtree_root, fanout, depth); +} + +/* Destroy children before parents, then reap chargers. */ +static void destroy_tree(void) +{ + int i; + + if (!nodes) + return; + + for (i = n_nodes - 1; i >= 0; i--) + cg_destroy(nodes[i].path); + free(nodes); + nodes = NULL; + + while (waitpid(-1, NULL, 0) > 0) + ; +} + +/* ---- cross-CPU charge (one child per leaf) ------------------------------ */ + +struct charge_args { + size_t bytes; /* anon this leaf holds */ + int base; /* index of the first CPU to fault on */ + int k; /* CPUs to spread the charge over */ +}; + +static int charge_leaf(const char *cgroup, void *arg) +{ + const struct charge_args *ca = arg; + int ppid = getppid(); + size_t per, off; + char *buf; + int j; + + buf = malloc(ca->bytes); + if (!buf) { + fprintf(stderr, "malloc() failed\n"); + return -1; + } + + /* Keep each CPU's slice page-aligned. */ + per = ca->bytes / ca->k / page_size * page_size; + + for (j = 0; j < ca->k; j++) { + off = (size_t)j * per; + if (pin_cpu(nth_cpu(ca->base + j))) { + free(buf); + return -1; + } + cg_touch_pages(buf + off, + j == ca->k - 1 ? ca->bytes - off : per); + } + + while (getppid() == ppid) + sleep(1); + + free(buf); + return 0; +} + +static int leaf_charge(size_t want, int k, size_t *bytes) +{ + size_t min_pages, pages, want_pages; + + if ((size_t)n_online_cpu > SIZE_MAX / FLUSH_MARGIN / + MEMCG_CHARGE_BATCH) + return -EOVERFLOW; + + /* Force more than one pending batch per CPU across the subtree. */ + min_pages = (size_t)FLUSH_MARGIN * MEMCG_CHARGE_BATCH * n_online_cpu; + pages = min_pages / n_leaves + !!(min_pages % n_leaves); + want_pages = want / page_size + !!(want % page_size); + if (pages < want_pages) + pages = want_pages; + if (pages < (size_t)k) + pages = k; + + if (pages > MAX_TOTAL_CHARGE / (size_t)n_leaves / page_size) + return -E2BIG; + + *bytes = pages * page_size; + return 0; +} + +static int start_chargers(int k, size_t bytes) +{ + struct charge_args ca = { .bytes = bytes, .k = k }; + long prev, cur; + bool ready; + int i, retries; + + for (i = 0; i < n_nodes; i++) { + if (!nodes[i].is_leaf) + continue; + if (cg_run_nowait(nodes[i].path, charge_leaf, &ca) < 0) { + ksft_print_msg("cannot start a charger on %s\n", + nodes[i].path); + return -1; + } + ca.base += k; + } + + /* Wait until every leaf is fully charged and stable. */ + for (retries = CHARGE_WAIT_RETRIES; retries; retries--) { + ready = true; + for (i = 0; i < n_nodes; i++) { + if (!nodes[i].is_leaf) + continue; + + cur = cg_read_long(nodes[i].path, "memory.current"); + if (cur < 0) { + ksft_print_msg("cannot read %s/memory.current: %s\n", + nodes[i].path, strerror(errno)); + return -1; + } + + prev = nodes[i].previous_current; + nodes[i].previous_current = cur; + if (cur < (long)bytes || cur != prev) + ready = false; + } + if (ready) + return 0; + usleep(DEFAULT_WAIT_INTERVAL_US / 10); + } + + for (i = 0; i < n_nodes; i++) { + if (nodes[i].is_leaf && nodes[i].previous_current < (long)bytes) { + ksft_print_msg("%s reached only %ld of %zu charged bytes\n", + nodes[i].path, nodes[i].previous_current, + bytes); + return -1; + } + } + ksft_print_msg("memory.current did not settle before the timeout\n"); + return -1; +} + +/* ---- the two readers ---------------------------------------------------- */ + +/* Flush the subtree and collect each cgroup's kfunc values. */ +static int read_bpf(int root_fd) +{ + DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts); + struct memcg_stat_cross_cpu *skel = NULL; + union bpf_iter_link_info linfo = {}; + struct bpf_link *link = NULL; + int ret = -1, err, i, mfd, fd; + char buf[4096]; + ssize_t r; + + skel = memcg_stat_cross_cpu__open(); + if (!skel) { + ksft_print_msg("skel open failed: %s (%d)\n", + strerror(errno), errno); + return -1; + } + err = bpf_program__set_autoload(skel->progs.memcg_kfuncs_probe, false); + if (err) { + ksft_print_msg("disabling capability probe failed: %s (%d)\n", + strerror(-err), err); + goto out; + } + err = bpf_map__set_max_entries(skel->maps.results, n_nodes + 8); + if (err) { + ksft_print_msg("set max_entries failed: %s (%d)\n", + strerror(-err), err); + goto out; + } + err = memcg_stat_cross_cpu__load(skel); + if (err) { + ksft_print_msg("skel load failed: %s (%d)\n", + strerror(-err), err); + goto out; + } + + linfo.cgroup.cgroup_fd = root_fd; + linfo.cgroup.order = BPF_CGROUP_ITER_DESCENDANTS_PRE; + opts.link_info = &linfo; + opts.link_info_len = sizeof(linfo); + + link = bpf_program__attach_iter(skel->progs.cgroup_memcg_stat_cross_cpu, + &opts); + err = libbpf_get_error(link); + if (err) { + link = NULL; + ksft_print_msg("attach iter failed: %s (%d)\n", + strerror(-err), err); + goto out; + } + + fd = bpf_iter_create(bpf_link__fd(link)); + if (fd < 0) { + ksft_print_msg("bpf_iter_create failed: %s (%d)\n", + strerror(errno), errno); + goto out; + } + do { + r = read(fd, buf, sizeof(buf)); + } while (r > 0 || (r < 0 && errno == EINTR)); + err = errno; + close(fd); + if (r < 0) { + ksft_print_msg("bpf walk failed: %s (%d)\n", + strerror(err), err); + goto out; + } + + mfd = bpf_map__fd(skel->maps.results); + for (i = 0; i < n_nodes; i++) + if (bpf_map_lookup_elem(mfd, &nodes[i].id, &nodes[i].bpf)) { + ksft_print_msg("no map entry for %s: %s (%d)\n", + nodes[i].path, strerror(errno), errno); + goto out; + } + ret = 0; +out: + bpf_link__destroy(link); + memcg_stat_cross_cpu__destroy(skel); + return ret; +} + +/* Read one cgroupfs snapshot. */ +static int read_files(int slot) +{ + int i; + + for (i = 0; i < n_nodes; i++) { + const char *path = nodes[i].path; + struct file_snap *f = &nodes[i].file[slot]; + + f->anon = cg_read_key_long(path, "memory.stat", "anon "); + f->file = cg_read_key_long(path, "memory.stat", "file "); + f->shmem = cg_read_key_long(path, "memory.stat", "shmem "); + f->file_mapped = cg_read_key_long(path, "memory.stat", + "file_mapped "); + f->pgfault = cg_read_key_long(path, "memory.stat", "pgfault "); + f->current = cg_read_long(path, "memory.current"); + + if (f->anon < 0 || f->file < 0 || f->shmem < 0 || + f->file_mapped < 0 || f->pgfault < 0 || f->current < 0) { + ksft_print_msg("reading the stats of %s failed\n", path); + return -1; + } + } + return 0; +} + +/* ---- comparison --------------------------------------------------------- */ + +static void dump_node(const struct cg_node *n) +{ + int s; + + ksft_print_msg("%s bpf : anon=%llu file=%llu shmem=%llu fmapped=%llu pgfault=%llu\n", + n->path, n->bpf.anon, n->bpf.file, n->bpf.shmem, + n->bpf.file_mapped, n->bpf.pgfault); + for (s = 0; s < 2; s++) + ksft_print_msg("%s file%d: anon=%ld file=%ld shmem=%ld fmapped=%ld pgfault=%ld\n", + n->path, s, n->file[s].anon, n->file[s].file, + n->file[s].shmem, n->file[s].file_mapped, + n->file[s].pgfault); +} + +/* Equal file samples require an exact BPF match; otherwise accept their range. */ +static bool bracketed(unsigned long long v, long a, long b) +{ + long lo = a < b ? a : b; + long hi = a < b ? b : a; + + return v >= (unsigned long long)lo && v <= (unsigned long long)hi; +} + +static int check_tree(size_t charged) +{ + unsigned long long root_anon = 0, leaf_anon = 0; + int i, bad = 0; + + for (i = 0; i < n_nodes; i++) { + const struct cg_node *n = &nodes[i]; + const struct memcg_stat_snapshot *b = &n->bpf; + const struct file_snap *f0 = &n->file[0], *f1 = &n->file[1]; + + if (!bracketed(b->anon, f0->anon, f1->anon) || + !bracketed(b->file, f0->file, f1->file) || + !bracketed(b->shmem, f0->shmem, f1->shmem) || + !bracketed(b->file_mapped, f0->file_mapped, + f1->file_mapped) || + !bracketed(b->pgfault, f0->pgfault, f1->pgfault)) { + ksft_print_msg("kfuncs disagree with memory.stat\n"); + dump_node(n); + bad++; + } + + /* Live page counters are bounds, not flushed statistics. */ + if (b->anon > b->usage_pages * (unsigned long long)page_size || + f1->anon > f1->current) { + ksft_print_msg("%s: anon above usage: bpf %llu/%llu file %ld/%ld\n", + n->path, b->anon, + b->usage_pages * (unsigned long long)page_size, + f1->anon, f1->current); + bad++; + } + + if (n->is_leaf) { + if (b->anon < charged) { + ksft_print_msg("%s: flushed anon %llu, charged %zu\n", + n->path, b->anon, charged); + bad++; + } + leaf_anon += b->anon; + } + if (i == 0) + root_anon = b->anon; + } + + if (root_anon != leaf_anon) { + ksft_print_msg("subtree root anon %llu, sum of the leaves %llu\n", + root_anon, leaf_anon); + bad++; + } + return bad ? -1 : 0; +} + +/* Accept PAGE_COUNTER_MAX values from either 32- or 64-bit kernels. */ +static int check_unlimited(void) +{ + unsigned long long max64 = (unsigned long long)INT64_MAX / page_size; + unsigned long long max32 = INT32_MAX; + + if (cg_read_strcmp(nodes[0].path, "memory.max", "max\n")) + return 0; + + if (nodes[0].bpf.max_pages != max64 && nodes[0].bpf.max_pages != max32) { + ksft_print_msg("memory.max reads max, kfunc reports %llu pages\n", + nodes[0].bpf.max_pages); + return -1; + } + return 0; +} + +/* ---- one case ----------------------------------------------------------- */ + +struct testcase { + const char *name; + int fanout; + int depth; + int cpus_per_leaf; /* K, or 0 for the bounded cross-CPU count */ + size_t resident_bytes; /* anon per leaf, raised if too small */ +}; + +static int run_case(const struct testcase *tc) +{ + int root_fd = -1, ret = KSFT_FAIL, err, k; + size_t charged; + + if (build_tree(tc->fanout, tc->depth, &root_fd)) { + ksft_print_msg("cannot build the tree\n"); + goto out; + } + + k = tc->cpus_per_leaf; + if (k <= 0) + k = n_cpu < MAX_CPUS_PER_LEAF ? n_cpu : MAX_CPUS_PER_LEAF; + else if (k > n_cpu) + k = n_cpu; + err = leaf_charge(tc->resident_bytes, k, &charged); + if (err == -E2BIG) { + ksft_print_msg("%s needs more than %luMB to trigger a flush on %ld online CPUs\n", + tc->name, MAX_TOTAL_CHARGE >> 20, n_online_cpu); + ret = KSFT_SKIP; + goto out; + } + if (err) { + ksft_print_msg("cannot calculate the charge for %s: %s (%d)\n", + tc->name, strerror(-err), err); + goto out; + } + + ksft_print_msg("%s: %d cgroups, %d leaves, %d/%d cpus, %zuKB per leaf\n", + tc->name, n_nodes, n_leaves, k, n_cpu, charged >> 10); + + if (start_chargers(k, charged)) + goto out; + + /* Read BPF first; memory.stat would consume the pending flush. */ + if (read_bpf(root_fd) || read_files(0) || read_files(1)) + goto out; + + if (!check_tree(charged) && !check_unlimited()) + ret = KSFT_PASS; +out: + if (root_fd >= 0) + close(root_fd); + destroy_tree(); + return ret; +} + +static const struct testcase cases[] = { + /* name, fanout, depth, K, anon per leaf */ + { "single_cpu_small_tree", 4, 2, 1, 2 << 20 }, + { "cross_cpu_small_tree", 4, 2, 0, 2 << 20 }, + { "single_cpu_large_tree", 4, 3, 1, 256 << 10 }, + { "cross_cpu_large_tree", 4, 3, 0, 256 << 10 }, +}; + +static bool memcg_kfuncs_available(void) +{ + static const char *const kfuncs[] = { + "bpf_get_mem_cgroup", + "bpf_put_mem_cgroup", + "bpf_mem_cgroup_flush_stats", + "bpf_mem_cgroup_page_state", + "bpf_mem_cgroup_vm_events", + }; + struct btf *btf; + int err, i; + + btf = btf__load_vmlinux_btf(); + err = libbpf_get_error(btf); + if (err) { + ksft_print_msg("cannot load vmlinux BTF: %s (%d)\n", + strerror(-err), err); + return false; + } + + for (i = 0; i < ARRAY_SIZE(kfuncs); i++) { + if (btf__find_by_name_kind(btf, kfuncs[i], BTF_KIND_FUNC) > 0) + continue; + ksft_print_msg("required kfunc %s is not in vmlinux BTF\n", + kfuncs[i]); + btf__free(btf); + return false; + } + if (btf__find_by_name_kind(btf, "bpf_iter_cgroup", BTF_KIND_FUNC) <= 0) { + ksft_print_msg("cgroup BPF iterator is not in vmlinux BTF\n"); + btf__free(btf); + return false; + } + btf__free(btf); + return true; +} + +static bool unsupported_bpf_feature_error(int err) +{ + return err == -EINVAL || err == -ENOENT || err == -EOPNOTSUPP; +} + +/* Return 1 if supported, 0 if unavailable, or a negative error. */ +static int probe_memcg_bpf_features(int root_fd) +{ + DECLARE_LIBBPF_OPTS(bpf_iter_attach_opts, opts); + struct memcg_stat_cross_cpu *skel; + union bpf_iter_link_info linfo = {}; + struct bpf_link *link = NULL; + int ret, err, iter_fd = -1; + + skel = memcg_stat_cross_cpu__open(); + if (!skel) { + err = errno ? -errno : -EINVAL; + ksft_print_msg("capability probe open failed: %s (%d)\n", + strerror(-err), err); + return err; + } + + err = bpf_program__set_autoload(skel->progs.cgroup_memcg_stat_cross_cpu, + false); + if (err) { + ksft_print_msg("disabling test program failed: %s (%d)\n", + strerror(-err), err); + ret = err; + goto out; + } + + err = memcg_stat_cross_cpu__load(skel); + if (err) { + ksft_print_msg("BPF capability probe load failed: %s (%d)\n", + strerror(-err), err); + ret = unsupported_bpf_feature_error(err) ? 0 : err; + goto out; + } + + linfo.cgroup.cgroup_fd = root_fd; + linfo.cgroup.order = BPF_CGROUP_ITER_SELF_ONLY; + opts.link_info = &linfo; + opts.link_info_len = sizeof(linfo); + link = bpf_program__attach_iter(skel->progs.memcg_kfuncs_probe, &opts); + err = libbpf_get_error(link); + if (err) { + link = NULL; + ksft_print_msg("BPF capability probe attach failed: %s (%d)\n", + strerror(-err), err); + ret = unsupported_bpf_feature_error(err) ? 0 : err; + goto out; + } + + iter_fd = bpf_iter_create(bpf_link__fd(link)); + if (iter_fd < 0) { + err = -errno; + ksft_print_msg("BPF capability probe iterator creation failed: %s (%d)\n", + strerror(-err), err); + ret = unsupported_bpf_feature_error(err) ? 0 : err; + goto out; + } + + ret = 1; +out: + if (iter_fd >= 0) + close(iter_fd); + bpf_link__destroy(link); + memcg_stat_cross_cpu__destroy(skel); + return ret; +} + +int main(int argc, char **argv) +{ + int feature_fd, i, ret; + + ksft_print_header(); + + /* Probe BTF before operations that may require privileges. */ + if (!memcg_kfuncs_available()) + ksft_exit_skip("memcg BPF kfuncs are not available\n"); + + if (cg_find_unified_root(root, sizeof(root), NULL)) + ksft_exit_skip("cgroup v2 isn't mounted\n"); + feature_fd = open(root, O_RDONLY | O_DIRECTORY); + if (feature_fd < 0) + ksft_exit_fail_msg("cannot open cgroup root: %s (%d)\n", + strerror(errno), errno); + ret = probe_memcg_bpf_features(feature_fd); + close(feature_fd); + if (!ret) + ksft_exit_skip("sleepable cgroup iterator or memcg kfuncs are not available\n"); + if (ret < 0) + ksft_exit_fail_msg("cannot probe BPF capabilities: %s (%d)\n", + strerror(-ret), ret); + + if (cg_read_strstr(root, "cgroup.controllers", "memory")) + ksft_exit_skip("memory controller isn't available\n"); + + if (cg_read_strstr(root, "cgroup.subtree_control", "memory")) + if (cg_write(root, "cgroup.subtree_control", "+memory")) + ksft_exit_skip("Failed to set memory controller\n"); + + CPU_ZERO(&allowed_cpus); + if (sched_getaffinity(0, sizeof(allowed_cpus), &allowed_cpus)) + ksft_exit_skip("cannot read CPU affinity\n"); + n_cpu = CPU_COUNT(&allowed_cpus); + if (n_cpu <= 0) + ksft_exit_skip("no CPU to run on\n"); + n_online_cpu = sysconf(_SC_NPROCESSORS_ONLN); + if (n_online_cpu <= 0) + ksft_exit_fail_msg("cannot determine the number of online CPUs\n"); + + page_size = sysconf(_SC_PAGESIZE); + if (page_size <= 0) + page_size = BUF_SIZE; + + subtree_root = cg_name(root, SUBTREE_NAME); + if (!subtree_root) + ksft_exit_skip("cannot build subtree root path\n"); + + /* Set the plan after all global skip checks. */ + ksft_set_plan(ARRAY_SIZE(cases)); + + for (i = 0; i < ARRAY_SIZE(cases); i++) { + switch (run_case(&cases[i])) { + case KSFT_PASS: + ksft_test_result_pass("%s\n", cases[i].name); + break; + case KSFT_SKIP: + ksft_test_result_skip("%s\n", cases[i].name); + break; + default: + ksft_test_result_fail("%s\n", cases[i].name); + break; + } + } + + free(subtree_root); + ksft_finished(); +} diff --git a/tools/testing/selftests/cgroup/test_memcontrol.c b/tools/testing/selftests/cgroup/test_memcontrol.c index 0ebf796f3cff..15ba46879504 100644 --- a/tools/testing/selftests/cgroup/test_memcontrol.c +++ b/tools/testing/selftests/cgroup/test_memcontrol.c @@ -26,7 +26,6 @@ static bool has_localevents; static bool has_recursiveprot; -static int page_size; int get_temp_fd(void) { @@ -56,28 +55,12 @@ int alloc_pagecache(int fd, size_t size) return -1; } -static char *alloc_and_populate_anon(size_t size) -{ - char *buf, *ptr; - - buf = malloc(size); - if (buf == NULL) { - fprintf(stderr, "malloc() failed\n"); - return NULL; - } - - for (ptr = buf; ptr < buf + size; ptr += page_size) - *ptr = 0; - - return buf; -} - int alloc_anon(const char *cgroup, void *arg) { size_t size = (unsigned long)arg; char *buf; - buf = alloc_and_populate_anon(size); + buf = cg_alloc_anon(size); if (!buf) return -1; @@ -195,7 +178,7 @@ static int alloc_anon_50M_check(const char *cgroup, void *arg) long anon, current; int ret = -1; - buf = alloc_and_populate_anon(size); + buf = cg_alloc_anon(size); if (!buf) return -1; @@ -420,7 +403,7 @@ static int alloc_anon_noexit(const char *cgroup, void *arg) size_t size = (unsigned long)arg; char *buf; - buf = alloc_and_populate_anon(size); + buf = cg_alloc_anon(size); if (!buf) return -1; @@ -1001,7 +984,7 @@ static int alloc_anon_50M_check_swap(const char *cgroup, void *arg) long mem_current, swap_current; int ret = -1; - buf = alloc_and_populate_anon(size); + buf = cg_alloc_anon(size); if (!buf) return -1; @@ -1793,10 +1776,6 @@ int main(int argc, char **argv) char root[PATH_MAX]; int i, proc_status; - page_size = sysconf(_SC_PAGE_SIZE); - if (page_size <= 0) - page_size = BUF_SIZE; - ksft_print_header(); ksft_set_plan(ARRAY_SIZE(tests)); if (cg_find_unified_root(root, sizeof(root), NULL)) -- 2.53.0-Meta hid carries its own ~150 lines of libbpf + bpftool + vmlinux.h + BPF-object + skeleton build machinery, copied from selftests/bpf. Replace it with an include of the shared tools/testing/selftests/lib.bpf.mk, so that the previous ~150 lines of BPF build configuration can now be achieved in only ~10 lines. The generated hid.skel.h public API remains byte-identical, and hid_bpf and hidraw are linked the same way. The shared fragment also supplies the common warning, language, system-include and adaptive BPF CPU flags. Remove four unused locals so hid.c builds with that warning set without a directory-wide suppression. Suggested-by: Eduard Zingerman Suggested-by: Mykola Lysenko Assisted-by: Claude:claude-opus-5 Signed-off-by: Ziyang Men --- tools/testing/selftests/hid/.gitignore | 1 + tools/testing/selftests/hid/Makefile | 179 ++---------------------- tools/testing/selftests/hid/progs/hid.c | 7 +- 3 files changed, 19 insertions(+), 168 deletions(-) diff --git a/tools/testing/selftests/hid/.gitignore b/tools/testing/selftests/hid/.gitignore index 933f483815b2..69943301dc1d 100644 --- a/tools/testing/selftests/hid/.gitignore +++ b/tools/testing/selftests/hid/.gitignore @@ -1,5 +1,6 @@ bpftool *.skel.h +*.bpf.d /host-tools /tools hid_bpf diff --git a/tools/testing/selftests/hid/Makefile b/tools/testing/selftests/hid/Makefile index 2f423de83147..1190a747036d 100644 --- a/tools/testing/selftests/hid/Makefile +++ b/tools/testing/selftests/hid/Makefile @@ -47,7 +47,6 @@ msg = else msg = @printf ' %-8s%s %s%s\n' "$(1)" "$(if $(2), [$(2)])" "$(notdir $(3))" "$(if $(4), $(4))"; MAKEFLAGS += --no-print-directory -submake_extras := feature_display=0 endif # override lib.mk's default rules @@ -58,172 +57,25 @@ override define CLEAN $(Q)$(RM) -r $(EXTRA_CLEAN) endef -include ../lib.mk - -TOOLSDIR := $(top_srcdir)/tools -LIBDIR := $(TOOLSDIR)/lib -BPFDIR := $(LIBDIR)/bpf -TOOLSINCDIR := $(TOOLSDIR)/include -BPFTOOLDIR := $(TOOLSDIR)/bpf/bpftool -SCRATCH_DIR := $(OUTPUT)/tools -BUILD_DIR := $(SCRATCH_DIR)/build -INCLUDE_DIR := $(SCRATCH_DIR)/include -BPFOBJ := $(BUILD_DIR)/libbpf/libbpf.a -ifneq ($(CROSS_COMPILE),) -HOST_BUILD_DIR := $(BUILD_DIR)/host -HOST_SCRATCH_DIR := $(OUTPUT)/host-tools -HOST_INCLUDE_DIR := $(HOST_SCRATCH_DIR)/include -else -HOST_BUILD_DIR := $(BUILD_DIR) -HOST_SCRATCH_DIR := $(SCRATCH_DIR) -HOST_INCLUDE_DIR := $(INCLUDE_DIR) -endif -HOST_BPFOBJ := $(HOST_BUILD_DIR)/libbpf/libbpf.a -RESOLVE_BTFIDS := $(HOST_BUILD_DIR)/resolve_btfids/resolve_btfids - -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 - -# Define simple and short `make test_progs`, `make test_sysctl`, etc targets -# to build individual tests. -# NOTE: Semicolon at the end is critical to override lib.mk's default static -# rule for binaries. -$(notdir $(TEST_GEN_PROGS)): %: $(OUTPUT)/% ; - -# sort removes libbpf duplicates when not cross-building -MAKE_DIRS := $(sort $(BUILD_DIR)/libbpf $(HOST_BUILD_DIR)/libbpf \ - $(HOST_BUILD_DIR)/bpftool $(HOST_BUILD_DIR)/resolve_btfids \ - $(INCLUDE_DIR)) -$(MAKE_DIRS): - $(call msg,MKDIR,,$@) - $(Q)mkdir -p $@ +# Build the legacy progs/*.c layout with the shared BPF rules. +BPF_SRCS := $(wildcard progs/*.c) +BPF_PROG_EXT := .c +# BPFDIR is defined when this recursive variable is expanded. +BPF_EXTRA_HDRS = $(wildcard progs/*.h) $(wildcard $(BPFDIR)/hid_bpf_*.h) \ + $(wildcard $(BPFDIR)/*.bpf.h) -DEFAULT_BPFTOOL := $(HOST_SCRATCH_DIR)/sbin/bpftool +include ../lib.mk +include ../lib.bpf.mk +# Preserve the existing install list. +TEST_GEN_FILES += $(BPF_OBJS) TEST_GEN_PROGS_EXTENDED += $(DEFAULT_BPFTOOL) -$(TEST_GEN_PROGS) $(TEST_GEN_PROGS_EXTENDED): $(BPFOBJ) - -BPFTOOL ?= $(DEFAULT_BPFTOOL) -$(DEFAULT_BPFTOOL): $(wildcard $(BPFTOOLDIR)/*.[ch] $(BPFTOOLDIR)/Makefile) \ - $(HOST_BPFOBJ) | $(HOST_BUILD_DIR)/bpftool - $(Q)$(MAKE) $(submake_extras) -C $(BPFTOOLDIR) \ - ARCH= CROSS_COMPILE= CC=$(HOSTCC) LD=$(HOSTLD) \ - EXTRA_CFLAGS='-g -O0' \ - OUTPUT=$(HOST_BUILD_DIR)/bpftool/ \ - LIBBPF_OUTPUT=$(HOST_BUILD_DIR)/libbpf/ \ - LIBBPF_DESTDIR=$(HOST_SCRATCH_DIR)/ \ - prefix= DESTDIR=$(HOST_SCRATCH_DIR)/ install-bin - -$(BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile) \ - | $(BUILD_DIR)/libbpf - $(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) OUTPUT=$(BUILD_DIR)/libbpf/ \ - EXTRA_CFLAGS='-g -O0' \ - DESTDIR=$(SCRATCH_DIR) prefix= all install_headers - -ifneq ($(BPFOBJ),$(HOST_BPFOBJ)) -$(HOST_BPFOBJ): $(wildcard $(BPFDIR)/*.[ch] $(BPFDIR)/Makefile) \ - | $(HOST_BUILD_DIR)/libbpf - $(Q)$(MAKE) $(submake_extras) -C $(BPFDIR) \ - EXTRA_CFLAGS='-g -O0' ARCH= CROSS_COMPILE= \ - OUTPUT=$(HOST_BUILD_DIR)/libbpf/ CC=$(HOSTCC) LD=$(HOSTLD) \ - DESTDIR=$(HOST_SCRATCH_DIR)/ prefix= all install_headers -endif - -$(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 - -$(RESOLVE_BTFIDS): $(HOST_BPFOBJ) | $(HOST_BUILD_DIR)/resolve_btfids \ - $(TOOLSDIR)/bpf/resolve_btfids/main.c \ - $(TOOLSDIR)/lib/rbtree.c \ - $(TOOLSDIR)/lib/zalloc.c \ - $(TOOLSDIR)/lib/string.c \ - $(TOOLSDIR)/lib/ctype.c \ - $(TOOLSDIR)/lib/str_error_r.c - $(Q)$(MAKE) $(submake_extras) -C $(TOOLSDIR)/bpf/resolve_btfids \ - CC=$(HOSTCC) LD=$(HOSTLD) AR=$(HOSTAR) \ - LIBBPF_INCLUDE=$(HOST_INCLUDE_DIR) \ - OUTPUT=$(HOST_BUILD_DIR)/resolve_btfids/ BPFOBJ=$(HOST_BPFOBJ) - -# Get Clang's default includes on this system, as opposed to those seen by -# '--target=bpf'. This fixes "missing" files on some architectures/distros, -# such as asm/byteorder.h, asm/socket.h, asm/sockios.h, sys/cdefs.h etc. -# -# Use '-idirafter': Don't interfere with include mechanics except where the -# build would have failed anyways. -define get_sys_includes -$(shell $(1) -v -E - &1 \ - | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \ -$(shell $(1) -dM -E - $@ +# Relink tests when the in-tree libbpf changes. +$(TEST_GEN_PROGS): $(BPFOBJ) $(OUTPUT)/%.o: %.c $(BPF_SKELS) hid_common.h $(call msg,CC,,$@) @@ -233,5 +85,4 @@ $(OUTPUT)/%: $(OUTPUT)/%.o $(call msg,BINARY,,$@) $(Q)$(LINK.c) $^ $(LDLIBS) -o $@ -EXTRA_CLEAN := $(SCRATCH_DIR) $(HOST_SCRATCH_DIR) feature bpftool \ - $(addprefix $(OUTPUT)/,*.o *.skel.h no_alu32) +EXTRA_CLEAN += $(OUTPUT)/*.o diff --git a/tools/testing/selftests/hid/progs/hid.c b/tools/testing/selftests/hid/progs/hid.c index b21fbb13c926..ec734e18d23a 100644 --- a/tools/testing/selftests/hid/progs/hid.c +++ b/tools/testing/selftests/hid/progs/hid.c @@ -111,7 +111,7 @@ int hid_user_raw_request(struct hid_hw_request_syscall_args *args) { struct hid_bpf_ctx *ctx; const size_t size = args->size; - int i, ret = 0; + int ret = 0; if (size > sizeof(args->data)) return -7; /* -E2BIG */ @@ -137,7 +137,7 @@ int hid_user_output_report(struct hid_hw_request_syscall_args *args) { struct hid_bpf_ctx *ctx; const size_t size = args->size; - int i, ret = 0; + int ret = 0; if (size > sizeof(args->data)) return -7; /* -E2BIG */ @@ -161,7 +161,7 @@ int hid_user_input_report(struct hid_hw_request_syscall_args *args) { struct hid_bpf_ctx *ctx; const size_t size = args->size; - int i, ret = 0; + int ret = 0; if (size > sizeof(args->data)) return -7; /* -E2BIG */ @@ -417,7 +417,6 @@ SEC("?struct_ops.s/hid_hw_output_report") int BPF_PROG(hid_test_hidraw_output_report, struct hid_bpf_ctx *hctx, __u64 source) { __u8 *data = hid_bpf_get_data(hctx, 0 /* offset */, 3 /* size */); - int ret; if (!data) return 0; /* EPERM check */ -- 2.53.0-Meta sched_ext carries its own ~130 lines of libbpf + bpftool + vmlinux.h + BPF-object + skeleton build machinery. Replace it with an include of the shared tools/testing/selftests/lib.bpf.mk, making sched_ext the third in-tree consumer of that fragment, after selftests/cgroup and selftests/hid. All generated skeletons and subskeletons keep the same public API, and the runner is built and linked the same way. Suggested-by: Eduard Zingerman Suggested-by: Mykola Lysenko Assisted-by: Claude:claude-opus-5 Signed-off-by: Ziyang Men --- tools/testing/selftests/sched_ext/Makefile | 149 ++++----------------- 1 file changed, 26 insertions(+), 123 deletions(-) diff --git a/tools/testing/selftests/sched_ext/Makefile b/tools/testing/selftests/sched_ext/Makefile index 5d2dffca0e91..856fe7dea685 100644 --- a/tools/testing/selftests/sched_ext/Makefile +++ b/tools/testing/selftests/sched_ext/Makefile @@ -14,48 +14,29 @@ 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 +# Build scheduler skeletons and subskeletons with the shared BPF rules. +BPF_SRCS := $(wildcard *.bpf.c) +BPF_SKEL_EXT := .bpf.skel.h +BPF_GEN_SUBSKEL := 1 +# Preserve the existing build/ layout. +BPF_OBJ_DIR := $(OUTPUT)/build/obj/sched_ext +BPF_SKEL_DIR := $(OUTPUT)/build/include +SCXOBJ_DIR := $(BPF_OBJ_DIR) -BPFTOOL ?= $(DEFAULT_BPFTOOL) +include ../lib.bpf.mk 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) + -I$(GENDIR) -I$(LIBDIR) -I$(TOOLSINCDIR) -I$(APIDIR) \ + -I$(CURDIR)/include -I$(SCXTOOLSINCDIR) # Silence some warnings when compiled with clang ifneq ($(LLVM),) @@ -64,102 +45,26 @@ endif LDFLAGS = -lelf -lz -lpthread -lzstd -IS_LITTLE_ENDIAN = $(shell $(CC) -dM -E - &1 \ - | sed -n '/<...> search starts here:/,/End of search list./{ s| \(/.*\)|-idirafter \1|p }') \ -$(shell $(1) $(2) -dM -E - $@ -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 $@ + $(BPF_EXTRA_CFLAGS) -$(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) +EXTRA_CLEAN += $(OUTPUT)/build ################ # 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))) +# Build every scheduler before each test. +all_test_bpfprogs := $(BPF_SKELS) auto-test-targets := \ create_dsq \ @@ -195,24 +100,22 @@ auto-test-targets := \ testcase-targets := $(addsuffix .o,$(addprefix $(SCXOBJ_DIR)/,$(auto-test-targets))) $(SCXOBJ_DIR)/runner.o: runner.c | $(SCXOBJ_DIR) $(BPFOBJ) - $(CC) $(CFLAGS) -c $< -o $@ + $(call msg,CC,,$@) + $(Q)$(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 $@ + $(call msg,CC,,$@) + $(Q)$(CC) $(CFLAGS) -c $< -o $@ $(SCXOBJ_DIR)/util.o: util.c | $(SCXOBJ_DIR) - $(CC) $(CFLAGS) -c $< -o $@ + $(call msg,CC,,$@) + $(Q)$(CC) $(CFLAGS) -c $< -o $@ $(OUTPUT)/runner: $(SCXOBJ_DIR)/runner.o $(SCXOBJ_DIR)/util.o $(BPFOBJ) $(testcase-targets) - @echo "$(testcase-targets)" - $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) + $(call msg,BINARY,,$@) + $(Q)$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) .DEFAULT_GOAL := all -- 2.53.0-Meta