diff mbox series

[RFC,6/6] selftests/cgroup: add a test for misc cgroup

Message ID 20231108002647.73784-7-tycho@tycho.pizza (mailing list archive)
State New
Headers show
Series tracking fd counts per cgroup | expand

Commit Message

Tycho Andersen Nov. 8, 2023, 12:26 a.m. UTC
From: Tycho Andersen <tandersen@netflix.com>

There's four tests here: a basic smoke test, and tests for clone/fork.
Ideally there'd be a test for the cancel_attach() path too.

Signed-off-by: Tycho Andersen <tandersen@netflix.com>
---
 tools/testing/selftests/cgroup/.gitignore  |   1 +
 tools/testing/selftests/cgroup/Makefile    |   2 +
 tools/testing/selftests/cgroup/test_misc.c | 385 +++++++++++++++++++++
 3 files changed, 388 insertions(+)
diff mbox series

Patch

diff --git a/tools/testing/selftests/cgroup/.gitignore b/tools/testing/selftests/cgroup/.gitignore
index 2732e0b29271..7e57580ed363 100644
--- a/tools/testing/selftests/cgroup/.gitignore
+++ b/tools/testing/selftests/cgroup/.gitignore
@@ -9,3 +9,4 @@  test_cpuset
 test_zswap
 test_hugetlb_memcg
 wait_inotify
+test_misc
diff --git a/tools/testing/selftests/cgroup/Makefile b/tools/testing/selftests/cgroup/Makefile
index 00b441928909..2e5b72947134 100644
--- a/tools/testing/selftests/cgroup/Makefile
+++ b/tools/testing/selftests/cgroup/Makefile
@@ -15,6 +15,7 @@  TEST_GEN_PROGS += test_cpu
 TEST_GEN_PROGS += test_cpuset
 TEST_GEN_PROGS += test_zswap
 TEST_GEN_PROGS += test_hugetlb_memcg
+TEST_GEN_PROGS += test_misc
 
 LOCAL_HDRS += $(selfdir)/clone3/clone3_selftests.h $(selfdir)/pidfd/pidfd.h
 
@@ -29,3 +30,4 @@  $(OUTPUT)/test_cpu: cgroup_util.c
 $(OUTPUT)/test_cpuset: cgroup_util.c
 $(OUTPUT)/test_zswap: cgroup_util.c
 $(OUTPUT)/test_hugetlb_memcg: cgroup_util.c
