All of lore.kernel.org
 help / color / mirror / Atom feed
From: Fernand Sieber <sieberf@amazon.com>
To: <mingo@redhat.com>, <peterz@infradead.org>,
	<juri.lelli@redhat.com>, <vincent.guittot@linaro.org>,
	<shuah@kernel.org>
Cc: <dietmar.eggemann@arm.com>, <rostedt@goodmis.org>,
	<bsegall@google.com>, <mgorman@suse.de>, <vschneid@redhat.com>,
	<kprateek.nayak@amd.com>, <linux-kernel@vger.kernel.org>,
	<linux-kselftest@vger.kernel.org>, <nh-open-source@amazon.com>,
	<hborghor@amazon.de>, <fmubeen@amazon.de>, <dwmw@amazon.co.uk>
Subject: [PATCH 2/2] selftests/sched: Add a core scheduling rescheduling IPI test
Date: Mon, 9 Nov 2026 21:48:27 +0200	[thread overview]
Message-ID: <20261109194827.87349-3-sieberf@amazon.com> (raw)
In-Reply-To: <20261109194827.87349-1-sieberf@amazon.com>

A core wide pick runs pick_task() on every sibling of the core, so
update_curr() runs on a remote runqueue and can reschedule it, which costs
a rescheduling IPI. Two siblings whose current entity has lost its slice
protection and never gets it back then trade those IPIs for as long as
both stay busy.

Add a test that counts them. It takes one SMT pair, gives its process
group a cookie, runs two spinners on each sibling - update_curr() returns
early while cfs_rq->h_nr_queued == 1 - and reads the rescheduling row of
/proc/interrupts around a five second window. A kernel that renews the
protection on a repick stays near 240 IPIs/CPU/s, one that does not
reaches thousands; the test fails above 1000.

The symptom needs an eager remote reschedule, so the test first checks
that preempting a task on a remote CPU costs an IPI at all and skips if it
does not, as it will under CONFIG_PREEMPT_LAZY=y. It also skips without an
SMT pair or without CONFIG_SCHED_CORE.

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          |   2 +-
 .../selftests/sched/cs_resched_ipi_test.c     | 377 ++++++++++++++++++
 4 files changed, 381 insertions(+), 3 deletions(-)
 create mode 100644 tools/testing/selftests/sched/cs_resched_ipi_test.c

diff --git a/tools/testing/selftests/sched/.gitignore b/tools/testing/selftests/sched/.gitignore
index 6996d4654d924..c4e31a7e0893b 100644
--- a/tools/testing/selftests/sched/.gitignore
+++ b/tools/testing/selftests/sched/.gitignore
@@ -1 +1,2 @@
 cs_prctl_test
+cs_resched_ipi_test
diff --git a/tools/testing/selftests/sched/Makefile b/tools/testing/selftests/sched/Makefile
index 099ee9213557a..9d11ed38b3473 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_resched_ipi_test
+TEST_PROGS := cs_prctl_test cs_resched_ipi_test
 
 include ../lib.mk
