diff mbox series

[RFC,1/4] lib: add new cgroup test API

Message ID 20200522012327.18991-1-liwang@redhat.com
State Superseded
Headers show
Series [RFC,1/4] lib: add new cgroup test API | expand

Commit Message

Li Wang May 22, 2020, 1:23 a.m. UTC
Many of our LTP tests need Control Group in the configuration,
this patch makes cgroup unified mounting at setup phase to be
possible. The method is extracted from mem.h with the purpose
of extendible for further developing, and trying to compatible
the current two versions of cgroup,

It's hard to make all APIs be strictly consistent because there
are many differences between v1 and v2. But it capsulate the detail
of cgroup mounting in high-level functions, which will be easier
to use cgroup without considering too much technical thing.   

Btw, test get passed on RHEL7(x86_64), RHEL8(ppc64le), Fedora32(x86_64).

Signed-off-by: Li Wang <liwang@redhat.com>
---
 include/tst_cgroup.h | 108 ++++++++++++++++++++
 include/tst_test.h   |   1 +
 lib/tst_cgroup.c     | 232 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 341 insertions(+)
 create mode 100644 include/tst_cgroup.h
 create mode 100644 lib/tst_cgroup.c

Comments

Jan Stancek May 26, 2020, 8:27 a.m. UTC | #1
----- Original Message -----

> +
> +/* cgroup v1 */
> +#define PATH_TMP_CG1_MEM	"/tmp/cgroup1_mem"
> +#define PATH_TMP_CG1_CST	"/tmp/cgroup1_cpuset"

It's only used for mount, so not sure if making it relative to TMPDIR
buys us anything.

> +
> +/* cgroup v1 */
> +void tst_mount_cgroup1(const char *name, const char *option,
> +			const char *mnt_path, const char *new_path)

I'd make all cgroup API start with tst_cgroup*.

Is the intent to provide API for both v1 and v2, or just higher level API
that hides the details?

