Skip to contentSkip to conversation

selftests/cgroup: add PSI pressure trigger and validation tests

PATCH Superseded Tao Cui 2 participants 4 messages Aug 13 – Aug 24 1 message not in the archive

Subsystems (2)

v3 of 6 Superseded Latest is v6 →

lore
Share & tools
lore

Tao Cui

author
#0 lore raw

PATCH v3 1/1 selftests/cgroup: add PSI pressure trigger and validation tests

From: Tao Cui <[email protected]>

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.

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

---
 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
tools/testing/selftests/cgroup/.gitignore +1 -0
index 952e4448bf07..ce2b907c57ea 100644
@@ -8,5 +8,6 @@ test_kill
 test_kmem
 test_memcontrol
 test_pids
+test_psi
 test_zswap
 wait_inotify
tools/testing/selftests/cgroup/Makefile +2 -0
index e01584c2189a..a8c69e37332a 100644
@@ -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)
tools/testing/selftests/cgroup/config +1 -0
index 39f979690dd3..8a3ef479e83d 100644
@@ -4,3 +4,4 @@ CONFIG_CGROUP_FREEZER=y
 CONFIG_CGROUP_SCHED=y
 CONFIG_MEMCG=y
 CONFIG_PAGE_COUNTER=y
+CONFIG_PSI=y
tools/testing/selftests/cgroup/test_psi.c new file +296 -0

296 lines not on this page · raw message

Signature · 1 line
2.43.0

A message that is not in the archive (sent off-list or not yet received): [email protected]

author
#2 lore raw
Quoted 32 lines · 在 2026/8/13 21:54, [email protected] 写道:

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] The child processes spawned to generate CPU pressure execute an unconditional infinite loop (for (;;);). If the test is interrupted (e.g., via SIGINT or a CI timeout sending SIGTERM), the parent process will exit immediately without executing the cleanup block (cg_killall()). This leaves ncpus + 1 child processes running forever, consuming 100% CPU on all cores and effectively locking up the system. --

--- Patch [1]: [PATCH v3] selftests/cgroup: add PSI pressure trigger and validation tests --- commit a3c72bd52fb11f36beba6ee125a9c7100e986a5a
Author: Tao Cui <[email protected]>
selftests/cgroup: add PSI pressure trigger and validation tests

This commit adds per-resource trigger smoke tests, a cgroup.pressure hide/show toggle test, and a CPU-pressure trigger test using over-subscription to the cgroup selftests.

Quoted 5 lines
tools/testing/selftests/cgroup/test_psi.c new file +0 -0
new file mode 100644
index 0000000000000..cfb412f614588

[ ... ]

tools/testing/selftests/cgroup/test_psi.c line 141

Quoted 5 lines
tools/testing/selftests/cgroup/test_psi.c new file +5 -0
+/* A child that burns CPU forever; stopped by cg_killall() in the parent. */
+static int hog_cpu(const char *cgroup, void *arg)
+{
+	for (;;)
+		;

[Severity: Medium]
Will this leave runaway CPU hogs consuming 100% CPU on all cores if the test is interrupted?

Quoted 8 lines

If the parent process is interrupted by a signal (like SIGINT or a CI runner timeout sending SIGTERM) while waiting for poll(), it will exit immediately without executing the cg_killall() cleanup block.

Could a fallback termination condition be added here, such as prctl(PR_SET_PDEATHSIG, SIGKILL) or checking getppid(), to prevent orphaned processes from locking up the system?

Agreed. The hogs run forever, so interrupting the parent during poll() skips cg_killall() and orphans them. I'll add prctl(PR_SET_PDEATHSIG, SIGKILL) at the start of each hog so the kernel reaps them if the parent dies -- for v4.

Quoted 3 lines
Quoted 2 lines
tools/testing/selftests/cgroup/test_psi.c new file +2 -0
+	return 0;
+}
#3 lore raw
In reply to Tao Cui
Quoted 152 lines · Tao Cui

From: Tao Cui <[email protected]>

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.

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

---
 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
tools/testing/selftests/cgroup/.gitignore +1 -0
index 952e4448bf07..ce2b907c57ea 100644
@@ -8,5 +8,6 @@ test_kill
 test_kmem
 test_memcontrol
 test_pids
