Add tests validating fair_server bandwidth management and CPU protection behavior. The fair_server is a DEADLINE server that provides CPU time to CFS tasks while enforcing bandwidth limits within the RT bandwidth allocation.
The fair_server_bandwidth_validation test validates that the kernel enforces per-CPU RT bandwidth limits when configuring fair_server runtime. It attempts to set all CPUs to 101% of the per-CPU RT bandwidth and verifies that at least one write is rejected, ensuring the kernel prevents misconfiguration that would exceed available bandwidth. The fair_server_cpu_protection test verifies that CFS tasks receive their allocated fair_server CPU time even when competing with high-priority SCHED_FIFO tasks on the same CPU. It measures actual CPU usage and validates it falls within expected tolerance of ±50%, ensuring the fair_server provides the bandwidth protection that CFS tasks rely on. Helper functions are added to dl_util for fair_server management. These include dl_fair_server_exists() to check if the fair_server interface is available, dl_get_fair_server_settings() to read per-CPU runtime and period values, dl_set_fair_server_runtime() to write per-CPU runtime configuration, dl_set_rt_bandwidth() to configure system RT bandwidth limits, and dl_get_process_cpu_time() to read process CPU time from /proc/PID/stat for validation purposes. Assisted-by: Claude Code: claude-sonnet-4-5@20250929 Signed-off-by: Juri Lelli <[email protected]> --- tools/testing/selftests/sched/deadline/Makefile | 5 +- tools/testing/selftests/sched/deadline/dl_util.c | 128 ++++++++++ tools/testing/selftests/sched/deadline/dl_util.h | 57 +++++ .../testing/selftests/sched/deadline/fair_server.c | 260 +++++++++++++++++++++ 4 files changed, 449 insertions(+), 1 deletion(-) diff --git a/tools/testing/selftests/sched/deadline/Makefile b/tools/testing/selftests/sched/deadline/Makefile index daa2f5d14e947..e7e16c610ee58 100644 --- a/tools/testing/selftests/sched/deadline/Makefile +++ b/tools/testing/selftests/sched/deadline/Makefile @@ -14,7 +14,7 @@ OUTPUT_DIR := $(OUTPUT) UTIL_OBJS := $(OUTPUT)/dl_util.o # Test object files (all .c files except runner.c, dl_util.c, cpuhog.c) -TEST_OBJS := $(OUTPUT)/basic.o $(OUTPUT)/bandwidth.o +TEST_OBJS := $(OUTPUT)/basic.o $(OUTPUT)/bandwidth.o $(OUTPUT)/fair_server.o # Runner binary links utility and test objects $(OUTPUT)/runner: runner.c $(UTIL_OBJS) $(TEST_OBJS) dl_test.h | $(OUTPUT_DIR) @@ -35,6 +35,9 @@ $(OUTPUT)/basic.o: basic.c dl_test.h dl_util.h | $(OUTPUT_DIR) $(OUTPUT)/bandwidth.o: bandwidth.c dl_test.h dl_util.h | $(OUTPUT_DIR) $(CC) $(CFLAGS) -c $< -o $@ +$(OUTPUT)/fair_server.o: fair_server.c dl_test.h dl_util.h | $(OUTPUT_DIR) + $(CC) $(CFLAGS) -c $< -o $@ + $(OUTPUT_DIR): mkdir -p $@ diff --git a/tools/testing/selftests/sched/deadline/dl_util.c b/tools/testing/selftests/sched/deadline/dl_util.c index 6727d622d72d3..ca34eee964d61 100644 --- a/tools/testing/selftests/sched/deadline/dl_util.c +++ b/tools/testing/selftests/sched/deadline/dl_util.c @@ -203,6 +203,80 @@ int dl_calc_max_bandwidth_percent(void) return available_percent > 0 ? available_percent : 1; } +static int write_proc_uint64(const char *path, uint64_t value) +{ + FILE *f; + int ret; + + f = fopen(path, "w"); + if (!f) + return -1; + + ret = fprintf(f, "%lu\n", value); + if (ret < 0) { + fclose(f); + return -1; + } + + /* fclose() flushes and may return error if kernel write fails */ + if (fclose(f) != 0) + return -1; + + return 0; +} + +int dl_set_rt_bandwidth(uint64_t runtime_us, uint64_t period_us) +{ + int ret; + + ret = write_proc_uint64("/proc/sys/kernel/sched_rt_runtime_us", + runtime_us); + if (ret < 0) + return ret; + + return write_proc_uint64("/proc/sys/kernel/sched_rt_period_us", + period_us); +} + +bool dl_fair_server_exists(void) +{ + return access("/sys/kernel/debug/sched/fair_server", F_OK) == 0; +} + +int dl_get_fair_server_settings(int cpu, uint64_t *runtime_ns, + uint64_t *period_ns) +{ + char runtime_path[256]; + char period_path[256]; + int ret; + + snprintf(runtime_path, sizeof(runtime_path), + "/sys/kernel/debug/sched/fair_server/cpu%d/runtime", cpu); + + ret = read_proc_uint64(runtime_path, runtime_ns); + if (ret < 0) + return ret; + + /* period_ns is optional */ + if (period_ns) { + snprintf(period_path, sizeof(period_path), + "/sys/kernel/debug/sched/fair_server/cpu%d/period", cpu); + return read_proc_uint64(period_path, period_ns); + } + + return 0; +} + +int dl_set_fair_server_runtime(int cpu, uint64_t runtime_ns) +{ + char path[256]; + + snprintf(path, sizeof(path), + "/sys/kernel/debug/sched/fair_server/cpu%d/runtime", cpu); + + return write_proc_uint64(path, runtime_ns); +} + /* * Process management */ @@ -321,6 +395,60 @@ int dl_wait_for_pid(pid_t pid, int timeout_ms) return -1; } +uint64_t dl_get_process_cpu_time(pid_t pid) +{ + char path[256]; + char line[1024]; + FILE *f; + uint64_t utime = 0, stime = 0; + int i; + char *p, *token, *saveptr; + + snprintf(path, sizeof(path), "/proc/%d/stat", pid); + f = fopen(path, "r"); + if (!f) + return 0; + + if (!fgets(line, sizeof(line), f)) { + fclose(f); + return 0; + } + + fclose(f); + + /* + * Parse /proc/PID/stat format: + * pid (comm) state ppid ... utime stime ... + * + * The comm field (field 2) can contain spaces and is enclosed in + * parentheses. Find the last ')' to skip past it, then parse the + * remaining space-separated fields. + * + * After the closing ')', fields are: + * 1=state 2=ppid 3=pgrp 4=sid 5=tty_nr 6=tty_pgrp 7=flags + * 8=min_flt 9=cmin_flt 10=maj_flt 11=cmaj_flt 12=utime 13=stime + */ + p = strrchr(line, ')'); + if (!p) + return 0; + + /* Skip past ') ' */ + p += 2; + + /* Tokenize remaining fields */ + token = strtok_r(p, " ", &saveptr); + for (i = 1; token && i <= 13; i++) { + if (i == 12) + utime = strtoull(token, NULL, 10); + else if (i == 13) + stime = strtoull(token, NULL, 10); + + token = strtok_r(NULL, " ", &saveptr); + } + + return utime + stime; +} + /* * CPU topology operations */ diff --git a/tools/testing/selftests/sched/deadline/dl_util.h b/tools/testing/selftests/sched/deadline/dl_util.h index f8046eb0cbd3b..511cc92ef1e3e 100644 --- a/tools/testing/selftests/sched/deadline/dl_util.h +++ b/tools/testing/selftests/sched/deadline/dl_util.h @@ -99,6 +99,52 @@ int dl_get_server_bandwidth_overhead(void); */ int dl_calc_max_bandwidth_percent(void); +/** + * dl_set_rt_bandwidth() - Set RT bandwidth settings + * @runtime_us: Runtime in microseconds + * @period_us: Period in microseconds + * + * Writes to /proc/sys/kernel/sched_rt_runtime_us and + * /proc/sys/kernel/sched_rt_period_us. Requires root privileges. + * + * Return: 0 on success, -1 on error + */ +int dl_set_rt_bandwidth(uint64_t runtime_us, uint64_t period_us); + +/** + * dl_get_fair_server_settings() - Read fair_server settings for a CPU + * @cpu: CPU number + * @runtime_ns: Pointer to store runtime in nanoseconds + * @period_ns: Pointer to store period in nanoseconds + * + * Reads from /sys/kernel/debug/sched/fair_server/cpuN/runtime and period. + * + * Return: 0 on success, -1 on error (including if fair_server doesn't exist) + */ +int dl_get_fair_server_settings(int cpu, uint64_t *runtime_ns, + uint64_t *period_ns); + +/** + * dl_set_fair_server_runtime() - Set fair_server runtime for a CPU + * @cpu: CPU number + * @runtime_ns: Runtime in nanoseconds + * + * Writes to /sys/kernel/debug/sched/fair_server/cpuN/runtime. + * Requires appropriate permissions. + * + * Return: 0 on success, -1 on error + */ +int dl_set_fair_server_runtime(int cpu, uint64_t runtime_ns); + +/** + * dl_fair_server_exists() - Check if fair_server interface exists + * + * Checks if /sys/kernel/debug/sched/fair_server directory exists. + * + * Return: true if fair_server interface exists, false otherwise + */ +bool dl_fair_server_exists(void); + /* * Process management */ @@ -148,6 +194,17 @@ int dl_find_cpuhogs(pid_t *pids, int max_pids); */ int dl_wait_for_pid(pid_t pid, int timeout_ms); +/** + * dl_get_process_cpu_time() - Get total CPU time for a process + * @pid: Process ID + * + * Reads utime and stime from /proc/<pid>/stat and returns total CPU + * time in clock ticks. + * + * Return: Total CPU ticks used, or 0 on error + */ +uint64_t dl_get_process_cpu_time(pid_t pid); + /* * CPU topology operations */ diff --git a/tools/testing/selftests/sched/deadline/fair_server.c b/tools/testing/selftests/sched/deadline/fair_server.c new file mode 100644 index 0000000000000..dbff6296090f2 --- /dev/null +++ b/tools/testing/selftests/sched/deadline/fair_server.c @@ -0,0 +1,260 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * SCHED_DEADLINE fair_server tests + * + * Validates fair_server bandwidth management and CPU protection behavior. + */ + +#define _GNU_SOURCE +#include <stdio.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/wait.h> +#include <signal.h> +#include <errno.h> +#include <string.h> +#include <sched.h> +#include "dl_test.h" +#include "dl_util.h" + +/* + * Test: Fair server bandwidth validation + * + * Verifies that the kernel rejects attempts to set fair_server bandwidth + * that exceeds available RT bandwidth, and preserves the original value. + */ +static enum dl_test_status test_fair_server_bandwidth_validation_run(void *ctx) +{ + uint64_t rt_runtime_us, rt_period_us; + uint64_t fair_runtime_ns, fair_period_ns; + uint64_t excessive_runtime_ns; + uint64_t *original_runtimes = NULL; + int num_cpus, i; + int write_succeeded = 0; + int write_failed = 0; + + /* Check if fair_server interface exists */ + if (!dl_fair_server_exists()) { + printf(" Fair server interface not found\n"); + return DL_TEST_SKIP; + } + + /* Read RT bandwidth settings */ + DL_FAIL_IF(dl_get_rt_bandwidth(&rt_runtime_us, &rt_period_us) < 0, + "Failed to read RT bandwidth settings"); + + printf(" RT bandwidth: %luµs / %luµs per CPU\n", + rt_runtime_us, rt_period_us); + + num_cpus = dl_get_online_cpus(); + DL_FAIL_IF(num_cpus <= 0, "Failed to get number of CPUs"); + + printf(" Number of online CPUs: %d\n", num_cpus); + + /* Read current fair_server settings for cpu0 to get period */ + DL_FAIL_IF(dl_get_fair_server_settings(0, &fair_runtime_ns, + &fair_period_ns) < 0, + "Failed to read fair_server settings"); + + printf(" Fair server period: %luns\n", fair_period_ns); + + /* Save original runtimes for all CPUs */ + original_runtimes = calloc(num_cpus, sizeof(uint64_t)); + DL_FAIL_IF(!original_runtimes, "Failed to allocate memory"); + + for (i = 0; i < num_cpus; i++) { + if (dl_get_fair_server_settings(i, &original_runtimes[i], + NULL) < 0) { + printf(" Warning: Cannot read CPU %d settings\n", i); + original_runtimes[i] = 0; + } + } + + /* + * Try to set each CPU's fair_server to 101% of RT bandwidth per CPU. + * This should exceed the per-CPU RT bandwidth limit and fail. + */ + excessive_runtime_ns = (rt_runtime_us * 101 / 100) * 1000; + + /* Scale to fair_server period if different from RT period */ + if (fair_period_ns != rt_period_us * 1000) + excessive_runtime_ns = excessive_runtime_ns * fair_period_ns / + (rt_period_us * 1000); + + printf(" Attempting to set all CPUs to %luns (101%% of RT bandwidth)\n", + excessive_runtime_ns); + + for (i = 0; i < num_cpus; i++) { + if (dl_set_fair_server_runtime(i, excessive_runtime_ns) == 0) { + write_succeeded++; + } else { + write_failed++; + printf(" CPU %d write rejected: %s\n", i, strerror(errno)); + } + } + + printf(" Result: %d writes succeeded, %d failed\n", + write_succeeded, write_failed); + + /* Restore original values */ + for (i = 0; i < num_cpus; i++) { + if (original_runtimes[i] > 0) + dl_set_fair_server_runtime(i, original_runtimes[i]); + } + + free(original_runtimes); + + /* + * Test passes if at least one write was rejected, + * showing bandwidth limit enforcement. + */ + if (write_failed > 0) { + printf(" SUCCESS: Bandwidth limit enforced (%d writes rejected)\n", + write_failed); + return DL_TEST_PASS; + } + + printf(" FAIL: All writes accepted, no bandwidth limit enforcement\n"); + return DL_TEST_FAIL; +} + +static struct dl_test test_fair_server_bandwidth_validation = { + .name = "fair_server_bandwidth_validation", + .description = "Verify fair_server bandwidth validation against RT bandwidth", + .run = test_fair_server_bandwidth_validation_run, +}; +REGISTER_DL_TEST(&test_fair_server_bandwidth_validation); + +/* + * Test: Fair server CPU protection under FIFO competition + * + * Verifies that fair_server provides CPU time to CFS tasks even when + * competing with high-priority FIFO tasks on the same CPU. + */ +static enum dl_test_status test_fair_server_cpu_protection_run(void *ctx) +{ + uint64_t fair_runtime_ns, fair_period_ns; + uint64_t initial_time, final_time, cpu_ticks_used; + uint64_t ticks_per_sec, test_duration = 12; + pid_t cfs_pid, fifo_pid; + int test_cpu = 2; + int expected_percent, cpu_percent; + int min_expected, max_expected; + cpu_set_t cpuset; + struct sched_param param; + + /* Check if fair_server interface exists */ + if (!dl_fair_server_exists()) { + printf(" Fair server interface not found\n"); + return DL_TEST_SKIP; + } + + /* Read fair_server settings */ + DL_FAIL_IF(dl_get_fair_server_settings(test_cpu, &fair_runtime_ns, + &fair_period_ns) < 0, + "Failed to read fair_server settings"); + + expected_percent = (fair_runtime_ns * 100) / fair_period_ns; + + printf(" Fair server (CPU %d): %luns / %luns (%d%%)\n", + test_cpu, fair_runtime_ns, fair_period_ns, expected_percent); + + ticks_per_sec = sysconf(_SC_CLK_TCK); + + /* Fork CFS cpuhog */ + cfs_pid = fork(); + if (cfs_pid < 0) { + DL_ERR("Failed to fork CFS task"); + return DL_TEST_FAIL; + } + + if (cfs_pid == 0) { + /* Child: CFS cpuhog pinned to test_cpu */ + CPU_ZERO(&cpuset); + CPU_SET(test_cpu, &cpuset); + sched_setaffinity(0, sizeof(cpuset), &cpuset); + + execl("./cpuhog", "cpuhog", "-t", "20", NULL); + exit(1); + } + + /* Wait for CFS task to stabilize */ + sleep(2); + + printf(" Measuring baseline CPU time...\n"); + initial_time = dl_get_process_cpu_time(cfs_pid); + + /* Fork FIFO cpuhog */ + fifo_pid = fork(); + if (fifo_pid < 0) { + kill(cfs_pid, SIGKILL); + waitpid(cfs_pid, NULL, 0); + DL_ERR("Failed to fork FIFO task"); + return DL_TEST_FAIL; + } + + if (fifo_pid == 0) { + /* Child: FIFO cpuhog pinned to test_cpu */ + CPU_ZERO(&cpuset); + CPU_SET(test_cpu, &cpuset); + sched_setaffinity(0, sizeof(cpuset), &cpuset); + + param.sched_priority = 50; + sched_setscheduler(0, SCHED_FIFO, ¶m); + + execl("./cpuhog", "cpuhog", "-t", "20", NULL); + exit(1); + } + + printf(" Starting FIFO competition for %lus...\n", test_duration); + + /* Wait for test duration */ + sleep(test_duration); + + printf(" Measuring final CPU time...\n"); + final_time = dl_get_process_cpu_time(cfs_pid); + + /* Cleanup */ + kill(cfs_pid, SIGKILL); + kill(fifo_pid, SIGKILL); + waitpid(cfs_pid, NULL, 0); + waitpid(fifo_pid, NULL, 0); + + /* Calculate CPU usage */ + cpu_ticks_used = final_time - initial_time; + cpu_percent = (cpu_ticks_used * 100) / (test_duration * ticks_per_sec); + + printf(" CPU ticks used: %lu / %lu\n", + cpu_ticks_used, test_duration * ticks_per_sec); + printf(" CFS task CPU usage: %d%%\n", cpu_percent); + + /* Allow ±50% tolerance (e.g., 5% ± 50% = 2.5% - 7.5%) */ + min_expected = expected_percent * 50 / 100; + max_expected = expected_percent * 150 / 100; + + if (min_expected < 1) + min_expected = 1; + + printf(" Expected range: %d%% - %d%%\n", min_expected, max_expected); + + if (cpu_percent >= min_expected && cpu_percent <= max_expected) { + printf(" SUCCESS: CFS task received %d%% CPU\n", cpu_percent); + return DL_TEST_PASS; + } else if (cpu_percent < min_expected) { + printf(" FAIL: CFS task received only %d%% (below %d%%)\n", + cpu_percent, min_expected); + return DL_TEST_FAIL; + } + + printf(" FAIL: CFS task received %d%% (above %d%%)\n", + cpu_percent, max_expected); + return DL_TEST_FAIL; +} + +static struct dl_test test_fair_server_cpu_protection = { + .name = "fair_server_cpu_protection", + .description = "Verify fair_server provides CPU protection under FIFO competition", + .run = test_fair_server_cpu_protection_run, +}; +REGISTER_DL_TEST(&test_fair_server_cpu_protection); -- 2.53.0