> +{
> +	if (tst_is_mounted(mnt_path))
> +		goto out;
> +
> +	SAFE_MKDIR(mnt_path, 0777);
> +	if (mount(name, mnt_path, "cgroup", 0, option) == -1) {
> +		if (errno == ENODEV) {
> +			if (rmdir(mnt_path) == -1)
> +				tst_res(TWARN | TERRNO, "rmdir %s failed", mnt_path);
> +			tst_brk(TCONF,
> +				 "Cgroup v1 is not configured in kernel");
> +		}

We should probably handle also EBUSY, for cases when controller is already part
of existing hierarchy. E.g. cpu,cpuacct is mounted together, and someone
tries to mount just cpu:
  mount("none", "/mnt/cgroup", "cgroup", MS_MGC_VAL, "cpu") = -1 EBUSY (Device or resource busy)

> +
> +void tst_write_memcg1(long memsz)

This should at least say memmax or something similar, if we add
functions for more knobs later.

I'm thinking if it would be worth having API more parametrized,
something like:

enum tst_cgroup_ctrl {
        TST_CGROUP_MEMCG = 1,
        TST_CGROUP_CPUSET = 2,
};

tst_cgroup_mount(enum tst_cgroup_ctrl)
tst_cgroup_umount(enum tst_cgroup_ctrl)
  // tests wouldn't need to use these ones directly
  // would be probably internal functions

tst_cgroup_version()
  // to get/check cgroup support in setup()

tst_cgroup_create(enum tst_cgroup_ctrl, const char *dir)
  // mounts cgroup if not already mounted
  // creates "dir", sets up subtree_control, etc.

tst_cgroup_cleanup()
  // cleans up everything, removes dirs, umounts what was mounted

tst_cgroup_move_current(enum tst_cgroup_ctrl, const char *dir)
  // writes getpid() to dir/"tasks"

tst_cgroup_mem_set_maxbytes(const char *dir, long memsz)
  // memcg specific function
Li Wang May 26, 2020, 10:01 a.m. UTC | #2
On Tue, May 26, 2020 at 4:27 PM Jan Stancek <jstancek@redhat.com> wrote:

>
> ----- Original Message -----
>
> > +
> > +/* cgroup v1 */
> > +#define PATH_TMP_CG1_MEM     "/tmp/cgroup1_mem"
> > +#define PATH_TMP_CG1_CST     "/tmp/cgroup1_cpuset"
>
> It's only used for mount, so not sure if making it relative to TMPDIR
> buys us anything.
>

I choose to use /tmp dir just because the cgroup is
mounted temporarily for LTP test and destroyed
after using. And I have no preference to use other
places(i.e. /mnt or /dev).


>
> > +
> > +/* cgroup v1 */
> > +void tst_mount_cgroup1(const char *name, const char *option,
> > +                     const char *mnt_path, const char *new_path)
>
> I'd make all cgroup API start with tst_cgroup*.
>

Good point. I agree to make the function start with that prefix.

The logic in this patch looks like:

tst_cgroup1_mount(...)
tst_cgroup2_mount(...)
// these two API as the basic/internal function for a different version of
cgroup mounting

tst_cgroup1_mem(){
    tst_cgroup1_mount("with mem para")
}
tst_cgroup2_mem(){
    tst_cgroup2_mem("no need set mem, becase v2 has only one heierarchy")
}
// these two API mount different cgroup hierarchy via tst_cgroup*_mount()

tst_cgroup_mem()
{
    //if v1
    tst_cgroup1_mem()
    // if v2
    tst_cgroup2_mem()
}
tst_cgroup_cpu()
{
    // if v1
    tst_cgroup1_cpuset();
    // if v2
    tst_cgroup2_cpuset("no cpuset, skip this invoke");
}
// actully, we only use tst_cgroup_*() in testcase, they as the finial API
to export to



>
> Is the intent to provide API for both v1 and v2, or just higher level API
> that hides the details?
>

The former. My original purpose is to provide unified APIs
for both v1 and v2. Testcase author does not need to care
about the difference of two versions, if he/she want to set
max bytes in tests, just invoke:

//in setup()
    tst_cgroup_mem()
//in main()
    tst_cgroup_mem_set_maxbytes()
//in cleanup()
    tst_cgroup_umount()



>
> > +{
> > +     if (tst_is_mounted(mnt_path))
> > +             goto out;
> > +
> > +     SAFE_MKDIR(mnt_path, 0777);
> > +     if (mount(name, mnt_path, "cgroup", 0, option) == -1) {
> > +             if (errno == ENODEV) {
> > +                     if (rmdir(mnt_path) == -1)
> > +                             tst_res(TWARN | TERRNO, "rmdir %s failed",
> mnt_path);
> > +                     tst_brk(TCONF,
> > +                              "Cgroup v1 is not configured in kernel");
> > +             }
>
> We should probably handle also EBUSY, for cases when controller is already
> part
> of existing hierarchy. E.g. cpu,cpuacct is mounted together, and someone
> tries to mount just cpu:
>   mount("none", "/mnt/cgroup", "cgroup", MS_MGC_VAL, "cpu") = -1 EBUSY
> (Device or resource busy)
>

That's true.

But in general, people are not permitted to use tst_cgroup*_mount()
directly, it is only as the low-level/internal function to hide details we
mount cgroup. My previous thought is that, in v1, cpu,cpuacct are bound
together(as system way) in tst_croup_cpu().



>
> > +
> > +void tst_write_memcg1(long memsz)
>
> This should at least say memmax or something similar, if we add
> functions for more knobs later.
>

Good suggestion.


>
> I'm thinking if it would be worth having API more parametrized,
> something like:
>

Sounds neat, we could have a try and then there
is no need to export too many functions with _mem()
or _cpu() suffixes.

I'd like to merge your method with my original basic
functions, it seems not very hard to achieve patch v2:

tst_cgroup_create(enum tst_cgroup_ctrl ctrl)
{
    tst_cgroup_version();

    switch(ctrl) {
    case TST_CGROUP_MEMCG:
        // if v1
        tst_cgroup1_mount(TST_CGROUP_MEMCG);
        // if v2
        tst_cgroup2_mount()
    break;
    case TST_CGROUP_CPUSET:
          //if v1
          tst_cgroup1_mount(TST_CGROUP_CPUSET);
         // if v2
         TCONF here...
    break;
    }
}



>
> enum tst_cgroup_ctrl {
>         TST_CGROUP_MEMCG = 1,
>         TST_CGROUP_CPUSET = 2,
> };
>
> tst_cgroup_mount(enum tst_cgroup_ctrl)
> tst_cgroup_umount(enum tst_cgroup_ctrl)
>   // tests wouldn't need to use these ones directly
>   // would be probably internal functions
>
> tst_cgroup_version()
>   // to get/check cgroup support in setup()
>
> tst_cgroup_create(enum tst_cgroup_ctrl, const char *dir)
>

Maybe we can drop the second parameter "dir", the mount
functions are internal and we just use path macros in sub-function
which like what I did.



>   // mounts cgroup if not already mounted
>   // creates "dir", sets up subtree_control, etc.
>
> tst_cgroup_cleanup()
>   // cleans up everything, removes dirs, umounts what was mounted
>
> tst_cgroup_move_current(enum tst_cgroup_ctrl, const char *dir)
>   // writes getpid() to dir/"tasks"
>
> tst_cgroup_mem_set_maxbytes(const char *dir, long memsz)
>   // memcg specific function
>
>
Jan Stancek May 27, 2020, 7:11 a.m. UTC | #3
----- Original Message -----

> > We should probably handle also EBUSY, for cases when controller is already
> > part
> > of existing hierarchy. E.g. cpu,cpuacct is mounted together, and someone
> > tries to mount just cpu:
> >   mount("none", "/mnt/cgroup", "cgroup", MS_MGC_VAL, "cpu") = -1 EBUSY
> > (Device or resource busy)
> >
> 
> That's true.
> 
> But in general, people are not permitted to use tst_cgroup*_mount()
> directly, it is only as the low-level/internal function to hide details we
> mount cgroup. My previous thought is that, in v1, cpu,cpuacct are bound
> together(as system way) in tst_croup_cpu().

They don't need to use tst_cgroup*_mount() directly, they could change their
system config and mount cpu,cpuacct,memcg together. Though chances of that
happening are low.

> > tst_cgroup_create(enum tst_cgroup_ctrl, const char *dir)
> >
> 
> Maybe we can drop the second parameter "dir", the mount
> functions are internal and we just use path macros in sub-function
> which like what I did.

I wanted to keep some flexibility in case test needs multiple cgroups.
I'll have a look at v1 you posted today.
diff mbox series

Patch

diff --git a/include/tst_cgroup.h b/include/tst_cgroup.h
new file mode 100644
index 000000000..78e646c2e
--- /dev/null
+++ b/include/tst_cgroup.h
@@ -0,0 +1,108 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2020 Red Hat, Inc.
+ */
+
+#ifndef TST_CGROUP_H
+#define TST_CGROUP_H
+
+#define PATH_SYS_CGROUP		"/sys/fs/cgroup"
+
+enum tst_cgroup_ver {
+	TST_CGROUP_V1 = 1,
+	TST_CGROUP_V2 = 2,
+};
+
+static enum tst_cgroup_ver tst_cg_ver;
+
+enum tst_cgroup_ver tst_cgroup_version(void);
+
+/* cgroup v1 */
+#define PATH_TMP_CG1_MEM	"/tmp/cgroup1_mem"
+#define PATH_TMP_CG1_CST	"/tmp/cgroup1_cpuset"
+#define PATH_CG1_MEM_LTP	PATH_TMP_CG1_MEM "/ltp"
+#define PATH_CG1_CST_LTP	PATH_TMP_CG1_CST "/ltp"
+#define PATH_MEMORY_LIMIT	PATH_CG1_MEM_LTP "/memory.limit_in_bytes"
+#define PATH_MEMORY_SW_LIMIT	PATH_CG1_MEM_LTP "/memory.memsw.limit_in_bytes"
+
+void tst_mount_cgroup1(const char *name, const char *option,
+			const char *mnt_path, const char *new_path);
+void tst_umount_cgroup1(const char *mnt_path, const char *new_path);
+
+void tst_mount_memcg1(void);
+void tst_umount_memcg1(void);
+void tst_write_memcg1(long memsz);
+
+void tst_mount_cpusetcg1(void);
+void tst_umount_cpusetcg1(void);
+void tst_write_cpusetcg1(void);
+/* cgroup v1 */
+
+/* cgroup v2 */
+#define PATH_TMP_CGROUP2	"/tmp/cgroup2"	/* cgroup v2 has only single hierarchy */
+#define PATH_TMP_CG2_LTP	PATH_TMP_CGROUP2 "/ltp"
+#define PATH_MEMORY_MAX		PATH_TMP_CG2_LTP "/memory.max"
+#define PATH_MEMORY_SW_MAX	PATH_TMP_CG2_LTP "/memory.swap.max"
+
+void tst_mount_cgroup2(const char *mnt_path, const char *new_path);
+void tst_umount_cgroup2(const char *mnt_path, const char *new_path);
+
+void tst_mount_memcg2(void);
+void tst_umount_memcg2(void);
+void tst_write_memcg2(long memsz);
+
+void tst_mount_cpusetcg2(void);
+/* cgroup v2 */
+
+static inline void tst_mount_memcg(void)
+{
+	tst_cg_ver = tst_cgroup_version();
+
+	if (tst_cg_ver & TST_CGROUP_V1)
+		tst_mount_memcg1();
+
+	if (tst_cg_ver & TST_CGROUP_V2)
+		tst_mount_memcg2();
+}
+
+static inline void tst_umount_memcg(void)
+{
+	if (tst_cg_ver & TST_CGROUP_V1)
+		tst_umount_memcg1();
+
+	if (tst_cg_ver & TST_CGROUP_V2)
+		tst_umount_memcg2();
+}
+
+static inline void tst_write_memcg(long memsz)
+{
+	if (tst_cg_ver & TST_CGROUP_V1)
+		tst_write_memcg1(memsz);
+
+	if (tst_cg_ver & TST_CGROUP_V2)
+		tst_write_memcg2(memsz);
+}
+
+static inline void tst_mount_cpusetcg(void)
+{
+	tst_cg_ver = tst_cgroup_version();
+
+	if (tst_cg_ver & TST_CGROUP_V1)
+		tst_mount_cpusetcg1();
+
+	if (tst_cg_ver & TST_CGROUP_V2)
+		tst_mount_cpusetcg2();
+}
+
+static inline void tst_umount_cpusetcg(void)
+{
+	if (tst_cg_ver & TST_CGROUP_V1)
+		tst_umount_cpusetcg1();
+}
+
+static inline void tst_write_cpusetcg(void)
+{
+	if (tst_cg_ver & TST_CGROUP_V1)
+		tst_write_cpusetcg1();
+}
+#endif /* TST_CGROUP_H */
diff --git a/include/tst_test.h b/include/tst_test.h
index 5bedb4f6f..b84f7b9dd 100644
--- a/include/tst_test.h
+++ b/include/tst_test.h
@@ -39,6 +39,7 @@ 
 #include "tst_capability.h"
 #include "tst_hugepage.h"
 #include "tst_assert.h"
+#include "tst_cgroup.h"
 
 /*
  * Reports testcase result.
diff --git a/lib/tst_cgroup.c b/lib/tst_cgroup.c
new file mode 100644
index 000000000..88374012f
--- /dev/null
+++ b/lib/tst_cgroup.c
@@ -0,0 +1,232 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) 2020 Red Hat, Inc.
+ */
+
+#define TST_NO_DEFAULT_MAIN
+
+#include <stdio.h>
+#include <sys/mount.h>
+
+#include "tst_test.h"
+#include "tst_safe_macros.h"
+#include "tst_safe_stdio.h"
+#include "tst_cgroup.h"
+#include "tst_device.h"
+
+static int tst_cgroup_check(const char *cgroup)
+{
+	char line[PATH_MAX];
+	FILE *file;
+	int cg_check = 0;
+
+	file = SAFE_FOPEN("/proc/filesystems", "r");
+	while (fgets(line, sizeof(line), file)) {
+		if (strstr(line, cgroup) != NULL) {
+			cg_check = 1;
+			break;
+		}
+	}
+	SAFE_FCLOSE(file);
+
+	return cg_check;
+}
+
+enum tst_cgroup_ver tst_cgroup_version(void)
+{
+	if (tst_cgroup_check("cgroup2")) {
+		if (!tst_is_mounted("cgroup2") && tst_is_mounted("cgroup"))
+			return TST_CGROUP_V1;
+		else
+			return TST_CGROUP_V2;
+	}
+
+	if (tst_cgroup_check("cgroup"))
+		return TST_CGROUP_V1;
+
+	tst_brk(TCONF, "Cgroup is not configured");
+}
+
+/* cgroup v1 */
+void tst_mount_cgroup1(const char *name, const char *option,
+			const char *mnt_path, const char *new_path)
+{
+	if (tst_is_mounted(mnt_path))
+		goto out;
+
+	SAFE_MKDIR(mnt_path, 0777);
+	if (mount(name, mnt_path, "cgroup", 0, option) == -1) {
+		if (errno == ENODEV) {
+			if (rmdir(mnt_path) == -1)
+				tst_res(TWARN | TERRNO, "rmdir %s failed", mnt_path);
+			tst_brk(TCONF,
+				 "Cgroup v1 is not configured in kernel");
+		}
+		tst_brk(TBROK | TERRNO, "mount %s", mnt_path);
+	}
+
+out:
+	SAFE_MKDIR(new_path, 0777);
+
+	tst_res(TINFO, "Cgroup(%s) v1 mount at %s success", option, mnt_path);
+}
+
+void tst_umount_cgroup1(const char *mnt_path, const char *new_path)
+{
+	FILE *fp;
+	int fd;
+	char s_new[BUFSIZ], s[BUFSIZ], value[BUFSIZ];
+
+	if (!tst_is_mounted(mnt_path))
+		return;
+
+	/* Move all processes in task to its parent node. */
+	sprintf(s, "%s/tasks", mnt_path);
+	fd = open(s, O_WRONLY);
+	if (fd == -1)
+		tst_res(TWARN | TERRNO, "open %s", s);
+
+	snprintf(s_new, BUFSIZ, "%s/tasks", new_path);
+	fp = fopen(s_new, "r");
+	if (fp == NULL)
+		tst_res(TWARN | TERRNO, "fopen %s", s_new);
+	if ((fd != -1) && (fp != NULL)) {
+		while (fgets(value, BUFSIZ, fp) != NULL)
+			if (write(fd, value, strlen(value) - 1)
+			    != (ssize_t)strlen(value) - 1)
+				tst_res(TWARN | TERRNO, "write %s", s);
+	}
+	if (fd != -1)
+		close(fd);
+	if (fp != NULL)
+		fclose(fp);
+	if (rmdir(new_path) == -1)
+		tst_res(TWARN | TERRNO, "rmdir %s", new_path);
+	if (umount(mnt_path) == -1)
+		tst_res(TWARN | TERRNO, "umount %s", mnt_path);
+	if (rmdir(mnt_path) == -1)
+		tst_res(TWARN | TERRNO, "rmdir %s", mnt_path);
+
+	tst_res(TINFO, "Cgroup v1 unmount success");
+}
+
+void tst_mount_memcg1(void)
+{
+	tst_mount_cgroup1("memcg", "memory", PATH_TMP_CG1_MEM, PATH_CG1_MEM_LTP);
+}
+
+void tst_umount_memcg1(void)
+{
+	tst_umount_cgroup1(PATH_TMP_CG1_MEM, PATH_CG1_MEM_LTP);
+}
+
+void tst_write_memcg1(long memsz)
+{
+	SAFE_FILE_PRINTF(PATH_CG1_MEM_LTP "/memory.limit_in_bytes", "%ld", memsz);
+
+	SAFE_FILE_PRINTF(PATH_CG1_MEM_LTP "/tasks", "%d", getpid());
+}
+
+void tst_mount_cpusetcg1(void)
+{
+	tst_mount_cgroup1("cpusetcg", "cpuset", PATH_TMP_CG1_CST, PATH_CG1_CST_LTP);
+}
+
+void tst_umount_cpusetcg1(void)
+{
+	tst_umount_cgroup1(PATH_TMP_CG1_CST, PATH_CG1_CST_LTP);
+}
+
+void tst_write_cpusetcg1(void)
+{
+	SAFE_FILE_PRINTF(PATH_CG1_CST_LTP "/tasks", "%d", getpid());
+}
+/* cgroup v1 */
+
+/* cgroup v2 */
+void tst_mount_cgroup2(const char *mnt_path, const char *new_path)
+{
+	if (tst_is_mounted(mnt_path))
+		goto out;
+
+	SAFE_MKDIR(mnt_path, 0777);
+	if (mount("cgroup2", mnt_path, "cgroup2", 0, NULL) == -1) {
+		if (errno == ENODEV) {
+			if (rmdir(mnt_path) == -1)
+				tst_res(TWARN | TERRNO, "rmdir %s failed",
+					 mnt_path);
+			tst_brk(TCONF,
+				 "Cgroup v2 is not configured in kernel");
+		}
+		tst_brk(TBROK | TERRNO, "mount %s", mnt_path);
+	}
+
+out:
+	SAFE_MKDIR(new_path, 0777);
+
+	tst_res(TINFO, "Cgroup v2 mount at %s success", mnt_path);
+}
+
+void tst_umount_cgroup2(const char *mnt_path, const char *new_path)
+{
+	FILE *fp;
+	int fd;
+	char s_new[BUFSIZ], s[BUFSIZ], value[BUFSIZ];
+
+	if (!tst_is_mounted(mnt_path))
+		return;
+
+	/* Move all processes in cgroup.procs to its parent node. */
+	sprintf(s, "%s/cgroup.procs", mnt_path);
+	fd = open(s, O_WRONLY);
+	if (fd == -1)
+		tst_res(TWARN | TERRNO, "open %s", s);
+
+	snprintf(s_new, BUFSIZ, "%s/cgroup.procs", new_path);
+	fp = fopen(s_new, "r");
+	if (fp == NULL)
+		tst_res(TWARN | TERRNO, "fopen %s", s_new);
+	if ((fd != -1) && (fp != NULL)) {
+		while (fgets(value, BUFSIZ, fp) != NULL)
+			if (write(fd, value, strlen(value) - 1)
+			    != (ssize_t)strlen(value) - 1)
+				tst_res(TWARN | TERRNO, "write %s", s);
+	}
+	if (fd != -1)
+		close(fd);
+	if (fp != NULL)
+		fclose(fp);
+	if (rmdir(new_path) == -1)
+		tst_res(TWARN | TERRNO, "rmdir %s", new_path);
+	if (umount(mnt_path) == -1)
+		tst_res(TWARN | TERRNO, "umount %s", mnt_path);
+	if (rmdir(mnt_path) == -1)
+		tst_res(TWARN | TERRNO, "rmdir %s", mnt_path);
+
+	tst_res(TINFO, "Cgroup v2 unmount success");
+}
+
+void tst_mount_memcg2(void)
+{
+	tst_mount_cgroup2(PATH_TMP_CGROUP2, PATH_TMP_CG2_LTP);
+
+	SAFE_FILE_PRINTF(PATH_TMP_CGROUP2 "/cgroup.subtree_control", "%s", "+memory");
+}
+
+void tst_umount_memcg2()
+{
+	tst_umount_cgroup2(PATH_TMP_CGROUP2, PATH_TMP_CG2_LTP);
+}
+
+void tst_write_memcg2(long memsz)
+{
+	SAFE_FILE_PRINTF(PATH_TMP_CG2_LTP "/memory.max", "%ld", memsz);
+
+	SAFE_FILE_PRINTF(PATH_TMP_CG2_LTP "/cgroup.procs", "%d", getpid());
+}
+
+void tst_mount_cpusetcg2(void)
+{
+	tst_brk(TCONF, "Cgroup v2 hasn't achieve cpuset subsystem");
+}
+/* cgroup v2 */