Linux Kernel Selftest development
 help / color / mirror / Atom feed
From: Tao Cui <cui.tao@linux.dev>
To: "Michal Koutný" <mkoutny@suse.com>, "Tejun Heo" <tj@kernel.org>
Cc: Johannes Weiner <hannes@cmpxchg.org>,
	Suren Baghdasaryan <surenb@google.com>,
	Shuah Khan <shuah@kernel.org>,
	cgroups@vger.kernel.org, linux-kselftest@vger.kernel.org,
	linux-kernel@vger.kernel.org, Ziyang Men <ziyang.meme@gmail.com>,
	Tao Cui <cuitao@kylinos.cn>,
	cui.tao@linux.dev
Subject: [PATCH v2] selftests/cgroup: add PSI pressure trigger and validation tests
Date: Tue, 28 Jul 2026 16:37:42 +0800	[thread overview]
Message-ID: <20260728083742.2359320-1-cui.tao@linux.dev> (raw)

From: Tao Cui <cuitao@kylinos.cn>

The cgroup selftests have no PSI coverage. Add test_psi.c: trigger
smoke tests (one per fd, IRQ full-only), cgroup.pressure toggle, and a
CPU-pressure trigger test using over-subscription. Skips when PSI is
disabled or a resource is absent.

Signed-off-by: Tao Cui <cuitao@kylinos.cn>
---
Changes since v1 (Michal Koutny, sashiko review):
- Trim errno checks to smoke level; loosen toggle range checks.
- Switch trigger test from memory to CPU pressure (deterministic, no SKIP).
- churn_memory() removed -- sysconf/shared-helper point is moot.
- Runner stays out of the cgroup; hogs reaped via cg_killall().
- Add PSI/IRQ skip-guards, zero-init buffers, .gitignore entry.
---
 tools/testing/selftests/cgroup/.gitignore |   1 +
 tools/testing/selftests/cgroup/Makefile   |   2 +
 tools/testing/selftests/cgroup/config     |   1 +
 tools/testing/selftests/cgroup/test_psi.c | 280 ++++++++++++++++++++++
 4 files changed, 284 insertions(+)
 create mode 100644 tools/testing/selftests/cgroup/test_psi.c

diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
index 952e4448bf07..ce2b907c57ea 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -8,5 +8,6 @@ test_kill
 test_kmem
 test_memcontrol
 test_pids
+test_psi
 test_zswap
 wait_inotify
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index e01584c2189a..a8c69e37332a 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -16,6 +16,7 @@ TEST_GEN_PROGS += test_kill
 TEST_GEN_PROGS += test_kmem
 TEST_GEN_PROGS += test_memcontrol
 TEST_GEN_PROGS += test_pids
+TEST_GEN_PROGS += test_psi
 TEST_GEN_PROGS += test_zswap
 
 LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
@@ -32,4 +33,5 @@ $(OUTPUT)/test_kill: $(LIBCGROUP_O)
 $(OUTPUT)/test_kmem: $(LIBCGROUP_O)
 $(OUTPUT)/test_memcontrol: $(LIBCGROUP_O)
 $(OUTPUT)/test_pids: $(LIBCGROUP_O)
+$(OUTPUT)/test_psi: $(LIBCGROUP_O)
 $(OUTPUT)/test_zswap: $(LIBCGROUP_O)
diff --git a/tools/testing/selftests/cgroup/config b/tools/testing/selftests/cgroup/config
index 39f979690dd3..8a3ef479e83d 100644
--- a/tools/testing/selftests/cgroup/config
+++ b/tools/testing/selftests/cgroup/config
@@ -4,3 +4,4 @@ CONFIG_CGROUP_FREEZER=y
 CONFIG_CGROUP_SCHED=y
 CONFIG_MEMCG=y
 CONFIG_PAGE_COUNTER=y
