mirror of https://lore.kernel.org/lkml/
 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 a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®