[LTP] [PATCH v3 2/3] cgroup: add CPU cgroup v2 tests ported from kselftest

Shaojie Sun <[email protected]> Fri, 31 Jul 2026 11:13:42 +0800
Newsgroups it.linux.lists.ltp
Message-ID <[email protected]>
Port the following kselftest CPU cgroup v2 tests to LTP:
  - test_cpucg_subtree_control (cgroup_cpu01)
  - test_cpucg_stats (cgroup_cpu02)
  - test_cpucg_nice (cgroup_cpu03)
  - test_cpucg_weight_overprovisioned (cgroup_cpu04)
  - test_cpucg_weight_underprovisioned (cgroup_cpu04)
  - test_cpucg_nested_weight_overprovisioned (cgroup_cpu04)
  - test_cpucg_nested_weight_underprovisioned (cgroup_cpu04)
  - test_cpucg_max (cgroup_cpu05)
  - test_cpucg_max_nested (cgroup_cpu05)

Converted from tools/testing/selftests/cgroup/test_cpu.c.

Signed-off-by: Shaojie Sun <[email protected]>
---
 testcases/kernel/controllers/cpu/.gitignore   |   5 +
 testcases/kernel/controllers/cpu/Makefile     |   9 +
 .../kernel/controllers/cpu/cgroup_cpu01.c     |  64 ++++
 .../kernel/controllers/cpu/cgroup_cpu02.c     | 103 ++++++
 .../kernel/controllers/cpu/cgroup_cpu03.c     | 115 ++++++
 .../kernel/controllers/cpu/cgroup_cpu04.c     | 332 ++++++++++++++++++
 .../kernel/controllers/cpu/cgroup_cpu05.c     | 176 ++++++++++
 .../controllers/cpu/cgroup_cpu_common.h       | 131 +++++++
 8 files changed, 935 insertions(+)
 create mode 100644 testcases/kernel/controllers/cpu/.gitignore
 create mode 100644 testcases/kernel/controllers/cpu/Makefile
 create mode 100644 testcases/kernel/controllers/cpu/cgroup_cpu01.c
 create mode 100644 testcases/kernel/controllers/cpu/cgroup_cpu02.c
 create mode 100644 testcases/kernel/controllers/cpu/cgroup_cpu03.c
 create mode 100644 testcases/kernel/controllers/cpu/cgroup_cpu04.c
 create mode 100644 testcases/kernel/controllers/cpu/cgroup_cpu05.c
 create mode 100644 testcases/kernel/controllers/cpu/cgroup_cpu_common.h

