[PATCH RFC 6/7] selftests/sched: Add SCHED_DEADLINE fair_server tests to kselftest
From: Juri Lelli
Date: Fri Mar 06 2026 - 11:17:54 EST
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 <juri.lelli@xxxxxxxxxx>
---
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