forked from Qortal/Brooklyn
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
723 lines
15 KiB
723 lines
15 KiB
/* SPDX-License-Identifier: GPL-2.0 */ |
|
|
|
#include <linux/limits.h> |
|
#include <sys/types.h> |
|
#include <sys/mman.h> |
|
#include <sys/wait.h> |
|
#include <unistd.h> |
|
#include <fcntl.h> |
|
#include <stdio.h> |
|
#include <errno.h> |
|
#include <signal.h> |
|
#include <string.h> |
|
#include <pthread.h> |
|
|
|
#include "../kselftest.h" |
|
#include "cgroup_util.h" |
|
|
|
static int touch_anon(char *buf, size_t size) |
|
{ |
|
int fd; |
|
char *pos = buf; |
|
|
|
fd = open("/dev/urandom", O_RDONLY); |
|
if (fd < 0) |
|
return -1; |
|
|
|
while (size > 0) { |
|
ssize_t ret = read(fd, pos, size); |
|
|
|
if (ret < 0) { |
|
if (errno != EINTR) { |
|
close(fd); |
|
return -1; |
|
} |
|
} else { |
|
pos += ret; |
|
size -= ret; |
|
} |
|
} |
|
close(fd); |
|
|
|
return 0; |
|
} |
|
|
|
static int alloc_and_touch_anon_noexit(const char *cgroup, void *arg) |
|
{ |
|
int ppid = getppid(); |
|
size_t size = (size_t)arg; |
|
void *buf; |
|
|
|
buf = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, |
|
0, 0); |
|
if (buf == MAP_FAILED) |
|
return -1; |
|
|
|
if (touch_anon((char *)buf, size)) { |
|
munmap(buf, size); |
|
return -1; |
|
} |
|
|
|
while (getppid() == ppid) |
|
sleep(1); |
|
|
|
munmap(buf, size); |
|
return 0; |
|
} |
|
|
|
/* |
|
* Create a child process that allocates and touches 100MB, then waits to be |
|
* killed. Wait until the child is attached to the cgroup, kill all processes |
|
* in that cgroup and wait until "cgroup.procs" is empty. At this point try to |
|
* destroy the empty cgroup. The test helps detect race conditions between |
|
* dying processes leaving the cgroup and cgroup destruction path. |
|
*/ |
|
static int test_cgcore_destroy(const char *root) |
|
{ |
|
int ret = KSFT_FAIL; |
|
char *cg_test = NULL; |
|
int child_pid; |
|
char buf[PAGE_SIZE]; |
|
|
|
cg_test = cg_name(root, "cg_test"); |
|
|
|
if (!cg_test) |
|
goto cleanup; |
|
|
|
for (int i = 0; i < 10; i++) { |
|
if (cg_create(cg_test)) |
|
goto cleanup; |
|
|
|
child_pid = cg_run_nowait(cg_test, alloc_and_touch_anon_noexit, |
|
(void *) MB(100)); |
|
|
|
if (child_pid < 0) |
|
goto cleanup; |
|
|
|
/* wait for the child to enter cgroup */ |
|
if (cg_wait_for_proc_count(cg_test, 1)) |
|
goto cleanup; |
|
|
|
if (cg_killall(cg_test)) |
|
goto cleanup; |
|
|
|
/* wait for cgroup to be empty */ |
|
while (1) { |
|
if (cg_read(cg_test, "cgroup.procs", buf, sizeof(buf))) |
|
goto cleanup; |
|
if (buf[0] == '\0') |
|
break; |
|
usleep(1000); |
|
} |
|
|
|
if (rmdir(cg_test)) |
|
goto cleanup; |
|
|
|
if (waitpid(child_pid, NULL, 0) < 0) |
|
goto cleanup; |
|
} |
|
ret = KSFT_PASS; |
|
cleanup: |
|
if (cg_test) |
|
cg_destroy(cg_test); |
|
free(cg_test); |
|
return ret; |
|
} |
|
|
|
/* |
|
* 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; |
|
int err; |
|
char *cg_test_a = NULL, *cg_test_b = NULL; |
|
char *cg_test_c = NULL, *cg_test_d = NULL; |
|
int cgroup_fd = -EBADF; |
|
pid_t pid; |
|
|
|
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; |
|
|
|
/* Test that we can directly clone into a new cgroup. */ |
|
cgroup_fd = dirfd_open_opath(cg_test_d); |
|
if (cgroup_fd < 0) |
|
goto cleanup; |
|
|
|
pid = clone_into_cgroup(cgroup_fd); |
|
if (pid < 0) { |
|
if (errno == ENOSYS) |
|
goto cleanup_pass; |
|
goto cleanup; |
|
} |
|
|
|
if (pid == 0) { |
|
if (raise(SIGSTOP)) |
|
exit(EXIT_FAILURE); |
|
exit(EXIT_SUCCESS); |
|
} |
|
|
|
err = cg_read_strcmp(cg_test_d, "cgroup.events", "populated 1\n"); |
|
|
|
(void)clone_reap(pid, WSTOPPED); |
|
(void)kill(pid, SIGCONT); |
|
(void)clone_reap(pid, WEXITED); |
|
|
|
if (err) |
|
goto cleanup; |
|
|
|
if (cg_read_strcmp(cg_test_d, "cgroup.events", "populated 0\n")) |
|
goto cleanup; |
|
|
|
/* Remove cgroup. */ |
|
if (cg_test_d) { |
|
cg_destroy(cg_test_d); |
|
free(cg_test_d); |
|
cg_test_d = NULL; |
|
} |
|
|
|
pid = clone_into_cgroup(cgroup_fd); |
|
if (pid < 0) |
|
goto cleanup_pass; |
|
if (pid == 0) |
|
exit(EXIT_SUCCESS); |
|
(void)clone_reap(pid, WEXITED); |
|
goto cleanup; |
|
|
|
cleanup_pass: |
|
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); |
|
if (cgroup_fd >= 0) |
|
close(cgroup_fd); |
|
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; |
|
|
|
if (!clone_into_cgroup_run_wait(child)) |
|
goto cleanup; |
|
|
|
if (errno == ENOSYS) |
|
goto cleanup_pass; |
|
|
|
if (errno != EOPNOTSUPP) |
|
goto cleanup; |
|
|
|
cleanup_pass: |
|
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; |
|
|
|
if (!clone_into_cgroup_run_wait(parent)) |
|
goto cleanup; |
|
|
|
ret = KSFT_PASS; |
|
|
|
cleanup: |
|
if (child) |
|
cg_destroy(child); |
|
if (parent) |
|
cg_destroy(parent); |
|
free(child); |
|
free(parent); |
|
return ret; |
|
} |
|
|
|
static void *dummy_thread_fn(void *arg) |
|
{ |
|
return (void *)(size_t)pause(); |
|
} |
|
|
|
/* |
|
* Test threadgroup migration. |
|
* All threads of a process are migrated together. |
|
*/ |
|
static int test_cgcore_proc_migration(const char *root) |
|
{ |
|
int ret = KSFT_FAIL; |
|
int t, c_threads = 0, n_threads = 13; |
|
char *src = NULL, *dst = NULL; |
|
pthread_t threads[n_threads]; |
|
|
|
src = cg_name(root, "cg_src"); |
|
dst = cg_name(root, "cg_dst"); |
|
if (!src || !dst) |
|
goto cleanup; |
|
|
|
if (cg_create(src)) |
|
goto cleanup; |
|
if (cg_create(dst)) |
|
goto cleanup; |
|
|
|
if (cg_enter_current(src)) |
|
goto cleanup; |
|
|
|
for (c_threads = 0; c_threads < n_threads; ++c_threads) { |
|
if (pthread_create(&threads[c_threads], NULL, dummy_thread_fn, NULL)) |
|
goto cleanup; |
|
} |
|
|
|
cg_enter_current(dst); |
|
if (cg_read_lc(dst, "cgroup.threads") != n_threads + 1) |
|
goto cleanup; |
|
|
|
ret = KSFT_PASS; |
|
|
|
cleanup: |
|
for (t = 0; t < c_threads; ++t) { |
|
pthread_cancel(threads[t]); |
|
} |
|
|
|
for (t = 0; t < c_threads; ++t) { |
|
pthread_join(threads[t], NULL); |
|
} |
|
|
|
cg_enter_current(root); |
|
|
|
if (dst) |
|
cg_destroy(dst); |
|
if (src) |
|
cg_destroy(src); |
|
free(dst); |
|
free(src); |
|
return ret; |
|
} |
|
|
|
static void *migrating_thread_fn(void *arg) |
|
{ |
|
int g, i, n_iterations = 1000; |
|
char **grps = arg; |
|
char lines[3][PATH_MAX]; |
|
|
|
for (g = 1; g < 3; ++g) |
|
snprintf(lines[g], sizeof(lines[g]), "0::%s", grps[g] + strlen(grps[0])); |
|
|
|
for (i = 0; i < n_iterations; ++i) { |
|
cg_enter_current_thread(grps[(i % 2) + 1]); |
|
|
|
if (proc_read_strstr(0, 1, "cgroup", lines[(i % 2) + 1])) |
|
return (void *)-1; |
|
} |
|
return NULL; |
|
} |
|
|
|
/* |
|
* Test single thread migration. |
|
* Threaded cgroups allow successful migration of a thread. |
|
*/ |
|
static int test_cgcore_thread_migration(const char *root) |
|
{ |
|
int ret = KSFT_FAIL; |
|
char *dom = NULL; |
|
char line[PATH_MAX]; |
|
char *grps[3] = { (char *)root, NULL, NULL }; |
|
pthread_t thr; |
|
void *retval; |
|
|
|
dom = cg_name(root, "cg_dom"); |
|
grps[1] = cg_name(root, "cg_dom/cg_src"); |
|
grps[2] = cg_name(root, "cg_dom/cg_dst"); |
|
if (!grps[1] || !grps[2] || !dom) |
|
goto cleanup; |
|
|
|
if (cg_create(dom)) |
|
goto cleanup; |
|
if (cg_create(grps[1])) |
|
goto cleanup; |
|
if (cg_create(grps[2])) |
|
goto cleanup; |
|
|
|
if (cg_write(grps[1], "cgroup.type", "threaded")) |
|
goto cleanup; |
|
if (cg_write(grps[2], "cgroup.type", "threaded")) |
|
goto cleanup; |
|
|
|
if (cg_enter_current(grps[1])) |
|
goto cleanup; |
|
|
|
if (pthread_create(&thr, NULL, migrating_thread_fn, grps)) |
|
goto cleanup; |
|
|
|
if (pthread_join(thr, &retval)) |
|
goto cleanup; |
|
|
|
if (retval) |
|
goto cleanup; |
|
|
|
snprintf(line, sizeof(line), "0::%s", grps[1] + strlen(grps[0])); |
|
if (proc_read_strstr(0, 1, "cgroup", line)) |
|
goto cleanup; |
|
|
|
ret = KSFT_PASS; |
|
|
|
cleanup: |
|
cg_enter_current(root); |
|
if (grps[2]) |
|
cg_destroy(grps[2]); |
|
if (grps[1]) |
|
cg_destroy(grps[1]); |
|
if (dom) |
|
cg_destroy(dom); |
|
free(grps[2]); |
|
free(grps[1]); |
|
free(dom); |
|
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), |
|
T(test_cgcore_proc_migration), |
|
T(test_cgcore_thread_migration), |
|
T(test_cgcore_destroy), |
|
}; |
|
#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; |
|
}
|
|
|