+CONFIG_PSI=y
diff --git a/tools/testing/selftests/cgroup/test_psi.c b/tools/testing/selftests/cgroup/test_psi.c
new file mode 100644
index 000000000000..51dc35e26013
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_psi.c
@@ -0,0 +1,280 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <linux/limits.h>
+
+#include "kselftest.h"
+#include "cgroup_util.h"
+
+/* How long to wait for the CPU-pressure trigger before giving up. */
+#define PSI_POLL_TIMEOUT_MS	5000
+
+static int pressure_open(const char *resource)
+{
+	char path[PATH_MAX];
+
+	snprintf(path, sizeof(path), "/proc/pressure/%s", resource);
+	return open(path, O_RDWR);
+}
+
+/* Write a trigger descriptor, including the trailing NUL the parser expects. */
+static ssize_t write_trigger(int fd, const char *trigger)
+{
+	return write(fd, trigger, strlen(trigger) + 1);
+}
+
+/* Trigger smoke test: second on same fd -> EBUSY; IRQ rejects "some". */
+static int test_proc_triggers(const char *root)
+{
+	static const char *const resources[] = { "io", "memory", "cpu" };
+	int ret = KSFT_FAIL;
+	int fd = -1;
+	int i;
+
+	(void)root;
+
+	for (i = 0; i < (int)ARRAY_SIZE(resources); i++) {
+		fd = pressure_open(resources[i]);
+		if (fd < 0) {
+			ksft_print_msg("open /proc/pressure/%s: %s\n",
+				       resources[i], strerror(errno));
+			goto cleanup;
+		}
+
+		errno = 0;
+		if (write_trigger(fd, "some 150000 2000000") <= 0) {
+			ksft_print_msg("%s: 'some' trigger rejected: %s\n",
+				       resources[i], strerror(errno));
+			goto cleanup;
+		}
+		/* A second trigger on the same fd must fail with EBUSY. */
+		errno = 0;
+		if (write_trigger(fd, "full 150000 2000000") != -1 || errno != EBUSY) {
+			ksft_print_msg("%s: second trigger expected EBUSY, got %s\n",
+				       resources[i], strerror(errno));
+			goto cleanup;
+		}
+
+		close(fd);
+		fd = -1;
+	}
+
+	/* IRQ is full-only, and only exists with CONFIG_IRQ_TIME_ACCOUNTING. */
+	fd = pressure_open("irq");
+	if (fd >= 0) {
+		errno = 0;
+		if (write_trigger(fd, "some 150000 1000000") != -1) {
+			ksft_print_msg("irq 'some': expected failure, got %s\n",
+				       strerror(errno));
+			goto cleanup;
+		}
+		close(fd);
+		fd = -1;
+	}
+
+	ret = KSFT_PASS;
+cleanup:
+	if (fd >= 0)
+		close(fd);
+	return ret;
+}
+
+/* cgroup.pressure 0/1 hides/shows the *.pressure files and round-trips. */
+static int test_cgroup_pressure_toggle(const char *root)
+{
+	char buf[BUF_SIZE] = { 0 };
+	char *cg = NULL;
+	int ret = KSFT_FAIL;
+
+	cg = cg_name(root, "psi_toggle_test");
+	if (!cg)
+		goto cleanup;
+	if (cg_create(cg))
+		goto cleanup;
+
+	if (cg_write(cg, "cgroup.pressure", "0")) {
+		ksft_print_msg("failed to disable cgroup.pressure\n");
+		goto cleanup;
+	}
+	if (cg_read(cg, "cgroup.pressure", buf, sizeof(buf)) < 0 || atoi(buf) != 0) {
+		ksft_print_msg("cgroup.pressure=0 readback: '%s'\n", buf);
+		goto cleanup;
+	}
+	if (cg_read(cg, "memory.pressure", buf, sizeof(buf)) >= 0) {
+		ksft_print_msg("memory.pressure visible while disabled\n");
+		goto cleanup;
+	}
+
+	if (cg_write(cg, "cgroup.pressure", "1")) {
+		ksft_print_msg("failed to re-enable cgroup.pressure\n");
+		goto cleanup;
+	}
+	if (cg_read(cg, "cgroup.pressure", buf, sizeof(buf)) < 0 || atoi(buf) != 1) {
+		ksft_print_msg("cgroup.pressure=1 readback: '%s'\n", buf);
+		goto cleanup;
+	}
+	if (cg_read(cg, "memory.pressure", buf, sizeof(buf)) < 0) {
+		ksft_print_msg("memory.pressure unreadable while enabled\n");
+		goto cleanup;
+	}
+
+	ret = KSFT_PASS;
+cleanup:
+	if (cg)
+		cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+/* Induce deterministic CPU pressure (more hogs than CPUs). */
+static int test_cgroup_trigger_fire(const char *root)
+{
+	char *cg = NULL, *cpupress = NULL;
+	int fd = -1, ret = KSFT_FAIL;
+	struct pollfd pfd;
+	long ncpus, i;
+	pid_t pid;
+
+	cg = cg_name(root, "psi_trigger_test");
+	if (!cg)
+		goto cleanup;
+	if (cg_create(cg))
+		goto cleanup;
+
+	cpupress = cg_control(cg, "cpu.pressure");
+	if (!cpupress)
+		goto cleanup;
+	fd = open(cpupress, O_RDWR);
+	if (fd < 0) {
+		ksft_print_msg("open cpu.pressure: %s\n", strerror(errno));
+		goto cleanup;
+	}
+
+	/* 1us threshold in a 1s window: any cpu stall fires it. */
+	errno = 0;
+	if (write_trigger(fd, "some 1 1000000") <= 0) {
+		ksft_print_msg("arming trigger failed: %s\n", strerror(errno));
+		goto cleanup;
+	}
+
+	ncpus = sysconf(_SC_NPROCESSORS_ONLN);
+	if (ncpus <= 0)
+		ncpus = 1;
+
+	pid = fork();
+	if (pid < 0) {
+		ksft_print_msg("fork: %s\n", strerror(errno));
+		goto cleanup;
+	}
+	if (pid == 0) {
+		/* Enter the cgroup, then over-subscribe it with CPU hogs. */
+		if (cg_enter_current(cg))
+			_exit(KSFT_FAIL);
+		for (i = 0; i < ncpus; i++) {
+			if (fork() == 0) {
+				for (;;)
+					asm volatile("" ::: "memory");
+				_exit(0);
+			}
+		}
+		for (;;)
+			asm volatile("" ::: "memory");	/* child is also a hog */
+		_exit(0);
+	}
+
+	pfd.fd = fd;
+	pfd.events = POLLPRI;
+	switch (poll(&pfd, 1, PSI_POLL_TIMEOUT_MS)) {
+	case -1:
+		ksft_print_msg("poll: %s\n", strerror(errno));
+		break;
+	case 0:
+		ksft_print_msg("no trigger event; could not induce cpu pressure\n");
+		ret = KSFT_SKIP;
+		break;
+	default:
+		if (pfd.revents & POLLPRI)
+			ret = KSFT_PASS;
+		else
+			ksft_print_msg("poll returned 0x%x\n", pfd.revents);
+		break;
+	}
+
+	/* Stop the hogs (bounded, so waitpid can't hang) and reap the child. */
+	cg_killall(cg);
+	waitpid(pid, NULL, 0);
+
+cleanup:
+	if (fd >= 0)
+		close(fd);
+	if (cg) {
+		cg_killall(cg);
+		cg_destroy(cg);
+	}
+	free(cpupress);
+	free(cg);
+	return ret;
+}
+
+#define TEST(x) { #x, x }
+
+struct psi_test {
+	const char *name;
+	int (*fn)(const char *root);
+};
+
+static struct psi_test tests[] = {
+	TEST(test_proc_triggers),
+	TEST(test_cgroup_pressure_toggle),
+	TEST(test_cgroup_trigger_fire),
+};
+
+int main(int argc, char **argv)
+{
+	char root[PATH_MAX];
+	int mempress_fd;
+	int i;
+
+	(void)argc;
+
+	ksft_print_header();
+	ksft_set_plan(ARRAY_SIZE(tests));
+
+	if (cg_find_unified_root(root, sizeof(root), NULL))
+		ksft_exit_skip("cgroup v2 isn't mounted\n");
+
+	/* PSI must be enabled (CONFIG_PSI=y, not default-disabled). */
+	mempress_fd = open("/proc/pressure/memory", O_RDONLY);
+	if (mempress_fd < 0)
+		ksft_exit_skip("PSI unavailable (CONFIG_PSI=n or psi=0)\n");
+	close(mempress_fd);
+
+	if (cg_read_strstr(root, "cgroup.controllers", "memory"))
+		ksft_exit_skip("memory controller isn't available\n");
+	if (cg_read_strstr(root, "cgroup.subtree_control", "memory"))
+		if (cg_write(root, "cgroup.subtree_control", "+memory"))
+			ksft_exit_skip("failed to enable memory controller\n");
+
+	for (i = 0; i < (int)ARRAY_SIZE(tests); i++) {
+		switch (tests[i].fn(root)) {
+		case KSFT_PASS:
+			ksft_test_result_pass("%s\n", tests[i].name);
+			break;
+		case KSFT_SKIP:
+			ksft_test_result_skip("%s\n", tests[i].name);
+			break;
+		default:
+			ksft_test_result_fail("%s\n", tests[i].name);
+			break;
+		}
+	}
+
+	ksft_finished();
+}
-- 
2.43.0


                 reply	other threads:[~2026-07-28  8:38 UTC|newest]

Thread overview: [no followups] expand[flat|nested]  mbox.gz  Atom feed

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=20260728083742.2359320-1-cui.tao@linux.dev \
    --to=cui.tao@linux.dev \
    --cc=cgroups@vger.kernel.org \
    --cc=cuitao@kylinos.cn \
    --cc=hannes@cmpxchg.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=linux-kselftest@vger.kernel.org \
    --cc=mkoutny@suse.com \
    --cc=shuah@kernel.org \
    --cc=surenb@google.com \
    --cc=tj@kernel.org \
    --cc=ziyang.meme@gmail.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