From 66892d877c8c921f993bff5be9e2978ea215537b Mon Sep 17 00:00:00 2001 From: Quentin Deslandes Date: Mon, 16 Feb 2026 19:39:04 +0100 Subject: [PATCH] udev/net: add IRQAffinityPolicy= option for .link files Add support for configuring IRQ affinity for network interfaces via systemd .link files. For now, the new IRQAffinityPolicy= option in the [Link] section only accepts "single", which pins all MSI IRQs to CPU 0. This allows declarative IRQ affinity configuration for network devices during udev processing, which is useful for optimizing network performance on multi-core systems. Further commits will expand the options supported by IRQAffinityPolicy=. --- src/udev/net/link-config-gperf.gperf | 2 + src/udev/net/link-config.c | 107 +++++++++++++++ src/udev/net/link-config.h | 11 ++ src/udev/net/test-link-config-tables.c | 1 + test/units/TEST-17-UDEV.irq-affinity.sh | 171 ++++++++++++++++++++++++ 5 files changed, 292 insertions(+) create mode 100755 test/units/TEST-17-UDEV.irq-affinity.sh diff --git a/src/udev/net/link-config-gperf.gperf b/src/udev/net/link-config-gperf.gperf index ef68dab339..0b63a1e446 100644 --- a/src/udev/net/link-config-gperf.gperf +++ b/src/udev/net/link-config-gperf.gperf @@ -132,6 +132,8 @@ Link.TxMaxCoalescedHighFrames, config_parse_coalesce_u32, Link.CoalescePacketRateSampleIntervalSec, config_parse_coalesce_sec, 0, offsetof(LinkConfig, coalesce.rate_sample_interval) /* Rx RPS CPU mask */ Link.ReceivePacketSteeringCPUMask, config_parse_rps_cpu_mask, 0, offsetof(LinkConfig, rps_cpu_mask) +/* IRQ affinity settings */ +Link.IRQAffinityPolicy, config_parse_irq_affinity_policy, 0, offsetof(LinkConfig, irq_affinity_policy) /* SR-IOV settings */ Link.SR-IOVVirtualFunctions, config_parse_sr_iov_num_vfs, 0, offsetof(LinkConfig, sr_iov_num_vfs) SR-IOV.VirtualFunction, config_parse_sr_iov_uint32, 0, offsetof(LinkConfig, sr_iov_by_section) diff --git a/src/udev/net/link-config.c b/src/udev/net/link-config.c index 3c4b886e67..4a1512365d 100644 --- a/src/udev/net/link-config.c +++ b/src/udev/net/link-config.c @@ -15,6 +15,7 @@ #include "creds-util.h" #include "device-private.h" #include "device-util.h" +#include "dirent-util.h" #include "escape.h" #include "ether-addr-util.h" #include "ethtool-util.h" @@ -270,6 +271,7 @@ int link_load_one(LinkConfigContext *ctx, const char *filename) { .eee_enabled = -1, .eee_tx_lpi_enabled = -1, .eee_tx_lpi_timer_usec = USEC_INFINITY, + .irq_affinity_policy = _IRQ_AFFINITY_POLICY_INVALID, }; FOREACH_ELEMENT(feature, config->features) @@ -922,6 +924,96 @@ static int link_apply_sr_iov_config(Link *link) { return 0; } +static int set_irq_affinity(Link *link, unsigned irq, unsigned cpu) { + _cleanup_free_ char *affinity_path = NULL, *mask_str = NULL; + unsigned n_groups = cpu / 32; + int r; + + assert(link); + + if (asprintf(&affinity_path, "/proc/irq/%u/smp_affinity", irq) < 0) + return log_oom(); + + /* Convert CPU number to hex bitmask. + * For CPU N, set bit N (1 << N). CPUs are split by comma-separated + * 32-bits groups. To assign CPU 32, we should write 1,00000000 */ + + if (asprintf(&mask_str, "%x", 1U << (cpu % 32)) < 0) + return log_oom(); + + for (unsigned i = 0; i < n_groups; i++) + if (!strextend(&mask_str, ",00000000")) + return log_oom(); + + r = write_string_file(affinity_path, mask_str, WRITE_STRING_FILE_DISABLE_BUFFER); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to set IRQ %u affinity to CPU %u: %m", irq, cpu); + + log_link_debug(link, "Set IRQ %u affinity to CPU %u.", irq, cpu); + + return 0; +} + +static int link_apply_irq_affinity_single(Link *link) { + _cleanup_closedir_ DIR *dir = NULL; + int r; + + assert(link); + assert(link->config); + assert(ASSERT_PTR(link->event)->dev); + + r = device_opendir(link->event->dev, "device/msi_irqs", &dir); + if (r < 0) { + if (r != -ENOENT) + return log_link_error_errno(link, r, "Failed to open device/msi_irqs: %m"); + log_link_debug_errno(link, r, "No MSI IRQs found, skipping IRQ affinity configuration: %m"); + return 0; + } + + FOREACH_DIRENT(de, dir, return log_link_error_errno(link, errno, "Failed to read directory device/msi_irqs: %m")) { + unsigned irq; + + r = safe_atou(de->d_name, &irq); + if (r < 0) + return log_link_error_errno(link, r, "Failed to convert parse IRQ number: %s", de->d_name); + + (void) set_irq_affinity(link, irq, /* cpu= */ 0); + } + + log_link_info(link, "Applied IRQ affinity policy 'single' (pinning to CPU 0)."); + + return 0; +} + +static int link_apply_irq_affinity(Link *link) { + _cleanup_(cpu_set_done) CPUSet effective_cpus = {}; + const char *syspath; + int r; + + assert(link); + assert(link->config); + assert(ASSERT_PTR(link->event)->dev); + + if (link->event->event_mode != EVENT_UDEV_WORKER) { + log_link_debug(link, "Running in test mode, skipping application of IRQ affinity settings."); + return 0; + } + + if (link->config->irq_affinity_policy < 0) + return 0; + + r = sd_device_get_syspath(link->event->dev, &syspath); + if (r < 0) + return log_link_warning_errno(link, r, "Failed to get syspath: %m"); + + switch (link->config->irq_affinity_policy) { + case IRQ_AFFINITY_POLICY_SINGLE: + return link_apply_irq_affinity_single(link); + default: + assert_not_reached(); + } +} + static int link_apply_rps_cpu_mask(Link *link) { _cleanup_free_ char *mask_str = NULL; LinkConfig *config; @@ -1051,6 +1143,10 @@ int link_apply_config(LinkConfigContext *ctx, Link *link) { if (r < 0) return r; + r = link_apply_irq_affinity(link); + if (r < 0) + return r; + return link_apply_udev_properties(link); } @@ -1400,3 +1496,14 @@ DEFINE_CONFIG_PARSE_ENUMV(config_parse_name_policy, name_policy, NamePolicy, DEFINE_CONFIG_PARSE_ENUMV(config_parse_alternative_names_policy, alternative_names_policy, NamePolicy, _NAMEPOLICY_INVALID); + +static const char* const irq_affinity_policy_table[_IRQ_AFFINITY_POLICY_MAX] = { + [IRQ_AFFINITY_POLICY_SINGLE] = "single", +}; + +DEFINE_STRING_TABLE_LOOKUP(irq_affinity_policy, IRQAffinityPolicy); +DEFINE_CONFIG_PARSE_ENUM_WITH_DEFAULT( + config_parse_irq_affinity_policy, + irq_affinity_policy, + IRQAffinityPolicy, + _IRQ_AFFINITY_POLICY_INVALID); diff --git a/src/udev/net/link-config.h b/src/udev/net/link-config.h index e00327c015..b6853cd922 100644 --- a/src/udev/net/link-config.h +++ b/src/udev/net/link-config.h @@ -22,6 +22,12 @@ typedef enum MACAddressPolicy { _MAC_ADDRESS_POLICY_INVALID = -EINVAL, } MACAddressPolicy; +typedef enum IRQAffinityPolicy { + IRQ_AFFINITY_POLICY_SINGLE, + _IRQ_AFFINITY_POLICY_MAX, + _IRQ_AFFINITY_POLICY_INVALID = -EINVAL, +} IRQAffinityPolicy; + typedef struct Link { UdevEvent *event; LinkConfig *config; @@ -113,6 +119,9 @@ struct LinkConfig { /* Rx RPS CPU mask */ CPUSet rps_cpu_mask; + /* IRQ affinity */ + IRQAffinityPolicy irq_affinity_policy; + /* SR-IOV */ uint32_t sr_iov_num_vfs; OrderedHashmap *sr_iov_by_section; @@ -136,6 +145,7 @@ int link_get_config(LinkConfigContext *ctx, Link *link); int link_apply_config(LinkConfigContext *ctx, Link *link); DECLARE_STRING_TABLE_LOOKUP(mac_address_policy, MACAddressPolicy); +DECLARE_STRING_TABLE_LOOKUP(irq_affinity_policy, IRQAffinityPolicy); /* gperf lookup function */ const struct ConfigPerfItem* link_config_gperf_lookup(const char *str, GPERF_LEN_TYPE length); @@ -150,3 +160,4 @@ CONFIG_PARSER_PROTOTYPE(config_parse_mac_address_policy); CONFIG_PARSER_PROTOTYPE(config_parse_name_policy); CONFIG_PARSER_PROTOTYPE(config_parse_alternative_names_policy); CONFIG_PARSER_PROTOTYPE(config_parse_rps_cpu_mask); +CONFIG_PARSER_PROTOTYPE(config_parse_irq_affinity_policy); diff --git a/src/udev/net/test-link-config-tables.c b/src/udev/net/test-link-config-tables.c index 499778505c..58b628bc9b 100644 --- a/src/udev/net/test-link-config-tables.c +++ b/src/udev/net/test-link-config-tables.c @@ -8,6 +8,7 @@ int main(int argc, char **argv) { test_setup_logging(LOG_DEBUG); test_table(MACAddressPolicy, mac_address_policy, MAC_ADDRESS_POLICY); + test_table(IRQAffinityPolicy, irq_affinity_policy, IRQ_AFFINITY_POLICY); return EXIT_SUCCESS; } diff --git a/test/units/TEST-17-UDEV.irq-affinity.sh b/test/units/TEST-17-UDEV.irq-affinity.sh new file mode 100755 index 0000000000..5fa8bd331f --- /dev/null +++ b/test/units/TEST-17-UDEV.irq-affinity.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: LGPL-2.1-or-later +set -eux +set -o pipefail + +# shellcheck source=test/units/util.sh +. "$(dirname "$0")"/util.sh + +# Test IRQAffinityPolicy configuration + +# Find a network interface with MSI IRQs (typically virtio-net in the VM) +# We need a real device to test actual IRQ affinity application +find_interface_with_msi_irqs() { + for iface in /sys/class/net/*; do + [[ -e "$iface" ]] || continue + iface_name=$(basename "$iface") + [[ "$iface_name" == "lo" ]] && continue + msi_irqs_path="$iface/device/msi_irqs" + if [[ -d "$msi_irqs_path" ]] && [[ -n "$(ls -A "$msi_irqs_path" 2>/dev/null)" ]]; then + echo "$iface_name" + return 0 + fi + done + return 1 +} + +get_interface_irqs() { + local iface="$1" + local msi_irqs_path="/sys/class/net/$iface/device/msi_irqs" + if [[ -d "$msi_irqs_path" ]]; then + ls "$msi_irqs_path" + fi +} + +check_irq_affinity() { + local irq="$1" + local expected_mask="$2" + local affinity + affinity=$(cat "/proc/irq/$irq/smp_affinity") + # Remove leading zeros and commas for comparison + affinity=$(echo "$affinity" | sed 's/^[0,]*//;s/,//g') + expected_mask=$(echo "$expected_mask" | sed 's/^[0,]*//;s/,//g') + [[ "$affinity" == "$expected_mask" ]] +} + +# Test 1: Verify IRQ affinity is actually applied on a real device +if iface=$(find_interface_with_msi_irqs); then + echo "Found interface with MSI IRQs: $iface" + + # Get the MAC address of the interface + mac=$(cat "/sys/class/net/$iface/address") + + # Get IRQs before applying policy + irqs=$(get_interface_irqs "$iface") + echo "Interface $iface has IRQs: $irqs" + + # Test 1a: test single policy + mkdir -p /run/systemd/network/ + cat >/run/systemd/network/00-test-irq-affinity.link </run/systemd/network/10-test-irq.link <&1) +assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq.link" "$output" + +# Test 3: Invalid policy values are rejected/warned +cat >/run/systemd/network/10-test-irq-invalid.link </run/systemd/network/10-test-irq-empty.link <