From e07262d8bcd0fab661c48af9522de52daf940fd8 Mon Sep 17 00:00:00 2001 From: Tom Hromatka Date: Wed, 17 May 2023 14:53:45 -0600 Subject: [PATCH] samples: Add a C example that creates a systemd scope Signed-off-by: Tom Hromatka Reviewed-by: Kamalesh Babulal --- samples/c/.gitignore | 16 + samples/c/Makefile.am | 5 +- samples/c/create_systemd_scope.c | 613 +++++++++++++++++++ samples/cmdline/systemd-with-idle-process.md | 3 + 4 files changed, 635 insertions(+), 2 deletions(-) create mode 100644 samples/c/.gitignore create mode 100644 samples/c/create_systemd_scope.c diff --git a/samples/c/.gitignore b/samples/c/.gitignore new file mode 100644 index 00000000..2111a963 --- /dev/null +++ b/samples/c/.gitignore @@ -0,0 +1,16 @@ +create_systemd_scope +empty_cgroup_v2 +get_all_controller +get_controller +get_mount_point +get_procs +get_setup_mode +get_variable_names +logger +proctest +read_stats +setuid +test_named_hierarchy +walk_task +walk_test +wrapper_test diff --git a/samples/c/Makefile.am b/samples/c/Makefile.am index 28d4ddd6..444ca545 100644 --- a/samples/c/Makefile.am +++ b/samples/c/Makefile.am @@ -7,7 +7,7 @@ noinst_PROGRAMS = setuid walk_test read_stats walk_task get_controller \ get_mount_point proctest get_all_controller \ get_variable_names test_named_hierarchy \ get_procs wrapper_test logger empty_cgroup_v2 \ - get_setup_mode + get_setup_mode create_systemd_scope setuid_SOURCES=setuid.c walk_test_SOURCES=walk_test.c @@ -23,6 +23,7 @@ get_procs_SOURCES=get_procs.c wrapper_test_SOURCES=wrapper_test.c logger_SOURCES=logger.c empty_cgroup_v2_SOURCES=empty_cgroup_v2.c -get_setup_mode=get_setup_mode.c +get_setup_mode_SOURCES=get_setup_mode.c +create_systemd_scope_SOURCES=create_systemd_scope.c endif diff --git a/samples/c/create_systemd_scope.c b/samples/c/create_systemd_scope.c new file mode 100644 index 00000000..2273d293 --- /dev/null +++ b/samples/c/create_systemd_scope.c @@ -0,0 +1,613 @@ +// SPDX-License-Identifier: LGPL-2.1-only +/** + * Sample program that shows how to use libcgroup to create a systemd scope + * + * This program is designed to meet the requirements outlined in the systemd + * cmdline example [1] via the libcgroup C APIs. + * + * [1] https://github.com/libcgroup/libcgroup/blob/main/samples/cmdline/systemd-with-idle-process.md + * + * Copyright (c) 2023 Oracle and/or its affiliates. + * Author: Tom Hromatka + */ + +/* + * To compile and link this program: + * (From the root of the libcgroup source code directory) + * $ ./bootstrap + * $ ./configure --sysconfdir=/etc --localstatedir=/var \ + * --enable-opaque-hierarchy="name=systemd" --enable-systemd \ + * --enable-python --enable-samples + * $ make + * + * Add the libcgroup idle thread to your PATH. (Some distros restrict the + * modification of the $PATH environment variable when invoking sudo, so you + * will need to manually copy the executable to your path.) + * $ sudo cp src/libcgroup_systemd_idle_thread /a/path/in/your/sudo/path + * + * To run this program: + * $ # Note that there are more options. Run `create_systemd_scope -h` for more info + * $ sudo LD_LIBRARY_PATH=src/.libs ./samples/c/create_systemd_scope \ + * --slice --scope + */ + +/* + */ + +#include +#include +#include +#include +#include +#include +#include + +#define TMP_CGNAME "tmp" +#define HIGH_CGNAME "high-priority" +#define MED_CGNAME "medium-priority" +#define LOW_CGNAME "low-priority" + +struct example_opts { + char slice[FILENAME_MAX]; + char scope[FILENAME_MAX]; + + struct cgroup_systemd_scope_opts systemd_opts; + + bool debug; +}; + +static const struct option long_opts[] = { + {"delegated", no_argument, NULL, 'd'}, + {"help", no_argument, NULL, 'h'}, + {"pid", required_argument, NULL, 'p'}, + {"scope", required_argument, NULL, 's'}, + {"slice", required_argument, NULL, 't'}, + {"verbose", no_argument, NULL, 'v'}, + {NULL, 0, NULL, 0} +}; + +static void usage(const char * const program_name) +{ + printf("Usage: %s -s -t [-p ]\n", program_name); + printf("Create a systemd scope using the libcgroup C APIs\n"); + printf(" -d, --delegated Instruct systemd to create a delegated scope\n"); + printf(" -p, --pid PID to be placed in the scope, if not provided,\n"); + printf(" libcgroup will place a default idle PID in the scope\n"); + printf(" -s, --scope Scope name, must end in .scope\n"); + printf(" -t, --slice Slice name, must end in .slice\n"); + printf(" -v, --verbose Enable libcgroup debug messages\n"); +} + +static int parse_opts(int argc, char *argv[], struct example_opts * const opts) +{ + int c; + + while ((c = getopt_long(argc, argv, "hs:t:p:dv", long_opts, NULL)) > 0) { + switch (c) { + case 'd': + opts->systemd_opts.delegated = true; + break; + case 'h': + usage(argv[0]); + exit(0); + case 'p': + opts->systemd_opts.pid = atoi(optarg); + if (opts->systemd_opts.pid <= 1) { + usage(argv[0]); + exit(1); + } + break; + case 's': + strncpy(opts->scope, optarg, FILENAME_MAX - 1); + break; + case 't': + strncpy(opts->slice, optarg, FILENAME_MAX - 1); + break; + case 'v': + opts->debug = true; + break; + default: + usage(argv[0]); + exit(1); + break; + } + } + + return 0; +} + +static int create_scope(const struct example_opts * const opts) +{ + int ret; + + printf("\n----------------------------------------------------------------\n"); + printf("Creating systemd scope, %s/%s,\n", opts->slice, opts->scope); + if (opts->systemd_opts.pid > 1) + printf("and placing PID, %d, in the scope\n", opts->systemd_opts.pid); + else + printf("and libcgroup will place an idle process in the scope\n"); + printf("----------------------------------------------------------------\n\n"); + + ret = cgroup_create_scope(opts->scope, opts->slice, &opts->systemd_opts); + if (ret == ECGINVAL) { + printf("An invalid parameter was passed into cgroup_create_scope()\n" + "Check your scope name, slice name, and systemd options carefully\n"); + goto error; + } else if (ret == ECGOTHER) { + printf("Libcgroup typically returns ECGOTHER when a system call fails.\n" + "These failures could be caused by being out of memory, lack of\n" + "permissions, etc. Enabling libcgroup debug messages may help\n" + "root cause the issue; pass in '-v' to this program\n"); + printf("The failing system call returned an errno of %d\n", + cgroup_get_last_errno()); + goto error; + } else if (ret > 0) { + printf("An unspecified error occurred - likely in the formation or\n" + "sending/receiving of the message to systemd to create the scope.\n" + "Ensure that this application has the requisite permissions and\n" + "capabilities to create a scope. Enabling libcgroup debug\n" + "messages may help root cause the failing instruction; pass in\n" + "'-v' to this program\n"); + printf("Libcgroup returned %d\n", ret); + goto error; + } + + /* + * Counterintuitively, this function returns 1 on success + */ + ret = cgroup_write_systemd_default_cgroup(opts->slice, opts->scope); + if (ret != 1) { + printf("Failed to set the libcgroup default scope/slice: %d: %s\n", + ret, cgroup_strerror(ret)); + ret = ECGFAIL; + goto error; + } + + /* + * cgroup_set_default_systemd_cgroup() will return 1 if a default slice/scope are + * set, and will return 0 otherwise. Ensure that a default slice/scope are set. + * This is critical because subsequent code in this example is assuming a default, + * and if the default is not set, then calls will be operating on the root cgroup. + */ + ret = cgroup_set_default_systemd_cgroup(); + if (ret != 1) { + printf("The default slice/scope are not properly set\n"); + ret = ECGFAIL; + goto error; + } + + ret = 0; + +error: + return ret; +} + +static int create_tmp_cgroup(const struct example_opts * const opts) +{ + struct cgroup *cg; + int ret = 0; + + cg = cgroup_new_cgroup(TMP_CGNAME); + if (!cg) { + printf("Failed to allocate the cgroup struct. Are we out of memory?\n"); + goto error; + } + + ret = cgroup_create_cgroup(cg, 1); + if (ret) { + printf("Failed to write the cgroup to /sys/fs/cgroup: %d: %s\n", + ret, cgroup_strerror(ret)); + goto error; + } + +error: + if (cg) + cgroup_free(&cg); + + return ret; +} + +static int move_pids_to_tmp_cgroup(const struct example_opts * const opts) +{ + int ret, pid_cnt, i; + pid_t *pids = NULL; + int saved_ret = 0; + struct cgroup *cg; + + /* + * Since we told libcgroup that our slice and scope are the default, we can + * operate with that as our "root" directory. If we hadn't set them as the + * default, then we would have had to build up the relative path - + * /. + */ + ret = cgroup_get_procs("/", NULL, &pids, &pid_cnt); + if (ret) { + printf("Failed to get the pids in %s: %d: %s\n", opts->scope, + ret, cgroup_strerror(ret)); + goto error; + } + + cg = cgroup_new_cgroup(TMP_CGNAME); + if (!cg) { + printf("Failed to allocate the cgroup struct. Are we out of memory?\n"); + goto error; + } + + for (i = 0; i < pid_cnt; i++) { + ret = cgroup_attach_task_pid(cg, pids[i]); + if (ret) { + printf("Failed to attach pid %d to %s\n", pids[i], TMP_CGNAME); + /* + * Instead of failing, let's save off the return code and continue + * moving processes to the new cgroup. Perhaps the failing PID was + * killed between when we read cgroup.procs and when we tried to + * move it + */ + saved_ret = ret; + } + } + +error: + if (pids) + free(pids); + + if (cg) + cgroup_free(&cg); + + if (ret == 0 && saved_ret) + ret = saved_ret; + + return ret; +} + +static int create_high_priority_cgroup(const struct example_opts * const opts) +{ + struct cgroup_controller *ctrl; + struct cgroup *cg; + int ret = 0; + + cg = cgroup_new_cgroup(HIGH_CGNAME); + if (!cg) { + printf("Failed to allocate the cgroup struct. Are we out of memory?\n"); + ret = ECGFAIL; + goto error; + } + + ctrl = cgroup_add_controller(cg, "cpu"); + if (!ctrl) { + printf("Failed to add the cpu controller to the %s cgroup struct\n", + HIGH_CGNAME); + ret = ECGFAIL; + goto error; + } + + ret = cgroup_set_value_string(ctrl, "cpu.weight", "600"); + if (ret) { + printf("Failed to set %s's cpu.weight: %d: %s\n", HIGH_CGNAME, ret, + cgroup_strerror(ret)); + goto error; + } + + ctrl = cgroup_add_controller(cg, "memory"); + if (!ctrl) { + printf("Failed to add the memory controller to the %s cgroup struct\n", + HIGH_CGNAME); + ret = ECGFAIL; + goto error; + } + + ret = cgroup_set_value_string(ctrl, "memory.low", "1G"); + if (ret) { + printf("Failed to set %s's memory.low: %d: %s\n", HIGH_CGNAME, ret, + cgroup_strerror(ret)); + goto error; + } + + ret = cgroup_create_cgroup(cg, 0); + if (ret) { + printf("Failed to write the %s cgroup to /sys/fs/cgroup: %d: %s\n", + HIGH_CGNAME, ret, cgroup_strerror(ret)); + goto error; + } + +error: + if (cg) + cgroup_free(&cg); + + return ret; +} + +static int create_medium_priority_cgroup(const struct example_opts * const opts) +{ + struct cgroup_controller *ctrl; + struct cgroup *cg; + int ret = 0; + + cg = cgroup_new_cgroup(MED_CGNAME); + if (!cg) { + printf("Failed to allocate the cgroup struct. Are we out of memory?\n"); + ret = ECGFAIL; + goto error; + } + + ctrl = cgroup_add_controller(cg, "cpu"); + if (!ctrl) { + printf("Failed to add the cpu controller to the %s cgroup struct\n", + MED_CGNAME); + ret = ECGFAIL; + goto error; + } + + ret = cgroup_set_value_string(ctrl, "cpu.weight", "300"); + if (ret) { + printf("Failed to set %s's cpu.weight: %d: %s\n", MED_CGNAME, ret, + cgroup_strerror(ret)); + goto error; + } + + ctrl = cgroup_add_controller(cg, "memory"); + if (!ctrl) { + printf("Failed to add the memory controller to the %s cgroup struct\n", + MED_CGNAME); + ret = ECGFAIL; + goto error; + } + + ret = cgroup_set_value_string(ctrl, "memory.high", "3G"); + if (ret) { + printf("Failed to set %s's memory.high: %d: %s\n", MED_CGNAME, ret, + cgroup_strerror(ret)); + goto error; + } + + ret = cgroup_create_cgroup(cg, 0); + if (ret) { + printf("Failed to write the %s cgroup to /sys/fs/cgroup: %d: %s\n", + MED_CGNAME, ret, cgroup_strerror(ret)); + goto error; + } + +error: + if (cg) + cgroup_free(&cg); + + return ret; +} + +static int create_low_priority_cgroup(const struct example_opts * const opts) +{ + struct cgroup_controller *ctrl; + struct cgroup *cg; + int ret = 0; + + cg = cgroup_new_cgroup(LOW_CGNAME); + if (!cg) { + printf("Failed to allocate the cgroup struct. Are we out of memory?\n"); + ret = ECGFAIL; + goto error; + } + + ctrl = cgroup_add_controller(cg, "cpu"); + if (!ctrl) { + printf("Failed to add the cpu controller to the %s cgroup struct\n", + LOW_CGNAME); + ret = ECGFAIL; + goto error; + } + + ret = cgroup_set_value_string(ctrl, "cpu.weight", "100"); + if (ret) { + printf("Failed to set %s's cpu.weight: %d: %s\n", LOW_CGNAME, ret, + cgroup_strerror(ret)); + goto error; + } + + ctrl = cgroup_add_controller(cg, "memory"); + if (!ctrl) { + printf("Failed to add the memory controller to the %s cgroup struct\n", + LOW_CGNAME); + ret = ECGFAIL; + goto error; + } + + ret = cgroup_set_value_string(ctrl, "memory.max", "2G"); + if (ret) { + printf("Failed to set %s's memory.high: %d: %s\n", LOW_CGNAME, ret, + cgroup_strerror(ret)); + goto error; + } + + ret = cgroup_create_cgroup(cg, 0); + if (ret) { + printf("Failed to write the %s cgroup to /sys/fs/cgroup: %d: %s\n", + LOW_CGNAME, ret, cgroup_strerror(ret)); + goto error; + } + +error: + if (cg) + cgroup_free(&cg); + + return ret; +} + +static int create_process(pid_t * const pid, const char * const cgname) +{ + int ret = 0; + + *pid = fork(); + if (*pid < 0) { + printf("Failed to create the process: %d\n", errno); + ret = ECGFAIL; + goto error; + } else if (*pid == 0) { + /* + * This is the child process. Move it to the requested cgroup + */ + ret = cgroup_change_cgroup_path(cgname, getpid(), NULL); + if (ret) + exit(0); + + while (true) + sleep(10); + } + +error: + if (ret) + *pid = -1; + + return ret; +} + +static int delete_tmp_cgroup(void) +{ + int ret, pid_cnt, i; + int saved_errno = 0; + pid_t *pids = NULL; + struct cgroup *cg; + + ret = cgroup_get_procs(TMP_CGNAME, NULL, &pids, &pid_cnt); + if (ret) { + printf("Failed to get the pids in %s: %d: %s\n", TMP_CGNAME, + ret, cgroup_strerror(ret)); + goto error; + } + + for (i = 0; i < pid_cnt; i++) { + ret = kill(pids[i], SIGTERM); + if (ret < 0) { + saved_errno = errno; + printf("Kill failed: %d\n", errno); + /* + * Do not bail out here as there may be other PIDS to process + */ + } + } + + cg = cgroup_new_cgroup(TMP_CGNAME); + if (!cg) { + printf("Failed to allocate the cgroup struct. Are we out of memory?\n"); + goto error; + } + + ret = cgroup_delete_cgroup(cg, 1); + if (ret) + printf("Failed to delete the %s cgroup: %d: %s\n", TMP_CGNAME, ret, + cgroup_strerror(ret)); + +error: + if (pids) + free(pids); + + if (cg) + cgroup_free(&cg); + + if (ret == 0 && saved_errno) + ret = ECGFAIL; + + return ret; +} + +static void wait_for_child(pid_t pid) +{ + int wstatus; + + (void)waitpid(pid, &wstatus, 0); + + if (WIFEXITED(wstatus)) + printf("pid %d exited with status %d\n", pid, WEXITSTATUS(wstatus)); + else if (WIFSIGNALED(wstatus)) + printf("pid %d exited due to signal %d\n", pid, WTERMSIG(wstatus)); +} + +int main(int argc, char *argv[]) +{ + pid_t high_pid, med_pid, low_pid; + struct example_opts opts = {0}; + int ret = 0; + + if (argc < 3) { + usage(argv[0]); + exit(1); + } + + ret = cgroup_set_default_scope_opts(&opts.systemd_opts); + if (ret) { + printf("Failed to set the default systemd options: %d\n", ret); + goto error; + } + + ret = parse_opts(argc, argv, &opts); + if (ret) { + printf("Failed to parse the command line options: %d\n", ret); + goto error; + } + + if (opts.debug) + cgroup_set_default_logger(CGROUP_LOG_DEBUG); + + ret = cgroup_init(); + if (ret) + goto error; + + ret = create_scope(&opts); + if (ret) + goto error; + + ret = create_tmp_cgroup(&opts); + if (ret) + goto error; + + ret = move_pids_to_tmp_cgroup(&opts); + if (ret) + goto error; + + ret = create_high_priority_cgroup(&opts); + if (ret) + goto error; + + ret = create_medium_priority_cgroup(&opts); + if (ret) + goto error; + + ret = create_low_priority_cgroup(&opts); + if (ret) + goto error; + + ret = create_process(&high_pid, HIGH_CGNAME); + if (ret) + goto error; + + ret = create_process(&med_pid, MED_CGNAME); + if (ret) + goto error; + + ret = create_process(&low_pid, LOW_CGNAME); + if (ret) + goto error; + + ret = delete_tmp_cgroup(); + if (ret) + goto error; + + printf("\n----------------------------------------------------------------\n"); + printf("Cgroup setup completed successfully\n"); + printf("\t* The scope %s was placed under slice %s\n", opts.scope, opts.slice); + printf("\t* Libcgroup initially placed an idle process in the scope,\n" + "\t but it has been removed by this program\n"); + printf("\t* PID %d has been placed in the %s cgroup\n", high_pid, HIGH_CGNAME); + printf("\t* PID %d has been placed in the %s cgroup\n", med_pid, MED_CGNAME); + printf("\t* PID %d has been placed in the %s cgroup\n", low_pid, LOW_CGNAME); + printf("\nThis program will wait for the aforementioned child processes to\n" + "exit before exiting itself. Systemd will automatically delete the\n" + "scope when there are no longer any processes running within the.\n" + "scope. Systemd will not automatically delete the slice.\n"); + printf("----------------------------------------------------------------\n\n"); + + wait_for_child(high_pid); + wait_for_child(med_pid); + wait_for_child(low_pid); + +error: + return ret; +} diff --git a/samples/cmdline/systemd-with-idle-process.md b/samples/cmdline/systemd-with-idle-process.md index 8e50e04b..b5acc8d4 100644 --- a/samples/cmdline/systemd-with-idle-process.md +++ b/samples/cmdline/systemd-with-idle-process.md @@ -11,6 +11,9 @@ The goal of this document is to outline the steps required to create a systemd scope and a child cgroup hierarchy using the libcgroup command line tools. +The following steps are encapsulated in a +[C example](../c/create_systemd_scope.c) using the libcgroup C APIs. + The following steps are encapsulated in a [libcgroup automated test](../../tests/ftests/086-sudo-systemd_cmdline_example.py). -- 2.47.2