diff options
-rw-r--r-- | man/iocost.conf.xml | 76 | ||||
-rw-r--r-- | man/rules/meson.build | 1 | ||||
-rw-r--r-- | rules.d/90-iocost.rules | 20 | ||||
-rw-r--r-- | rules.d/meson.build | 1 | ||||
-rw-r--r-- | src/udev/iocost/iocost.c | 334 | ||||
-rw-r--r-- | src/udev/iocost/iocost.conf | 17 | ||||
-rw-r--r-- | src/udev/meson.build | 3 |
7 files changed, 452 insertions, 0 deletions
diff --git a/man/iocost.conf.xml b/man/iocost.conf.xml new file mode 100644 index 0000000000..be74244267 --- /dev/null +++ b/man/iocost.conf.xml @@ -0,0 +1,76 @@ +<?xml version='1.0'?> +<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN" + "http://www.oasis-open.org/docbook/xml/4.2/docbookx.dtd"> +<!-- SPDX-License-Identifier: LGPL-2.1-or-later --> + +<refentry id="iocost.conf" xmlns:xi="http://www.w3.org/2001/XInclude"> + <refentryinfo> + <title>iocost.conf</title> + <productname>systemd</productname> + </refentryinfo> + + <refmeta> + <refentrytitle>iocost.conf</refentrytitle> + <manvolnum>5</manvolnum> + </refmeta> + + <refnamediv> + <refname>iocost.conf</refname> + <refpurpose>Configuration files for the iocost solution manager</refpurpose> + </refnamediv> + + <refsynopsisdiv> + <para> + <filename>/etc/systemd/iocost.conf</filename> + <filename>/etc/systemd/iocost.conf.d/*.conf</filename> + </para> + </refsynopsisdiv> + + <refsect1> + <title>Description</title> + + <para>This file configures the behavior of <literal>iocost</literal>, a tool mostly used by + <citerefentry><refentrytitle>systemd-udevd</refentrytitle><manvolnum>8</manvolnum></citerefentry> rules + to automatically apply I/O cost solutions to <filename>/sys/fs/cgroup/io.cost.*</filename>.</para> + + <para>The qos and model values are calculated based on benchmarks collected on the + <ulink url="https://github.com/iocost-benchmark/iocost-benchmarks">iocost-benchmark</ulink> + project and turned into a set of solutions that go from most to least isolated. + Isolation allows the system to remain responsive in face of high I/O load. + Which solutions are available for a device can be queried from the udev metadata attached to it. By + default the naive solution is used, which provides the most bandwidth.</para> + </refsect1> + + <xi:include href="standard-conf.xml" xpointer="main-conf" /> + + <refsect1> + <title>Options</title> + + <para>All options are configured in the [IOCost] section:</para> + + <variablelist class='config-directives'> + + <varlistentry> + <term><varname>TargetSolution=</varname></term> + + <listitem><para>Chooses which I/O cost solution (identified by named string) should be used + for the devices in this system. The known solutions can be queried from the udev metadata + attached to the devices. If a device does not have the specified solution, the first one + listed in <varname>IOCOST_SOLUTIONS</varname> is used instead.</para> + + <para>E.g. <literal>TargetSolution=isolated-bandwidth</literal>.</para></listitem> + </varlistentry> + </variablelist> + </refsect1> + + <refsect1> + <title>See Also</title> + <para> + <citerefentry><refentrytitle>udevadm</refentrytitle><manvolnum>8</manvolnum></citerefentry>, + <ulink url="https://github.com/iocost-benchmark/iocost-benchmarks">The iocost-benchmarks github project</ulink>, + <ulink url="https://github.com/facebookexperimental/resctl-demo/tree/main/resctl-bench/doc">The resctl-bench + documentation details how the values are obtained</ulink> + </para> + </refsect1> + +</refentry> diff --git a/man/rules/meson.build b/man/rules/meson.build index cdf98eaaf0..ca3b471281 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -24,6 +24,7 @@ manpages = [ ['hostnamectl', '1', [], 'ENABLE_HOSTNAMED'], ['hwdb', '7', [], 'ENABLE_HWDB'], ['integritytab', '5', [], 'HAVE_LIBCRYPTSETUP'], + ['iocost.conf', '5', [], ''], ['journal-remote.conf', '5', ['journal-remote.conf.d'], 'HAVE_MICROHTTPD'], ['journal-upload.conf', '5', ['journal-upload.conf.d'], 'HAVE_MICROHTTPD'], ['journalctl', '1', [], ''], diff --git a/rules.d/90-iocost.rules b/rules.d/90-iocost.rules new file mode 100644 index 0000000000..50f778a0ae --- /dev/null +++ b/rules.d/90-iocost.rules @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +SUBSYSTEM!="block", GOTO="iocost_end" + +ENV{DEVTYPE}=="partition", GOTO="iocost_end" + +ACTION=="remove", GOTO="iocost_end" + +ENV{ID_MODEL}!="", IMPORT{builtin}="hwdb 'block::name:$env{ID_MODEL}:fwrev:$env{ID_REVISION}:'" + +ENV{IOCOST_SOLUTIONS}!="", RUN+="iocost apply $env{DEVNAME}" + +LABEL="iocost_end" diff --git a/rules.d/meson.build b/rules.d/meson.build index 09edd58da2..7280f5b995 100644 --- a/rules.d/meson.build +++ b/rules.d/meson.build @@ -28,6 +28,7 @@ rules = [ '78-sound-card.rules', '80-net-setup-link.rules', '81-net-dhcp.rules', + '90-iocost.rules', )], [files('80-drivers.rules'), diff --git a/src/udev/iocost/iocost.c b/src/udev/iocost/iocost.c new file mode 100644 index 0000000000..54b50b4a8d --- /dev/null +++ b/src/udev/iocost/iocost.c @@ -0,0 +1,334 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include <errno.h> +#include <getopt.h> +#include <stdio.h> +#include <unistd.h> + +#include "sd-device.h" + +#include "alloc-util.h" +#include "build.h" +#include "cgroup-util.h" +#include "conf-parser.h" +#include "devnum-util.h" +#include "device-util.h" +#include "main-func.h" +#include "path-util.h" +#include "pretty-print.h" +#include "verbs.h" + +static char *arg_target_solution = NULL; +STATIC_DESTRUCTOR_REGISTER(arg_target_solution, freep); + +static int parse_config(void) { + static const ConfigTableItem items[] = { + { "IOCost", "TargetSolution", config_parse_string, 0, &arg_target_solution }, + }; + return config_parse( + NULL, + "/etc/udev/iocost.conf", + NULL, + "IOCost\0", + config_item_table_lookup, + items, + CONFIG_PARSE_WARN, + NULL, + NULL); +} + +static int help(void) { + printf("%s [OPTIONS...]\n\n" + "Set up iocost model and qos solutions for block devices\n" + "\nCommands:\n" + " apply <path> [SOLUTION] Apply solution for the device if\n" + " found, do nothing otherwise\n" + " query <path> Query the known solution for\n" + " the device\n" + "\nOptions:\n" + " -h --help Show this help\n" + " --version Show package version\n", + program_invocation_short_name); + + return 0; +} + +static int parse_argv(int argc, char *argv[]) { + enum { + ARG_VERSION = 0x100, + }; + + static const struct option options[] = { + { "help", no_argument, NULL, 'h' }, + { "version", no_argument, NULL, ARG_VERSION }, + {} + }; + + int c; + + assert(argc >= 1); + assert(argv); + + while ((c = getopt_long(argc, argv, "h", options, NULL)) >= 0) + switch (c) { + + case 'h': + return help(); + + case ARG_VERSION: + return version(); + + case '?': + return -EINVAL; + + default: + assert_not_reached(); + } + + return 1; +} + +static int get_known_solutions(sd_device *device, char ***ret_solutions) { + _cleanup_free_ char **s = NULL; + const char *value; + int r; + + assert(ret_solutions); + + r = sd_device_get_property_value(device, "IOCOST_SOLUTIONS", &value); + if (r < 0) + return r; + + s = strv_split(value, WHITESPACE); + if (!s) + return -ENOMEM; + + *ret_solutions = TAKE_PTR(s); + + return 0; +} + +static int choose_solution(char **solutions, const char **ret_name) { + assert(ret_name); + + if (strv_isempty(solutions)) + return log_error_errno( + SYNTHETIC_ERRNO(EINVAL), "IOCOST_SOLUTIONS exists in hwdb but is empty."); + + if (arg_target_solution && strv_find(solutions, arg_target_solution)) { + *ret_name = arg_target_solution; + log_debug("Selected solution based on target solution: %s", *ret_name); + } else { + *ret_name = solutions[0]; + log_debug("Selected first available solution: %s", *ret_name); + } + + return 0; +} + +static int query_named_solution( + sd_device *device, + const char *name, + const char **ret_model, + const char **ret_qos) { + + _cleanup_strv_free_ char **solutions = NULL; + _cleanup_free_ char *upper_name = NULL, *qos_key = NULL, *model_key = NULL; + const char *qos = NULL, *model = NULL; + int r; + + assert(ret_qos); + assert(ret_model); + + /* If NULL is passed we query the default solution, which is the first one listed + * in the IOCOST_SOLUTIONS key or the one specified by the TargetSolution setting. + */ + if (!name) { + r = get_known_solutions(device, &solutions); + if (r == -ENOENT) + return log_device_debug_errno(device, r, "No entry found for device, skipping iocost logic."); + if (r < 0) + return log_device_error_errno(device, r, "Failed to query solutions from device: %m"); + + r = choose_solution(solutions, &name); + if (r < 0) + return r; + } + + upper_name = strdup(name); + if (!upper_name) + return log_oom(); + + ascii_strupper(upper_name); + string_replace_char(upper_name, '-', '_'); + + qos_key = strjoin("IOCOST_QOS_", upper_name); + if (!qos_key) + return log_oom(); + + model_key = strjoin("IOCOST_MODEL_", upper_name); + if (!model_key) + return log_oom(); + + r = sd_device_get_property_value(device, qos_key, &qos); + if (r == -ENOENT) + return log_device_debug_errno(device, r, "No value found for key %s, skipping iocost logic.", qos_key); + if (r < 0) + return log_device_error_errno(device, r, "Failed to obtain model for iocost solution from device: %m"); + + r = sd_device_get_property_value(device, model_key, &model); + if (r == -ENOENT) + return log_device_debug_errno(device, r, "No value found for key %s, skipping iocost logic.", model_key); + if (r < 0) + return log_device_error_errno(device, r, "Failed to obtain model for iocost solution from device: %m"); + + *ret_qos = qos; + *ret_model = model; + + return 0; +} + +static int apply_solution_for_path(const char *path, const char *name) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + _cleanup_free_ char *qos = NULL, *model = NULL; + const char *qos_params = NULL, *model_params = NULL; + dev_t devnum; + int r; + + r = sd_device_new_from_path(&device, path); + if (r < 0) + return log_error_errno(r, "Error looking up device: %m"); + + r = query_named_solution(device, name, &model_params, &qos_params); + if (r == -ENOENT) + return 0; + if (r < 0) + return r; + + r = sd_device_get_devnum(device, &devnum); + if (r < 0) + return log_device_error_errno(device, r, "Error getting devnum: %m"); + + if (asprintf(&qos, DEVNUM_FORMAT_STR " enable=1 ctrl=user %s", DEVNUM_FORMAT_VAL(devnum), qos_params) < 0) + return log_oom(); + + if (asprintf(&model, DEVNUM_FORMAT_STR " model=linear ctrl=user %s", DEVNUM_FORMAT_VAL(devnum), model_params) < 0) + return log_oom(); + + log_debug("Applying iocost parameters to %s using solution '%s'\n" + "\tio.cost.qos: %s\n" + "\tio.cost.model: %s\n", path, name ?: "default", qos, model); + + r = cg_set_attribute("io", NULL, "io.cost.qos", qos); + if (r < 0) { + log_device_full_errno(device, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to set io.cost.qos: %m"); + return r == -ENOENT ? 0 : r; + } + + r = cg_set_attribute("io", NULL, "io.cost.model", model); + if (r < 0) { + log_device_full_errno(device, r == -ENOENT ? LOG_DEBUG : LOG_ERR, r, "Failed to set io.cost.model: %m"); + return r == -ENOENT ? 0 : r; + } + + return 0; +} + +static int query_solutions_for_path(const char *path) { + _cleanup_(sd_device_unrefp) sd_device *device = NULL; + _cleanup_strv_free_ char **solutions = NULL; + const char *default_solution = NULL; + const char *model_name = NULL; + int r; + + r = sd_device_new_from_path(&device, path); + if (r < 0) + return log_error_errno(r, "Error looking up device: %m"); + + r = sd_device_get_property_value(device, "ID_MODEL_FROM_DATABASE", &model_name); + if (r == -ENOENT) { + log_device_debug(device, "Missing ID_MODEL_FROM_DATABASE property, trying ID_MODEL"); + r = sd_device_get_property_value(device, "ID_MODEL", &model_name); + if (r == -ENOENT) { + log_device_info(device, "Device model not found"); + return 0; + } + } + if (r < 0) + return log_device_error_errno(device, r, "Model name for device %s is unknown", path); + + r = get_known_solutions(device, &solutions); + if (r == -ENOENT) { + log_device_info(device, "Attribute IOCOST_SOLUTIONS missing, model not found in hwdb."); + return 0; + } + if (r < 0) + return log_device_error_errno(device, r, "Couldn't access IOCOST_SOLUTIONS for device %s, model name %s on hwdb: %m\n", path, model_name); + + r = choose_solution(solutions, &default_solution); + if (r < 0) + return r; + + log_info("Known solutions for %s model name: \"%s\"\n" + "Preferred solution: %s\n" + "Solution that would be applied: %s", + path, model_name, + arg_target_solution, default_solution); + + STRV_FOREACH(s, solutions) { + const char *model = NULL, *qos = NULL; + + r = query_named_solution(device, *s, &model, &qos); + if (r < 0 || !model || !qos) + continue; + + log_info("%s: io.cost.qos: %s\n" + "%s: io.cost.model: %s", *s, qos, *s, model); + } + + return 0; +} + +static int verb_query(int argc, char *argv[], void *userdata) { + return query_solutions_for_path(ASSERT_PTR(argv[1])); +} + +static int verb_apply(int argc, char *argv[], void *userdata) { + return apply_solution_for_path( + ASSERT_PTR(argv[1]), + argc > 2 ? ASSERT_PTR(argv[2]) : NULL); +} + +static int iocost_main(int argc, char *argv[]) { + static const Verb verbs[] = { + { "query", 2, 2, 0, verb_query }, + { "apply", 2, 3, 0, verb_apply }, + {}, + }; + + return dispatch_verb(argc, argv, verbs, NULL); +} + +static int run(int argc, char *argv[]) { + int r; + + log_setup(); + + r = parse_argv(argc, argv); + if (r <= 0) + return r; + + (void) parse_config(); + + if (!arg_target_solution) { + arg_target_solution = strdup("naive"); + if (!arg_target_solution) + return log_oom(); + } + + log_debug("Target solution: %s.", arg_target_solution); + + return iocost_main(argc, argv); +} + +DEFINE_MAIN_FUNCTION(run); diff --git a/src/udev/iocost/iocost.conf b/src/udev/iocost/iocost.conf new file mode 100644 index 0000000000..394ea349ee --- /dev/null +++ b/src/udev/iocost/iocost.conf @@ -0,0 +1,17 @@ +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it under the +# terms of the GNU Lesser General Public License as published by the Free +# Software Foundation; either version 2.1 of the License, or (at your option) +# any later version. +# +# Entries in this file show the compile time defaults. Local configuration +# should be created by either modifying this file. Defaults can be restored by +# simply deleting this file. +# +# Use 'systemd-analyze cat-config udev/iocost.conf' to display the full config. +# +# See iocost.conf(5) for details. + +[IOCost] +#TargetSolution=naive diff --git a/src/udev/meson.build b/src/udev/meson.build index 5b44dd8d7d..081948d223 100644 --- a/src/udev/meson.build +++ b/src/udev/meson.build @@ -116,6 +116,7 @@ udev_progs = [['ata_id/ata_id.c'], ['cdrom_id/cdrom_id.c'], ['fido_id/fido_id.c', 'fido_id/fido_id_desc.c'], + ['iocost/iocost.c'], ['scsi_id/scsi_id.c', 'scsi_id/scsi_serial.c'], ['v4l_id/v4l_id.c'], @@ -149,6 +150,8 @@ endforeach if install_sysconfdir_samples install_data('udev.conf', install_dir : sysconfdir / 'udev') + install_data('iocost/iocost.conf', + install_dir : sysconfdir / 'udev') endif custom_target( |