[Devel] [PATCH VZ10 1/2] kselftest/cgroup/freezer: test freezing cgroups with kernel threads

Pavel Tikhomirov ptikhomirov at virtuozzo.com
Tue Jun 16 14:54:26 MSK 2026


Add a selftest for the "cgroup-v2/freezer: allow freezing with kthreads"
change: a cgroup whose only task is a kernel thread must still be
freezable, since kthreads are ignored by the freezer and offset against
the frozen task count.

The test uses a helper module (cg_freezer_kthread) that spawns a spare
kernel thread and exports its pid via /proc, so the test can move it
into a freezer cgroup and check that the cgroup reports frozen. This
also introduces the test_modules/ build directory and the module
load/unload helpers shared with later freezer tests.

Note: requires kernel-devel package to build the helper modules.

https://virtuozzo.atlassian.net/browse/VSTOR-119676
Feature: cgroup/freeze: improvements
Signed-off-by: Pavel Tikhomirov <ptikhomirov at virtuozzo.com>
---
 tools/testing/selftests/cgroup/Makefile       |   1 +
 tools/testing/selftests/cgroup/config         |   2 +
 tools/testing/selftests/cgroup/test_freezer.c | 122 ++++++++++++++++++
 .../selftests/cgroup/test_modules/Makefile    |  16 +++
 .../cgroup/test_modules/cg_freezer_kthread.c  |  73 +++++++++++
 5 files changed, 214 insertions(+)
 create mode 100644 tools/testing/selftests/cgroup/test_modules/Makefile
 create mode 100644 tools/testing/selftests/cgroup/test_modules/cg_freezer_kthread.c

diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index 2b8395feccd4c..a0b52fb9bb341 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -6,6 +6,7 @@ all: ${HELPER_PROGS}
 TEST_FILES     := with_stress.sh
 TEST_PROGS     := test_stress.sh test_cpuset_prs.sh test_cpuset_v1_hp.sh
 TEST_GEN_FILES := wait_inotify
+TEST_GEN_MODS_DIR := test_modules
 # Keep the lists lexicographically sorted
 TEST_GEN_PROGS  = test_core
 TEST_GEN_PROGS += test_cpu
diff --git a/tools/testing/selftests/cgroup/config b/tools/testing/selftests/cgroup/config
index ac6625ba19945..c87a6df7edb53 100644
--- a/tools/testing/selftests/cgroup/config
+++ b/tools/testing/selftests/cgroup/config
@@ -8,3 +8,5 @@ CONFIG_NUMA=y
 CONFIG_MEMFD_CREATE=y
 CONFIG_TRANSPARENT_HUGEPAGE=y
 CONFIG_CACHESTAT_SYSCALL=y
+CONFIG_MODULES=y
+CONFIG_MODULE_UNLOAD=y
diff --git a/tools/testing/selftests/cgroup/test_freezer.c b/tools/testing/selftests/cgroup/test_freezer.c
index 8730645d363a7..0adc5b6c4ac78 100644
--- a/tools/testing/selftests/cgroup/test_freezer.c
+++ b/tools/testing/selftests/cgroup/test_freezer.c
@@ -21,6 +21,61 @@
 #define debug(args...)
 #endif
 
+/*
+ * Directory containing the test binary. The helper modules live in a
+ * test_modules/ subdirectory next to it (rsync'd there by TEST_GEN_MODS_DIR),
+ * so resolve them relative to the binary rather than the current working
+ * directory - the test may be run from anywhere.
+ */
+static const char *exe_dir(void)
+{
+	static char dir[PATH_MAX];
+	static int done;
+	char *slash;
+	ssize_t n;
+
+	if (done)
+		return dir;
+	done = 1;
+
+	n = readlink("/proc/self/exe", dir, sizeof(dir) - 1);
+	if (n < 0) {
+		strcpy(dir, ".");
+		return dir;
+	}
+	dir[n] = '\0';
+
+	slash = strrchr(dir, '/');
+	if (slash)
+		*slash = '\0';
+	else
+		strcpy(dir, ".");
+
+	return dir;
+}
+
+/*
+ * Load/unload a helper module from the test_modules/ directory next to the
+ * test binary. Returns 0 on success.
+ */
+static int mod_load(const char *mod)
+{
+	char cmd[2 * PATH_MAX];
+
+	snprintf(cmd, sizeof(cmd), "insmod %s/test_modules/%s.ko 2>/dev/null",
+		 exe_dir(), mod);
+	return system(cmd) ? -1 : 0;
+}
+
+static void mod_unload(const char *mod)
+{
+	char cmd[PATH_MAX];
+
+	snprintf(cmd, sizeof(cmd), "rmmod %s 2>/dev/null", mod);
+	if (system(cmd))
+		debug("Failed to unload module %s\n", mod);
+}
+
 /*
  * Check if the cgroup is frozen by looking at the cgroup.events::frozen value.
  */
@@ -804,6 +859,72 @@ static int test_cgfreezer_vfork(const char *root)
 	return ret;
 }
 
+/*
+ * Read the pid of the spare kernel thread spawned by cg_freezer_kthread.
+ */
+static int read_kthread_pid(void)
+{
+	int pid = -1;
+	FILE *f;
+
+	f = fopen("/proc/cg_freezer_kthread", "r");
+	if (!f)
+		return -1;
+	if (fscanf(f, "%d", &pid) != 1)
+		pid = -1;
+	fclose(f);
+	return pid;
+}
+
+/*
+ * Test that a cgroup whose only task is a kernel thread can be frozen.
+ * Kernel threads are ignored by the freezer and offset against the frozen
+ * task count, so the cgroup must report frozen in short time.
+ */
+static int test_cgfreezer_kthread(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *cgroup = NULL;
+	int kpid;
+
+	if (mod_load("cg_freezer_kthread"))
+		return KSFT_SKIP;
+
+	kpid = read_kthread_pid();
+	if (kpid <= 0)
+		goto cleanup;
+
+	cgroup = cg_name(root, "cg_test_kthread");
+	if (!cgroup)
+		goto cleanup;
+
+	if (cg_create(cgroup))
+		goto cleanup;
+
+	/* Move the spare kernel thread into the freezer cgroup. */
+	if (cg_enter(cgroup, kpid))
+		goto cleanup;
+
+	/*
+	 * Freezing must succeed quickly even though the only task is a
+	 * kernel thread.
+	 */
+	if (cg_freeze_wait(cgroup, true))
+		goto cleanup;
+
+	ret = KSFT_PASS;
+
+cleanup:
+	if (cgroup)
+		cg_freeze_nowait(cgroup, false);
+	/* Unloading stops the kthread, which then leaves the cgroup. */
+	mod_unload("cg_freezer_kthread");
+	if (cgroup)
+		cg_destroy(cgroup);
+	free(cgroup);
+	return ret;
+}
+
 #define T(x) { x, #x }
 struct cgfreezer_test {
 	int (*fn)(const char *root);
@@ -819,6 +940,7 @@ struct cgfreezer_test {
 	T(test_cgfreezer_stopped),
 	T(test_cgfreezer_ptraced),
 	T(test_cgfreezer_vfork),
+	T(test_cgfreezer_kthread),
 };
 #undef T
 
diff --git a/tools/testing/selftests/cgroup/test_modules/Makefile b/tools/testing/selftests/cgroup/test_modules/Makefile
new file mode 100644
index 0000000000000..dd401a4345d4b
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_modules/Makefile
@@ -0,0 +1,16 @@
+TESTMODS_DIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST)))))
+KDIR ?= /lib/modules/$(shell uname -r)/build
+
+obj-m += cg_freezer_kthread.o
+
+# Ensure that KDIR exists, otherwise skip the compilation
+modules:
+ifneq ("$(wildcard $(KDIR))", "")
+	$(Q)$(MAKE) -C $(KDIR) modules KBUILD_EXTMOD=$(TESTMODS_DIR)
+endif
+
+# Ensure that KDIR exists, otherwise skip the clean target
+clean:
+ifneq ("$(wildcard $(KDIR))", "")
+	$(Q)$(MAKE) -C $(KDIR) clean KBUILD_EXTMOD=$(TESTMODS_DIR)
+endif
diff --git a/tools/testing/selftests/cgroup/test_modules/cg_freezer_kthread.c b/tools/testing/selftests/cgroup/test_modules/cg_freezer_kthread.c
new file mode 100644
index 0000000000000..577aa8e0d3735
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_modules/cg_freezer_kthread.c
@@ -0,0 +1,73 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Helper module for the cgroup-v2 freezer selftest
+ * "test_cgfreezer_kthread".
+ *
+ * It spawns one spare kernel thread that just sleeps until the module is
+ * unloaded, and exposes its (init pid namespace) pid via
+ * /proc/cg_freezer_kthread.  The selftest reads that pid, moves the kthread
+ * into a freezer cgroup and checks that the cgroup can still be frozen --
+ * kernel threads are ignored by the freezer and offset against the frozen
+ * count, so a cgroup whose only task is a kthread must report frozen.
+ */
+#define pr_fmt(fmt) KBUILD_MODNAME ": " fmt
+
+#include <linux/module.h>
+#include <linux/kernel.h>
+#include <linux/sched.h>
+#include <linux/kthread.h>
+#include <linux/proc_fs.h>
+#include <linux/seq_file.h>
+#include <linux/pid.h>
+#include <linux/pid_namespace.h>
+
+static struct task_struct *spare_kthread;
+
+static int cg_freezer_kthread_fn(void *data)
+{
+	while (!kthread_should_stop()) {
+		set_current_state(TASK_INTERRUPTIBLE);
+		if (kthread_should_stop()) {
+			set_current_state(TASK_RUNNING);
+			break;
+		}
+		schedule_timeout(HZ);
+	}
+	return 0;
+}
+
+static int cg_freezer_kthread_show(struct seq_file *m, void *v)
+{
+	seq_printf(m, "%d\n", task_pid_nr_ns(spare_kthread, &init_pid_ns));
+	return 0;
+}
+
+static int __init cg_freezer_kthread_init(void)
+{
+	spare_kthread = kthread_run(cg_freezer_kthread_fn, NULL,
+				    "cg_freezer_kthr");
+	if (IS_ERR(spare_kthread))
+		return PTR_ERR(spare_kthread);
+
+	if (!proc_create_single("cg_freezer_kthread", 0444, NULL,
+				cg_freezer_kthread_show)) {
+		kthread_stop(spare_kthread);
+		return -ENOMEM;
+	}
+
+	pr_info("spare kthread pid %d\n",
+		task_pid_nr_ns(spare_kthread, &init_pid_ns));
+	return 0;
+}
+
+static void __exit cg_freezer_kthread_exit(void)
+{
+	remove_proc_entry("cg_freezer_kthread", NULL);
+	kthread_stop(spare_kthread);
+}
+
+module_init(cg_freezer_kthread_init);
+module_exit(cg_freezer_kthread_exit);
+MODULE_LICENSE("GPL");
+MODULE_AUTHOR("Pavel Tikhomirov <ptikhomirov at virtuozzo.com>");
+MODULE_DESCRIPTION("cgroup freezer test: spawn a spare kernel thread");
-- 
2.54.0



More information about the Devel mailing list