feat: add plugin_irq
This new plugin enables the tuning of individual interrupt affinities. Signed-off-by: Adriaan Schmidt <adriaan.schmidt@siemens.com>
This commit is contained in:
parent
1fb7c803b6
commit
1aeb223b74
1 changed files with 272 additions and 0 deletions
272
tuned/plugins/plugin_irq.py
Normal file
272
tuned/plugins/plugin_irq.py
Normal file
|
|
@ -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<n>`, where `<n>` 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)
|
||||
Loading…
Reference in a new issue