+test_psi
 test_zswap
 wait_inotify
tools/testing/selftests/cgroup/Makefile +2 -0
index e01584c2189a..a8c69e37332a 100644
@@ -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)
tools/testing/selftests/cgroup/config +1 -0
index 39f979690dd3..8a3ef479e83d 100644
@@ -4,3 +4,4 @@ CONFIG_CGROUP_FREEZER=y
 CONFIG_CGROUP_SCHED=y
 CONFIG_MEMCG=y
 CONFIG_PAGE_COUNTER=y
+CONFIG_PSI=y
tools/testing/selftests/cgroup/test_psi.c new file +69 -0
new file mode 100644
index 000000000000..cfb412f61458
@@ -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_TIMEOUhas this check T_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) {

tools/testing/selftests/cgroup/test_psi.c line 79

Quoted 10 lines
tools/testing/selftests/cgroup/test_psi.c new file +10 -0
+                       ksft_print_msg("irq: 'full' trigger rejected: %s\n",
+                                      strerror(errno));
+                       goto out;
+               }
+       }
+
+       ret = KSFT_PASS;
+out:
+       close(fd);
+       return ret;

You don't really need "ret" variable. Just "return KSFT_FAIL;" here is enough.

tools/testing/selftests/cgroup/test_psi.c line 92

