Linux Kernel Selftest development
 help / color / mirror / Atom feed
From: Fernand Sieber <sieberf@amazon.com>
To: Peter Zijlstra <peterz@infradead.org>,
	Ingo Molnar <mingo@redhat.com>,
	Juri Lelli <juri.lelli@redhat.com>,
	Vincent Guittot <vincent.guittot@linaro.org>,
	Shuah Khan <shuah@kernel.org>
Cc: Dietmar Eggemann <dietmar.eggemann@arm.com>,
	Steven Rostedt <rostedt@goodmis.org>,
	Ben Segall <bsegall@google.com>, Mel Gorman <mgorman@suse.de>,
	Valentin Schneider <vschneid@redhat.com>,
	K Prateek Nayak <kprateek.nayak@amd.com>,
	Daniel Bristot de Oliveira <bristot@kernel.org>,
	Joel Fernandes <joelagnelf@nvidia.com>,
	Josh Don <joshdon@google.com>, <linux-kernel@vger.kernel.org>,
	<linux-kselftest@vger.kernel.org>, <nh-open-source@amazon.com>,
	Fahad Mubeen <fmubeen@amazon.de>,
	"David Woodhouse" <dwmw@amazon.co.uk>,
	Misha Karataiev <karataev@amazon.com>,
	Hendrik Borghorst <hborghor@amazon.de>,
	Ibrahim Elshahed <lshahed@amazon.de>,
	Fernand Sieber <sieberf@amazon.com>
Subject: [PATCH 5/5] selftests/sched: Add a core-sched forced-idle DL server test
Date: Thu, 12 Nov 2026 18:15:48 +0200	[thread overview]
Message-ID: <20261112161548.78189-6-sieberf@amazon.com> (raw)
In-Reply-To: <20261112161548.78189-1-sieberf@amazon.com>

Nothing in selftests notices a deferred DL server that is charged for
core-sched forced idle and so never activates: the fair server keeps its
reservation in debugfs while the CFS tasks it reserves for get a few
hundredths of a percent of a CPU.

Add cs_dl_server_test. It pins a SCHED_FIFO 50 spinner with its own core
cookie to one SMT sibling and an uncookied CFS spinner to the other, so
the second sibling is forced idle whenever the FIFO task runs, then
checks the CFS task's share of that CPU over five seconds against the
fair server's reservation under /sys/kernel/debug/sched/. The floor is
half the reservation, 2.5% by default, since every slice the server gets
costs a forced-idle hand-over. It skips without SMT, without core
scheduling, or without the debugfs reservation.

At 2 cores x 2 threads the share measures 4.0-5.2% once both this idle
charge and the core-wide DL server pick are fixed, and 0.0% with either
missing, so it passes only with both.

Signed-off-by: Fernand Sieber <sieberf@amazon.com>
---
 tools/testing/selftests/sched/.gitignore      |   1 +
 tools/testing/selftests/sched/Makefile        |   4 +-
 tools/testing/selftests/sched/config          |   4 +-
 .../selftests/sched/cs_dl_server_test.c       | 402 ++++++++++++++++++
 4 files changed, 408 insertions(+), 3 deletions(-)
 create mode 100644 tools/testing/selftests/sched/cs_dl_server_test.c

diff --git a/tools/testing/selftests/sched/.gitignore b/tools/testing/selftests/sched/.gitignore
index 6996d4654d924..4e3a0254e0abc 100644
--- a/tools/testing/selftests/sched/.gitignore
+++ b/tools/testing/selftests/sched/.gitignore
@@ -1 +1,2 @@
 cs_prctl_test
+cs_dl_server_test
diff --git a/tools/testing/selftests/sched/Makefile b/tools/testing/selftests/sched/Makefile
index 099ee9213557a..fa50d1385bd68 100644
--- a/tools/testing/selftests/sched/Makefile
+++ b/tools/testing/selftests/sched/Makefile
@@ -8,7 +8,7 @@ CFLAGS += -O2 -Wall -g -I./ $(KHDR_INCLUDES) -Wl,-rpath=./ \
 	  $(CLANG_FLAGS)
 LDLIBS += -lpthread
 
