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 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.