Quoted 13 lines
tools/testing/selftests/cgroup/test_psi.c new file +13 -0
+}
+
+/*
+ * 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;

Again, you don't need "ret" variable here.

tools/testing/selftests/cgroup/test_psi.c line 101

Quoted 9 lines
tools/testing/selftests/cgroup/test_psi.c new file +9 -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;

Nope. The way we usually do the cleanup is by jumping to the appropriate label instead of adding extra state flags. In this case that would look something like this:

       cg = cg_name(root, "psi_toggle_test");
       if (!cg)
               return KSFT_FAIL;

       if (cg_create(cg)) {
               ksft_print_msg("cg_create: %s\n", strerror(errno));
               goto free_cg;
       }

       if (cg_write(cg, "cgroup.pressure", "0")) {
               ksft_print_msg("write cgroup.pressure=0: %s\n", strerror(errno));
               goto cleanup;
       }
...
       return KSFT_PASS;
cleanup:
       cg_destroy(cg);
free_cg:
       free(cg);
       return KSFT_FAIL;
}
Quoted 30 lines
tools/testing/selftests/cgroup/test_psi.c new file +30 -0
+
+       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)

tools/testing/selftests/cgroup/test_psi.c line 141

Quoted 10 lines
tools/testing/selftests/cgroup/test_psi.c new file +10 -0
+               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 (;;)
+               ;

This loop looks strange. Maybe:

       for (;;) {}

tools/testing/selftests/cgroup/test_psi.c line 153

Quoted 12 lines
tools/testing/selftests/cgroup/test_psi.c new file +12 -0
+       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;

Please define each variable on a separate line.

tools/testing/selftests/cgroup/test_psi.c line 163

Quoted 10 lines
tools/testing/selftests/cgroup/test_psi.c new file +10 -0
+       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;

Same as above. Jump to the appropriate label.

Quoted 11 lines
tools/testing/selftests/cgroup/test_psi.c new file +11 -0
+
+       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;
+       }
+
+       /*

tools/testing/selftests/cgroup/test_psi.c line 184

Quoted 10 lines
tools/testing/selftests/cgroup/test_psi.c new file +10 -0
+        * 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) {

I don't think this syscall will ever return 0, so you just need to check for -1. Also reporting -1 in the error log isn't very informative. Instead report strerror(errno).

Quoted 9 lines
tools/testing/selftests/cgroup/test_psi.c new file +9 -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));

tools/testing/selftests/cgroup/test_psi.c line 203

Quoted 10 lines
tools/testing/selftests/cgroup/test_psi.c new file +10 -0
+                       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;

So, goto cleanup here but break everywhere else? Consistency would be nice, so please use break here as well.

tools/testing/selftests/cgroup/test_psi.c line 206

Quoted 3 lines
tools/testing/selftests/cgroup/test_psi.c new file +3 -0
+       case 0:
+               ksft_print_msg("trigger did not fire (could not induce CPU pressure)\n");
+               ret = KSFT_SKIP;

This means your trigger timed out. Why is this treated as KSFT_SKIP and not as KSFT_FAIL?

Quoted 77 lines
tools/testing/selftests/cgroup/test_psi.c new file +77 -0
+               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:

tools/testing/selftests/cgroup/test_psi.c line 293

Quoted 10 lines
tools/testing/selftests/cgroup/test_psi.c new file +10 -0
+                       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;
+               }
+       }

Please convert this to use
https://docs.kernel.org/dev-tools/kselftest.html#test-harness as I suggested before. This code will look much simpler. Even if some other tests are not using it, there is no reason for new tests not to take advantage of the existing harness. You will find planty of examples which use them.

Quoted 6 lines
tools/testing/selftests/cgroup/test_psi.c new file +3 -1
+
+       ksft_finished();
+}
--

2.43.0

author
#4 lore raw

Hi, Suren

Quoted 157 lines · Suren Baghdasaryan
Quoted 156 lines · Tao Cui

From: Tao Cui <[email protected]>

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.

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

---
 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
tools/testing/selftests/cgroup/.gitignore +1 -0
index 952e4448bf07..ce2b907c57ea 100644
@@ -8,5 +8,6 @@ test_kill
 test_kmem
 test_memcontrol
 test_pids
+test_psi
 test_zswap
 wait_inotify
tools/testing/selftests/cgroup/Makefile +2 -0
index e01584c2189a..a8c69e37332a 100644
@@ -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)
tools/testing/selftests/cgroup/config +1 -0
index 39f979690dd3..8a3ef479e83d 100644
@@ -4,3 +4,4 @@ CONFIG_CGROUP_FREEZER=y
 CONFIG_CGROUP_SCHED=y
 CONFIG_MEMCG=y
 CONFIG_PAGE_COUNTER=y
+CONFIG_PSI=y
tools/testing/selftests/cgroup/test_psi.c new file +73 -0
new file mode 100644
index 000000000000..cfb412f61458
@@ -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_TIMEOUhas this check T_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;
+               }
Quoted 9 lines

tools/testing/selftests/cgroup/test_psi.c line 79

Quoted 6 lines
tools/testing/selftests/cgroup/test_psi.c new file +6 -0
+       }
+
+       ret = KSFT_PASS;
+out:
+       close(fd);
+       return ret;

You don't really need "ret" variable. Just "return KSFT_FAIL;" here is enough.

Done. The harness conversion took care of this one: the switch/case reporting is gone and the failure paths are just ASSERTs now.

Quoted 43 lines

tools/testing/selftests/cgroup/test_psi.c line 92

Quoted 13 lines
tools/testing/selftests/cgroup/test_psi.c new file +13 -0
+}
+
+/*
+ * 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;

Again, you don't need "ret" variable here.

tools/testing/selftests/cgroup/test_psi.c line 101

Quoted 9 lines
tools/testing/selftests/cgroup/test_psi.c new file +9 -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;

Nope. The way we usually do the cleanup is by jumping to the appropriate label instead of adding extra state flags. In this case that would look something like this:

       cg = cg_name(root, "psi_toggle_test");
       if (!cg)
               return KSFT_FAIL;

       if (cg_create(cg)) {
               ksft_print_msg("cg_create: %s\n", strerror(errno));
               goto free_cg;
       }

       if (cg_write(cg, "cgroup.pressure", "0")) {
               ksft_print_msg("write cgroup.pressure=0: %s\n", strerror(errno));
               goto cleanup;
       }
Quoted 9 lines

...

       return KSFT_PASS;
cleanup:
       cg_destroy(cg);
free_cg:
       free(cg);
       return KSFT_FAIL;
}

You're right, that reads better. I ended up going one step further and moving the teardown into FIXTURE_TEARDOWN() instead: the cgroup is created inside the test and self->cg is NULL until then, so the teardown is a no-op for the tests that don't create one. That way there are no labels and no state flags left.

Quoted 43 lines
Quoted 30 lines
tools/testing/selftests/cgroup/test_psi.c new file +30 -0
+
+       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)

tools/testing/selftests/cgroup/test_psi.c line 141

Quoted 10 lines
tools/testing/selftests/cgroup/test_psi.c new file +10 -0
+               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 (;;)
+               ;

This loop looks strange. Maybe:

Quoted 2 lines
       for (;;) {}

Fixed.

Quoted 6 lines
Quoted 6 lines
tools/testing/selftests/cgroup/test_psi.c new file +6 -0
+       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
Quoted 9 lines

tools/testing/selftests/cgroup/test_psi.c line 153

Quoted 6 lines

+ * trigger fires once the cgroup stalls on CPU.
+ */

