727 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
			
		
		
	
	
			727 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			C
		
	
	
	
	
	
| // SPDX-License-Identifier: GPL-2.0
 | |
| 
 | |
| #define _GNU_SOURCE
 | |
| #include <linux/limits.h>
 | |
| #include <sys/sysinfo.h>
 | |
| #include <sys/wait.h>
 | |
| #include <errno.h>
 | |
| #include <pthread.h>
 | |
| #include <stdio.h>
 | |
| #include <time.h>
 | |
| 
 | |
| #include "../kselftest.h"
 | |
| #include "cgroup_util.h"
 | |
| 
 | |
| enum hog_clock_type {
 | |
| 	// Count elapsed time using the CLOCK_PROCESS_CPUTIME_ID clock.
 | |
| 	CPU_HOG_CLOCK_PROCESS,
 | |
| 	// Count elapsed time using system wallclock time.
 | |
| 	CPU_HOG_CLOCK_WALL,
 | |
| };
 | |
| 
 | |
| struct cpu_hogger {
 | |
| 	char *cgroup;
 | |
| 	pid_t pid;
 | |
| 	long usage;
 | |
| };
 | |
| 
 | |
| struct cpu_hog_func_param {
 | |
| 	int nprocs;
 | |
| 	struct timespec ts;
 | |
| 	enum hog_clock_type clock_type;
 | |
| };
 | |
| 
 | |
| /*
 | |
|  * This test creates two nested cgroups with and without enabling
 | |
|  * the cpu controller.
 | |
|  */
 | |
| static int test_cpucg_subtree_control(const char *root)
 | |
| {
 | |
| 	char *parent = NULL, *child = NULL, *parent2 = NULL, *child2 = NULL;
 | |
| 	int ret = KSFT_FAIL;
 | |
| 
 | |
| 	// Create two nested cgroups with the cpu controller enabled.
 | |
| 	parent = cg_name(root, "cpucg_test_0");
 | |
| 	if (!parent)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(parent))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	child = cg_name(parent, "cpucg_test_child");
 | |
| 	if (!child)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(child))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_read_strstr(child, "cgroup.controllers", "cpu"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	// Create two nested cgroups without enabling the cpu controller.
 | |
| 	parent2 = cg_name(root, "cpucg_test_1");
 | |
| 	if (!parent2)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(parent2))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	child2 = cg_name(parent2, "cpucg_test_child");
 | |
| 	if (!child2)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(child2))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (!cg_read_strstr(child2, "cgroup.controllers", "cpu"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	ret = KSFT_PASS;
 | |
| 
 | |
| cleanup:
 | |
| 	cg_destroy(child);
 | |
| 	free(child);
 | |
| 	cg_destroy(child2);
 | |
| 	free(child2);
 | |
| 	cg_destroy(parent);
 | |
| 	free(parent);
 | |
| 	cg_destroy(parent2);
 | |
| 	free(parent2);
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| static void *hog_cpu_thread_func(void *arg)
 | |
| {
 | |
| 	while (1)
 | |
| 		;
 | |
| 
 | |
| 	return NULL;
 | |
| }
 | |
| 
 | |
| static 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;
 | |
| }
 | |
| 
 | |
| static int hog_cpus_timed(const char *cgroup, 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;
 | |
| 	int i, ret;
 | |
| 
 | |
| 	ret = clock_gettime(CLOCK_MONOTONIC, &ts_start);
 | |
| 	if (ret != 0)
 | |
| 		return ret;
 | |
| 
 | |
| 	for (i = 0; i < param->nprocs; i++) {
 | |
| 		pthread_t tid;
 | |
| 
 | |
| 		ret = pthread_create(&tid, NULL, &hog_cpu_thread_func, NULL);
 | |
| 		if (ret != 0)
 | |
| 			return ret;
 | |
| 	}
 | |
| 
 | |
| 	while (ts_remaining.tv_sec > 0 || ts_remaining.tv_nsec > 0) {
 | |
| 		struct timespec ts_total;
 | |
| 
 | |
| 		ret = nanosleep(&ts_remaining, NULL);
 | |
| 		if (ret && errno != EINTR)
 | |
| 			return ret;
 | |
| 
 | |
| 		if (param->clock_type == CPU_HOG_CLOCK_PROCESS) {
 | |
| 			ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_total);
 | |
| 			if (ret != 0)
 | |
| 				return ret;
 | |
| 		} else {
 | |
| 			struct timespec ts_current;
 | |
| 
 | |
| 			ret = clock_gettime(CLOCK_MONOTONIC, &ts_current);
 | |
| 			if (ret != 0)
 | |
| 				return ret;
 | |
| 
 | |
| 			ts_total = timespec_sub(&ts_current, &ts_start);
 | |
| 		}
 | |
| 
 | |
| 		ts_remaining = timespec_sub(&ts_run, &ts_total);
 | |
| 	}
 | |
| 
 | |
| 	return 0;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * Creates a cpu cgroup, burns a CPU for a few quanta, and verifies that
 | |
|  * cpu.stat shows the expected output.
 | |
|  */
 | |
| static int test_cpucg_stats(const char *root)
 | |
| {
 | |
| 	int ret = KSFT_FAIL;
 | |
| 	long usage_usec, user_usec, system_usec;
 | |
| 	long usage_seconds = 2;
 | |
| 	long expected_usage_usec = usage_seconds * USEC_PER_SEC;
 | |
| 	char *cpucg;
 | |
| 
 | |
| 	cpucg = cg_name(root, "cpucg_test");
 | |
| 	if (!cpucg)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(cpucg))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
 | |
| 	user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
 | |
| 	system_usec = cg_read_key_long(cpucg, "cpu.stat", "system_usec");
 | |
| 	if (usage_usec != 0 || user_usec != 0 || system_usec != 0)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	struct cpu_hog_func_param param = {
 | |
| 		.nprocs = 1,
 | |
| 		.ts = {
 | |
| 			.tv_sec = usage_seconds,
 | |
| 			.tv_nsec = 0,
 | |
| 		},
 | |
| 		.clock_type = CPU_HOG_CLOCK_PROCESS,
 | |
| 	};
 | |
| 	if (cg_run(cpucg, hog_cpus_timed, (void *)¶m))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
 | |
| 	user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
 | |
| 	if (user_usec <= 0)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (!values_close(usage_usec, expected_usage_usec, 1))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	ret = KSFT_PASS;
 | |
| 
 | |
| cleanup:
 | |
| 	cg_destroy(cpucg);
 | |
| 	free(cpucg);
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| static int
 | |
| run_cpucg_weight_test(
 | |
| 		const char *root,
 | |
| 		pid_t (*spawn_child)(const struct cpu_hogger *child),
 | |
| 		int (*validate)(const struct cpu_hogger *children, int num_children))
 | |
| {
 | |
| 	int ret = KSFT_FAIL, i;
 | |
| 	char *parent = NULL;
 | |
| 	struct cpu_hogger children[3] = {};
 | |
| 
 | |
| 	parent = cg_name(root, "cpucg_test_0");
 | |
| 	if (!parent)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(parent))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	for (i = 0; i < ARRAY_SIZE(children); i++) {
 | |
| 		children[i].cgroup = cg_name_indexed(parent, "cpucg_child", i);
 | |
| 		if (!children[i].cgroup)
 | |
| 			goto cleanup;
 | |
| 
 | |
| 		if (cg_create(children[i].cgroup))
 | |
| 			goto cleanup;
 | |
| 
 | |
| 		if (cg_write_numeric(children[i].cgroup, "cpu.weight",
 | |
| 					50 * (i + 1)))
 | |
| 			goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	for (i = 0; i < ARRAY_SIZE(children); i++) {
 | |
| 		pid_t pid = spawn_child(&children[i]);
 | |
| 		if (pid <= 0)
 | |
| 			goto cleanup;
 | |
| 		children[i].pid = pid;
 | |
| 	}
 | |
| 
 | |
| 	for (i = 0; i < ARRAY_SIZE(children); i++) {
 | |
| 		int retcode;
 | |
| 
 | |
| 		waitpid(children[i].pid, &retcode, 0);
 | |
| 		if (!WIFEXITED(retcode))
 | |
| 			goto cleanup;
 | |
| 		if (WEXITSTATUS(retcode))
 | |
| 			goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	for (i = 0; i < ARRAY_SIZE(children); i++)
 | |
| 		children[i].usage = cg_read_key_long(children[i].cgroup,
 | |
| 				"cpu.stat", "usage_usec");
 | |
| 
 | |
| 	if (validate(children, ARRAY_SIZE(children)))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	ret = KSFT_PASS;
 | |
| cleanup:
 | |
| 	for (i = 0; i < ARRAY_SIZE(children); i++) {
 | |
| 		cg_destroy(children[i].cgroup);
 | |
| 		free(children[i].cgroup);
 | |
| 	}
 | |
| 	cg_destroy(parent);
 | |
| 	free(parent);
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| static pid_t weight_hog_ncpus(const struct cpu_hogger *child, int ncpus)
 | |
| {
 | |
| 	long usage_seconds = 10;
 | |
| 	struct cpu_hog_func_param param = {
 | |
| 		.nprocs = ncpus,
 | |
| 		.ts = {
 | |
| 			.tv_sec = usage_seconds,
 | |
| 			.tv_nsec = 0,
 | |
| 		},
 | |
| 		.clock_type = CPU_HOG_CLOCK_WALL,
 | |
| 	};
 | |
| 	return cg_run_nowait(child->cgroup, hog_cpus_timed, (void *)¶m);
 | |
| }
 | |
| 
 | |
| static pid_t weight_hog_all_cpus(const struct cpu_hogger *child)
 | |
| {
 | |
| 	return weight_hog_ncpus(child, get_nprocs());
 | |
| }
 | |
| 
 | |
| static int
 | |
| overprovision_validate(const struct cpu_hogger *children, int num_children)
 | |
| {
 | |
| 	int ret = KSFT_FAIL, i;
 | |
| 
 | |
| 	for (i = 0; i < num_children - 1; i++) {
 | |
| 		long delta;
 | |
| 
 | |
| 		if (children[i + 1].usage <= children[i].usage)
 | |
| 			goto cleanup;
 | |
| 
 | |
| 		delta = children[i + 1].usage - children[i].usage;
 | |
| 		if (!values_close(delta, children[0].usage, 35))
 | |
| 			goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	ret = KSFT_PASS;
 | |
| cleanup:
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * First, this test creates the following hierarchy:
 | |
|  * A
 | |
|  * A/B     cpu.weight = 50
 | |
|  * A/C     cpu.weight = 100
 | |
|  * A/D     cpu.weight = 150
 | |
|  *
 | |
|  * A separate process is then created for each child cgroup which spawns as
 | |
|  * many threads as there are cores, and hogs each CPU as much as possible
 | |
|  * for some time interval.
 | |
|  *
 | |
|  * Once all of the children have exited, we verify that each child cgroup
 | |
|  * was given proportional runtime as informed by their cpu.weight.
 | |
|  */
 | |
| static int test_cpucg_weight_overprovisioned(const char *root)
 | |
| {
 | |
| 	return run_cpucg_weight_test(root, weight_hog_all_cpus,
 | |
| 			overprovision_validate);
 | |
| }
 | |
| 
 | |
| static pid_t weight_hog_one_cpu(const struct cpu_hogger *child)
 | |
| {
 | |
| 	return weight_hog_ncpus(child, 1);
 | |
| }
 | |
| 
 | |
| static int
 | |
| underprovision_validate(const struct cpu_hogger *children, int num_children)
 | |
| {
 | |
| 	int ret = KSFT_FAIL, i;
 | |
| 
 | |
| 	for (i = 0; i < num_children - 1; i++) {
 | |
| 		if (!values_close(children[i + 1].usage, children[0].usage, 15))
 | |
| 			goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	ret = KSFT_PASS;
 | |
| cleanup:
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * First, this test creates the following hierarchy:
 | |
|  * A
 | |
|  * A/B     cpu.weight = 50
 | |
|  * A/C     cpu.weight = 100
 | |
|  * A/D     cpu.weight = 150
 | |
|  *
 | |
|  * A separate process is then created for each child cgroup which spawns a
 | |
|  * single thread that hogs a CPU. The testcase is only run on systems that
 | |
|  * have at least one core per-thread in the child processes.
 | |
|  *
 | |
|  * Once all of the children have exited, we verify that each child cgroup
 | |
|  * had roughly the same runtime despite having different cpu.weight.
 | |
|  */
 | |
| static int test_cpucg_weight_underprovisioned(const char *root)
 | |
| {
 | |
| 	// Only run the test if there are enough cores to avoid overprovisioning
 | |
| 	// the system.
 | |
| 	if (get_nprocs() < 4)
 | |
| 		return KSFT_SKIP;
 | |
| 
 | |
| 	return run_cpucg_weight_test(root, weight_hog_one_cpu,
 | |
| 			underprovision_validate);
 | |
| }
 | |
| 
 | |
| static int
 | |
| run_cpucg_nested_weight_test(const char *root, bool overprovisioned)
 | |
| {
 | |
| 	int ret = KSFT_FAIL, i;
 | |
| 	char *parent = NULL, *child = NULL;
 | |
| 	struct cpu_hogger leaf[3] = {};
 | |
| 	long nested_leaf_usage, child_usage;
 | |
| 	int nprocs = get_nprocs();
 | |
| 
 | |
| 	if (!overprovisioned) {
 | |
| 		if (nprocs < 4)
 | |
| 			/*
 | |
| 			 * Only run the test if there are enough cores to avoid overprovisioning
 | |
| 			 * the system.
 | |
| 			 */
 | |
| 			return KSFT_SKIP;
 | |
| 		nprocs /= 4;
 | |
| 	}
 | |
| 
 | |
| 	parent = cg_name(root, "cpucg_test");
 | |
| 	child = cg_name(parent, "cpucg_child");
 | |
| 	if (!parent || !child)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(parent))
 | |
| 		goto cleanup;
 | |
| 	if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(child))
 | |
| 		goto cleanup;
 | |
| 	if (cg_write(child, "cgroup.subtree_control", "+cpu"))
 | |
| 		goto cleanup;
 | |
| 	if (cg_write(child, "cpu.weight", "1000"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
 | |
| 		const char *ancestor;
 | |
| 		long weight;
 | |
| 
 | |
| 		if (i == 0) {
 | |
| 			ancestor = parent;
 | |
| 			weight = 1000;
 | |
| 		} else {
 | |
| 			ancestor = child;
 | |
| 			weight = 5000;
 | |
| 		}
 | |
| 		leaf[i].cgroup = cg_name_indexed(ancestor, "cpucg_leaf", i);
 | |
| 		if (!leaf[i].cgroup)
 | |
| 			goto cleanup;
 | |
| 
 | |
| 		if (cg_create(leaf[i].cgroup))
 | |
| 			goto cleanup;
 | |
| 
 | |
| 		if (cg_write_numeric(leaf[i].cgroup, "cpu.weight", weight))
 | |
| 			goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
 | |
| 		pid_t pid;
 | |
| 		struct cpu_hog_func_param param = {
 | |
| 			.nprocs = nprocs,
 | |
| 			.ts = {
 | |
| 				.tv_sec = 10,
 | |
| 				.tv_nsec = 0,
 | |
| 			},
 | |
| 			.clock_type = CPU_HOG_CLOCK_WALL,
 | |
| 		};
 | |
| 
 | |
| 		pid = cg_run_nowait(leaf[i].cgroup, hog_cpus_timed,
 | |
| 				(void *)¶m);
 | |
| 		if (pid <= 0)
 | |
| 			goto cleanup;
 | |
| 		leaf[i].pid = pid;
 | |
| 	}
 | |
| 
 | |
| 	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
 | |
| 		int retcode;
 | |
| 
 | |
| 		waitpid(leaf[i].pid, &retcode, 0);
 | |
| 		if (!WIFEXITED(retcode))
 | |
| 			goto cleanup;
 | |
| 		if (WEXITSTATUS(retcode))
 | |
| 			goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
 | |
| 		leaf[i].usage = cg_read_key_long(leaf[i].cgroup,
 | |
| 				"cpu.stat", "usage_usec");
 | |
| 		if (leaf[i].usage <= 0)
 | |
| 			goto cleanup;
 | |
| 	}
 | |
| 
 | |
| 	nested_leaf_usage = leaf[1].usage + leaf[2].usage;
 | |
| 	if (overprovisioned) {
 | |
| 		if (!values_close(leaf[0].usage, nested_leaf_usage, 15))
 | |
| 			goto cleanup;
 | |
| 	} else if (!values_close(leaf[0].usage * 2, nested_leaf_usage, 15))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 
 | |
| 	child_usage = cg_read_key_long(child, "cpu.stat", "usage_usec");
 | |
| 	if (child_usage <= 0)
 | |
| 		goto cleanup;
 | |
| 	if (!values_close(child_usage, nested_leaf_usage, 1))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	ret = KSFT_PASS;
 | |
| cleanup:
 | |
| 	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
 | |
| 		cg_destroy(leaf[i].cgroup);
 | |
| 		free(leaf[i].cgroup);
 | |
| 	}
 | |
| 	cg_destroy(child);
 | |
| 	free(child);
 | |
| 	cg_destroy(parent);
 | |
| 	free(parent);
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * First, this test creates the following hierarchy:
 | |
|  * A
 | |
|  * A/B     cpu.weight = 1000
 | |
|  * A/C     cpu.weight = 1000
 | |
|  * A/C/D   cpu.weight = 5000
 | |
|  * A/C/E   cpu.weight = 5000
 | |
|  *
 | |
|  * A separate process is then created for each leaf, which spawn nproc threads
 | |
|  * that burn a CPU for a few seconds.
 | |
|  *
 | |
|  * Once all of those processes have exited, we verify that each of the leaf
 | |
|  * cgroups have roughly the same usage from cpu.stat.
 | |
|  */
 | |
| static int
 | |
| test_cpucg_nested_weight_overprovisioned(const char *root)
 | |
| {
 | |
| 	return run_cpucg_nested_weight_test(root, true);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * First, this test creates the following hierarchy:
 | |
|  * A
 | |
|  * A/B     cpu.weight = 1000
 | |
|  * A/C     cpu.weight = 1000
 | |
|  * A/C/D   cpu.weight = 5000
 | |
|  * A/C/E   cpu.weight = 5000
 | |
|  *
 | |
|  * A separate process is then created for each leaf, which nproc / 4 threads
 | |
|  * that burns a CPU for a few seconds.
 | |
|  *
 | |
|  * Once all of those processes have exited, we verify that each of the leaf
 | |
|  * cgroups have roughly the same usage from cpu.stat.
 | |
|  */
 | |
| static int
 | |
| test_cpucg_nested_weight_underprovisioned(const char *root)
 | |
| {
 | |
| 	return run_cpucg_nested_weight_test(root, false);
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * This test creates a cgroup with some maximum value within a period, and
 | |
|  * verifies that a process in the cgroup is not overscheduled.
 | |
|  */
 | |
| static int test_cpucg_max(const char *root)
 | |
| {
 | |
| 	int ret = KSFT_FAIL;
 | |
| 	long usage_usec, user_usec;
 | |
| 	long usage_seconds = 1;
 | |
| 	long expected_usage_usec = usage_seconds * USEC_PER_SEC;
 | |
| 	char *cpucg;
 | |
| 
 | |
| 	cpucg = cg_name(root, "cpucg_test");
 | |
| 	if (!cpucg)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(cpucg))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_write(cpucg, "cpu.max", "1000"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	struct cpu_hog_func_param param = {
 | |
| 		.nprocs = 1,
 | |
| 		.ts = {
 | |
| 			.tv_sec = usage_seconds,
 | |
| 			.tv_nsec = 0,
 | |
| 		},
 | |
| 		.clock_type = CPU_HOG_CLOCK_WALL,
 | |
| 	};
 | |
| 	if (cg_run(cpucg, hog_cpus_timed, (void *)¶m))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
 | |
| 	user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
 | |
| 	if (user_usec <= 0)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (user_usec >= expected_usage_usec)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (values_close(usage_usec, expected_usage_usec, 95))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	ret = KSFT_PASS;
 | |
| 
 | |
| cleanup:
 | |
| 	cg_destroy(cpucg);
 | |
| 	free(cpucg);
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| /*
 | |
|  * This test verifies that a process inside of a nested cgroup whose parent
 | |
|  * group has a cpu.max value set, is properly throttled.
 | |
|  */
 | |
| static int test_cpucg_max_nested(const char *root)
 | |
| {
 | |
| 	int ret = KSFT_FAIL;
 | |
| 	long usage_usec, user_usec;
 | |
| 	long usage_seconds = 1;
 | |
| 	long expected_usage_usec = usage_seconds * USEC_PER_SEC;
 | |
| 	char *parent, *child;
 | |
| 
 | |
| 	parent = cg_name(root, "cpucg_parent");
 | |
| 	child = cg_name(parent, "cpucg_child");
 | |
| 	if (!parent || !child)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(parent))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_create(child))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (cg_write(parent, "cpu.max", "1000"))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	struct cpu_hog_func_param param = {
 | |
| 		.nprocs = 1,
 | |
| 		.ts = {
 | |
| 			.tv_sec = usage_seconds,
 | |
| 			.tv_nsec = 0,
 | |
| 		},
 | |
| 		.clock_type = CPU_HOG_CLOCK_WALL,
 | |
| 	};
 | |
| 	if (cg_run(child, hog_cpus_timed, (void *)¶m))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	usage_usec = cg_read_key_long(child, "cpu.stat", "usage_usec");
 | |
| 	user_usec = cg_read_key_long(child, "cpu.stat", "user_usec");
 | |
| 	if (user_usec <= 0)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (user_usec >= expected_usage_usec)
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	if (values_close(usage_usec, expected_usage_usec, 95))
 | |
| 		goto cleanup;
 | |
| 
 | |
| 	ret = KSFT_PASS;
 | |
| 
 | |
| cleanup:
 | |
| 	cg_destroy(child);
 | |
| 	free(child);
 | |
| 	cg_destroy(parent);
 | |
| 	free(parent);
 | |
| 
 | |
| 	return ret;
 | |
| }
 | |
| 
 | |
| #define T(x) { x, #x }
 | |
| struct cpucg_test {
 | |
| 	int (*fn)(const char *root);
 | |
| 	const char *name;
 | |
| } tests[] = {
 | |
| 	T(test_cpucg_subtree_control),
 | |
| 	T(test_cpucg_stats),
 | |
| 	T(test_cpucg_weight_overprovisioned),
 | |
| 	T(test_cpucg_weight_underprovisioned),
 | |
| 	T(test_cpucg_nested_weight_overprovisioned),
 | |
| 	T(test_cpucg_nested_weight_underprovisioned),
 | |
| 	T(test_cpucg_max),
 | |
| 	T(test_cpucg_max_nested),
 | |
| };
 | |
| #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), NULL))
 | |
| 		ksft_exit_skip("cgroup v2 isn't mounted\n");
 | |
| 
 | |
| 	if (cg_read_strstr(root, "cgroup.subtree_control", "cpu"))
 | |
| 		if (cg_write(root, "cgroup.subtree_control", "+cpu"))
 | |
| 			ksft_exit_skip("Failed to set cpu 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;
 | |
| }
 |