+$(OUTPUT)/test_misc: cgroup_util.c
diff --git a/tools/testing/selftests/cgroup/test_misc.c b/tools/testing/selftests/cgroup/test_misc.c
new file mode 100644
index 000000000000..8f15d899ed4a
--- /dev/null
+++ b/tools/testing/selftests/cgroup/test_misc.c
@@ -0,0 +1,385 @@ 
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <sys/socket.h>
+#include <limits.h>
+#include <string.h>
+#include <signal.h>
+#include <syscall.h>
+#include <sched.h>
+#include <sys/wait.h>
+
+#include "../kselftest.h"
+#include "cgroup_util.h"
+
+#define N 100
+
+static int open_N_fds(const char *cgroup, void *arg)
+{
+	int i;
+	long nofile;
+
+	for (i = 0; i < N; i++) {
+		int fd;
+
+		fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+		if (fd < 0) {
+			ksft_print_msg("%d socket: %s\n", i, strerror(errno));
+			return 1;
+		}
+	}
+
+	/*
+	 * N+3 std fds + 1 fd for "misc.current"
+	 */
+	nofile = cg_read_key_long(cgroup, "misc.current", "nofile ");
+	if (nofile != N+3+1) {
+		ksft_print_msg("bad open files count: %ld\n", nofile);
+		return 1;
+	}
+
+	return 0;
+}
+
+static int test_misc_cg_basic(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *foo;
+
+	foo = cg_name(root, "foo");
+	if (!foo)
+		goto cleanup;
+
+	if (cg_create(foo)) {
+		perror("cg_create");
+		ksft_print_msg("cg_create failed\n");
+		goto cleanup;
+	}
+
+	if (cg_write(root, "cgroup.subtree_control", "+misc")) {
+		ksft_print_msg("cg_write failed\n");
+		goto cleanup;
+	}
+
+	ret = cg_run(foo, open_N_fds, NULL);
+	if (ret < 0) {
+		ksft_print_msg("cg_run failed\n");
+		goto cleanup;
+	}
+
+	if (ret == 0)
+		ret = KSFT_PASS;
+
+cleanup:
+	cg_destroy(foo);
+	free(foo);
+	return ret;
+}
+
+static int open_N_fds_and_sleep(const char *root, void *arg)
+{
+	int i, *sock = arg;
+
+	for (i = 0; i < N; i++) {
+		int fd;
+
+		fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+		if (fd < 0) {
+			ksft_print_msg("%d socket: %s\n", i, strerror(errno));
+			return 1;
+		}
+	}
+
+	if (write(*sock, "c", 1) != 1) {
+		ksft_print_msg("%d write: %s\n", i, strerror(errno));
+		return 1;
+	}
+
+	while (1)
+		sleep(1000);
+}
+
+#define COPIES 5
+static int test_misc_cg_threads(const char *root)
+{
+	int ret = KSFT_FAIL, i;
+	char *foo;
+	int pids[COPIES] = {};
+	long nofile;
+
+	foo = cg_name(root, "foo");
+	if (!foo)
+		goto cleanup;
+
+	if (cg_create(foo)) {
+		ksft_print_msg("cg_create failed\n");
+		goto cleanup;
+	}
+
+	if (cg_write(root, "cgroup.subtree_control", "+misc")) {
+		ksft_print_msg("cg_write failed\n");
+		goto cleanup;
+	}
+
+	for (i = 0; i < COPIES; i++) {
+		char c;
+		int sk_pair[2];
+
+		if (socketpair(PF_LOCAL, SOCK_SEQPACKET, 0, sk_pair) < 0) {
+			ksft_print_msg("socketpair failed %s\n", strerror(errno));
+			goto cleanup;
+		}
+
+		pids[i] = cg_run_nowait(foo, open_N_fds_and_sleep, sk_pair+1);
+		if (pids[i] < 0) {
+			perror("cg_run_nowait");
+			ksft_print_msg("cg_run failed\n");
+			goto cleanup;
+		}
+		close(sk_pair[1]);
+
+		if (read(sk_pair[0], &c, 1) != 1) {
+			ksft_print_msg("%d read: %s\n", i, strerror(errno));
+			goto cleanup;
+		}
+		close(sk_pair[0]);
+	}
+
+	/*
+	 * We expect COPIES * (N + 3 stdfs + 2 socketpair fds).
+	 */
+	nofile = cg_read_key_long(foo, "misc.current", "nofile ");
+	if (nofile != COPIES*(N+3+2)) {
+		ksft_print_msg("bad open files count: %ld != %d\n", nofile, COPIES*(N+3+1));
+		goto cleanup;
+	}
+
+	ret = KSFT_PASS;
+cleanup:
+	for (i = 0; i < COPIES; i++) {
+		if (pids[i] >= 0) {
+			kill(pids[i], SIGKILL);
+			waitpid(pids[i], NULL, 0);
+		}
+	}
+	cg_destroy(foo);
+	free(foo);
+	return ret;
+}
+
+static int test_shared_files_count(const char *root)
+{
+	char *foo, c;
+	int dfd, ret = KSFT_FAIL, sk_pair[2];
+	pid_t pid;
+	long nofile;
+
+	if (socketpair(PF_LOCAL, SOCK_SEQPACKET, 0, sk_pair) < 0) {
+		ksft_print_msg("socketpair failed %s\n", strerror(errno));
+		return ret;
+	}
+
+	foo = cg_name(root, "foo");
+	if (!foo)
+		goto cleanup;
+
+	if (cg_write(root, "cgroup.subtree_control", "+misc")) {
+		ksft_print_msg("cg_write failed\n");
+		goto cleanup;
+	}
+
+	if (cg_create(foo)) {
+		ksft_print_msg("cg_create failed\n");
+		goto cleanup;
+	}
+
+	dfd = dirfd_open_opath(foo);
+	if (dfd < 0) {
+		perror("cgroup dir open");
+		goto cleanup;
+	}
+
+	pid = clone_into_cgroup(dfd, CLONE_FILES);
+	if (pid < 0) {
+		perror("clone");
+		goto cleanup;
+	}
+
+	if (pid == 0) {
+		close(sk_pair[0]);
+		exit(open_N_fds_and_sleep(foo, sk_pair+1));
+	}
+
+	errno = 0;
+	nofile = read(sk_pair[0], &c, 1);
+	if (nofile != 1) {
+		ksft_print_msg("read: %s\n", strerror(errno));
+		goto cleanup;
+	}
+	close(sk_pair[0]);
+
+	/*
+	 * We have two threads with a shared fd table, so the fds should be
+	 * counted only once.
+	 * We expect N + 3 stdfs + 2 socketpair fds.
+	 */
+	nofile = cg_read_key_long(foo, "misc.current", "nofile ");
+	if (nofile != (N+3+2)) {
+		ksft_print_msg("bad open files count: %ld != %d\n", nofile, N+3+1);
+		goto cleanup;
+	}
+
+	ret = KSFT_PASS;
+cleanup:
+	close(sk_pair[0]);
+	close(sk_pair[1]);
+	close(dfd);
+	kill(pid, SIGKILL);
+	waitpid(pid, NULL, 0);
+	cg_destroy(foo);
+	free(foo);
+	return ret;
+}
+
+static int test_misc_cg_threads_shared_files(const char *root)
+{
+	pid_t pid;
+	int status;
+
+	/*
+	 * get a fresh process to share fd tables so we don't pollute the test
+	 * suite's fd table in the case of failure.
+	 */
+	pid = fork();
+	if (pid < 0) {
+		perror("fork");
+		return KSFT_FAIL;
+	}
+
+	if (pid == 0)
+		exit(test_shared_files_count(root));
+
+	if (waitpid(pid, &status, 0) != pid) {
+		ksft_print_msg("wait failed\n");
+		return KSFT_FAIL;
+	}
+
+	if (!WIFEXITED(status)) {
+		ksft_print_msg("died with %x\n", status);
+		return KSFT_FAIL;
+	}
+
+	return WEXITSTATUS(status);
+}
+
+#define EXTRA 5
+static int open_more_than_N_fds(const char *cgroup, void *arg)
+{
+	int emfiles = 0, i;
+
+	for (i = 0; i < N+EXTRA; i++) {
+		int fd;
+
+		fd = socket(AF_UNIX, SOCK_SEQPACKET, 0);
+		if (fd < 0) {
+			if (errno != EMFILE) {
+				ksft_print_msg("%d socket: %s\n", i, strerror(errno));
+				return 1;
+			}
+
+			emfiles++;
+		}
+	}
+
+	/*
+	 * We have 3 existing stdfds open, plus the 100 that we tried to open,
+	 * plus the five extra.
+	 */
+	if (emfiles != EXTRA+3) {
+		ksft_print_msg("got %d EMFILEs\n", emfiles);
+		return 1;
+	}
+	return 0;
+}
+
+static int test_misc_cg_emfile_count(const char *root)
+{
+	int ret = KSFT_FAIL;
+	char *foo;
+	char nofile[128];
+	long nofile_events;
+
+	foo = cg_name(root, "foo");
+	if (!foo)
+		goto cleanup;
+
+	if (cg_create(foo)) {
+		ksft_print_msg("cg_create failed\n");
+		goto cleanup;
+	}
+
+	if (cg_write(root, "cgroup.subtree_control", "+misc")) {
+		ksft_print_msg("cg_write failed\n");
+		goto cleanup;
+	}
+
+	snprintf(nofile, sizeof(nofile), "nofile %d", N);
+	if (cg_write(foo, "misc.max", nofile)) {
+		ksft_print_msg("cg_write failed\n");
+		goto cleanup;
+	}
+
+	if (cg_run(foo, open_more_than_N_fds, NULL)) {
+		perror("cg_run");
+		ksft_print_msg("cg_run failed\n");
+		goto cleanup;
+	}
+
+	nofile_events = cg_read_key_long(foo, "misc.events", "nofile.max ");
+	if (nofile_events != EXTRA+3) {
+		ksft_print_msg("bad nofile events: %ld\n", nofile_events);
+		goto cleanup;
+	}
+
+	ret = KSFT_PASS;
+cleanup:
+	cg_destroy(foo);
+	free(foo);
+	return ret;
+}
+
+#define T(x) { x, #x }
+struct misccg_test {
+	int (*fn)(const char *root);
+	const char *name;
+} tests[] = {
+	T(test_misc_cg_basic),
+	T(test_misc_cg_threads),
+	T(test_misc_cg_threads_shared_files),
+	T(test_misc_cg_emfile_count),
+};
+#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");
+	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;
+}