From 1aeb223b74a3683da6ca36bfc8ba23fcd7a73572 Mon Sep 17 00:00:00 2001 From: Adriaan Schmidt Date: Tue, 21 May 2024 08:51:31 +0200 Subject: [PATCH] feat: add plugin_irq This new plugin enables the tuning of individual interrupt affinities. Signed-off-by: Adriaan Schmidt --- tuned/plugins/plugin_irq.py | 272 ++++++++++++++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 tuned/plugins/plugin_irq.py diff --git a/tuned/plugins/plugin_irq.py b/tuned/plugins/plugin_irq.py new file mode 100644 index 00000000..09684383 --- /dev/null +++ b/tuned/plugins/plugin_irq.py @@ -0,0 +1,272 @@ +from . import hotplug +from .decorators import * +import tuned.consts as consts +import tuned.logs + +import errno +import os + +log = tuned.logs.get() + +# the plugin manages each IRQ as a "device" and keeps a IrqInfo object for it +class IrqInfo(object): + def __init__(self, irq): + self.irq = irq + self.device = "irq%s" % irq + self.unchangeable = False + self.original_affinity = None + +class IrqPlugin(hotplug.Plugin): + r""" + `irq`:: + + Allows tuning of IRQ affinities, and thus re-implements functionality + already present in the `scheduler` plugin. However, this plugin offers + more flexibility, as it allows tuning of individual interrupts with + different affinities. When using the `irq` plugin, make sure to disable + IRQ processing in the `scheduler` plugin by setting its option + [option]`irq_process=false`. + The plugin handles individual IRQs as `devices`, and multiple plugin + instances can be defined, each addressing different devices/irqs. + The `device` names used by the plugin are `irq`, where `` is the + IRQ number. The special `device` `DEFAULT` controls values written to + `/proc/irq/default_smp_affinity`, which applies to all non-active IRQs. + === + The option [option]`affinity` controls the IRQ affinity to be set. It is + a string in "cpulist" format (such as 1,3-4). If the configured affinity + is empty, then the affinity of the respective IRQs is not touched. + === + The option [option]`mode` is a string which can either be `set` (default) + or `intersect`. In `set` mode the [option]`affinity` is always written + as configured, whereas in `intersect` mode, the new affinity will be + calculated as the intersection of the current and the configured affinity. + If that intersection is empty, the configured affinity will be used. + -- + .Example moving all IRQs to CPU0, except irq16, which is directed to CPU2 + ==== + ---- + [irq_special] + type=irq + devices=irq16 + affinity=2 + [irq] + affinity=0 + ---- + ==== + """ + + def __init__(self, monitor_repository, storage_factory, hardware_inventory, device_matcher, device_matcher_udev, plugin_instance_factory, global_cfg, variables): + super(IrqPlugin, self).__init__(monitor_repository, storage_factory, hardware_inventory, device_matcher, device_matcher_udev, plugin_instance_factory, global_cfg, variables) + self._irqs = {} + + # + # plugin-level methods: devices and plugin options + # + def _init_devices(self): + """Read /proc/irq to collect devices + """ + self._devices_supported = True + self._free_devices = set() + self._assigned_devices = set() + for i in os.listdir("/proc/irq"): + p = os.path.join("/proc/irq", i) + if os.path.isdir(p) and i.isdigit(): + info = IrqInfo(i) + self._irqs[i] = info + self._free_devices.add(info.device) + # add the virtual device for default_smp_affinity + default_info = IrqInfo("DEFAULT") + default_info.device = "DEFAULT" + self._irqs["DEFAULT"] = default_info + self._free_devices.add(default_info.device) + + @classmethod + def _get_config_options(cls): + return { + "affinity": "", + "mode": "set", + } + + # + # instance-level methods: implement the Instance interface + # + def _instance_init(self, instance): + instance._has_static_tuning = True + instance._has_dynamic_tuning = False + + affinity = instance.options.get("affinity") + affinity_list = self._cmd.cpulist_unpack(affinity) + if len(affinity.strip()) == 0: + # empty affinity in profile -> assume it's intentional + log.info("Instance '%s' configured with empty affinity. Deactivating." % instance.name) + instance._active = False + elif len(affinity_list) == 0: + # non-empty affinity string evaluates to empty list -> assume parse error + log.error("Instance '%s' with invalid affinity '%s'. Deactivating." % (instance.name, affinity)) + instance._active = False + + mode = instance.options.get("mode") + if mode not in ["set", "intersect"]: + log.error("Invalid operating mode '%s' for instance '%s'. Using the default 'set' instead." + % (mode, instance.name)) + instance.options["mode"] = "set" + + def _instance_cleanup(self, instance): + pass + + def _instance_apply_static(self, instance): + log.debug("Applying IRQ affinities (%s)" % instance.name) + super(IrqPlugin, self)._instance_apply_static(instance) + + def _instance_unapply_static(self, instance, rollback): + log.debug("Unapplying IRQ affinities (%s)" % instance.name) + super(IrqPlugin, self)._instance_unapply_static(instance, rollback) + + def _instance_verify_static(self, instance, ignore_missing, devices): + log.debug("Verifying IRQ affinities (%s)" % instance.name) + return super(IrqPlugin, self)._instance_verify_static(instance, ignore_missing, devices) + + # + # "low-level" methods to get/set irq affinities + # + def _get_irq_affinity(self, irq): + """Get current IRQ affinity from the kernel + + Args: + irq (str): IRQ number (as string) or "DEFAULT" + + Returns: + affinity (set): set of all CPUs that belong to the IRQ affinity mask, + if reading of the affinity fails, an empty set is returned + """ + try: + filename = "/proc/irq/default_smp_affinity" if irq == "DEFAULT" else "/proc/irq/%s/smp_affinity" % irq + with open(filename, "r") as f: + affinity_hex = f.readline().strip() + return set(self._cmd.hex2cpulist(affinity_hex)) + except (OSError, IOError) as e: + log.debug("Failed to read SMP affinity of IRQ %s: %s" % (irq, e)) + return set() + + def _set_irq_affinity(self, irq, affinity, restoring): + """Set IRQ affinity in the kernel + + Args: + irq (str): IRQ number (as string) or "DEFAULT" + affinity (set): affinity mask as set of CPUs + restoring (bool): are we rolling back a previous change? + + Returns: + status (int): 0 on success, -2 if changing the affinity is not + supported, -1 if some other error occurs + """ + try: + affinity_hex = self._cmd.cpulist2hex(list(affinity)) + log.debug("Setting SMP affinity of IRQ %s to '%s'" % (irq, affinity_hex)) + filename = "/proc/irq/default_smp_affinity" if irq == "DEFAULT" else "/proc/irq/%s/smp_affinity" % irq + with open(filename, "w") as f: + f.write(affinity_hex) + return 0 + except (OSError, IOError) as e: + # EIO is returned by + # kernel/irq/proc.c:write_irq_affinity() if changing + # the affinity is not supported + # (at least on kernels 3.10 and 4.18) + if hasattr(e, "errno") and e.errno == errno.EIO and not restoring: + log.debug("Setting SMP affinity of IRQ %s is not supported" % irq) + return -2 + else: + log.error("Failed to set SMP affinity of IRQ %s to '%s': %s" % (irq, affinity_hex, e)) + return -1 + + # + # "high-level" methods: apply tuning while saving original affinities + # + def _apply_irq_affinity(self, irqinfo, affinity, mode): + """Apply IRQ affinity tuning + + Args: + irqinfo (IrqInfo): IRQ that should be tuned + affinity (set): desired affinity + """ + original = self._get_irq_affinity(irqinfo.irq) + if mode == "intersect": + # intersection of affinity and original, if that is empty fall back to configured affinity + affinity = affinity & original or affinity + if irqinfo.unchangeable or affinity == original: + return + res = self._set_irq_affinity(irqinfo.irq, affinity, False) + if res == 0: + if irqinfo.original_affinity is None: + irqinfo.original_affinity = original + elif res == -2: + irqinfo.unchangeable = True + + def _restore_irq_affinity(self, irqinfo): + """Restore IRQ affinity + + Args: + irqinfo (IrqInfo): IRQ that should be restored + """ + if irqinfo.unchangeable or irqinfo.original_affinity is None: + return + self._set_irq_affinity(irqinfo.irq, irqinfo.original_affinity, True) + irqinfo.original_affinity = None + + def _verify_irq_affinity(self, irqinfo, affinity, mode): + """Verify IRQ affinity tuning + + Args: + irqinfo (IrqInfo): IRQ that should be verified + affinity (set): desired affinity + + Returns: + status (bool): True if verification successful, False otherwise + """ + if irqinfo.unchangeable: + return True + affinity_description = "IRQ %s affinity" % irqinfo.irq + desired_affinity = affinity + desired_affinity_string = self._cmd.cpulist2string(self._cmd.cpulist_pack(list(desired_affinity))) + current_affinity = self._get_irq_affinity(irqinfo.irq) + current_affinity_string = self._cmd.cpulist2string(self._cmd.cpulist_pack(list(current_affinity))) + if mode == "intersect": + # In intersect mode, we don't use a strict comparison; it's sufficient + # if the current affinity is a subset of the desired one + desired_affinity_string = "subset of " + desired_affinity_string + if ((mode == "intersect" and current_affinity <= desired_affinity) or + (mode == "set" and current_affinity == desired_affinity)): + log.info(consts.STR_VERIFY_PROFILE_VALUE_OK + % (affinity_description, desired_affinity_string)) + return True + else: + log.error(consts.STR_VERIFY_PROFILE_VALUE_FAIL + % (affinity_description, current_affinity_string, desired_affinity_string)) + return False + + # + # command definitions: entry to device-specific tuning + # + @command_custom("mode", per_device=False, priority=-10) + def _mode(self, enabling, value, verify, ignore_missing): + if (enabling or verify) and value is not None: + # Store the operating mode of the current instance in the plugin + # object, from where it is read by the "affinity" command. + # This works because instances are processed sequentially by the engine. + self._mode_val = value + + @command_custom("affinity", per_device=True) + def _affinity(self, enabling, value, device, verify, ignore_missing): + irq = "DEFAULT" if device == "DEFAULT" else device[len("irq"):] + if irq not in self._irqs: + log.error("Unknown device: %s" % device) + return None + irqinfo = self._irqs[irq] + if verify: + affinity = set(self._cmd.cpulist_unpack(value)) + return self._verify_irq_affinity(irqinfo, affinity, self._mode_val) + if enabling: + affinity = set(self._cmd.cpulist_unpack(value)) + return self._apply_irq_affinity(irqinfo, affinity, self._mode_val) + else: + return self._restore_irq_affinity(irqinfo)