-TEST_GEN_FILES := cs_prctl_test
-TEST_PROGS := cs_prctl_test
+TEST_GEN_FILES := cs_prctl_test cs_dl_server_test
+TEST_PROGS := cs_prctl_test cs_dl_server_test
 
 include ../lib.mk
diff --git a/tools/testing/selftests/sched/config b/tools/testing/selftests/sched/config
index 1bb8bf6d7fd4c..57b2439f75eed 100644
--- a/tools/testing/selftests/sched/config
+++ b/tools/testing/selftests/sched/config
@@ -1 +1,3 @@
-# empty
+CONFIG_SCHED_CORE=y
+CONFIG_SCHED_SMT=y
+CONFIG_DEBUG_FS=y
diff --git a/tools/testing/selftests/sched/cs_dl_server_test.c b/tools/testing/selftests/sched/cs_dl_server_test.c
new file mode 100644
index 0000000000000..38aa005080f1f
--- /dev/null
+++ b/tools/testing/selftests/sched/cs_dl_server_test.c
@@ -0,0 +1,402 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Verify that core-scheduling forced idle does not starve the fair deadline
+ * server.
+ *
+ * A CPU is forced idle when it has runnable tasks of its own but its SMT
+ * sibling runs a task with an incompatible core-sched cookie. If that time is
+ * charged to the rq's deferred DL servers, the fair server's runtime drains
+ * while it is throttled, its activation is pushed out every period, and the
+ * CFS tasks it reserves bandwidth for get next to nothing.
+ *
+ * The test pins a cookied SCHED_FIFO spinner to one SMT sibling and an
+ * uncookied CFS spinner to the other, so the second sibling is forced idle
+ * whenever the FIFO task runs, and checks that the CFS task still gets a
+ * sensible fraction of the fair server's reservation.
+ *
+ * Copyright (c) 2026 Amazon.com, Inc. or its affiliates.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <limits.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "../kselftest.h"
+
+#ifndef PR_SCHED_CORE
+#define PR_SCHED_CORE			62
+#define PR_SCHED_CORE_GET		0
+#define PR_SCHED_CORE_CREATE		1 /* create unique core_sched cookie */
+#define PR_SCHED_CORE_SHARE_TO		2 /* push core_sched cookie to pid */
+#define PR_SCHED_CORE_SHARE_FROM	3 /* pull core_sched cookie to pid */
+#define PR_SCHED_CORE_MAX		4
+#endif
+
+enum pid_type {PIDTYPE_PID = 0, PIDTYPE_TGID, PIDTYPE_PGID};
+
+#define SIBLINGS_LIST	"/sys/devices/system/cpu/cpu0/topology/thread_siblings_list"
+#define FAIR_SERVER	"/sys/kernel/debug/sched/fair_server"
+
+#define RUN_TIME	5	/* measurement window, in seconds */
+#define FIFO_PRIO	50
+
+/*
+ * The reservation is only approached, not reached: every slice the fair server
+ * gets costs the sibling a forced-idle hand-over, and the core-wide pick runs
+ * at tick granularity. Ask for half of it, which is far above the few hundredths
+ * of a percent a starved server leaves.
+ */
+#define MIN_SHARE_OF_RESERVATION	0.5
+
+/* Status a spinner child reports to the parent over its readiness pipe. */
+#define CHILD_READY	'R'
+#define CHILD_EPERM	'P'
+#define CHILD_ERROR	'E'
+
+/* Tell the parent how setup went; it decides whether to run or to skip. */
+static void signal_status(int fd, char status)
+{
+	if (write(fd, &status, 1) != 1) {
+		ksft_perror("write to readiness pipe");
+		exit(1);
+	}
+	if (status != CHILD_READY)
+		exit(1);
+}
+
+/*
+ * Burn CPU until killed. A constant controlling expression keeps the compiler
+ * from removing the loop, so no volatile is needed to hold it in place.
+ */
+static void spin_forever(void)
+{
+	while (1)
+		;
+}
+
+static int set_affinity(int cpu)
+{
+	cpu_set_t mask;
+
+	CPU_ZERO(&mask);
+	CPU_SET(cpu, &mask);
+
+	return sched_setaffinity(0, sizeof(mask), &mask);
+}
+
+/*
+ * Read the first two distinct CPU ids out of a cpu list such as "0-1", "0,4"
+ * or "0,4-5". Returns 0 on success, -1 if the list holds fewer than two ids.
+ */
+static int parse_sibling_pair(const char *list, int *sib0, int *sib1)
+{
+	int found = 0, sib[2] = { -1, -1 };
+	const char *p = list;
+
+	while (*p && found < 2) {
+		int first, last, n = 0;
+
+		if (sscanf(p, "%d-%d%n", &first, &last, &n) != 2) {
+			if (sscanf(p, "%d%n", &first, &n) != 1)
+				return -1;
+			last = first;
+		}
+		for (; first <= last && found < 2; first++)
+			sib[found++] = first;
+		p += n;
+		if (*p != ',')
+			break;
+		p++;
+	}
+
+	if (found < 2)
+		return -1;
+
+	*sib0 = sib[0];
+	*sib1 = sib[1];
+
+	return 0;
+}
+
+static int read_sibling_pair(int *sib0, int *sib1)
+{
+	char list[256];
+	FILE *f;
+	char *s;
+
+	f = fopen(SIBLINGS_LIST, "r");
+	if (!f)
+		return -1;
+	s = fgets(list, sizeof(list), f);
+	fclose(f);
+	if (!s)
+		return -1;
+
+	return parse_sibling_pair(list, sib0, sib1);
+}
+
+static int read_u64_file(const char *path, unsigned long long *val)
+{
+	FILE *f;
+	int ret;
+
+	f = fopen(path, "r");
+	if (!f)
+		return -1;
+	ret = fscanf(f, "%llu", val);
+	fclose(f);
+
+	return ret == 1 ? 0 : -1;
+}
+
+/* Read the fair server reservation of @cpu, in nanoseconds. */
+static int read_fair_server(int cpu, unsigned long long *runtime,
+			    unsigned long long *period)
+{
+	char path[PATH_MAX];
+
+	snprintf(path, sizeof(path), FAIR_SERVER "/cpu%d/runtime", cpu);
+	if (read_u64_file(path, runtime))
+		return -1;
+
+	snprintf(path, sizeof(path), FAIR_SERVER "/cpu%d/period", cpu);
+	if (read_u64_file(path, period))
+		return -1;
+
+	return 0;
+}
+
+/* CPU time consumed by @pid so far, in seconds, from /proc/<pid>/stat. */
+static double process_runtime(pid_t pid)
+{
+	char path[PATH_MAX];
+	unsigned long utime, stime;
+	FILE *f;
+	int fields;
+
+	snprintf(path, sizeof(path), "/proc/%d/stat", pid);
+	f = fopen(path, "r");
+	if (!f)
+		return -1;
+
+	/* Skip the first 13 fields and read the 14th and 15th. */
+	fields = fscanf(f, "%*d %*s %*c %*d %*d %*d %*d %*d %*u %*u %*u %*u %*u %lu %lu",
+			&utime, &stime);
+	fclose(f);
+	if (fields != 2)
+		return -1;
+
+	return (double)(utime + stime) / sysconf(_SC_CLK_TCK);
+}
+
+static double monotonic_now(void)
+{
+	struct timespec ts;
+
+	if (clock_gettime(CLOCK_MONOTONIC, &ts))
+		return -1;
+
+	return ts.tv_sec + ts.tv_nsec / 1000000000.0;
+}
+
+/*
+ * Diagnostic only: how long the FIFO task's CPU was forced idle. Needs
+ * CONFIG_SCHEDSTATS and schedstats enabled, so it must not gate the result.
+ */
+static void print_forced_idle(pid_t pid)
+{
+	char path[PATH_MAX], line[256];
+	FILE *f;
+
+	snprintf(path, sizeof(path), "/proc/%d/sched", pid);
+	f = fopen(path, "r");
+	if (!f)
+		return;
+
+	while (fgets(line, sizeof(line), f)) {
+		if (strstr(line, "core_forceidle_sum")) {
+			line[strcspn(line, "\n")] = '\0';
+			ksft_print_msg("FIFO task %s\n", line);
+			break;
+		}
+	}
+	fclose(f);
+}
+
+/* Cookied SCHED_FIFO spinner. Only this task gets a cookie. */
+static void fifo_child(int cpu, int ready_fd)
+{
+	struct sched_param param = { .sched_priority = FIFO_PRIO };
+
+	if (set_affinity(cpu)) {
+		ksft_perror("FIFO child sched_setaffinity");
+		signal_status(ready_fd, CHILD_ERROR);
+	}
+	if (prctl(PR_SCHED_CORE, PR_SCHED_CORE_CREATE, 0, PIDTYPE_PID, 0)) {
+		ksft_perror("FIFO child PR_SCHED_CORE_CREATE");
+		signal_status(ready_fd, CHILD_ERROR);
+	}
+	if (sched_setscheduler(0, SCHED_FIFO, &param)) {
+		if (errno == EPERM)
+			signal_status(ready_fd, CHILD_EPERM);
+		ksft_perror("FIFO child sched_setscheduler");
+		signal_status(ready_fd, CHILD_ERROR);
+	}
+	signal_status(ready_fd, CHILD_READY);
+	spin_forever();
+}
+
+/* Uncookied CFS spinner on the FIFO task's SMT sibling. */
+static void cfs_child(int cpu, int ready_fd)
+{
+	if (set_affinity(cpu)) {
+		ksft_perror("CFS child sched_setaffinity");
+		signal_status(ready_fd, CHILD_ERROR);
+	}
+	signal_status(ready_fd, CHILD_READY);
+	spin_forever();
+}
+
+static char wait_ready(int fd)
+{
+	char status;
+
+	if (read(fd, &status, 1) != 1) {
+		ksft_perror("read from readiness pipe");
+		return CHILD_ERROR;
+	}
+
+	return status;
+}
+
+static void kill_child(pid_t *pid)
+{
+	if (*pid <= 0)
+		return;
+	kill(*pid, SIGKILL);
+	waitpid(*pid, NULL, 0);
+	*pid = -1;
+}
+
+int main(void)
+{
+	double fifo_runtime, cfs_start, cfs_end, t_start, t_end, share, want;
+	unsigned long long runtime, period;
+	pid_t fifo_pid = -1, cfs_pid = -1;
+	int fifo_ready[2], cfs_ready[2];
+	unsigned long long cookie;
+	int sib0, sib1;
+	char status;
+
+	ksft_print_header();
+	ksft_set_plan(1);
+
+	if (read_sibling_pair(&sib0, &sib1)) {
+		ksft_test_result_skip("CPU0 has no SMT sibling\n");
+		ksft_finished();
+	}
+	ksft_print_msg("SMT siblings: CPU%d (FIFO) and CPU%d (CFS)\n", sib0, sib1);
+
+	if (prctl(PR_SCHED_CORE, PR_SCHED_CORE_GET, 0, PIDTYPE_PID,
+		  (unsigned long)&cookie)) {
+		ksft_test_result_skip("core scheduling unavailable: %s\n",
+				      strerror(errno));
+		ksft_finished();
+	}
+
+	if (read_fair_server(sib1, &runtime, &period)) {
+		ksft_test_result_skip("cannot read " FAIR_SERVER "/cpu%d, debugfs not mounted?\n",
+				      sib1);
+		ksft_finished();
+	}
+	if (!runtime || !period) {
+		ksft_test_result_skip("fair server on CPU%d is disabled (%llu/%llu)\n",
+				      sib1, runtime, period);
+		ksft_finished();
+	}
+	ksft_print_msg("CPU%d fair server reservation: %llu/%llu ns = %.3f%%\n",
+		       sib1, runtime, period, 100.0 * runtime / period);
+
+	if (pipe(fifo_ready) || pipe(cfs_ready))
+		ksft_exit_fail_perror("pipe");
+
+	fifo_pid = fork();
+	if (!fifo_pid) {
+		close(fifo_ready[0]);
+		close(cfs_ready[0]);
+		close(cfs_ready[1]);
+		fifo_child(sib0, fifo_ready[1]);
+	} else if (fifo_pid < 0) {
+		ksft_exit_fail_perror("fork FIFO child");
+	}
+	close(fifo_ready[1]);
+
+	status = wait_ready(fifo_ready[0]);
+	close(fifo_ready[0]);
+	if (status != CHILD_READY) {
+		kill_child(&fifo_pid);
+		close(cfs_ready[0]);
+		close(cfs_ready[1]);
+		if (status == CHILD_EPERM) {
+			ksft_test_result_skip("need privileges for SCHED_FIFO\n");
+			ksft_finished();
+		}
+		ksft_exit_fail_msg("FIFO child setup failed\n");
+	}
+
+	cfs_pid = fork();
+	if (!cfs_pid) {
+		close(cfs_ready[0]);
+		cfs_child(sib1, cfs_ready[1]);
+	} else if (cfs_pid < 0) {
+		kill_child(&fifo_pid);
+		ksft_exit_fail_perror("fork CFS child");
+	}
+	close(cfs_ready[1]);
+
+	status = wait_ready(cfs_ready[0]);
+	close(cfs_ready[0]);
+	if (status != CHILD_READY) {
+		kill_child(&cfs_pid);
+		kill_child(&fifo_pid);
+		ksft_exit_fail_msg("CFS child setup failed\n");
+	}
+
+	cfs_start = process_runtime(cfs_pid);
+	t_start = monotonic_now();
+	sleep(RUN_TIME);
+	t_end = monotonic_now();
+	cfs_end = process_runtime(cfs_pid);
+	fifo_runtime = process_runtime(fifo_pid);
+
+	print_forced_idle(fifo_pid);
+	kill_child(&cfs_pid);
+	kill_child(&fifo_pid);
+
+	if (cfs_start < 0 || cfs_end < 0 || fifo_runtime < 0)
+		ksft_exit_fail_msg("cannot read /proc/<pid>/stat\n");
+	if (t_start < 0 || t_end <= t_start)
+		ksft_exit_fail_msg("cannot read CLOCK_MONOTONIC\n");
+
+	share = (cfs_end - cfs_start) / (t_end - t_start);
+	want = MIN_SHARE_OF_RESERVATION * runtime / period;
+
+	ksft_print_msg("FIFO task ran %.3fs, CFS task ran %.3fs over %.3fs\n",
+		       fifo_runtime, cfs_end - cfs_start, t_end - t_start);
+	ksft_print_msg("CFS share of CPU%d: %.3f%%, need %.3f%%\n",
+		       sib1, share * 100, want * 100);
+
+	ksft_test_result(share >= want,
+			 "CFS task gets the fair server reservation while forced idle\n");
+
+	ksft_finished();
+}
-- 
2.47.3




