Linux cgroups development
 help / color / mirror / Atom feed
* [PATCH v3] selftests/cgroup: add PSI pressure trigger and validation tests
@ 2026-08-13 13:37 Tao Cui
  0 siblings, 0 replies; only message in thread
From: Tao Cui @ 2026-08-13 13:37 UTC (permalink / raw)
  To: Michal Koutný, Suren Baghdasaryan
  Cc: Tejun Heo, Johannes Weiner, Shuah Khan, cgroups, linux-kselftest,
	linux-kernel, Ziyang Men, Tao Cui, cui.tao

From: Tao Cui <cuitao@kylinos.cn>

The cgroup selftests have no PSI coverage. Add test_psi.c: per-resource
trigger smoke tests (one trigger per fd, IRQ full-only), a
cgroup.pressure hide/show toggle test, 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 v2 (Suren Baghdasaryan, Michal Koutny review):
- Restructure the trigger test into per-resource cases (io, memory, cpu,
  irq) so a failure points at the specific resource; irq is skipped when
  /proc/pressure/irq is absent.
- Spawn the CPU hogs with cg_run_nowait() instead of open-coding fork(),
  and arm the trigger with a 2s window so unprivileged users can set it.
- Address the remaining review comments on cleanup and robustness:
  guard teardown with a "created" flag, use cg_read_strcmp() instead of
  atoi(), report strerror() on errors, and fix the unused-parameter and
  sign-compare nits.

Changes since v1 (Michal Koutny, sashiko review):
- Keep trigger tests smoke-level; switch the firing test from memory to
  CPU pressure; drop churn_memory().
- Keep the runner out of the cgroup; add PSI/IRQ skip-guards and a
  .gitignore entry.

Link: https://lore.kernel.org/all/20260724025826.504586-1-cui.tao@linux.dev/
Link: https://lore.kernel.org/all/20260728083742.2359320-1-cui.tao@linux.dev/
---
 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 | 296 ++++++++++++++++++++++
 4 files changed, 300 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..cfb412f61458
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_psi.c
@@ -0,0 +1,296 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <linux/limits.h>
+
+#include "kselftest.h"
+#include "cgroup_util.h"
+
+#define PSI_POLL_TIMEOUT_MS	5000
+
+/* PSI triggers are written with a trailing NUL the kernel parser expects. */
+static ssize_t write_trigger(int fd, const char *trigger)
+{
+	return write(fd, trigger, strlen(trigger) + 1);
+}
+
+static int pressure_open(const char *resource)
+{
+	char path[PATH_MAX];
+	int fd;
+
+	snprintf(path, sizeof(path), "/proc/pressure/%s", resource);
+	fd = open(path, O_RDWR);
+	if (fd < 0)
+		ksft_print_msg("open %s: %s\n", path, strerror(errno));
+	return fd;
+}
+
+/*
+ * /proc/pressure/<resource> accepts exactly one trigger per file
+ * descriptor. For io, memory and cpu verify that a "some" trigger arms
+ * and that a second trigger on the same fd is rejected with EBUSY. For
+ * irq, which only tracks "full", verify that "some" is rejected and
+ * "full" arms. irq is optional -- it only exists with IRQ-time
+ * accounting -- so a missing /proc/pressure/irq is SKIP, not FAIL.
+ */
+static int test_proc_trigger(const char *resource, bool full_only)
+{
+	int fd, ret = KSFT_FAIL;
+
+	fd = pressure_open(resource);
+	if (fd < 0)
+		return full_only ? KSFT_SKIP : KSFT_FAIL;
+
+	if (!full_only) {
+		if (write_trigger(fd, "some 150000 2000000") <= 0) {
+			ksft_print_msg("%s: 'some' trigger rejected: %s\n",
+				       resource, strerror(errno));
+			goto out;
+		}
+		if (write_trigger(fd, "full 150000 2000000") != -1 ||
+		    errno != EBUSY) {
+			ksft_print_msg("%s: second trigger not EBUSY\n",
+				       resource);
+			goto out;
+		}
+	} else {
+		if (write_trigger(fd, "some 150000 2000000") != -1) {
+			ksft_print_msg("irq: 'some' trigger unexpectedly accepted\n");
+			goto out;
+		}
+		if (write_trigger(fd, "full 150000 2000000") <= 0) {
+			ksft_print_msg("irq: 'full' trigger rejected: %s\n",
+				       strerror(errno));
+			goto out;
+		}
+	}
+
+	ret = KSFT_PASS;
+out:
+	close(fd);
+	return ret;
+}
+
+/*
+ * cgroup.pressure gates visibility of the per-resource *.pressure files
+ * inside a cgroup: writing 0 hides them, writing 1 shows them again.
+ * Drive one hide/show cycle and check that memory.pressure appears and
+ * disappears along with it.
+ */
+static int test_cgroup_pressure_toggle(const char *root)
+{
+	char buf[BUF_SIZE];
+	char *cg = NULL;
+	int ret = KSFT_FAIL, created = 0;
+
+	cg = cg_name(root, "psi_toggle_test");
+	if (!cg)
+		goto cleanup;
+	if (cg_create(cg)) {
+		ksft_print_msg("cg_create: %s\n", strerror(errno));
+		goto cleanup;
+	}
+	created = 1;
+
+	if (cg_write(cg, "cgroup.pressure", "0")) {
+		ksft_print_msg("write cgroup.pressure=0: %s\n", strerror(errno));
+		goto cleanup;
+	}
+	if (cg_read_strcmp(cg, "cgroup.pressure", "0\n")) {
+		ksft_print_msg("cgroup.pressure readback != 0\n");
+		goto cleanup;
+	}
+	if (cg_read(cg, "memory.pressure", buf, sizeof(buf)) >= 0) {
+		ksft_print_msg("memory.pressure readable while hidden\n");
+		goto cleanup;
+	}
+
+	if (cg_write(cg, "cgroup.pressure", "1")) {
+		ksft_print_msg("write cgroup.pressure=1: %s\n", strerror(errno));
+		goto cleanup;
+	}
+	if (cg_read_strcmp(cg, "cgroup.pressure", "1\n")) {
+		ksft_print_msg("cgroup.pressure readback != 1\n");
+		goto cleanup;
+	}
+	if (cg_read(cg, "memory.pressure", buf, sizeof(buf)) < 0) {
+		ksft_print_msg("memory.pressure unreadable after enabling\n");
+		goto cleanup;
+	}
+
+	ret = KSFT_PASS;
+cleanup:
+	if (created)
+		cg_destroy(cg);
+	free(cg);
+	return ret;
+}
+
+/* A child that burns CPU forever; stopped by cg_killall() in the parent. */
+static int hog_cpu(const char *cgroup, void *arg)
+{
+	for (;;)
+		;
+	return 0;
+}
+
+/*
+ * Arm a "some" trigger on a cgroup's cpu.pressure, oversubscribe the
+ * cgroup with more spinning hogs than there are CPUs, and check that the
+ * trigger fires once the cgroup stalls on CPU.
+ */
+static int test_cgroup_trigger_fire(const char *root)
+{
+	char *cg = NULL, *cpupress = NULL;
+	int fd = -1, ret = KSFT_FAIL, created = 0, i;
+	long ncpus;
+
+	cg = cg_name(root, "psi_trigger_test");
+	if (!cg)
+		goto cleanup;
+	if (cg_create(cg)) {
+		ksft_print_msg("cg_create: %s\n", strerror(errno));
+		goto cleanup;
+	}
+	created = 1;
+
+	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;
+	}
+
+	/*
+	 * 1usec threshold over a 2s window: any CPU stall fires it. The 2s
+	 * window is the smallest unprivileged users are allowed to arm.
+	 */
+	if (write_trigger(fd, "some 1 2000000") <= 0) {
+		ksft_print_msg("arm trigger: %s\n", strerror(errno));
+		goto cleanup;
+	}
+
+	ncpus = sysconf(_SC_NPROCESSORS_ONLN);
+	if (ncpus <= 0) {
+		ksft_print_msg("sysconf(_SC_NPROCESSORS_ONLN) returned %ld\n",
+			       ncpus);
+		goto cleanup;
+	}
+
+	/* ncpus+1 hogs guarantee CPU contention inside the cgroup. */
+	for (i = 0; i < ncpus + 1; i++) {
+		if (cg_run_nowait(cg, hog_cpu, NULL) < 0) {
+			ksft_print_msg("spawn hog %d: %s\n", i, strerror(errno));
+			goto cleanup;
+		}
+	}
+
+	struct pollfd pfd = { .fd = fd, .events = POLLPRI };
+
+	switch (poll(&pfd, 1, PSI_POLL_TIMEOUT_MS)) {
+	case -1:
+		ksft_print_msg("poll: %s\n", strerror(errno));
+		goto cleanup;
+	case 0:
+		ksft_print_msg("trigger did not fire (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;
+	}
+
+cleanup:
+	if (fd >= 0)
+		close(fd);
+	if (created) {
+		cg_killall(cg);
+		cg_destroy(cg);
+	}
+	free(cpupress);
+	free(cg);
+	return ret;
+}
+
+struct psi_proc_test {
+	const char *name;
+	const char *resource;
+	bool full_only;
+};
+static const struct psi_proc_test proc_tests[] = {
+	{ "proc_trigger_io",     "io",     false },
+	{ "proc_trigger_memory", "memory", false },
+	{ "proc_trigger_cpu",    "cpu",    false },
+	{ "proc_trigger_irq",    "irq",    true  },
+};
+
+struct psi_cg_test {
+	const char *name;
+	int (*fn)(const char *root);
+};
+static const struct psi_cg_test cg_tests[] = {
+	{ "cgroup_pressure_toggle", test_cgroup_pressure_toggle },
+	{ "cgroup_trigger_fire",    test_cgroup_trigger_fire },
+};
+
+int main(int argc, char **argv)
+{
+	char root[PATH_MAX];
+	int psi_fd, i;
+
+	ksft_print_header();
+	ksft_set_plan(ARRAY_SIZE(proc_tests) + ARRAY_SIZE(cg_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 disabled on the cmdline). */
+	psi_fd = open("/proc/pressure/memory", O_RDONLY);
+	if (psi_fd < 0)
+		ksft_exit_skip("PSI unavailable (CONFIG_PSI=n or psi=0)\n");
+	close(psi_fd);
+
+	for (i = 0; i < ARRAY_SIZE(proc_tests); i++) {
+		switch (test_proc_trigger(proc_tests[i].resource,
+					  proc_tests[i].full_only)) {
+		case KSFT_PASS:
+			ksft_test_result_pass("%s\n", proc_tests[i].name);
+			break;
+		case KSFT_SKIP:
+			ksft_test_result_skip("%s\n", proc_tests[i].name);
+			break;
+		default:
+			ksft_test_result_fail("%s\n", proc_tests[i].name);
+			break;
+		}
+	}
+
+	for (i = 0; i < ARRAY_SIZE(cg_tests); i++) {
+		switch (cg_tests[i].fn(root)) {
+		case KSFT_PASS:
+			ksft_test_result_pass("%s\n", cg_tests[i].name);
+			break;
+		case KSFT_SKIP:
+			ksft_test_result_skip("%s\n", cg_tests[i].name);
+			break;
+		default:
+			ksft_test_result_fail("%s\n", cg_tests[i].name);
+			break;
+		}
+	}
+
+	ksft_finished();
+}
-- 
2.43.0


^ permalink raw reply related	[flat|nested] only message in thread

only message in thread, other threads:[~2026-08-13 13:37 UTC | newest]

Thread overview: (only message) (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-08-13 13:37 [PATCH v3] selftests/cgroup: add PSI pressure trigger and validation tests Tao Cui

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox