| /* SPDX-License-Identifier: GPL-2.0 */ | |
| #include <linux/limits.h> | |
| #include <sys/types.h> | |
| #include <unistd.h> | |
| #include <stdio.h> | |
| #include <errno.h> | |
| #include "../kselftest.h" | |
| #include "cgroup_util.h" | |
| /* | |
| * A(0) - B(0) - C(1) | |
| * \ D(0) | |
| * | |
| * A, B and C's "populated" fields would be 1 while D's 0. | |
| * test that after the one process in C is moved to root, | |
| * A,B and C's "populated" fields would flip to "0" and file | |
| * modified events will be generated on the | |
| * "cgroup.events" files of both cgroups. | |
| */ | |
| static int test_cgcore_populated(const char *root) | |
| { | |
| int ret = KSFT_FAIL; | |
| char *cg_test_a = NULL, *cg_test_b = NULL; | |
| char *cg_test_c = NULL, *cg_test_d = NULL; | |
| cg_test_a = cg_name(root, "cg_test_a"); | |
| cg_test_b = cg_name(root, "cg_test_a/cg_test_b"); | |
| cg_test_c = cg_name(root, "cg_test_a/cg_test_b/cg_test_c"); | |
| cg_test_d = cg_name(root, "cg_test_a/cg_test_b/cg_test_d"); | |
| if (!cg_test_a || !cg_test_b || !cg_test_c || !cg_test_d) | |
| goto cleanup; | |
| if (cg_create(cg_test_a)) | |
| goto cleanup; | |
| if (cg_create(cg_test_b)) | |
| goto cleanup; | |
| if (cg_create(cg_test_c)) | |
| goto cleanup; | |
| if (cg_create(cg_test_d)) | |
| goto cleanup; | |
| if (cg_enter_current(cg_test_c)) | |
| goto cleanup; | |
| if (cg_read_strcmp(cg_test_a, "cgroup.events", "populated 1\n")) | |
| goto cleanup; | |
| if (cg_read_strcmp(cg_test_b, "cgroup.events", "populated 1\n")) | |
| goto cleanup; | |
| if (cg_read_strcmp(cg_test_c, "cgroup.events", "populated 1\n")) | |
| goto cleanup; | |
| if (cg_read_strcmp(cg_test_d, "cgroup.events", "populated 0\n")) | |
| goto cleanup; | |
| if (cg_enter_current(root)) | |
| goto cleanup; | |
| if (cg_read_strcmp(cg_test_a, "cgroup.events", "populated 0\n")) | |
| goto cleanup; | |
| if (cg_read_strcmp(cg_test_b, "cgroup.events", "populated 0\n")) | |
| goto cleanup; | |
| if (cg_read_strcmp(cg_test_c, "cgroup.events", "populated 0\n")) | |
| goto cleanup; | |
| if (cg_read_strcmp(cg_test_d, "cgroup.events", "populated 0\n")) | |
| goto cleanup; | |
| ret = KSFT_PASS; | |
| cleanup: | |
| if (cg_test_d) | |
| cg_destroy(cg_test_d); | |
| if (cg_test_c) | |
| cg_destroy(cg_test_c); | |
| if (cg_test_b) | |
| cg_destroy(cg_test_b); | |
| if (cg_test_a) | |
| cg_destroy(cg_test_a); | |
| free(cg_test_d); | |
| free(cg_test_c); | |
| free(cg_test_b); | |
| free(cg_test_a); | |
| return ret; | |
| } | |
| /* | |
| * A (domain threaded) - B (threaded) - C (domain) | |
| * | |
| * test that C can't be used until it is turned into a | |
| * threaded cgroup. "cgroup.type" file will report "domain (invalid)" in | |
| * these cases. Operations which fail due to invalid topology use | |
| * EOPNOTSUPP as the errno. | |
| */ | |
| static int test_cgcore_invalid_domain(const char *root) | |
| { | |
| int ret = KSFT_FAIL; | |
| char *grandparent = NULL, *parent = NULL, *child = NULL; | |
| grandparent = cg_name(root, "cg_test_grandparent"); | |
| parent = cg_name(root, "cg_test_grandparent/cg_test_parent"); | |
| child = cg_name(root, "cg_test_grandparent/cg_test_parent/cg_test_child"); | |
| if (!parent || !child || !grandparent) | |
| goto cleanup; | |
| if (cg_create(grandparent)) | |
| goto cleanup; | |
| if (cg_create(parent)) | |
| goto cleanup; | |
| if (cg_create(child)) | |
| goto cleanup; | |
| if (cg_write(parent, "cgroup.type", "threaded")) | |
| goto cleanup; | |
| if (cg_read_strcmp(child, "cgroup.type", "domain invalid\n")) | |
| goto cleanup; | |
| if (!cg_enter_current(child)) | |
| goto cleanup; | |
| if (errno != EOPNOTSUPP) | |
| goto cleanup; | |
| ret = KSFT_PASS; | |
| cleanup: | |
| cg_enter_current(root); | |
| if (child) | |
| cg_destroy(child); | |
| if (parent) | |
| cg_destroy(parent); | |
| if (grandparent) | |
| cg_destroy(grandparent); | |
| free(child); | |
| free(parent); | |
| free(grandparent); | |
| return ret; | |
| } | |
| /* | |
| * Test that when a child becomes threaded | |
| * the parent type becomes domain threaded. | |
| */ | |
| static int test_cgcore_parent_becomes_threaded(const char *root) | |
| { | |
| int ret = KSFT_FAIL; | |
| char *parent = NULL, *child = NULL; | |
| parent = cg_name(root, "cg_test_parent"); | |
| child = cg_name(root, "cg_test_parent/cg_test_child"); | |
| if (!parent || !child) | |
| goto cleanup; | |
| if (cg_create(parent)) | |
| goto cleanup; | |
| if (cg_create(child)) | |
| goto cleanup; | |
| if (cg_write(child, "cgroup.type", "threaded")) | |
| goto cleanup; | |
| if (cg_read_strcmp(parent, "cgroup.type", "domain threaded\n")) | |
| goto cleanup; | |
| ret = KSFT_PASS; | |
| cleanup: | |
| if (child) | |
| cg_destroy(child); | |
| if (parent) | |
| cg_destroy(parent); | |
| free(child); | |
| free(parent); | |
| return ret; | |
| } | |
| /* | |
| * Test that there's no internal process constrain on threaded cgroups. | |
| * You can add threads/processes on a parent with a controller enabled. | |
| */ | |
| static int test_cgcore_no_internal_process_constraint_on_threads(const char *root) | |
| { | |
| int ret = KSFT_FAIL; | |
| char *parent = NULL, *child = NULL; | |
| if (cg_read_strstr(root, "cgroup.controllers", "cpu") || | |
| cg_write(root, "cgroup.subtree_control", "+cpu")) { | |
| ret = KSFT_SKIP; | |
| goto cleanup; | |
| } | |
| parent = cg_name(root, "cg_test_parent"); | |
| child = cg_name(root, "cg_test_parent/cg_test_child"); | |
| if (!parent || !child) | |
| goto cleanup; | |
| if (cg_create(parent)) | |
| goto cleanup; | |
| if (cg_create(child)) | |
| goto cleanup; | |
| if (cg_write(parent, "cgroup.type", "threaded")) | |
| goto cleanup; | |
| if (cg_write(child, "cgroup.type", "threaded")) | |
| goto cleanup; | |
| if (cg_write(parent, "cgroup.subtree_control", "+cpu")) | |
| goto cleanup; | |
| if (cg_enter_current(parent)) | |
| goto cleanup; | |
| ret = KSFT_PASS; | |
| cleanup: | |
| cg_enter_current(root); | |
| cg_enter_current(root); | |
| if (child) | |
| cg_destroy(child); | |
| if (parent) | |
| cg_destroy(parent); | |
| free(child); | |
| free(parent); | |
| return ret; | |
| } | |
| /* | |
| * Test that you can't enable a controller on a child if it's not enabled | |
| * on the parent. | |
| */ | |
| static int test_cgcore_top_down_constraint_enable(const char *root) | |
| { | |
| int ret = KSFT_FAIL; | |
| char *parent = NULL, *child = NULL; | |
| parent = cg_name(root, "cg_test_parent"); | |
| child = cg_name(root, "cg_test_parent/cg_test_child"); | |
| if (!parent || !child) | |
| goto cleanup; | |
| if (cg_create(parent)) | |
| goto cleanup; | |
| if (cg_create(child)) | |
| goto cleanup; | |
| if (!cg_write(child, "cgroup.subtree_control", "+memory")) | |
| goto cleanup; | |
| ret = KSFT_PASS; | |
| cleanup: | |
| if (child) | |
| cg_destroy(child); | |
| if (parent) | |
| cg_destroy(parent); | |
| free(child); | |
| free(parent); | |
| return ret; | |
| } | |
| /* | |
| * Test that you can't disable a controller on a parent | |
| * if it's enabled in a child. | |
| */ | |
| static int test_cgcore_top_down_constraint_disable(const char *root) | |
| { | |
| int ret = KSFT_FAIL; | |
| char *parent = NULL, *child = NULL; | |
| parent = cg_name(root, "cg_test_parent"); | |
| child = cg_name(root, "cg_test_parent/cg_test_child"); | |
| if (!parent || !child) | |
| goto cleanup; | |
| if (cg_create(parent)) | |
| goto cleanup; | |
| if (cg_create(child)) | |
| goto cleanup; | |
| if (cg_write(parent, "cgroup.subtree_control", "+memory")) | |
| goto cleanup; | |
| if (cg_write(child, "cgroup.subtree_control", "+memory")) | |
| goto cleanup; | |
| if (!cg_write(parent, "cgroup.subtree_control", "-memory")) | |
| goto cleanup; | |
| ret = KSFT_PASS; | |
| cleanup: | |
| if (child) | |
| cg_destroy(child); | |
| if (parent) | |
| cg_destroy(parent); | |
| free(child); | |
| free(parent); | |
| return ret; | |
| } | |
| /* | |
| * Test internal process constraint. | |
| * You can't add a pid to a domain parent if a controller is enabled. | |
| */ | |
| static int test_cgcore_internal_process_constraint(const char *root) | |
| { | |
| int ret = KSFT_FAIL; | |
| char *parent = NULL, *child = NULL; | |
| parent = cg_name(root, "cg_test_parent"); | |
| child = cg_name(root, "cg_test_parent/cg_test_child"); | |
| if (!parent || !child) | |
| goto cleanup; | |
| if (cg_create(parent)) | |
| goto cleanup; | |
| if (cg_create(child)) | |
| goto cleanup; | |
| if (cg_write(parent, "cgroup.subtree_control", "+memory")) | |
| goto cleanup; | |
| if (!cg_enter_current(parent)) | |
| goto cleanup; | |
| ret = KSFT_PASS; | |
| cleanup: | |
| if (child) | |
| cg_destroy(child); | |
| if (parent) | |
| cg_destroy(parent); | |
| free(child); | |
| free(parent); | |
| return ret; | |
| } | |
| #define T(x) { x, #x } | |
| struct corecg_test { | |
| int (*fn)(const char *root); | |
| const char *name; | |
| } tests[] = { | |
| T(test_cgcore_internal_process_constraint), | |
| T(test_cgcore_top_down_constraint_enable), | |
| T(test_cgcore_top_down_constraint_disable), | |
| T(test_cgcore_no_internal_process_constraint_on_threads), | |
| T(test_cgcore_parent_becomes_threaded), | |
| T(test_cgcore_invalid_domain), | |
| T(test_cgcore_populated), | |
| }; | |
| #undef T | |
| int main(int argc, char *argv[]) | |
| { | |
| char root[PATH_MAX]; | |
| int i, ret = EXIT_SUCCESS; | |
| if (cg_find_unified_root(root, sizeof(root))) | |
| ksft_exit_skip("cgroup v2 isn't mounted\n"); | |
| if (cg_read_strstr(root, "cgroup.subtree_control", "memory")) | |
| if (cg_write(root, "cgroup.subtree_control", "+memory")) | |
| ksft_exit_skip("Failed to set memory controller\n"); | |
| for (i = 0; i < ARRAY_SIZE(tests); i++) { | |
| switch (tests[i].fn(root)) { | |
| case KSFT_PASS: | |
| ksft_test_result_pass("%s\n", tests[i].name); | |
| break; | |
| case KSFT_SKIP: | |
| ksft_test_result_skip("%s\n", tests[i].name); | |
| break; | |
| default: | |
| ret = EXIT_FAILURE; | |
| ksft_test_result_fail("%s\n", tests[i].name); | |
| break; | |
| } | |
| } | |
| return ret; | |
| } |