Amazon Development Centre (South Africa) (Proprietary) Limited
29 Gogosoa Street, Observatory, Cape Town, Western Cape, 7925, South Africa
Registration Number: 2004 / 034463 / 07


      parent reply	other threads:[~2026-11-12 16:17 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-12 16:15 [PATCH 0/5] sched: Fix DL server starvation under core scheduling Fernand Sieber
2026-11-12 16:15 ` [PATCH 1/5] sched/core: Clear stale rq->dl_server when core-sched replaces a pick Fernand Sieber
2026-11-12 16:15 ` [PATCH 2/5] sched/core: Keep a sibling's DL server link in the core-wide pick Fernand Sieber
2026-11-12 16:15 ` [PATCH 3/5] sched/core: Rank DL server picks as deadline " Fernand Sieber
2026-11-12 16:15 ` [PATCH 4/5] sched/deadline: Don't charge core-sched forced idle to the DL servers Fernand Sieber
2026-11-12 16:15 ` Fernand Sieber [this message]

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20261112161548.78189-6-sieberf@amazon.com \
    --to=sieberf@amazon.com \
    --cc=bristot@kernel.org \
    --cc=bsegall@google.com \
    --cc=dietmar.eggemann@arm.com \
    --cc=dwmw@amazon.co.uk \
    --cc=fmubeen@amazon.de \
    --cc=hborghor@amazon.de \
    --cc=joelagnelf@nvidia.com \
    --cc=joshdon@google.com \
    --cc=juri.lelli@redhat.com \
    --cc=karataev@amazon.com \
    --cc=kprateek.nayak@amd.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=lshahed@amazon.de \
    --cc=mgorman@suse.de \
    --cc=mingo@redhat.com \
    --cc=nh-open-source@amazon.com \
    --cc=peterz@infradead.org \
    --cc=rostedt@goodmis.org \
    --cc=shuah@kernel.org \
    --cc=vincent.guittot@linaro.org \
    --cc=vschneid@redhat.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox