cachepc-linux

Fork of AMDESE/linux with modifications for CachePC side-channel attack
git clone https://git.sinitax.com/sinitax/cachepc-linux
Log | Files | Refs | README | LICENSE | sfeed.txt

test_cpu.c (16164B)


      1// SPDX-License-Identifier: GPL-2.0
      2
      3#define _GNU_SOURCE
      4#include <linux/limits.h>
      5#include <sys/sysinfo.h>
      6#include <sys/wait.h>
      7#include <errno.h>
      8#include <pthread.h>
      9#include <stdio.h>
     10#include <time.h>
     11
     12#include "../kselftest.h"
     13#include "cgroup_util.h"
     14
     15enum hog_clock_type {
     16	// Count elapsed time using the CLOCK_PROCESS_CPUTIME_ID clock.
     17	CPU_HOG_CLOCK_PROCESS,
     18	// Count elapsed time using system wallclock time.
     19	CPU_HOG_CLOCK_WALL,
     20};
     21
     22struct cpu_hogger {
     23	char *cgroup;
     24	pid_t pid;
     25	long usage;
     26};
     27
     28struct cpu_hog_func_param {
     29	int nprocs;
     30	struct timespec ts;
     31	enum hog_clock_type clock_type;
     32};
     33
     34/*
     35 * This test creates two nested cgroups with and without enabling
     36 * the cpu controller.
     37 */
     38static int test_cpucg_subtree_control(const char *root)
     39{
     40	char *parent = NULL, *child = NULL, *parent2 = NULL, *child2 = NULL;
     41	int ret = KSFT_FAIL;
     42
     43	// Create two nested cgroups with the cpu controller enabled.
     44	parent = cg_name(root, "cpucg_test_0");
     45	if (!parent)
     46		goto cleanup;
     47
     48	if (cg_create(parent))
     49		goto cleanup;
     50
     51	if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
     52		goto cleanup;
     53
     54	child = cg_name(parent, "cpucg_test_child");
     55	if (!child)
     56		goto cleanup;
     57
     58	if (cg_create(child))
     59		goto cleanup;
     60
     61	if (cg_read_strstr(child, "cgroup.controllers", "cpu"))
     62		goto cleanup;
     63
     64	// Create two nested cgroups without enabling the cpu controller.
     65	parent2 = cg_name(root, "cpucg_test_1");
     66	if (!parent2)
     67		goto cleanup;
     68
     69	if (cg_create(parent2))
     70		goto cleanup;
     71
     72	child2 = cg_name(parent2, "cpucg_test_child");
     73	if (!child2)
     74		goto cleanup;
     75
     76	if (cg_create(child2))
     77		goto cleanup;
     78
     79	if (!cg_read_strstr(child2, "cgroup.controllers", "cpu"))
     80		goto cleanup;
     81
     82	ret = KSFT_PASS;
     83
     84cleanup:
     85	cg_destroy(child);
     86	free(child);
     87	cg_destroy(child2);
     88	free(child2);
     89	cg_destroy(parent);
     90	free(parent);
     91	cg_destroy(parent2);
     92	free(parent2);
     93
     94	return ret;
     95}
     96
     97static void *hog_cpu_thread_func(void *arg)
     98{
     99	while (1)
    100		;
    101
    102	return NULL;
    103}
    104
    105static struct timespec
    106timespec_sub(const struct timespec *lhs, const struct timespec *rhs)
    107{
    108	struct timespec zero = {
    109		.tv_sec = 0,
    110		.tv_nsec = 0,
    111	};
    112	struct timespec ret;
    113
    114	if (lhs->tv_sec < rhs->tv_sec)
    115		return zero;
    116
    117	ret.tv_sec = lhs->tv_sec - rhs->tv_sec;
    118
    119	if (lhs->tv_nsec < rhs->tv_nsec) {
    120		if (ret.tv_sec == 0)
    121			return zero;
    122
    123		ret.tv_sec--;
    124		ret.tv_nsec = NSEC_PER_SEC - rhs->tv_nsec + lhs->tv_nsec;
    125	} else
    126		ret.tv_nsec = lhs->tv_nsec - rhs->tv_nsec;
    127
    128	return ret;
    129}
    130
    131static int hog_cpus_timed(const char *cgroup, void *arg)
    132{
    133	const struct cpu_hog_func_param *param =
    134		(struct cpu_hog_func_param *)arg;
    135	struct timespec ts_run = param->ts;
    136	struct timespec ts_remaining = ts_run;
    137	struct timespec ts_start;
    138	int i, ret;
    139
    140	ret = clock_gettime(CLOCK_MONOTONIC, &ts_start);
    141	if (ret != 0)
    142		return ret;
    143
    144	for (i = 0; i < param->nprocs; i++) {
    145		pthread_t tid;
    146
    147		ret = pthread_create(&tid, NULL, &hog_cpu_thread_func, NULL);
    148		if (ret != 0)
    149			return ret;
    150	}
    151
    152	while (ts_remaining.tv_sec > 0 || ts_remaining.tv_nsec > 0) {
    153		struct timespec ts_total;
    154
    155		ret = nanosleep(&ts_remaining, NULL);
    156		if (ret && errno != EINTR)
    157			return ret;
    158
    159		if (param->clock_type == CPU_HOG_CLOCK_PROCESS) {
    160			ret = clock_gettime(CLOCK_PROCESS_CPUTIME_ID, &ts_total);
    161			if (ret != 0)
    162				return ret;
    163		} else {
    164			struct timespec ts_current;
    165
    166			ret = clock_gettime(CLOCK_MONOTONIC, &ts_current);
    167			if (ret != 0)
    168				return ret;
    169
    170			ts_total = timespec_sub(&ts_current, &ts_start);
    171		}
    172
    173		ts_remaining = timespec_sub(&ts_run, &ts_total);
    174	}
    175
    176	return 0;
    177}
    178
    179/*
    180 * Creates a cpu cgroup, burns a CPU for a few quanta, and verifies that
    181 * cpu.stat shows the expected output.
    182 */
    183static int test_cpucg_stats(const char *root)
    184{
    185	int ret = KSFT_FAIL;
    186	long usage_usec, user_usec, system_usec;
    187	long usage_seconds = 2;
    188	long expected_usage_usec = usage_seconds * USEC_PER_SEC;
    189	char *cpucg;
    190
    191	cpucg = cg_name(root, "cpucg_test");
    192	if (!cpucg)
    193		goto cleanup;
    194
    195	if (cg_create(cpucg))
    196		goto cleanup;
    197
    198	usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
    199	user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
    200	system_usec = cg_read_key_long(cpucg, "cpu.stat", "system_usec");
    201	if (usage_usec != 0 || user_usec != 0 || system_usec != 0)
    202		goto cleanup;
    203
    204	struct cpu_hog_func_param param = {
    205		.nprocs = 1,
    206		.ts = {
    207			.tv_sec = usage_seconds,
    208			.tv_nsec = 0,
    209		},
    210		.clock_type = CPU_HOG_CLOCK_PROCESS,
    211	};
    212	if (cg_run(cpucg, hog_cpus_timed, (void *)&param))
    213		goto cleanup;
    214
    215	usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
    216	user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
    217	if (user_usec <= 0)
    218		goto cleanup;
    219
    220	if (!values_close(usage_usec, expected_usage_usec, 1))
    221		goto cleanup;
    222
    223	ret = KSFT_PASS;
    224
    225cleanup:
    226	cg_destroy(cpucg);
    227	free(cpucg);
    228
    229	return ret;
    230}
    231
    232static int
    233run_cpucg_weight_test(
    234		const char *root,
    235		pid_t (*spawn_child)(const struct cpu_hogger *child),
    236		int (*validate)(const struct cpu_hogger *children, int num_children))
    237{
    238	int ret = KSFT_FAIL, i;
    239	char *parent = NULL;
    240	struct cpu_hogger children[3] = {NULL};
    241
    242	parent = cg_name(root, "cpucg_test_0");
    243	if (!parent)
    244		goto cleanup;
    245
    246	if (cg_create(parent))
    247		goto cleanup;
    248
    249	if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
    250		goto cleanup;
    251
    252	for (i = 0; i < ARRAY_SIZE(children); i++) {
    253		children[i].cgroup = cg_name_indexed(parent, "cpucg_child", i);
    254		if (!children[i].cgroup)
    255			goto cleanup;
    256
    257		if (cg_create(children[i].cgroup))
    258			goto cleanup;
    259
    260		if (cg_write_numeric(children[i].cgroup, "cpu.weight",
    261					50 * (i + 1)))
    262			goto cleanup;
    263	}
    264
    265	for (i = 0; i < ARRAY_SIZE(children); i++) {
    266		pid_t pid = spawn_child(&children[i]);
    267		if (pid <= 0)
    268			goto cleanup;
    269		children[i].pid = pid;
    270	}
    271
    272	for (i = 0; i < ARRAY_SIZE(children); i++) {
    273		int retcode;
    274
    275		waitpid(children[i].pid, &retcode, 0);
    276		if (!WIFEXITED(retcode))
    277			goto cleanup;
    278		if (WEXITSTATUS(retcode))
    279			goto cleanup;
    280	}
    281
    282	for (i = 0; i < ARRAY_SIZE(children); i++)
    283		children[i].usage = cg_read_key_long(children[i].cgroup,
    284				"cpu.stat", "usage_usec");
    285
    286	if (validate(children, ARRAY_SIZE(children)))
    287		goto cleanup;
    288
    289	ret = KSFT_PASS;
    290cleanup:
    291	for (i = 0; i < ARRAY_SIZE(children); i++) {
    292		cg_destroy(children[i].cgroup);
    293		free(children[i].cgroup);
    294	}
    295	cg_destroy(parent);
    296	free(parent);
    297
    298	return ret;
    299}
    300
    301static pid_t weight_hog_ncpus(const struct cpu_hogger *child, int ncpus)
    302{
    303	long usage_seconds = 10;
    304	struct cpu_hog_func_param param = {
    305		.nprocs = ncpus,
    306		.ts = {
    307			.tv_sec = usage_seconds,
    308			.tv_nsec = 0,
    309		},
    310		.clock_type = CPU_HOG_CLOCK_WALL,
    311	};
    312	return cg_run_nowait(child->cgroup, hog_cpus_timed, (void *)&param);
    313}
    314
    315static pid_t weight_hog_all_cpus(const struct cpu_hogger *child)
    316{
    317	return weight_hog_ncpus(child, get_nprocs());
    318}
    319
    320static int
    321overprovision_validate(const struct cpu_hogger *children, int num_children)
    322{
    323	int ret = KSFT_FAIL, i;
    324
    325	for (i = 0; i < num_children - 1; i++) {
    326		long delta;
    327
    328		if (children[i + 1].usage <= children[i].usage)
    329			goto cleanup;
    330
    331		delta = children[i + 1].usage - children[i].usage;
    332		if (!values_close(delta, children[0].usage, 35))
    333			goto cleanup;
    334	}
    335
    336	ret = KSFT_PASS;
    337cleanup:
    338	return ret;
    339}
    340
    341/*
    342 * First, this test creates the following hierarchy:
    343 * A
    344 * A/B     cpu.weight = 50
    345 * A/C     cpu.weight = 100
    346 * A/D     cpu.weight = 150
    347 *
    348 * A separate process is then created for each child cgroup which spawns as
    349 * many threads as there are cores, and hogs each CPU as much as possible
    350 * for some time interval.
    351 *
    352 * Once all of the children have exited, we verify that each child cgroup
    353 * was given proportional runtime as informed by their cpu.weight.
    354 */
    355static int test_cpucg_weight_overprovisioned(const char *root)
    356{
    357	return run_cpucg_weight_test(root, weight_hog_all_cpus,
    358			overprovision_validate);
    359}
    360
    361static pid_t weight_hog_one_cpu(const struct cpu_hogger *child)
    362{
    363	return weight_hog_ncpus(child, 1);
    364}
    365
    366static int
    367underprovision_validate(const struct cpu_hogger *children, int num_children)
    368{
    369	int ret = KSFT_FAIL, i;
    370
    371	for (i = 0; i < num_children - 1; i++) {
    372		if (!values_close(children[i + 1].usage, children[0].usage, 15))
    373			goto cleanup;
    374	}
    375
    376	ret = KSFT_PASS;
    377cleanup:
    378	return ret;
    379}
    380
    381/*
    382 * First, this test creates the following hierarchy:
    383 * A
    384 * A/B     cpu.weight = 50
    385 * A/C     cpu.weight = 100
    386 * A/D     cpu.weight = 150
    387 *
    388 * A separate process is then created for each child cgroup which spawns a
    389 * single thread that hogs a CPU. The testcase is only run on systems that
    390 * have at least one core per-thread in the child processes.
    391 *
    392 * Once all of the children have exited, we verify that each child cgroup
    393 * had roughly the same runtime despite having different cpu.weight.
    394 */
    395static int test_cpucg_weight_underprovisioned(const char *root)
    396{
    397	// Only run the test if there are enough cores to avoid overprovisioning
    398	// the system.
    399	if (get_nprocs() < 4)
    400		return KSFT_SKIP;
    401
    402	return run_cpucg_weight_test(root, weight_hog_one_cpu,
    403			underprovision_validate);
    404}
    405
    406static int
    407run_cpucg_nested_weight_test(const char *root, bool overprovisioned)
    408{
    409	int ret = KSFT_FAIL, i;
    410	char *parent = NULL, *child = NULL;
    411	struct cpu_hogger leaf[3] = {NULL};
    412	long nested_leaf_usage, child_usage;
    413	int nprocs = get_nprocs();
    414
    415	if (!overprovisioned) {
    416		if (nprocs < 4)
    417			/*
    418			 * Only run the test if there are enough cores to avoid overprovisioning
    419			 * the system.
    420			 */
    421			return KSFT_SKIP;
    422		nprocs /= 4;
    423	}
    424
    425	parent = cg_name(root, "cpucg_test");
    426	child = cg_name(parent, "cpucg_child");
    427	if (!parent || !child)
    428		goto cleanup;
    429
    430	if (cg_create(parent))
    431		goto cleanup;
    432	if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
    433		goto cleanup;
    434
    435	if (cg_create(child))
    436		goto cleanup;
    437	if (cg_write(child, "cgroup.subtree_control", "+cpu"))
    438		goto cleanup;
    439	if (cg_write(child, "cpu.weight", "1000"))
    440		goto cleanup;
    441
    442	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
    443		const char *ancestor;
    444		long weight;
    445
    446		if (i == 0) {
    447			ancestor = parent;
    448			weight = 1000;
    449		} else {
    450			ancestor = child;
    451			weight = 5000;
    452		}
    453		leaf[i].cgroup = cg_name_indexed(ancestor, "cpucg_leaf", i);
    454		if (!leaf[i].cgroup)
    455			goto cleanup;
    456
    457		if (cg_create(leaf[i].cgroup))
    458			goto cleanup;
    459
    460		if (cg_write_numeric(leaf[i].cgroup, "cpu.weight", weight))
    461			goto cleanup;
    462	}
    463
    464	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
    465		pid_t pid;
    466		struct cpu_hog_func_param param = {
    467			.nprocs = nprocs,
    468			.ts = {
    469				.tv_sec = 10,
    470				.tv_nsec = 0,
    471			},
    472			.clock_type = CPU_HOG_CLOCK_WALL,
    473		};
    474
    475		pid = cg_run_nowait(leaf[i].cgroup, hog_cpus_timed,
    476				(void *)&param);
    477		if (pid <= 0)
    478			goto cleanup;
    479		leaf[i].pid = pid;
    480	}
    481
    482	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
    483		int retcode;
    484
    485		waitpid(leaf[i].pid, &retcode, 0);
    486		if (!WIFEXITED(retcode))
    487			goto cleanup;
    488		if (WEXITSTATUS(retcode))
    489			goto cleanup;
    490	}
    491
    492	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
    493		leaf[i].usage = cg_read_key_long(leaf[i].cgroup,
    494				"cpu.stat", "usage_usec");
    495		if (leaf[i].usage <= 0)
    496			goto cleanup;
    497	}
    498
    499	nested_leaf_usage = leaf[1].usage + leaf[2].usage;
    500	if (overprovisioned) {
    501		if (!values_close(leaf[0].usage, nested_leaf_usage, 15))
    502			goto cleanup;
    503	} else if (!values_close(leaf[0].usage * 2, nested_leaf_usage, 15))
    504		goto cleanup;
    505
    506
    507	child_usage = cg_read_key_long(child, "cpu.stat", "usage_usec");
    508	if (child_usage <= 0)
    509		goto cleanup;
    510	if (!values_close(child_usage, nested_leaf_usage, 1))
    511		goto cleanup;
    512
    513	ret = KSFT_PASS;
    514cleanup:
    515	for (i = 0; i < ARRAY_SIZE(leaf); i++) {
    516		cg_destroy(leaf[i].cgroup);
    517		free(leaf[i].cgroup);
    518	}
    519	cg_destroy(child);
    520	free(child);
    521	cg_destroy(parent);
    522	free(parent);
    523
    524	return ret;
    525}
    526
    527/*
    528 * First, this test creates the following hierarchy:
    529 * A
    530 * A/B     cpu.weight = 1000
    531 * A/C     cpu.weight = 1000
    532 * A/C/D   cpu.weight = 5000
    533 * A/C/E   cpu.weight = 5000
    534 *
    535 * A separate process is then created for each leaf, which spawn nproc threads
    536 * that burn a CPU for a few seconds.
    537 *
    538 * Once all of those processes have exited, we verify that each of the leaf
    539 * cgroups have roughly the same usage from cpu.stat.
    540 */
    541static int
    542test_cpucg_nested_weight_overprovisioned(const char *root)
    543{
    544	return run_cpucg_nested_weight_test(root, true);
    545}
    546
    547/*
    548 * First, this test creates the following hierarchy:
    549 * A
    550 * A/B     cpu.weight = 1000
    551 * A/C     cpu.weight = 1000
    552 * A/C/D   cpu.weight = 5000
    553 * A/C/E   cpu.weight = 5000
    554 *
    555 * A separate process is then created for each leaf, which nproc / 4 threads
    556 * that burns a CPU for a few seconds.
    557 *
    558 * Once all of those processes have exited, we verify that each of the leaf
    559 * cgroups have roughly the same usage from cpu.stat.
    560 */
    561static int
    562test_cpucg_nested_weight_underprovisioned(const char *root)
    563{
    564	return run_cpucg_nested_weight_test(root, false);
    565}
    566
    567/*
    568 * This test creates a cgroup with some maximum value within a period, and
    569 * verifies that a process in the cgroup is not overscheduled.
    570 */
    571static int test_cpucg_max(const char *root)
    572{
    573	int ret = KSFT_FAIL;
    574	long usage_usec, user_usec;
    575	long usage_seconds = 1;
    576	long expected_usage_usec = usage_seconds * USEC_PER_SEC;
    577	char *cpucg;
    578
    579	cpucg = cg_name(root, "cpucg_test");
    580	if (!cpucg)
    581		goto cleanup;
    582
    583	if (cg_create(cpucg))
    584		goto cleanup;
    585
    586	if (cg_write(cpucg, "cpu.max", "1000"))
    587		goto cleanup;
    588
    589	struct cpu_hog_func_param param = {
    590		.nprocs = 1,
    591		.ts = {
    592			.tv_sec = usage_seconds,
    593			.tv_nsec = 0,
    594		},
    595		.clock_type = CPU_HOG_CLOCK_WALL,
    596	};
    597	if (cg_run(cpucg, hog_cpus_timed, (void *)&param))
    598		goto cleanup;
    599
    600	usage_usec = cg_read_key_long(cpucg, "cpu.stat", "usage_usec");
    601	user_usec = cg_read_key_long(cpucg, "cpu.stat", "user_usec");
    602	if (user_usec <= 0)
    603		goto cleanup;
    604
    605	if (user_usec >= expected_usage_usec)
    606		goto cleanup;
    607
    608	if (values_close(usage_usec, expected_usage_usec, 95))
    609		goto cleanup;
    610
    611	ret = KSFT_PASS;
    612
    613cleanup:
    614	cg_destroy(cpucg);
    615	free(cpucg);
    616
    617	return ret;
    618}
    619
    620/*
    621 * This test verifies that a process inside of a nested cgroup whose parent
    622 * group has a cpu.max value set, is properly throttled.
    623 */
    624static int test_cpucg_max_nested(const char *root)
    625{
    626	int ret = KSFT_FAIL;
    627	long usage_usec, user_usec;
    628	long usage_seconds = 1;
    629	long expected_usage_usec = usage_seconds * USEC_PER_SEC;
    630	char *parent, *child;
    631
    632	parent = cg_name(root, "cpucg_parent");
    633	child = cg_name(parent, "cpucg_child");
    634	if (!parent || !child)
    635		goto cleanup;
    636
    637	if (cg_create(parent))
    638		goto cleanup;
    639
    640	if (cg_write(parent, "cgroup.subtree_control", "+cpu"))
    641		goto cleanup;
    642
    643	if (cg_create(child))
    644		goto cleanup;
    645
    646	if (cg_write(parent, "cpu.max", "1000"))
    647		goto cleanup;
    648
    649	struct cpu_hog_func_param param = {
    650		.nprocs = 1,
    651		.ts = {
    652			.tv_sec = usage_seconds,
    653			.tv_nsec = 0,
    654		},
    655		.clock_type = CPU_HOG_CLOCK_WALL,
    656	};
    657	if (cg_run(child, hog_cpus_timed, (void *)&param))
    658		goto cleanup;
    659
    660	usage_usec = cg_read_key_long(child, "cpu.stat", "usage_usec");
    661	user_usec = cg_read_key_long(child, "cpu.stat", "user_usec");
    662	if (user_usec <= 0)
    663		goto cleanup;
    664
    665	if (user_usec >= expected_usage_usec)
    666		goto cleanup;
    667
    668	if (values_close(usage_usec, expected_usage_usec, 95))
    669		goto cleanup;
    670
    671	ret = KSFT_PASS;
    672
    673cleanup:
    674	cg_destroy(child);
    675	free(child);
    676	cg_destroy(parent);
    677	free(parent);
    678
    679	return ret;
    680}
    681
    682#define T(x) { x, #x }
    683struct cpucg_test {
    684	int (*fn)(const char *root);
    685	const char *name;
    686} tests[] = {
    687	T(test_cpucg_subtree_control),
    688	T(test_cpucg_stats),
    689	T(test_cpucg_weight_overprovisioned),
    690	T(test_cpucg_weight_underprovisioned),
    691	T(test_cpucg_nested_weight_overprovisioned),
    692	T(test_cpucg_nested_weight_underprovisioned),
    693	T(test_cpucg_max),
    694	T(test_cpucg_max_nested),
    695};
    696#undef T
    697
    698int main(int argc, char *argv[])
    699{
    700	char root[PATH_MAX];
    701	int i, ret = EXIT_SUCCESS;
    702
    703	if (cg_find_unified_root(root, sizeof(root)))
    704		ksft_exit_skip("cgroup v2 isn't mounted\n");
    705
    706	if (cg_read_strstr(root, "cgroup.subtree_control", "cpu"))
    707		if (cg_write(root, "cgroup.subtree_control", "+cpu"))
    708			ksft_exit_skip("Failed to set cpu controller\n");
    709
    710	for (i = 0; i < ARRAY_SIZE(tests); i++) {
    711		switch (tests[i].fn(root)) {
    712		case KSFT_PASS:
    713			ksft_test_result_pass("%s\n", tests[i].name);
    714			break;
    715		case KSFT_SKIP:
    716			ksft_test_result_skip("%s\n", tests[i].name);
    717			break;
    718		default:
    719			ret = EXIT_FAILURE;
    720			ksft_test_result_fail("%s\n", tests[i].name);
    721			break;
    722		}
    723	}
    724
    725	return ret;
    726}