tools/testing/selftests/cgroup/test_psi.c new file +4 -0
+static int test_cgroup_trigger_fire(const char *root)
+{
+       char *cg = NULL, *cpupress = NULL;
+       int fd = -1, ret = KSFT_FAIL, created = 0, i;

Please define each variable on a separate line.

Fixed.

Quoted 30 lines

tools/testing/selftests/cgroup/test_psi.c line 163

Quoted 10 lines
tools/testing/selftests/cgroup/test_psi.c new file +10 -0
+       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;

Same as above. Jump to the appropriate label.

Quoted 17 lines
tools/testing/selftests/cgroup/test_psi.c new file +17 -0
+
+       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;
Quoted 9 lines

tools/testing/selftests/cgroup/test_psi.c line 184

Quoted 4 lines
tools/testing/selftests/cgroup/test_psi.c new file +4 -0
+       }
+
+       ncpus = sysconf(_SC_NPROCESSORS_ONLN);
+       if (ncpus <= 0) {

I don't think this syscall will ever return 0, so you just need to check for -1. Also reporting -1 in the error log isn't very informative. Instead report strerror(errno).

Agreed, now checks only for -1 and reports strerror(errno) via TH_LOG.

Quoted 14 lines
Quoted 14 lines
tools/testing/selftests/cgroup/test_psi.c new file +14 -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 };
Quoted 9 lines

tools/testing/selftests/cgroup/test_psi.c line 203

Quoted 5 lines
tools/testing/selftests/cgroup/test_psi.c new file +5 -0
+
+       switch (poll(&pfd, 1, PSI_POLL_TIMEOUT_MS)) {
+       case -1:
+               ksft_print_msg("poll: %s\n", strerror(errno));
+               goto cleanup;

So, goto cleanup here but break everywhere else? Consistency would be nice, so please use break here as well.

The switch is gone along with the ret bookkeeping, so this is moot
now: poll() collapsed into a single ASSERT_EQ(1, poll(...)).

Quoted 6 lines

tools/testing/selftests/cgroup/test_psi.c line 206

Quoted 3 lines
tools/testing/selftests/cgroup/test_psi.c new file +3 -0
+       case 0:
+               ksft_print_msg("trigger did not fire (could not induce CPU pressure)\n");
+               ret = KSFT_SKIP;

This means your trigger timed out. Why is this treated as KSFT_SKIP and not as KSFT_FAIL?

Fair point, I agree. With ncpus+1 hogs and a 1usec threshold over a 2s window the cgroup must stall, so if the trigger doesn't fire something is actually broken. I made it a FAIL.

Quoted 87 lines
Quoted 87 lines
tools/testing/selftests/cgroup/test_psi.c new file +86 -0
+               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;
+               }
Quoted 9 lines
Quoted 1 line

+ }

Please convert this to use
https://docs.kernel.org/dev-tools/kselftest.html#test-harness as I suggested before. This code will look much simpler. Even if some other tests are not using it, there is no reason for new tests not to take advantage of the existing harness. You will find planty of examples which use them.

Converted, thanks for the pointer. Each case is a TEST_F(psi, ...) now; the cgroup v2 and PSI availability checks live in FIXTURE_SETUP() and SKIP there, and the hog killing and cgroup destruction live in FIXTURE_TEARDOWN(). It does make the code simpler, as you predicted. The hogs also set PR_SET_PDEATHSIG on themselves now, so an interrupted run doesn't leave orphans pinning every CPU.

With that the file shrinks from 296 to 206 lines. I tested it on a few setups (PSI on and off, with and without /proc/pressure/irq): 6/6 pass where everything is available, clean skips otherwise.

Thanks a lot for the review and for your patience with all the iterations.

Thanks,
Tao>> +

Quoted 5 lines
Quoted 5 lines
tools/testing/selftests/cgroup/test_psi.c new file +2 -1
+       ksft_finished();
+}
--

2.43.0