diff --git a/tools/testing/selftests/sched/config b/tools/testing/selftests/sched/config
index 1bb8bf6d7fd4c..cc3f25e8f01b5 100644
--- a/tools/testing/selftests/sched/config
+++ b/tools/testing/selftests/sched/config
@@ -1 +1 @@
-# empty
+CONFIG_SCHED_CORE=y
diff --git a/tools/testing/selftests/sched/cs_resched_ipi_test.c b/tools/testing/selftests/sched/cs_resched_ipi_test.c
new file mode 100644
index 0000000000000..d38fd6b200fcd
--- /dev/null
+++ b/tools/testing/selftests/sched/cs_resched_ipi_test.c
@@ -0,0 +1,377 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Core scheduling must not make two SMT siblings trade rescheduling IPIs while
+ * both of them keep running the same CPU-bound task.
+ *
+ * When a core has a cookie, pick_next_task() does a core wide pick, which runs
+ * pick_task() - and with it update_curr() - on every sibling's runqueue. If
+ * update_curr() finds the sibling's current entity past its protected period it
+ * reschedules that sibling remotely, which is a real IPI. A sibling that picks
+ * the same task again has to renew the protected period; if it does not, the
+ * next core wide pick reschedules it again and the two siblings ping-pong
+ * rescheduling IPIs for as long as the load lasts.
+ *
+ * The test puts two spinners on each sibling of one SMT pair, because
+ * update_curr() returns early while cfs_rq->h_nr_queued == 1, gives the process
+ * group a cookie, and counts the rescheduling IPIs both siblings take.
+ *
+ * It only means something on a kernel whose remote reschedule really sends an
+ * IPI. Under CONFIG_PREEMPT_LAZY=y resched_curr_lazy() asks for
+ * TIF_NEED_RESCHED_LAZY and __resched_curr() sends no IPI, so the bug has no
+ * IPI symptom there. Rather than guess the preemption model from uname or from
+ * a config file, the test probes for it: it has one task preempt a running task
+ * on a remote CPU and skips unless that is seen to cost an IPI.
+ */
+#define _GNU_SOURCE
+#include <errno.h>
+#include <limits.h>
+#include <sched.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/prctl.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
+#endif
+
+/* enum pid_type, not exported to userspace */
+#define PIDTYPE_PID			0
+#define PIDTYPE_PGID			2
+
+#define SPINNERS_PER_CPU		2
+#define MEASURE_SECS			5
+#define SETTLE_USECS			500000
+#define PROBE_WAKEUPS			200
+#define PROBE_GAP_USECS			2000
+
+/*
+ * A kernel that renews the protected period on a repick still reschedules a
+ * sibling once per real task switch, so the limit has to sit above that:
+ * measured 233-247 IPIs/CPU/s with the renewal and 554-3819 without it, on
+ * HZ=250 and HZ=1000 kernels under the same load, and ~3200 against ~3 on a
+ * two socket Xeon.
+ */
+#define IPI_RATE_LIMIT			1000
+
+#define CPU_PATH			"/sys/devices/system/cpu"
+#define RESCHED_ROW			"Rescheduling interrupts"
+
+struct irq_counts {
+	unsigned long resched[CPU_SETSIZE];
+	int ncols;
+};
+
+static pid_t spinners[2 * SPINNERS_PER_CPU];
+static int nr_spinners;
+
+static void add_row(char *line, const int *cpu_of_col, int ncols, unsigned long *out)
+{
+	char *tok, *save;
+	int col = 0;
+
+	tok = strchr(line, ':');
+	if (!tok)
+		return;
+
+	for (tok = strtok_r(tok + 1, " \t\n", &save); tok && col < ncols;
+	     tok = strtok_r(NULL, " \t\n", &save)) {
+		if (*tok < '0' || *tok > '9')
+			break;
+		out[cpu_of_col[col++]] += strtoul(tok, NULL, 10);
+	}
+}
+
+/*
+ * Sample the rescheduling IPI counters, indexed by CPU number.
+ * The header row of /proc/interrupts names the columns, which are the online
+ * CPUs in order, so an offline CPU does not shift the mapping. Both x86 ("RES")
+ * and arm64 ("IPI0") spell the rescheduling IPI out in the trailing
+ * description, which is what we match on.
+ */
+static int sample_irqs(struct irq_counts *c)
+{
+	int cpu_of_col[CPU_SETSIZE];
+	char *line = NULL;
+	size_t len = 0;
+	int ncols = 0;
+	FILE *f;
+
+	memset(c, 0, sizeof(*c));
+
+	f = fopen("/proc/interrupts", "re");
+	if (!f)
+		return -1;
+
+	while (getline(&line, &len, f) > 0) {
+		if (!ncols) {
+			char *tok, *save;
+			int cpu;
+
+			for (tok = strtok_r(line, " \t\n", &save); tok;
+			     tok = strtok_r(NULL, " \t\n", &save)) {
+				if (strncmp(tok, "CPU", 3))
+					break;
+				cpu = atoi(tok + 3);
+				if (cpu < 0 || cpu >= CPU_SETSIZE || ncols == CPU_SETSIZE)
+					break;
+				cpu_of_col[ncols++] = cpu;
+			}
+			continue;
+		}
+
+		if (strstr(line, RESCHED_ROW))
+			add_row(line, cpu_of_col, ncols, c->resched);
+	}
+
+	free(line);
+	fclose(f);
+
+	c->ncols = ncols;
+	return ncols ? 0 : -1;
+}
+
+static double elapsed_since(const struct timespec *t0)
+{
+	struct timespec t1;
+
+	clock_gettime(CLOCK_MONOTONIC, &t1);
+	return (t1.tv_sec - t0->tv_sec) + (t1.tv_nsec - t0->tv_nsec) / 1e9;
+}
+
+static void pin_to(int cpu)
+{
+	cpu_set_t set;
+
+	CPU_ZERO(&set);
+	CPU_SET(cpu, &set);
+	if (sched_setaffinity(0, sizeof(set), &set))
+		ksft_exit_fail_perror("sched_setaffinity");
+}
+
+static int read_first_line(const char *path, char *buf, size_t len)
+{
+	FILE *f = fopen(path, "re");
+
+	if (!f)
+		return -1;
+	if (!fgets(buf, len, f)) {
+		fclose(f);
+		return -1;
+	}
+	fclose(f);
+	return 0;
+}
+
+/*
+ * Return a pair of online SMT siblings, or false if the machine has none.
+ * thread_siblings_list only exists for online CPUs, and names only online
+ * siblings, so a readable file with two entries is a usable pair.
+ */
+static bool find_smt_pair(int *cpu_a, int *cpu_b)
+{
+	char path[PATH_MAX], buf[256];
+	int cpu, a, b;
+
+	for (cpu = 0; cpu < CPU_SETSIZE; cpu++) {
+		snprintf(path, sizeof(path), "%s/cpu%d/topology/thread_siblings_list",
+			 CPU_PATH, cpu);
+		if (read_first_line(path, buf, sizeof(buf)))
+			continue;
+		if (sscanf(buf, "%d%*[,-]%d", &a, &b) != 2 || a == b)
+			continue;
+		if (a >= CPU_SETSIZE || b >= CPU_SETSIZE)
+			continue;
+		*cpu_a = a;
+		*cpu_b = b;
+		return true;
+	}
+
+	return false;
+}
+
+static void spinner(int cpu)
+{
+	unsigned long x = 0;
+
+	pin_to(cpu);
+	for (;;) {
+		x++;
+		asm volatile("" ::: "memory");
+	}
+}
+
+static void spinners_start(int cpu, int nr)
+{
+	pid_t pid;
+	int i;
+
+	for (i = 0; i < nr; i++) {
+		pid = fork();
+		if (pid < 0)
+			ksft_exit_fail_perror("fork");
+		if (!pid) {
+			spinner(cpu);
+			_exit(EXIT_FAILURE);
+		}
+		spinners[nr_spinners++] = pid;
+	}
+}
+
+static void spinners_stop(void)
+{
+	while (nr_spinners--) {
+		kill(spinners[nr_spinners], SIGKILL);
+		waitpid(spinners[nr_spinners], NULL, 0);
+	}
+	nr_spinners = 0;
+}
+
+/* Rescheduling IPIs per CPU per second taken by the pair over the window */
+static double measure(int cpu_a, int cpu_b, int secs)
+{
+	struct irq_counts before, after;
+	struct timespec t0;
+	double secs_run;
+
+	usleep(SETTLE_USECS);
+
+	if (sample_irqs(&before))
+		ksft_exit_fail_msg("cannot read %s from /proc/interrupts\n", RESCHED_ROW);
+	clock_gettime(CLOCK_MONOTONIC, &t0);
+	sleep(secs);
+	secs_run = elapsed_since(&t0);
+	if (sample_irqs(&after) || after.ncols != before.ncols)
+		ksft_exit_fail_msg("/proc/interrupts changed shape mid-test\n");
+
+	return (after.resched[cpu_a] - before.resched[cpu_a] +
+		after.resched[cpu_b] - before.resched[cpu_b]) / 2.0 / secs_run;
+}
+
+/*
+ * Does preempting a running task on a remote CPU cost a rescheduling IPI on
+ * this kernel? Wake a task pinned to a busy cpu_a from cpu_b, repeatedly, and
+ * count what cpu_a takes. A sibling of cpu_a shares its cache, so the wakeup is
+ * an enqueue on cpu_a's runqueue and not a queued remote call. On an eager
+ * kernel every wakeup preempts the spinner and costs an IPI; under
+ * CONFIG_PREEMPT_LAZY=y it sets a flag on cpu_a and costs none.
+ */
+static bool remote_preempt_sends_ipi(int cpu_a, int cpu_b)
+{
+	struct irq_counts before, after;
+	unsigned long ipis;
+	int fd[2], i;
+	pid_t pid;
+	char c = 0;
+
+	if (pipe(fd))
+		ksft_exit_fail_perror("pipe");
+
+	pid = fork();
+	if (pid < 0)
+		ksft_exit_fail_perror("fork");
+	if (!pid) {
+		close(fd[1]);
+		pin_to(cpu_a);
+		while (read(fd[0], &c, 1) == 1)
+			;
+		_exit(EXIT_SUCCESS);
+	}
+	close(fd[0]);
+
+	pin_to(cpu_b);
+	spinners_start(cpu_a, 1);
+	usleep(SETTLE_USECS);
+
+	if (sample_irqs(&before))
+		ksft_exit_fail_msg("cannot read %s from /proc/interrupts\n", RESCHED_ROW);
+	for (i = 0; i < PROBE_WAKEUPS; i++) {
+		if (write(fd[1], &c, 1) != 1)
+			ksft_exit_fail_perror("write");
+		usleep(PROBE_GAP_USECS);
+	}
+	if (sample_irqs(&after))
+		ksft_exit_fail_msg("cannot read %s from /proc/interrupts\n", RESCHED_ROW);
+
+	spinners_stop();
+	close(fd[1]);
+	kill(pid, SIGKILL);
+	waitpid(pid, NULL, 0);
+
+	ipis = after.resched[cpu_a] - before.resched[cpu_a];
+	ksft_print_msg("%lu rescheduling IPIs on cpu%d for %d remote preemptions\n",
+		       ipis, cpu_a, PROBE_WAKEUPS);
+
+	return ipis >= PROBE_WAKEUPS / 4;
+}
+
+int main(void)
+{
+	double baseline, rate;
+	unsigned long long cookie = 0;
+	cpu_set_t all;
+	int cpu_a, cpu_b;
+
+	ksft_print_header();
+	ksft_set_plan(1);
+
+	if (atexit(spinners_stop))
+		ksft_exit_fail_perror("atexit");
+
+	if (sched_getaffinity(0, sizeof(all), &all))
+		ksft_exit_fail_perror("sched_getaffinity");
+
+	if (!find_smt_pair(&cpu_a, &cpu_b))
+		ksft_exit_skip("no online SMT sibling pair\n");
+	ksft_print_msg("SMT pair cpu%d/cpu%d\n", cpu_a, cpu_b);
+
+	if (!CPU_ISSET(cpu_a, &all) || !CPU_ISSET(cpu_b, &all))
+		ksft_exit_skip("cpu%d/cpu%d not in our affinity mask\n", cpu_a, cpu_b);
+
+	if (!remote_preempt_sends_ipi(cpu_a, cpu_b))
+		ksft_exit_skip("remote preemption sends no IPI, needs CONFIG_PREEMPT_LAZY=n\n");
+
+	if (sched_setaffinity(0, sizeof(all), &all))
+		ksft_exit_fail_perror("sched_setaffinity");
+
+	/* same load without a cookie: nothing here should reschedule a sibling */
+	spinners_start(cpu_a, SPINNERS_PER_CPU);
+	spinners_start(cpu_b, SPINNERS_PER_CPU);
+	baseline = measure(cpu_a, cpu_b, MEASURE_SECS);
+	spinners_stop();
+
+	ksft_print_msg("%.1f IPIs/CPU/s without a cookie\n", baseline);
+	if (baseline > IPI_RATE_LIMIT)
+		ksft_exit_skip("%.1f IPIs/CPU/s before core scheduling is even on\n", baseline);
+
+	/* our own group, so that the cookie does not reach the test harness */
+	if (setpgid(0, 0))
+		ksft_exit_fail_perror("setpgid");
+	if (prctl(PR_SCHED_CORE, PR_SCHED_CORE_CREATE, 0, PIDTYPE_PGID, 0))
+		ksft_exit_skip("PR_SCHED_CORE_CREATE: %s, needs CONFIG_SCHED_CORE=y\n",
+			       strerror(errno));
+	if (prctl(PR_SCHED_CORE, PR_SCHED_CORE_GET, 0, PIDTYPE_PID,
+		  (unsigned long)&cookie) || !cookie)
+		ksft_exit_fail_msg("no cookie after PR_SCHED_CORE_CREATE\n");
+
+	spinners_start(cpu_a, SPINNERS_PER_CPU);
+	spinners_start(cpu_b, SPINNERS_PER_CPU);
+	rate = measure(cpu_a, cpu_b, MEASURE_SECS);
+	spinners_stop();
+
+	ksft_print_msg("%d cookied spinners per sibling: %.1f IPIs/CPU/s\n",
+		       SPINNERS_PER_CPU, rate);
+	ksft_test_result(rate <= IPI_RATE_LIMIT,
+			 "cookied SMT siblings keep below %d IPIs/CPU/s\n", IPI_RATE_LIMIT);
+
+	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-09 19:49 UTC|newest]

Thread overview: 3+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-11-09 19:48 [PATCH 0/2] sched/fair: Stop core-scheduled siblings trading rescheduling IPIs Fernand Sieber
2026-11-09 19:48 ` [PATCH 1/2] sched/fair: Renew slice protection when current is picked again Fernand Sieber
2026-11-09 19:48 ` 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=20261109194827.87349-3-sieberf@amazon.com \
    --to=sieberf@amazon.com \
    --cc=bsegall@google.com \
    --cc=dietmar.eggemann@arm.com \
    --cc=dwmw@amazon.co.uk \
    --cc=fmubeen@amazon.de \
    --cc=hborghor@amazon.de \
    --cc=juri.lelli@redhat.com \
    --cc=kprateek.nayak@amd.com \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.