diff --git a/testcases/kernel/controllers/cpu/.gitignore b/testcases/kernel/controllers/cpu/.gitignore
new file mode 100644
index 000000000..1a81d332a
--- /dev/null
+++ b/testcases/kernel/controllers/cpu/.gitignore
@@ -0,0 +1,5 @@
+/cgroup_cpu01
+/cgroup_cpu02
+/cgroup_cpu03
+/cgroup_cpu04
+/cgroup_cpu05
diff --git a/testcases/kernel/controllers/cpu/Makefile b/testcases/kernel/controllers/cpu/Makefile
new file mode 100644
index 000000000..d1fbcba3e
--- /dev/null
+++ b/testcases/kernel/controllers/cpu/Makefile
@@ -0,0 +1,9 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+top_srcdir		?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+
+LDLIBS			+= -lpthread
+
+include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/kernel/controllers/cpu/cgroup_cpu01.c b/testcases/kernel/controllers/cpu/cgroup_cpu01.c
new file mode 100644
index 000000000..64ae8bc80
--- /dev/null
+++ b/testcases/kernel/controllers/cpu/cgroup_cpu01.c
@@ -0,0 +1,64 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Richard Palethorpe <[email protected]>
+ */
+
+/*\
+ * Conversion of kselftest test_cpucg_subtree_control from
+ * :kselftest:`cgroup/test_cpu.c`.
+ *
+ * This test creates two nested cgroups with and without enabling
+ * the cpu controller.
+ *
+ * The LTP API automatically adds controllers to subtree_control when
+ * a child cgroup is added. So unlike the kselftest we remove the
+ * controller after it being added automatically.
+ */
+
+#define _GNU_SOURCE
+#include "tst_test.h"
+
+static struct tst_cg_group *cg_parent_0, *cg_child_0;
+static struct tst_cg_group *cg_parent_1, *cg_child_1;
+
+static void test_cpucg_subtree_control(void)
+{
+	cg_parent_0 = tst_cg_group_mk(tst_cg, "cpucg_test_0");
+	cg_child_0 = tst_cg_group_mk(cg_parent_0, "cpucg_child_0");
+
+	cg_parent_1 = tst_cg_group_mk(tst_cg, "cpucg_test_1");
+	cg_child_1 = tst_cg_group_mk(cg_parent_1, "cpucg_child_1");
+	SAFE_CG_PRINT(cg_parent_1, "cgroup.subtree_control", "-cpu");
+
+	TST_EXP_POSITIVE(
+		SAFE_CG_OCCURSIN(cg_child_0, "cgroup.controllers", "cpu"),
+		"child with cpu enabled should have cpu controller");
+
+	TST_EXP_POSITIVE(
+		!SAFE_CG_OCCURSIN(cg_child_1, "cgroup.controllers", "cpu"),
+		"child without cpu enabled should not have cpu controller");
+
+	cg_child_1 = tst_cg_group_rm(cg_child_1);
+	cg_parent_1 = tst_cg_group_rm(cg_parent_1);
+	cg_child_0 = tst_cg_group_rm(cg_child_0);
+	cg_parent_0 = tst_cg_group_rm(cg_parent_0);
+}
+
+static void cleanup(void)
+{
+	if (cg_child_1)
+		cg_child_1 = tst_cg_group_rm(cg_child_1);
+	if (cg_parent_1)
+		cg_parent_1 = tst_cg_group_rm(cg_parent_1);
+	if (cg_child_0)
+		cg_child_0 = tst_cg_group_rm(cg_child_0);
+	if (cg_parent_0)
+		cg_parent_0 = tst_cg_group_rm(cg_parent_0);
+}
+
+static struct tst_test test = {
+	.cleanup = cleanup,
+	.test_all = test_cpucg_subtree_control,
+	.needs_cgroup_ver = TST_CG_V2,
+	.needs_cgroup_ctrls = (const char *const []){ "cpu", NULL },
+};
diff --git a/testcases/kernel/controllers/cpu/cgroup_cpu02.c b/testcases/kernel/controllers/cpu/cgroup_cpu02.c
new file mode 100644
index 000000000..00a8d7c22
--- /dev/null
+++ b/testcases/kernel/controllers/cpu/cgroup_cpu02.c
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Richard Palethorpe <[email protected]>
+ * Copyright (c) 2026 Shaojie Sun <[email protected]>
+ */
+
+/*\
+ * Conversion of kselftest test_cpucg_stats from
+ * :kselftest:`cgroup/test_cpu.c`.
+ *
+ * Creates a cpu cgroup, burns a CPU for a few quanta, and verifies that
+ * cpu.stat shows the expected output.
+ */
+
+#define _GNU_SOURCE
+#include <time.h>
+#include <sys/wait.h>
+
+#include "tst_test.h"
+#include "cgroup_cpu_common.h"
+
+#define USEC_PER_SEC 1000000L
+
+static struct tst_cg_group *cg_test;
+
+static void test_cpucg_stats(void)
+{
+	long usage_usec, user_usec, system_usec;
+	long usage_seconds = 2;
+	long expected_usage_usec = usage_seconds * USEC_PER_SEC;
+
+	cg_test = tst_cg_group_mk(tst_cg, "cpucg_test");
+
+	SAFE_CG_LINES_SCANF(cg_test, "cpu.stat",
+			    "usage_usec %ld", &usage_usec);
+	SAFE_CG_LINES_SCANF(cg_test, "cpu.stat",
+			    "user_usec %ld", &user_usec);
+	SAFE_CG_LINES_SCANF(cg_test, "cpu.stat",
+			    "system_usec %ld", &system_usec);
+
+	if (usage_usec != 0 || user_usec != 0 || system_usec != 0) {
+		tst_res(TFAIL,
+			"Initial cpu.stat values not zero: usage=%ld user=%ld system=%ld",
+			usage_usec, user_usec, system_usec);
+		goto cleanup;
+	}
+
+	if (!SAFE_FORK()) {
+		struct cpu_hog_func_param param = {
+			.nprocs = 1,
+			.ts = {
+				.tv_sec = usage_seconds,
+				.tv_nsec = 0,
+			},
+			.clock_type = CPU_HOG_CLOCK_PROCESS,
+		};
+
+		SAFE_CG_PRINTF(cg_test, "cgroup.procs", "%d", getpid());
+		hog_cpus_timed(&param);
+		exit(0);
+	}
+
+	SAFE_WAITPID(-1, NULL, 0);
+
+	SAFE_CG_LINES_SCANF(cg_test, "cpu.stat",
+			    "usage_usec %ld", &usage_usec);
+	SAFE_CG_LINES_SCANF(cg_test, "cpu.stat",
+			    "user_usec %ld", &user_usec);
+
+	if (user_usec <= 0) {
+		tst_res(TFAIL, "user_usec=%ld should be > 0 after CPU hog",
+			user_usec);
+		goto cleanup;
+	}
+
+	if (!values_close(usage_usec, expected_usage_usec, 1)) {
+		tst_res(TFAIL,
+			"usage_usec=%ld should be close to expected=%ld (diff > 1%%)",
+			usage_usec, expected_usage_usec);
+		goto cleanup;
+	}
+
+	tst_res(TPASS, "cpu.stat values valid: usage=%ld user=%ld",
+		usage_usec, user_usec);
+
+cleanup:
+	cg_test = tst_cg_group_rm(cg_test);
+}
+
+static void cleanup(void)
+{
+	if (cg_test)
+		cg_test = tst_cg_group_rm(cg_test);
+}
+
+static struct tst_test test = {
+	.cleanup = cleanup,
+	.test_all = test_cpucg_stats,
+	.forks_child = 1,
+	.needs_root = 1,
+	.needs_cgroup_ver = TST_CG_V2,
+	.needs_cgroup_ctrls = (const char *const []){ "cpu", NULL },
+};
diff --git a/testcases/kernel/controllers/cpu/cgroup_cpu03.c b/testcases/kernel/controllers/cpu/cgroup_cpu03.c
new file mode 100644
index 000000000..3088fcc56
--- /dev/null
+++ b/testcases/kernel/controllers/cpu/cgroup_cpu03.c
@@ -0,0 +1,115 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Richard Palethorpe <[email protected]>
+ * Copyright (c) 2026 Shaojie Sun <[email protected]>
+ */
+
+/*\
+ * Conversion of kselftest test_cpucg_nice from
+ * :kselftest:`cgroup/test_cpu.c`.
+ *
+ * Creates a nice process that consumes CPU and checks that the elapsed
+ * nice time in the cgroup is close to the expected time.
+ */
+
+#define _GNU_SOURCE
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "tst_test.h"
+#include "cgroup_cpu_common.h"
+
+#define USEC_PER_SEC 1000000L
+
+static struct tst_cg_group *cg_child;
+
+static long read_nice_usec(void)
+{
+	char buf[256];
+	long val = -1;
+	char *line, *saveptr;
+
+	SAFE_CG_READ(cg_child, "cpu.stat", buf, sizeof(buf));
+
+	line = strtok_r(buf, "\n", &saveptr);
+	while (line) {
+		if (sscanf(line, "nice_usec %ld", &val) == 1)
+			return val;
+		line = strtok_r(NULL, "\n", &saveptr);
+	}
+
+	return -1;
+}
+
+static void test_cpucg_nice(void)
+{
+	long user_usec, nice_usec;
+	long usage_seconds = 2;
+	long expected_nice_usec = usage_seconds * USEC_PER_SEC;
+
+	cg_child = tst_cg_group_mk(tst_cg, "cpucg_test");
+
+	SAFE_CG_LINES_SCANF(cg_child, "cpu.stat",
+			    "user_usec %ld", &user_usec);
+
+	nice_usec = read_nice_usec();
+	if (nice_usec < 0)
+		tst_brk(TCONF, "nice_usec not available in cpu.stat");
+
+	if (user_usec != 0 || nice_usec != 0) {
+		tst_res(TFAIL,
+			"Initial cpu.stat values not zero: user=%ld nice=%ld", user_usec, nice_usec);
+		goto cleanup;
+	}
+
+	if (!SAFE_FORK()) {
+		struct cpu_hog_func_param param = {
+			.nprocs = 1,
+			.ts = {
+				.tv_sec = usage_seconds,
+				.tv_nsec = 0,
+			},
+			.clock_type = CPU_HOG_CLOCK_PROCESS,
+		};
+		SAFE_CG_PRINTF(cg_child, "cgroup.procs", "%d", getpid());
+
+		errno = 0;
+		if (nice(1) == -1 && errno != 0)
+			exit(1);
+		hog_cpus_timed(&param);
+		exit(0);
+	}
+
+	SAFE_WAITPID(-1, NULL, 0);
+
+	nice_usec = read_nice_usec();
+
+	if (!values_close(nice_usec, expected_nice_usec, 1)) {
+		tst_res(TFAIL,
+			"nice_usec=%ld should be close to expected=%ld",
+			nice_usec, expected_nice_usec);
+		goto cleanup;
+	}
+
+	tst_res(TPASS, "nice_usec=%ld is close to expected=%ld",
+		nice_usec, expected_nice_usec);
+
+cleanup:
+	cg_child = tst_cg_group_rm(cg_child);
+}
+
+static void cleanup(void)
+{
+	if (cg_child)
+		cg_child = tst_cg_group_rm(cg_child);
+}
+
+static struct tst_test test = {
+	.cleanup = cleanup,
+	.test_all = test_cpucg_nice,
+	.forks_child = 1,
+	.needs_root = 1,
+	.needs_cgroup_ver = TST_CG_V2,
+	.needs_cgroup_ctrls = (const char *const []){ "cpu", NULL },
+};
diff --git a/testcases/kernel/controllers/cpu/cgroup_cpu04.c b/testcases/kernel/controllers/cpu/cgroup_cpu04.c
new file mode 100644
index 000000000..7dd24624b
--- /dev/null
+++ b/testcases/kernel/controllers/cpu/cgroup_cpu04.c
@@ -0,0 +1,332 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Richard Palethorpe <[email protected]>
+ * Copyright (c) 2026 Shaojie Sun <[email protected]>
+ */
+
+/*\
+ * Conversion of kselftest cpu weight tests from
+ * :kselftest:`cgroup/test_cpu.c`.
+ *
+ * Includes:
+ *
+ * - test_cpucg_weight_overprovisioned
+ * - test_cpucg_weight_underprovisioned
+ * - test_cpucg_nested_weight_overprovisioned
+ * - test_cpucg_nested_weight_underprovisioned
+ */
+
+#define _GNU_SOURCE
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "tst_test.h"
+#include "cgroup_cpu_common.h"
+
+#define HOG_SECONDS 10
+
+static struct tst_cg_group *cg_parent;
+static struct tst_cg_group *cg_child;
+static struct tst_cg_group *cg_leaf[3];
+
+static pid_t fork_hog_child(struct tst_cg_group *cg, int nprocs,
+			     enum hog_clock_type clock_type)
+{
+	pid_t pid;
+
+	pid = SAFE_FORK();
+	if (pid == 0) {
+		struct cpu_hog_func_param param = {
+			.nprocs = nprocs,
+			.ts = {
+				.tv_sec = HOG_SECONDS,
+				.tv_nsec = 0,
+			},
+			.clock_type = clock_type,
+		};
+
+		SAFE_CG_PRINTF(cg, "cgroup.procs", "%d", getpid());
+		hog_cpus_timed(&param);
+		exit(0);
+	}
+
+	return pid;
+}
+
+static void wait_children(pid_t *pids, int n)
+{
+	int i;
+
+	for (i = 0; i < n; i++)
+		SAFE_WAITPID(pids[i], NULL, 0);
+}
+
+static void test_cpucg_weight_overprovisioned(void)
+{
+	int i, nprocs;
+	pid_t pids[3];
+	long usage[3];
+	long delta;
+
+	cg_parent = tst_cg_group_mk(tst_cg, "cpucg_test_0");
+
+	for (i = 0; i < 3; i++) {
+		cg_leaf[i] = tst_cg_group_mk(cg_parent, "cpucg_child_%d", i);
+		SAFE_CG_PRINTF(cg_leaf[i], "cpu.weight", "%d", 50 * (i + 1));
+	}
+
+	nprocs = tst_ncpus_available();
+	for (i = 0; i < 3; i++)
+		pids[i] = fork_hog_child(cg_leaf[i], nprocs,
+					 CPU_HOG_CLOCK_WALL);
+
+	wait_children(pids, 3);
+
+	for (i = 0; i < 3; i++) {
+		SAFE_CG_LINES_SCANF(cg_leaf[i], "cpu.stat",
+				    "usage_usec %ld", &usage[i]);
+	}
+
+	if (usage[1] <= usage[0] || usage[2] <= usage[1]) {
+		tst_res(TFAIL,
+			"CPU usage not monotonic with weight: %ld %ld %ld", usage[0], usage[1], usage[2]);
+		goto cleanup;
+	}
+
+	for (i = 0; i < 2; i++) {
+		delta = usage[i + 1] - usage[i];
+		if (!values_close(delta, usage[0], 35)) {
+			tst_res(TFAIL,
+				"weight delta %ld not close to base %ld (idx=%d)",
+				delta, usage[0], i);
+			goto cleanup;
+		}
+	}
+
+	tst_res(TPASS, "weight overprovisioned: usage %ld %ld %ld",
+		usage[0], usage[1], usage[2]);
+
+cleanup:
+	for (i = 2; i >= 0; i--) {
+		if (cg_leaf[i])
+			cg_leaf[i] = tst_cg_group_rm(cg_leaf[i]);
+	}
+	cg_parent = tst_cg_group_rm(cg_parent);
+}
+
+static void test_cpucg_weight_underprovisioned(void)
+{
+	int i;
+	pid_t pids[3];
+	long usage[3];
+
+	if (tst_ncpus_available() < 4) {
+		tst_res(TCONF, "Need at least 4 CPUs for underprovisioned test");
+		return;
+	}
+
+	cg_parent = tst_cg_group_mk(tst_cg, "cpucg_test_1");
+
+	for (i = 0; i < 3; i++) {
+		cg_leaf[i] = tst_cg_group_mk(cg_parent, "cpucg_child_%d", i);
+		SAFE_CG_PRINTF(cg_leaf[i], "cpu.weight", "%d", 50 * (i + 1));
+	}
+
+	for (i = 0; i < 3; i++)
+		pids[i] = fork_hog_child(cg_leaf[i], 1, CPU_HOG_CLOCK_WALL);
+
+	wait_children(pids, 3);
+
+	for (i = 0; i < 3; i++) {
+		SAFE_CG_LINES_SCANF(cg_leaf[i], "cpu.stat",
+				    "usage_usec %ld", &usage[i]);
+	}
+
+	if (!values_close(usage[0], usage[1], 15) ||
+	    !values_close(usage[0], usage[2], 15)) {
+		tst_res(TFAIL,
+			"Underprovisioned usages not equal: %ld %ld %ld", usage[0], usage[1], usage[2]);
+		goto cleanup;
+	}
+
+	tst_res(TPASS, "weight underprovisioned: usage %ld %ld %ld",
+		usage[0], usage[1], usage[2]);
+
+cleanup:
+	for (i = 2; i >= 0; i--) {
+		if (cg_leaf[i])
+			cg_leaf[i] = tst_cg_group_rm(cg_leaf[i]);
+	}
+	cg_parent = tst_cg_group_rm(cg_parent);
+}
+
+static void test_cpucg_nested_weight_overprovisioned(void)
+{
+	int i, nprocs;
+	pid_t pids[3];
+	long usage[3], nested_leaf_usage, child_usage;
+
+	cg_parent = tst_cg_group_mk(tst_cg, "cpucg_nested_test");
+	cg_child = tst_cg_group_mk(cg_parent, "cpucg_child");
+
+	SAFE_CG_PRINT(cg_child, "cpu.weight", "1000");
+
+	cg_leaf[0] = tst_cg_group_mk(cg_parent, "cpucg_leaf_0");
+	SAFE_CG_PRINT(cg_leaf[0], "cpu.weight", "1000");
+
+	for (i = 1; i < 3; i++) {
+		cg_leaf[i] = tst_cg_group_mk(cg_child, "cpucg_leaf_%d", i);
+		SAFE_CG_PRINT(cg_leaf[i], "cpu.weight", "5000");
+	}
+
+	nprocs = tst_ncpus_available();
+	for (i = 0; i < 3; i++)
+		pids[i] = fork_hog_child(cg_leaf[i], nprocs,
+					 CPU_HOG_CLOCK_WALL);
+
+	wait_children(pids, 3);
+
+	for (i = 0; i < 3; i++) {
+		SAFE_CG_LINES_SCANF(cg_leaf[i], "cpu.stat",
+				    "usage_usec %ld", &usage[i]);
+	}
+
+	nested_leaf_usage = usage[1] + usage[2];
+	if (!values_close(usage[0], nested_leaf_usage, 15)) {
+		tst_res(TFAIL,
+			"Nested weight over: leaf0=%ld not close to leaf1+leaf2=%ld", usage[0], nested_leaf_usage);
+		goto cleanup;
+	}
+
+	SAFE_CG_LINES_SCANF(cg_child, "cpu.stat",
+			    "usage_usec %ld", &child_usage);
+	if (!values_close(child_usage, nested_leaf_usage, 1)) {
+		tst_res(TFAIL,
+			"Child usage=%ld not close to nested leaf usage=%ld",
+			child_usage, nested_leaf_usage);
+		goto cleanup;
+	}
+
+	tst_res(TPASS,
+		"nested weight overprovisioned: leaf0=%ld leaf1+leaf2=%ld",
+		usage[0], nested_leaf_usage);
+
+cleanup:
+	for (i = 2; i >= 0; i--) {
+		if (cg_leaf[i])
+			cg_leaf[i] = tst_cg_group_rm(cg_leaf[i]);
+	}
+	if (cg_child)
+		cg_child = tst_cg_group_rm(cg_child);
+	if (cg_parent)
+		cg_parent = tst_cg_group_rm(cg_parent);
+}
+
+static void test_cpucg_nested_weight_underprovisioned(void)
+{
+	int i, nprocs;
+	pid_t pids[3];
+	long usage[3], nested_leaf_usage, child_usage;
+
+	if (tst_ncpus_available() < 4) {
+		tst_res(TCONF,
+			"Need at least 4 CPUs for nested underprovisioned test");
+		return;
+	}
+
+	cg_parent = tst_cg_group_mk(tst_cg, "cpucg_nested_test_2");
+	cg_child = tst_cg_group_mk(cg_parent, "cpucg_child");
+
+	SAFE_CG_PRINT(cg_child, "cpu.weight", "1000");
+
+	cg_leaf[0] = tst_cg_group_mk(cg_parent, "cpucg_leaf_0");
+	SAFE_CG_PRINT(cg_leaf[0], "cpu.weight", "1000");
+
+	for (i = 1; i < 3; i++) {
+		cg_leaf[i] = tst_cg_group_mk(cg_child, "cpucg_leaf_%d", i);
+		SAFE_CG_PRINT(cg_leaf[i], "cpu.weight", "5000");
+	}
+
+	nprocs = tst_ncpus_available() / 4;
+	if (nprocs < 1)
+		nprocs = 1;
+
+	for (i = 0; i < 3; i++)
+		pids[i] = fork_hog_child(cg_leaf[i], nprocs,
+					 CPU_HOG_CLOCK_WALL);
+
+	wait_children(pids, 3);
+
+	for (i = 0; i < 3; i++) {
+		SAFE_CG_LINES_SCANF(cg_leaf[i], "cpu.stat",
+				    "usage_usec %ld", &usage[i]);
+	}
+
+	nested_leaf_usage = usage[1] + usage[2];
+	if (!values_close(usage[0] * 2, nested_leaf_usage, 15)) {
+		tst_res(TFAIL,
+			"Nested weight under: leaf0=%ld leaf1+leaf2=%ld, leaf0*2=%ld not close to leaf1+leaf2",
+			usage[0], nested_leaf_usage, usage[0] * 2);
+		goto cleanup;
+	}
+
+	SAFE_CG_LINES_SCANF(cg_child, "cpu.stat",
+			    "usage_usec %ld", &child_usage);
+	if (!values_close(child_usage, nested_leaf_usage, 1)) {
+		tst_res(TFAIL,
+			"Child usage=%ld not close to nested leaf usage=%ld",
+			child_usage, nested_leaf_usage);
+		goto cleanup;
+	}
+
+	tst_res(TPASS,
+		"nested weight underprovisioned: leaf0=%ld leaf1+leaf2=%ld",
+		usage[0], nested_leaf_usage);
+
+cleanup:
+	for (i = 2; i >= 0; i--) {
+		if (cg_leaf[i])
+			cg_leaf[i] = tst_cg_group_rm(cg_leaf[i]);
+	}
+	if (cg_child)
+		cg_child = tst_cg_group_rm(cg_child);
+	if (cg_parent)
+		cg_parent = tst_cg_group_rm(cg_parent);
+}
+
+static void (*const test_list[])(void) = {
+	test_cpucg_weight_overprovisioned,
+	test_cpucg_weight_underprovisioned,
+	test_cpucg_nested_weight_overprovisioned,
+	test_cpucg_nested_weight_underprovisioned,
+};
+
+static void run(unsigned int n)
+{
+	test_list[n]();
+}
+
+static void cleanup(void)
+{
+	int i;
+
+	for (i = 2; i >= 0; i--) {
+		if (cg_leaf[i])
+			cg_leaf[i] = tst_cg_group_rm(cg_leaf[i]);
+	}
+	if (cg_child)
+		cg_child = tst_cg_group_rm(cg_child);
+	if (cg_parent)
+		cg_parent = tst_cg_group_rm(cg_parent);
+}
+
+static struct tst_test test = {
+	.cleanup = cleanup,
+	.test = run,
+	.tcnt = ARRAY_SIZE(test_list),
+	.forks_child = 1,
+	.timeout = 60,
+	.needs_root = 1,
+	.needs_cgroup_ver = TST_CG_V2,
+	.needs_cgroup_ctrls = (const char *const []){ "cpu", NULL },
+};
diff --git a/testcases/kernel/controllers/cpu/cgroup_cpu05.c b/testcases/kernel/controllers/cpu/cgroup_cpu05.c
new file mode 100644
index 000000000..4a31b35b9
--- /dev/null
+++ b/testcases/kernel/controllers/cpu/cgroup_cpu05.c
@@ -0,0 +1,176 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Richard Palethorpe <[email protected]>
+ * Copyright (c) 2026 Shaojie Sun <[email protected]>
+ */
+
+/*\
+ * Conversion of kselftest cpu.max tests from
+ * :kselftest:`cgroup/test_cpu.c`.
+ *
+ * Includes:
+ *
+ * - test_cpucg_max: creates a cgroup with cpu.max quota and verifies
+ *   a process is properly throttled.
+ * - test_cpucg_max_nested: verifies a process inside a nested cgroup
+ *   whose parent has cpu.max set is properly throttled.
+ */
+
+#define _GNU_SOURCE
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "tst_test.h"
+#include "cgroup_cpu_common.h"
+
+#define USEC_PER_SEC 1000000L
+
+static struct tst_cg_group *cg_parent, *cg_child;
+
+static void test_cpucg_max(void)
+{
+	long quota_usec = 1000;
+	long period_usec = 100000;
+	long duration_seconds = 1;
+	long usage_usec;
+	long n_periods, remainder_usec, expected_usage_usec;
+	long duration_usec = duration_seconds * USEC_PER_SEC;
+
+	cg_parent = tst_cg_group_mk(tst_cg, "cpucg_max_test");
+
+	SAFE_CG_PRINTF(cg_parent, "cpu.max", "%ld %ld", quota_usec, period_usec);
+
+	if (!SAFE_FORK()) {
+		struct cpu_hog_func_param param = {
+			.nprocs = 1,
+			.ts = {
+				.tv_sec = duration_seconds,
+				.tv_nsec = 0,
+			},
+			.clock_type = CPU_HOG_CLOCK_WALL,
+		};
+
+		SAFE_CG_PRINTF(cg_parent, "cgroup.procs", "%d", getpid());
+		hog_cpus_timed(&param);
+		exit(0);
+	}
+
+	SAFE_WAITPID(-1, NULL, 0);
+
+	SAFE_CG_LINES_SCANF(cg_parent, "cpu.stat",
+			    "usage_usec %ld", &usage_usec);
+
+	/*
+	 * The following calculation applies only since
+	 * the cpu hog is set to run as per wall-clock time
+	 */
+	n_periods = duration_usec / period_usec;
+	remainder_usec = duration_usec - n_periods * period_usec;
+	expected_usage_usec = n_periods * quota_usec +
+			      MIN(remainder_usec, quota_usec);
+
+	if (!values_close(usage_usec, expected_usage_usec, 10)) {
+		tst_res(TFAIL,
+			"cpu.max: usage=%ld expected=%ld",
+			usage_usec, expected_usage_usec);
+		goto cleanup;
+	}
+
+	tst_res(TPASS, "cpu.max: usage=%ld close to expected=%ld",
+		usage_usec, expected_usage_usec);
+
+cleanup:
+	cg_parent = tst_cg_group_rm(cg_parent);
+}
+
+static void test_cpucg_max_nested(void)
+{
+	long quota_usec = 1000;
+	long period_usec = 100000;
+	long duration_seconds = 1;
+	long usage_usec;
+	long n_periods, remainder_usec, expected_usage_usec;
+	long duration_usec = duration_seconds * USEC_PER_SEC;
+
+	cg_parent = tst_cg_group_mk(tst_cg, "cpucg_max_parent");
+	cg_child = tst_cg_group_mk(cg_parent, "cpucg_max_child");
+
+	SAFE_CG_PRINTF(cg_parent, "cpu.max", "%ld %ld", quota_usec, period_usec);
+
+	if (!SAFE_FORK()) {
+		struct cpu_hog_func_param param = {
+			.nprocs = 1,
+			.ts = {
+				.tv_sec = duration_seconds,
+				.tv_nsec = 0,
+			},
+			.clock_type = CPU_HOG_CLOCK_WALL,
+		};
+
+		SAFE_CG_PRINTF(cg_child, "cgroup.procs", "%d", getpid());
+		hog_cpus_timed(&param);
+		exit(0);
+	}
+
+	SAFE_WAITPID(-1, NULL, 0);
+
+	SAFE_CG_LINES_SCANF(cg_child, "cpu.stat",
+			    "usage_usec %ld", &usage_usec);
+
+	/*
+	 * The following calculation applies only since
+	 * the cpu hog is set to run as per wall-clock time
+	 */
+	n_periods = duration_usec / period_usec;
+	remainder_usec = duration_usec - n_periods * period_usec;
+	expected_usage_usec = n_periods * quota_usec +
+			      MIN(remainder_usec, quota_usec);
+
+	if (!values_close(usage_usec, expected_usage_usec, 10)) {
+		tst_res(TFAIL,
+			"cpu.max nested: usage=%ld expected=%ld",
+			usage_usec, expected_usage_usec);
+		goto cleanup;
+	}
+
+	tst_res(TPASS, "cpu.max nested: usage=%ld close to expected=%ld",
+		usage_usec, expected_usage_usec);
+
+cleanup:
+	if (cg_child)
+		cg_child = tst_cg_group_rm(cg_child);
+	if (cg_parent)
+		cg_parent = tst_cg_group_rm(cg_parent);
+}
+
+static void (*const test_list[])(void) = {
+	test_cpucg_max,
+	test_cpucg_max_nested,
+};
+
+static void run(unsigned int n)
+{
+	test_list[n]();
+}
+
+static void cleanup(void)
+{
+	if (cg_child)
+		cg_child = tst_cg_group_rm(cg_child);
+	if (cg_parent)
+		cg_parent = tst_cg_group_rm(cg_parent);
+}
+
+static struct tst_test test = {
+	.cleanup = cleanup,
+	.test = run,
+	.tcnt = ARRAY_SIZE(test_list),
+	.forks_child = 1,
+	.needs_root = 1,
+	.needs_kconfigs = (const char *[]) {
+		"CONFIG_CFS_BANDWIDTH=y",
+		NULL
+	},
+	.needs_cgroup_ver = TST_CG_V2,
+	.needs_cgroup_ctrls = (const char *const []){ "cpu", NULL },
+};
diff --git a/testcases/kernel/controllers/cpu/cgroup_cpu_common.h b/testcases/kernel/controllers/cpu/cgroup_cpu_common.h
new file mode 100644
index 000000000..dbcfd025f
--- /dev/null
+++ b/testcases/kernel/controllers/cpu/cgroup_cpu_common.h
@@ -0,0 +1,131 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2025 Richard Palethorpe <[email protected]>
+ * Copyright (c) 2026 Shaojie Sun <[email protected]>
+ */
+/*
+ * Common helpers for cgroup cpu controller tests.
+ *
+ * Converted from kselftest
+ * tools/testing/selftests/cgroup/test_cpu.c
+ */
+
+#ifndef CGROUP_CPU_COMMON_H__
+#define CGROUP_CPU_COMMON_H__
+
+#include <pthread.h>
+#include <time.h>
+#include <stdlib.h>
+
+#include "tst_test.h"
+
+#ifndef NSEC_PER_SEC
+#define NSEC_PER_SEC 1000000000L
+#endif
+
+enum hog_clock_type {
+	CPU_HOG_CLOCK_PROCESS,
+	CPU_HOG_CLOCK_WALL,
+};
+
+struct cpu_hog_func_param {
+	int nprocs;
+	struct timespec ts;
+	enum hog_clock_type clock_type;
+};
+
+static inline void *hog_cpu_thread_func(void *arg)
+{
+	(void)arg;
+
+	while (1)
+		;
+
+	return NULL;
+}
+
+static inline struct timespec
+timespec_sub(const struct timespec *lhs, const struct timespec *rhs)
+{
+	struct timespec zero = { .tv_sec = 0, .tv_nsec = 0 };
+	struct timespec ret;
+
+	if (lhs->tv_sec < rhs->tv_sec)
+		return zero;
+
+	ret.tv_sec = lhs->tv_sec - rhs->tv_sec;
+
+	if (lhs->tv_nsec < rhs->tv_nsec) {
+		if (ret.tv_sec == 0)
+			return zero;
+
+		ret.tv_sec--;
+		ret.tv_nsec = NSEC_PER_SEC - rhs->tv_nsec + lhs->tv_nsec;
+	} else {
+		ret.tv_nsec = lhs->tv_nsec - rhs->tv_nsec;
+	}
+
+	return ret;
+}
+
+/*
+ * CPU hog function that creates nprocs spinning threads.
+ * To be called in a forked child process already in the target cgroup.
+ * Returns only on success; failures terminate the child via tst_brk().
+ */
+static inline void hog_cpus_timed(void *arg)
+{
+	const struct cpu_hog_func_param *param =
+		(struct cpu_hog_func_param *)arg;
+	struct timespec ts_run = param->ts;
+	struct timespec ts_remaining = ts_run;
+	struct timespec ts_start;
+	struct timespec ts_total;
+	int ret, i;
+	pthread_t tid;
+
+	ret = clock_gettime(CLOCK_MONOTONIC, &ts_start);
+	if (ret != 0)
+		tst_brk(TBROK | TERRNO, "clock_gettime(CLOCK_MONOTONIC) failed");
+
+	for (i = 0; i < param->nprocs; i++) {
+		ret = pthread_create(&tid, NULL, &hog_cpu_thread_func, NULL);
+		if (ret != 0)
+			tst_brk(TBROK | TERRNO, "pthread_create failed");
+	}
+
+	while (ts_remaining.tv_sec > 0 || ts_remaining.tv_nsec > 0) {
+		ret = nanosleep(&ts_remaining, NULL);
+		if (ret && errno != EINTR)
+			tst_brk(TBROK | TERRNO, "nanosleep failed");
+
+		if (param->clock_type == CPU_HOG_CLOCK_PROCESS) {
+			ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID,
+					    &ts_total);
+			if (ret != 0)
+				tst_brk(TBROK | TERRNO,
+					"clock_gettime(CLOCK_PROCESS_CPUTIME_ID) failed");
+		} else {
+			struct timespec ts_current;
+
+			ret = clock_gettime(CLOCK_MONOTONIC, &ts_current);
+			if (ret != 0)
+				tst_brk(TBROK | TERRNO,
+					"clock_gettime(CLOCK_MONOTONIC) failed");
+
+			ts_total = timespec_sub(&ts_current, &ts_start);
+		}
+
+		ts_remaining = timespec_sub(&ts_run, &ts_total);
+	}
+}
+
+/*
+ * Check if two values differ by less than err percent.
+ */
+static inline int values_close(long a, long b, int err)
+{
+	return labs(a - b) <= ((a + b) / 100) * err;
+}
+
+#endif /* CGROUP_CPU_COMMON_H__ */

-- 
Mailing list info: https://lists.linux.it/listinfo/ltp