[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(¶m); + 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(¶m); + 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(¶m); + 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(¶m); + 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(¶m); + 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