systemd-udevd: configure NIC IRQs CPU affinity (#40304)
# Context #40195 defines the initial proposal and the motivation behind this PR. This PR introduces 3 new options for `.link` files `[Link]` section: - `IRQAffinityPolicy=` - `IRQAffinity=` - `IRQAffinityNUMA=` The purpose is to allow `systemd-udevd` to configure a NIC's IRQs affinity to specific CPU(s). `IRQAffinityPolicy=` supports two policies: - `single`: assign all the NIC IRQs to CPU 0, or the first CPU in the CPU set resulting from the union of `IRQAffinity=` and `IRQAffinityNUMA=`. - `spread`: assign all the NIC IRQs to all the CPUs (or the union of `IRQAffinity=` and `IRQAffinityNUMA=` if defined) in a round-robin fashion while optimizing for cache locally while spreading apart queues on CPUs as much as possible. Both `IRQAffinity=` and `IRQAffinityNUMA=` behaves as filters to reduce the CPU set to assign IRQs to, and are only valid if `IRQAffinityPolicy=` is defined. # Spreading IRQs This section describes the algorithm responsible for spreading IRQs over different CPUs to maximize performance. ## 1. Discover CPU topology Read from `/sys/devices/system/cpu/cpu*/topology` to identify: - L3 cache domains (dies) - Physical cores VS hyperthreads - NUMA nodes - Core ordering within each die ``` Example: Dual-socket server with 2 dies per socket, 4 cores per die ┌─────────────────────────────────────────────────────────────────┐ │ NUMA Node 0 │ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │ │ │ Die 0 (L3 Cache) │ │ Die 1 (L3 Cache) │ │ │ │ ┌────┐┌────┐┌────┐┌────│ │ ┌────┐┌────┐┌────┐┌────│ │ │ │ │ C0 ││ C1 ││ C2 ││ C3 │ │ │ C4 ││ C5 ││ C6 ││ C7 │ │ │ │ │0,16││1,17││2,18││3,19│ │ │4,20││5,21││6,22││7,23│ │ │ │ └────┘└────┘└────┘└────│ │ └────┘└────┘└────┘└────│ │ │ └─────────────────────────┘ └─────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────┐ │ NUMA Node 1 │ │ ┌─────────────────────────┐ ┌─────────────────────────┐ │ │ │ Die 2 (L3 Cache) │ │ Die 3 (L3 Cache) │ │ │ │ ┌────┐┌────┐┌────┐┌────│ │ ┌────┐┌────┐┌────┐┌────│ │ │ │ │ C8 ││ C9 ││C10 ││C11 │ │ │C12 ││C13 ││C14 ││C15 │ │ │ │ │8,24││9,25││10,2││11,2│ │ │12,2││13,2││14,3││15,3│ │ │ │ └────┘└────┘└────┘└────│ │ └────┘└────┘└────┘└────│ │ │ └─────────────────────────┘ └─────────────────────────┘ │ └─────────────────────────────────────────────────────────────────┘ (Numbers show CPU IDs: first HT, second HT) ``` ## 2. Filter the first hyperthread only Use only the first hyperthread of each physical core to avoid SMT contention. Two IRQs on sibling HTs contend for ALU/cache without cache benefit. ``` Before: CPUs 0-31 (16 cores × 2 HTs) After: CPUs 0-15 (first HT of each core) ``` ## 3. Equidistant permutations Reorder dies and CPUs so consecutive selections are maximally spread apart. ``` Original order: [0, 1, 2, 3] (adjacent dies/CPUs) | v Equidistant: [0, 2, 1, 3] (spread apart) ``` This ensures that even if only 2 IRQs are assigned, they land on dies 0 and 2 (not 0 and 1), maximizing physical distance. The permutation is also applied within each die: ``` Die permutation: Die0 -> Die2 -> Die1 -> Die3 Within each die: ┌───────────────────────────────────────────────┐ │ Die 0: [C0,C1,C2,C3] -> [C0,C2,C1,C3] │ │ Die 1: [C4,C5,C6,C7] -> [C4,C6,C5,C7] │ │ Die 2: [C8,C9,C10,C11] -> [C8,C10,C9,C11] │ │ Die 3: [C12,C13,C14,C15] -> [C12,C14,C13,C15] │ └───────────────────────────────────────────────┘ ``` ## 4. Round-robin selection across dies Pick one CPU from each die in rotation, following permuted order. ``` Round 1: Die0->C0 Die2->C8 Die1->C4 Die3->C12 | | | | v v v v IRQs: [IRQ0] [IRQ1] [IRQ2] [IRQ3] Round 2: Die0->C2 Die2->C10 Die1->C6 Die3->C14 | | | | v v v v IRQs: [IRQ4] [IRQ5] [IRQ6] [IRQ7] ``` If there are more IRQs than physical cores, this logic wraps around and reuse CPUs. Only the first hyperthread of each core is used to avoid cache line contention between queues. Closes #40195.
This commit is contained in:
@@ -1080,6 +1080,62 @@
|
||||
<xi:include href="version-info.xml" xpointer="v256"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term><varname>IRQAffinityPolicy=</varname></term>
|
||||
<listitem>
|
||||
<para>Specifies the IRQ distribution strategy for network interface MSI/MSI-X interrupts. Takes one
|
||||
of <literal>spread</literal> or <literal>single</literal>. When set to <literal>spread</literal>,
|
||||
queue IRQs are distributed across CPUs using a topology-aware maximum distance algorithm that
|
||||
prefers CPUs on different NUMA nodes, then different physical cores, then different hyperthreads.
|
||||
When set to <literal>single</literal>, all IRQs are pinned to a single CPU (CPU 0 by default, or
|
||||
the first CPU in the set specified by <varname>IRQAffinity=</varname>). When there are more IRQs
|
||||
than available CPUs, queues wrap around using round-robin assignment. When unset, no affinity
|
||||
management is performed. Setting this to an empty string explicitly disables affinity management.</para>
|
||||
|
||||
<para>This option only applies to devices with MSI/MSI-X interrupts discoverable via
|
||||
<filename>/sys/class/net/<replaceable>iface</replaceable>/device/msi_irqs/</filename>. Virtual
|
||||
devices (veth, tap) and legacy INTx devices are skipped with a notice logged.</para>
|
||||
|
||||
<para>Note that if <citerefentry><refentrytitle>irqbalance</refentrytitle><manvolnum>1</manvolnum>
|
||||
</citerefentry> or similar IRQ management daemons are running, they may override the configured
|
||||
affinity. Consider disabling such daemons or configuring them to exclude managed interfaces.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v260"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term><varname>IRQAffinity=</varname></term>
|
||||
<listitem>
|
||||
<para>Filters the set of CPUs eligible for IRQ placement. Takes a list of CPU indices or ranges
|
||||
separated by either whitespace or commas (e.g., <literal>0-3</literal>, <literal>0,2,4,6</literal>,
|
||||
<literal>0-3,8-11</literal>). This option works in conjunction with
|
||||
<varname>IRQAffinityPolicy=</varname> and <varname>IRQAffinityNUMA=</varname> to constrain which
|
||||
CPUs receive network IRQs. When specified with <literal>spread</literal> policy, only the listed
|
||||
CPUs are considered for IRQ distribution. When specified with <literal>single</literal> policy,
|
||||
IRQs are pinned to the first CPU in the allowed set instead of CPU 0. Has no effect if
|
||||
<varname>IRQAffinityPolicy=</varname> is not set.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v260"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term><varname>IRQAffinityNUMA=</varname></term>
|
||||
<listitem>
|
||||
<para>Filters CPUs to those belonging to the specified NUMA node. Takes either
|
||||
<literal>local</literal> or an explicit NUMA node number (0, 1, 2, ...). When set to
|
||||
<literal>local</literal>, the NUMA node local to the NIC's PCIe slot is used (determined from
|
||||
<filename>/sys/class/net/<replaceable>iface</replaceable>/device/numa_node</filename>). If the
|
||||
device's NUMA node cannot be determined (e.g., non-NUMA system), a warning is logged and IRQ
|
||||
affinity configuration is skipped.</para>
|
||||
|
||||
<para>When both <varname>IRQAffinity=</varname> and <varname>IRQAffinityNUMA=</varname> are
|
||||
specified, their intersection is used. If the intersection results in an empty set, an error is
|
||||
logged and no affinity is applied. Has no effect if <varname>IRQAffinityPolicy=</varname> is not
|
||||
set.</para>
|
||||
|
||||
<xi:include href="version-info.xml" xpointer="v260"/>
|
||||
</listitem>
|
||||
</varlistentry>
|
||||
<varlistentry>
|
||||
<term><varname>ReceiveVLANCTAGHardwareAcceleration=</varname></term>
|
||||
<listitem>
|
||||
|
||||
@@ -81,3 +81,12 @@ int cmp_uint16(const uint16_t *a, const uint16_t *b) {
|
||||
|
||||
return CMP(*a, *b);
|
||||
}
|
||||
|
||||
int cmp_unsigned(const unsigned *a, const unsigned *b) {
|
||||
/* This is called from qsort()s inner loops. Correctly implemented qsort will never pass NULL so we
|
||||
just suppress the check via POINTER_MAY_BE_NULL instead of assert() to avoid the runtime cost. */
|
||||
POINTER_MAY_BE_NULL(a);
|
||||
POINTER_MAY_BE_NULL(b);
|
||||
|
||||
return CMP(*a, *b);
|
||||
}
|
||||
|
||||
@@ -44,3 +44,4 @@ void qsort_r_safe(void *base, size_t nmemb, size_t size, comparison_userdata_fn_
|
||||
|
||||
int cmp_int(const int *a, const int *b);
|
||||
int cmp_uint16(const uint16_t *a, const uint16_t *b);
|
||||
int cmp_unsigned(const unsigned *a, const unsigned *b);
|
||||
|
||||
+74
-10
@@ -89,6 +89,22 @@ int apply_numa_policy(const NUMAPolicy *policy) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int numa_node_get_cpus(size_t node, CPUSet *ret) {
|
||||
char p[STRLEN("/sys/devices/system/node/node//cpulist") + DECIMAL_STR_MAX(size_t) + 1];
|
||||
_cleanup_free_ char *cpulist = NULL;
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
|
||||
xsprintf(p, "/sys/devices/system/node/node%zu/cpulist", node);
|
||||
|
||||
r = read_virtual_file(p, SIZE_MAX, &cpulist, /* ret_size= */ NULL);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
return parse_cpu_set(cpulist, ret);
|
||||
}
|
||||
|
||||
int numa_to_cpu_set(const NUMAPolicy *policy, CPUSet *ret) {
|
||||
_cleanup_(cpu_set_done) CPUSet s = {};
|
||||
int r;
|
||||
@@ -97,20 +113,11 @@ int numa_to_cpu_set(const NUMAPolicy *policy, CPUSet *ret) {
|
||||
assert(ret);
|
||||
|
||||
for (size_t i = 0; i < policy->nodes.allocated * 8; i++) {
|
||||
_cleanup_free_ char *l = NULL;
|
||||
char p[STRLEN("/sys/devices/system/node/node//cpulist") + DECIMAL_STR_MAX(size_t) + 1];
|
||||
|
||||
if (!CPU_ISSET_S(i, policy->nodes.allocated, policy->nodes.set))
|
||||
continue;
|
||||
|
||||
xsprintf(p, "/sys/devices/system/node/node%zu/cpulist", i);
|
||||
|
||||
r = read_one_line_file(p, &l);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
_cleanup_(cpu_set_done) CPUSet part = {};
|
||||
r = parse_cpu_set(l, &part);
|
||||
r = numa_node_get_cpus(i, &part);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
@@ -154,6 +161,63 @@ static int numa_max_node(void) {
|
||||
return max_node;
|
||||
}
|
||||
|
||||
int numa_get_node_from_cpu(unsigned cpu, unsigned *ret) {
|
||||
_cleanup_closedir_ DIR *d = NULL;
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
|
||||
d = opendir("/sys/devices/system/node");
|
||||
if (!d)
|
||||
return -errno;
|
||||
|
||||
FOREACH_DIRENT(de, d, break) {
|
||||
char p[STRLEN("/sys/devices/system/node/node/cpulist") + DECIMAL_STR_MAX(unsigned) + 1];
|
||||
_cleanup_(cpu_set_done) CPUSet cpus = {};
|
||||
_cleanup_free_ char *cpulist = NULL;
|
||||
const char *n;
|
||||
unsigned node;
|
||||
|
||||
if (de->d_type != DT_DIR)
|
||||
continue;
|
||||
|
||||
n = startswith(de->d_name, "node");
|
||||
if (!n)
|
||||
continue;
|
||||
|
||||
r = safe_atou(n, &node);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to parse node number %s to unsigned, ignoring: %m", n);
|
||||
continue;
|
||||
}
|
||||
|
||||
xsprintf(p, "/sys/devices/system/node/node%u/cpulist", node);
|
||||
|
||||
r = read_virtual_file(p, SIZE_MAX, &cpulist, /* ret_size= */ NULL);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to read %s, ignoring: %m", p);
|
||||
continue;
|
||||
}
|
||||
|
||||
r = parse_cpu_set(cpulist, &cpus);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to parse cpu set %s, ignoring: %m", cpulist);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (CPU_ISSET_S(cpu, cpus.allocated, cpus.set)) {
|
||||
*ret = node;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* CPU not found in any NUMA node, assume node 0 */
|
||||
log_debug("CPU %u not found in any NUMA node, assuming node 0.", cpu);
|
||||
*ret = 0;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int numa_mask_add_all(CPUSet *mask) {
|
||||
int m;
|
||||
|
||||
|
||||
@@ -29,8 +29,11 @@ static inline void numa_policy_reset(NUMAPolicy *p) {
|
||||
}
|
||||
|
||||
int apply_numa_policy(const NUMAPolicy *policy);
|
||||
int numa_node_get_cpus(size_t node, CPUSet *ret);
|
||||
int numa_to_cpu_set(const NUMAPolicy *policy, CPUSet *ret);
|
||||
|
||||
int numa_get_node_from_cpu(unsigned cpu, unsigned *ret);
|
||||
|
||||
int numa_mask_add_all(CPUSet *mask);
|
||||
|
||||
DECLARE_STRING_TABLE_LOOKUP(mpol, int);
|
||||
|
||||
@@ -132,6 +132,10 @@ 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)
|
||||
Link.IRQAffinity, config_parse_cpu_set, 0, offsetof(LinkConfig, irq_affinity_cpus)
|
||||
Link.IRQAffinityNUMA, config_parse_irq_affinity_numa, 0, offsetof(LinkConfig, irq_affinity_numa)
|
||||
/* 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)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
|
||||
#include <linux/netdevice.h>
|
||||
#include <net/if.h>
|
||||
#include <net/if_arp.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@@ -15,6 +16,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"
|
||||
@@ -31,13 +33,16 @@
|
||||
#include "netif-util.h"
|
||||
#include "netlink-util.h"
|
||||
#include "network-util.h"
|
||||
#include "numa-util.h"
|
||||
#include "parse-util.h"
|
||||
#include "path-util.h"
|
||||
#include "proc-cmdline.h"
|
||||
#include "random-util.h"
|
||||
#include "socket-util.h"
|
||||
#include "sort-util.h"
|
||||
#include "specifier.h"
|
||||
#include "stat-util.h"
|
||||
#include "stdio-util.h"
|
||||
#include "string-table.h"
|
||||
#include "string-util.h"
|
||||
#include "strv.h"
|
||||
@@ -78,6 +83,7 @@ static LinkConfig* link_config_free(LinkConfig *config) {
|
||||
free(config->wol_password_file);
|
||||
erase_and_free(config->wol_password);
|
||||
cpu_set_done(&config->rps_cpu_mask);
|
||||
cpu_set_done(&config->irq_affinity_cpus);
|
||||
|
||||
ordered_hashmap_free(config->sr_iov_by_section);
|
||||
|
||||
@@ -270,6 +276,8 @@ 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,
|
||||
.irq_affinity_numa = IRQ_AFFINITY_NUMA_UNSET,
|
||||
};
|
||||
|
||||
FOREACH_ELEMENT(feature, config->features)
|
||||
@@ -922,6 +930,741 @@ static int link_apply_sr_iov_config(Link *link) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Get the local NUMA node for a network device from sysfs.
|
||||
* Returns -ENOENT if numa_node file doesn't exist or shows -1 (no NUMA). */
|
||||
static int link_get_device_numa_node(Link *link, unsigned *ret) {
|
||||
int r, node;
|
||||
|
||||
assert(link);
|
||||
assert(link->event);
|
||||
assert(link->event->dev);
|
||||
assert(ret);
|
||||
|
||||
r = device_get_sysattr_int(link->event->dev, "device/numa_node", &node);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* -1 means no NUMA node (non-NUMA system or device not associated with a node) */
|
||||
if (node < 0)
|
||||
return -ENOENT;
|
||||
|
||||
*ret = (unsigned) node;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* CPU topology information for IRQ affinity spread algorithm. */
|
||||
typedef struct CPUTopology {
|
||||
unsigned cpu;
|
||||
unsigned numa_node;
|
||||
unsigned package_id;
|
||||
unsigned die_id; /* L3 cache domain / chiplet */
|
||||
unsigned core_id;
|
||||
bool is_first_thread; /* First hyperthread of a physical core */
|
||||
} CPUTopology;
|
||||
|
||||
/* Die (L3 cache domain) information for spread algorithm */
|
||||
typedef struct DieInfo {
|
||||
unsigned die_id;
|
||||
unsigned *cpus; /* CPUs in this die (first HT only, sorted by core) */
|
||||
size_t cpu_count;
|
||||
size_t next_idx; /* For round-robin within die */
|
||||
} DieInfo;
|
||||
|
||||
/* Returns the first thread of a CPU siblings list */
|
||||
static int cpu_topology_get_first_thread(sd_device *cpu_node, unsigned *ret) {
|
||||
const char *content, *end;
|
||||
int r;
|
||||
|
||||
assert(cpu_node);
|
||||
assert(ret);
|
||||
|
||||
r = sd_device_get_sysattr_value(cpu_node, "topology/thread_siblings_list", &content);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
end = content + strcspn(content, ",-");
|
||||
|
||||
_cleanup_free_ char *first = strndup(content, end - content);
|
||||
if (!first)
|
||||
return -ENOMEM;
|
||||
|
||||
return safe_atou(first, ret);
|
||||
}
|
||||
|
||||
static int cpu_topology_compare(const CPUTopology *a, const CPUTopology *b) {
|
||||
int r;
|
||||
|
||||
assert(a);
|
||||
assert(b);
|
||||
|
||||
/* Sort by die first (for L3 cache grouping), then core, then CPU number */
|
||||
r = CMP(a->die_id, b->die_id);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
r = CMP(a->core_id, b->core_id);
|
||||
if (r != 0)
|
||||
return r;
|
||||
|
||||
return CMP(a->cpu, b->cpu);
|
||||
}
|
||||
|
||||
/* Comparison function for sorting CPUs by CPU number (for die ID assignment) */
|
||||
static int cpu_number_compare(const CPUTopology *a, const CPUTopology *b) {
|
||||
assert(a);
|
||||
assert(b);
|
||||
|
||||
return CMP(a->cpu, b->cpu);
|
||||
}
|
||||
|
||||
/* Assign logical die IDs based on L3 cache sharing topology.
|
||||
*
|
||||
* For IRQ spreading, the goal is to distribute interrupts across CPUs that
|
||||
* don't share cache, minimizing cache line contention when processing packets.
|
||||
* The L3 cache boundary is the key locality domain: CPUs sharing an L3 can
|
||||
* exchange data cheaply, while cross-L3 communication is expensive.
|
||||
*
|
||||
* We use L3 shared_cpu_list rather than the kernel's physical die_id because:
|
||||
* - On AMD EPYC, multiple CCXs on the same physical die have separate L3 caches
|
||||
* - On Intel with Sub-NUMA Clustering, one die may have multiple L3 domains
|
||||
* - L3 sharing reflects actual data locality, not physical packaging */
|
||||
static int assign_sequential_die_ids(CPUTopology *cpus, size_t count) {
|
||||
_cleanup_strv_free_ char **l3_groups = NULL;
|
||||
int r;
|
||||
|
||||
assert(cpus);
|
||||
|
||||
/* First, sort CPUs by CPU number for consistent discovery order */
|
||||
typesafe_qsort(cpus, count, cpu_number_compare);
|
||||
|
||||
/* Assign die IDs based on order of L3 shared_cpu_list discovery */
|
||||
FOREACH_ARRAY(cpu, cpus, count) {
|
||||
_cleanup_(sd_device_unrefp) sd_device *cpu_node = NULL;
|
||||
char cpu_path[STRLEN("/sys/devices/system/cpu/cpu") + DECIMAL_STR_MAX(unsigned) + 1];
|
||||
const char *l3_list;
|
||||
unsigned die_id = 0;
|
||||
bool found = false;
|
||||
|
||||
xsprintf(cpu_path, "/sys/devices/system/cpu/cpu%u", cpu->cpu);
|
||||
r = sd_device_new_from_syspath(&cpu_node, cpu_path);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = sd_device_get_sysattr_value(cpu_node, "cache/index3/shared_cpu_list", &l3_list);
|
||||
if (r < 0) {
|
||||
/* No L3 info, fall back to package ID */
|
||||
cpu->die_id = cpu->package_id;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Check if we've seen this L3 group before */
|
||||
STRV_FOREACH(g, l3_groups) {
|
||||
if (streq(*g, l3_list)) {
|
||||
cpu->die_id = die_id;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
die_id++;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
/* New L3 group, assign next sequential die ID */
|
||||
cpu->die_id = die_id;
|
||||
r = strv_extend(&l3_groups, l3_list);
|
||||
if (r < 0)
|
||||
return r;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int discover_cpu_topology(CPUTopology **ret, size_t *ret_count) {
|
||||
_cleanup_(sd_device_unrefp) sd_device *parent_node = NULL;
|
||||
_cleanup_free_ CPUTopology *cpus = NULL;
|
||||
const char *name;
|
||||
size_t count = 0;
|
||||
int r;
|
||||
|
||||
assert(ret);
|
||||
assert(ret_count);
|
||||
|
||||
r = sd_device_new_from_syspath(&parent_node, "/sys/devices/system/cpu");
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
FOREACH_DEVICE_CHILD_WITH_SUFFIX(parent_node, cpu_node, name) {
|
||||
char topo_path[STRLEN("/sys/devices/system/cpu/cpu/topology") + DECIMAL_STR_MAX(unsigned) + 1];
|
||||
const char *n;
|
||||
unsigned cpu, online, first_thread;
|
||||
|
||||
n = startswith(name, "cpu");
|
||||
if (!n)
|
||||
continue;
|
||||
|
||||
r = safe_atou(n, &cpu);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to convert %s to unsigned, skipping: %m", n);
|
||||
continue;
|
||||
}
|
||||
|
||||
r = device_get_sysattr_unsigned(cpu_node, "online", &online);
|
||||
if (r == -ENOENT)
|
||||
online = 1; /* CPU 0 lacks 'online' file, assume online */
|
||||
else if (r < 0 || online == 0)
|
||||
continue;
|
||||
|
||||
/* Check if topology directory exists (filters out cpu0 on some systems) */
|
||||
xsprintf(topo_path, "/sys/devices/system/cpu/cpu%u/topology", cpu);
|
||||
if (access(topo_path, F_OK) < 0) {
|
||||
log_debug_errno(errno, "Failed to access %s, ignoring: %m", topo_path);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!GREEDY_REALLOC(cpus, count + 1))
|
||||
return -ENOMEM;
|
||||
|
||||
cpus[count].cpu = cpu;
|
||||
|
||||
r = numa_get_node_from_cpu(cpu, &cpus[count].numa_node);
|
||||
if (r < 0) {
|
||||
log_debug_errno(r, "Failed to get NUMA node for CPU %u, assuming NUMA node 0: %m", cpu);
|
||||
cpus[count].numa_node = 0;
|
||||
}
|
||||
|
||||
r = device_get_sysattr_unsigned(cpu_node, "topology/physical_package_id", &cpus[count].package_id);
|
||||
if (r < 0) {
|
||||
log_device_debug_errno(cpu_node, r, "Failed to get physical_package_id, assuming package ID 0: %m");
|
||||
cpus[count].package_id = 0;
|
||||
}
|
||||
|
||||
/* die_id will be assigned later by assign_sequential_die_ids() */
|
||||
cpus[count].die_id = 0;
|
||||
|
||||
r = device_get_sysattr_unsigned(cpu_node, "topology/core_id", &cpus[count].core_id);
|
||||
if (r < 0) {
|
||||
log_device_debug_errno(cpu_node, r, "Failed to get core_id, assuming core ID %u: %m", cpu);
|
||||
cpus[count].core_id = cpu;
|
||||
}
|
||||
|
||||
r = cpu_topology_get_first_thread(cpu_node, &first_thread);
|
||||
if (r < 0)
|
||||
cpus[count].is_first_thread = true;
|
||||
else
|
||||
cpus[count].is_first_thread = (first_thread == cpu);
|
||||
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
return -ENOENT;
|
||||
|
||||
/* Assign sequential die IDs based on L3 discovery order */
|
||||
r = assign_sequential_die_ids(cpus, count);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Sort CPUs by topology for consistent ordering */
|
||||
typesafe_qsort(cpus, count, cpu_topology_compare);
|
||||
|
||||
*ret = TAKE_PTR(cpus);
|
||||
*ret_count = count;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Reorder indices so consecutive elements are maximally spread apart.
|
||||
*
|
||||
* Uses recursive divide-and-conquer: split in half, permute each half,
|
||||
* then interleave. This ensures elements originally far apart become adjacent.
|
||||
*
|
||||
* Example trace for [0,1,2,3,4,5,6,7]:
|
||||
* split into [0,1,2,3] and [4,5,6,7]
|
||||
* recurse left: [0,1,2,3] -> [0,2,1,3]
|
||||
* recurse right: [4,5,6,7] -> [4,6,5,7]
|
||||
* interleave -> [0,4,2,6,1,5,3,7]
|
||||
*
|
||||
* The first N elements of the output are roughly evenly distributed across the
|
||||
* original range, for any N. This is useful when assigning IRQs to CPUs: if a
|
||||
* NIC has fewer IRQs than CPUs, the assigned CPUs will still be spread across
|
||||
* the CPUs rather than all at the beginning. */
|
||||
static int equidist_permute(size_t *indices, size_t n_indices) {
|
||||
_cleanup_free_ size_t *left = NULL, *right = NULL;
|
||||
size_t left_count, right_count;
|
||||
size_t li = 0, ri = 0, ti = 0;
|
||||
int r;
|
||||
|
||||
assert(indices);
|
||||
|
||||
if (n_indices <= 1)
|
||||
return 0;
|
||||
|
||||
left_count = DIV_ROUND_UP(n_indices, 2);
|
||||
right_count = n_indices - left_count;
|
||||
|
||||
/* Recursively permute each half */
|
||||
left = newdup(size_t, indices, left_count);
|
||||
right = newdup(size_t, &indices[left_count], right_count);
|
||||
if (!left || !right)
|
||||
return log_oom();
|
||||
|
||||
r = equidist_permute(left, left_count);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
r = equidist_permute(right, right_count);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Interleave: left[0], right[0], left[1], right[1], ... */
|
||||
for (size_t i = 0; i < n_indices; i++) {
|
||||
if (i % 2 == 0 && li < left_count)
|
||||
indices[ti++] = left[li++];
|
||||
else if (ri < right_count)
|
||||
indices[ti++] = right[ri++];
|
||||
else if (li < left_count)
|
||||
indices[ti++] = left[li++];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void die_info_free(DieInfo *dies, size_t count) {
|
||||
assert(dies || count == 0);
|
||||
|
||||
FOREACH_ARRAY(die, dies, count)
|
||||
free(die->cpus);
|
||||
free(dies);
|
||||
}
|
||||
|
||||
/* Build die information from topology, grouping CPUs by L3/die and filtering to first HT only */
|
||||
static int build_die_info(const CPUTopology *topology, size_t topology_count, DieInfo **ret, size_t *ret_count) {
|
||||
DieInfo *dies = NULL;
|
||||
size_t die_count = 0;
|
||||
int r;
|
||||
|
||||
assert(topology);
|
||||
assert(ret);
|
||||
assert(ret_count);
|
||||
|
||||
CLEANUP_ARRAY(dies, die_count, die_info_free);
|
||||
|
||||
FOREACH_ARRAY(cpu_topology, topology, topology_count) {
|
||||
DieInfo *die = NULL;
|
||||
|
||||
/* Only consider first hyperthreads for initial spread */
|
||||
if (!cpu_topology->is_first_thread)
|
||||
continue;
|
||||
|
||||
/* Find or create die entry */
|
||||
for (size_t j = 0; j < die_count; j++)
|
||||
if (dies[j].die_id == cpu_topology->die_id) {
|
||||
die = &dies[j];
|
||||
break;
|
||||
}
|
||||
|
||||
if (!die) {
|
||||
if (!GREEDY_REALLOC(dies, die_count + 1))
|
||||
return log_oom();
|
||||
die = &dies[die_count++];
|
||||
*die = (DieInfo) { .die_id = cpu_topology->die_id };
|
||||
}
|
||||
|
||||
if (!GREEDY_REALLOC(die->cpus, die->cpu_count + 1))
|
||||
return log_oom();
|
||||
|
||||
die->cpus[die->cpu_count++] = cpu_topology->cpu;
|
||||
}
|
||||
|
||||
/* Sort dies by die_id for determinism, then apply equidist to CPUs within each die */
|
||||
FOREACH_ARRAY(die, dies, die_count) {
|
||||
_cleanup_free_ unsigned *reordered = NULL;
|
||||
_cleanup_free_ size_t *indices = new(size_t, die->cpu_count);
|
||||
if (!indices)
|
||||
return log_oom();
|
||||
|
||||
for (size_t j = 0; j < die->cpu_count; j++)
|
||||
indices[j] = j;
|
||||
|
||||
r = equidist_permute(indices, die->cpu_count);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Reorder CPUs according to equidist permutation */
|
||||
reordered = new(unsigned, die->cpu_count);
|
||||
if (!reordered)
|
||||
return log_oom();
|
||||
|
||||
for (size_t j = 0; j < die->cpu_count; j++)
|
||||
reordered[j] = die->cpus[indices[j]];
|
||||
|
||||
memcpy(die->cpus, reordered, die->cpu_count * sizeof(unsigned));
|
||||
}
|
||||
|
||||
*ret = TAKE_PTR(dies);
|
||||
*ret_count = die_count;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Select CPUs for IRQ affinity spreading with optimal topology distribution.
|
||||
*
|
||||
* Algorithm:
|
||||
* 1. Group CPUs by die (L3 cache domain), using only first hyperthreads
|
||||
* 2. Apply equidistant permutation to both die order and CPUs within each die,
|
||||
* so consecutive selections are maximally spread (e.g., [0,1,2,3] -> [0,2,1,3])
|
||||
* 3. Round-robin across dies, picking one CPU per die per round
|
||||
* 4. If more IRQs than physical cores, wrap around and reuse the same CPUs
|
||||
*
|
||||
* Ensures each IRQ gets a dedicated physical core before any core handles
|
||||
* multiple IRQs. Two IRQs on one physical core time-share but benefit from warm
|
||||
* cache, whereas spreading across SMT siblings causes resource contention with
|
||||
* no cache benefit.
|
||||
* Maximizes physical distance between consecutively assigned IRQs, improving
|
||||
* cache distribution even when only a few IRQs are assigned. */
|
||||
static int select_spread_cpus(
|
||||
const CPUTopology *topology,
|
||||
size_t topology_count,
|
||||
size_t n_irqs,
|
||||
unsigned **ret,
|
||||
size_t *ret_count) {
|
||||
|
||||
_cleanup_free_ unsigned *selected = NULL;
|
||||
_cleanup_free_ size_t *die_order = NULL;
|
||||
DieInfo *dies = NULL;
|
||||
size_t die_count = 0, selected_count = 0;
|
||||
int r;
|
||||
|
||||
assert(topology);
|
||||
assert(ret);
|
||||
assert(ret_count);
|
||||
|
||||
CLEANUP_ARRAY(dies, die_count, die_info_free);
|
||||
|
||||
selected = new(unsigned, n_irqs);
|
||||
if (!selected)
|
||||
return -ENOMEM;
|
||||
|
||||
/* Build die information with first HT CPUs only */
|
||||
r = build_die_info(topology, topology_count, &dies, &die_count);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
if (die_count == 0)
|
||||
return -ENOENT;
|
||||
|
||||
/* Create equidistant die ordering */
|
||||
die_order = new(size_t, die_count);
|
||||
if (!die_order)
|
||||
return -ENOMEM;
|
||||
|
||||
for (size_t i = 0; i < die_count; i++)
|
||||
die_order[i] = i;
|
||||
|
||||
r = equidist_permute(die_order, die_count);
|
||||
if (r < 0)
|
||||
return r;
|
||||
|
||||
/* Round-robin across dies, picking one CPU from each die at a time */
|
||||
size_t dies_exhausted = 0;
|
||||
while (selected_count < n_irqs) {
|
||||
bool made_progress = false;
|
||||
|
||||
for (size_t i = 0; i < die_count && selected_count < n_irqs; i++) {
|
||||
DieInfo *die = &dies[die_order[i]];
|
||||
|
||||
if (die->next_idx >= die->cpu_count)
|
||||
continue;
|
||||
|
||||
selected[selected_count++] = die->cpus[die->next_idx++];
|
||||
made_progress = true;
|
||||
|
||||
if (die->next_idx >= die->cpu_count)
|
||||
dies_exhausted++;
|
||||
}
|
||||
|
||||
if (made_progress)
|
||||
continue;
|
||||
|
||||
/* All first HTs exhausted, wrap around for remaining IRQs */
|
||||
if (dies_exhausted < die_count)
|
||||
break;
|
||||
|
||||
/* Reset all dies for round-robin wrap */
|
||||
FOREACH_ARRAY(die, dies, die_count)
|
||||
die->next_idx = 0;
|
||||
dies_exhausted = 0;
|
||||
}
|
||||
|
||||
*ret = TAKE_PTR(selected);
|
||||
*ret_count = selected_count;
|
||||
|
||||
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_spread(Link *link, const CPUSet *allowed_cpus) {
|
||||
_cleanup_closedir_ DIR *dir = NULL;
|
||||
_cleanup_free_ CPUTopology *topology = NULL;
|
||||
_cleanup_free_ unsigned *irqs = NULL;
|
||||
_cleanup_free_ unsigned *spread_cpus = NULL;
|
||||
size_t topology_count = 0, irq_count = 0, spread_count = 0;
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
|
||||
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);
|
||||
|
||||
if (!GREEDY_REALLOC(irqs, irq_count + 1))
|
||||
return log_oom();
|
||||
|
||||
irqs[irq_count++] = irq;
|
||||
}
|
||||
|
||||
if (irq_count == 0) {
|
||||
log_link_debug(link, "No IRQs found, skipping spread.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
typesafe_qsort(irqs, irq_count, cmp_unsigned);
|
||||
|
||||
r = discover_cpu_topology(&topology, &topology_count);
|
||||
if (r < 0)
|
||||
return log_link_error_errno(link, r, "Failed to discover CPU topology: %m");
|
||||
|
||||
/* Filter topology by allowed CPUs if specified */
|
||||
if (allowed_cpus && allowed_cpus->set) {
|
||||
_cleanup_free_ CPUTopology *filtered_topology = new(CPUTopology, topology_count);
|
||||
size_t filtered_count = 0;
|
||||
|
||||
if (!filtered_topology)
|
||||
return log_oom();
|
||||
|
||||
for (size_t i = 0; i < topology_count; i++)
|
||||
if (CPU_ISSET_S(topology[i].cpu, allowed_cpus->allocated, allowed_cpus->set))
|
||||
filtered_topology[filtered_count++] = topology[i];
|
||||
|
||||
if (filtered_count == 0) {
|
||||
log_link_warning(link, "IRQAffinity= filter excludes all CPUs, skipping spread.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_link_debug(link, "Filtered to %zu CPUs (from %zu) based on IRQAffinity=.", filtered_count, topology_count);
|
||||
|
||||
free_and_replace(topology, filtered_topology);
|
||||
topology_count = filtered_count;
|
||||
}
|
||||
|
||||
log_link_debug(link, "Spreading %zu IRQs across %zu CPUs.", irq_count, topology_count);
|
||||
|
||||
/* Select CPUs using maximum distance algorithm */
|
||||
r = select_spread_cpus(topology, topology_count, irq_count, &spread_cpus, &spread_count);
|
||||
if (r < 0)
|
||||
return log_link_error_errno(link, r, "Failed to select spread CPUs: %m");
|
||||
|
||||
for (size_t i = 0; i < spread_count; i++)
|
||||
(void) set_irq_affinity(link, irqs[i], spread_cpus[i]);
|
||||
|
||||
log_link_info(link, "Applied IRQ affinity policy 'spread' across %zu CPUs for %zu IRQs.",
|
||||
MIN(topology_count, irq_count), irq_count);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int link_apply_irq_affinity_single(Link *link, const CPUSet *allowed_cpus) {
|
||||
_cleanup_closedir_ DIR *dir = NULL;
|
||||
unsigned target_cpu = 0;
|
||||
int r;
|
||||
|
||||
assert(link);
|
||||
assert(link->config);
|
||||
assert(ASSERT_PTR(link->event)->dev);
|
||||
|
||||
/* If IRQAffinity= is specified, use the first allowed CPU instead of CPU 0 */
|
||||
if (allowed_cpus && allowed_cpus->set) {
|
||||
bool found = false;
|
||||
|
||||
for (unsigned cpu = 0; cpu < allowed_cpus->allocated * 8; cpu++)
|
||||
if (CPU_ISSET_S(cpu, allowed_cpus->allocated, allowed_cpus->set)) {
|
||||
target_cpu = cpu;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
log_link_warning(link, "IRQAffinity= filter excludes all CPUs, skipping single.");
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
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, target_cpu);
|
||||
}
|
||||
|
||||
log_link_info(link, "Applied IRQ affinity policy 'single' (pinning to CPU %u).", target_cpu);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int link_apply_irq_affinity(Link *link) {
|
||||
_cleanup_(cpu_set_done) CPUSet effective_cpus = {};
|
||||
const char *syspath;
|
||||
unsigned numa_node = IRQ_AFFINITY_NUMA_UNSET;
|
||||
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");
|
||||
|
||||
/* Compute effective CPU set from IRQAffinity= and IRQAffinityNUMA= */
|
||||
if (link->config->irq_affinity_numa != IRQ_AFFINITY_NUMA_UNSET) {
|
||||
_cleanup_(cpu_set_done) CPUSet numa_cpus = {};
|
||||
|
||||
/* Resolve "local" to the actual NUMA node */
|
||||
if (link->config->irq_affinity_numa == IRQ_AFFINITY_NUMA_LOCAL) {
|
||||
r = link_get_device_numa_node(link, &numa_node);
|
||||
if (r < 0) {
|
||||
log_link_warning_errno(
|
||||
link, r,
|
||||
"Failed to determine local NUMA node for device, skipping IRQ affinity configuration: %m");
|
||||
return 0;
|
||||
}
|
||||
log_link_debug(link, "Device is on NUMA node %u.", numa_node);
|
||||
} else
|
||||
numa_node = link->config->irq_affinity_numa;
|
||||
|
||||
/* Get CPUs for the NUMA node */
|
||||
r = numa_node_get_cpus(numa_node, &numa_cpus);
|
||||
if (r < 0) {
|
||||
log_link_warning_errno(
|
||||
link, r,
|
||||
"Failed to get CPUs for NUMA node %u, skipping IRQ affinity configuration: %m",
|
||||
numa_node);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* If IRQAffinity= is also specified, compute intersection */
|
||||
if (link->config->irq_affinity_cpus.set) {
|
||||
/* Compute intersection of IRQAffinity= and NUMA CPUs */
|
||||
size_t max_allocated = MAX(numa_cpus.allocated, link->config->irq_affinity_cpus.allocated);
|
||||
|
||||
r = cpu_set_realloc(&effective_cpus, max_allocated * 8);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
|
||||
for (size_t i = 0; i < max_allocated * 8; i++) {
|
||||
bool in_numa = i < numa_cpus.allocated * 8 &&
|
||||
CPU_ISSET_S(i, numa_cpus.allocated, numa_cpus.set);
|
||||
bool in_affinity = i < link->config->irq_affinity_cpus.allocated * 8 &&
|
||||
CPU_ISSET_S(i, link->config->irq_affinity_cpus.allocated, link->config->irq_affinity_cpus.set);
|
||||
|
||||
if (in_numa && in_affinity) {
|
||||
r = cpu_set_add(&effective_cpus, i);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
}
|
||||
}
|
||||
|
||||
/* Check if intersection is empty */
|
||||
if (!effective_cpus.set || CPU_COUNT_S(effective_cpus.allocated, effective_cpus.set) == 0) {
|
||||
log_link_warning(
|
||||
link,
|
||||
"IRQAffinity= and IRQAffinityNUMA= intersection is empty, skipping IRQ affinity configuration.");
|
||||
return 0;
|
||||
}
|
||||
|
||||
log_link_debug(link, "Using intersection of IRQAffinity= and NUMA node %u CPUs.", numa_node);
|
||||
} else {
|
||||
/* Only NUMA filtering, use NUMA CPUs directly */
|
||||
effective_cpus = TAKE_STRUCT(numa_cpus);
|
||||
log_link_debug(link, "Using CPUs from NUMA node %u.", numa_node);
|
||||
}
|
||||
} else if (link->config->irq_affinity_cpus.set) {
|
||||
/* Only IRQAffinity= specified, copy it */
|
||||
r = cpu_set_add_set(&effective_cpus, &link->config->irq_affinity_cpus);
|
||||
if (r < 0)
|
||||
return log_oom();
|
||||
}
|
||||
/* else: no filtering, effective_cpus remains empty (meaning use all CPUs) */
|
||||
|
||||
switch (link->config->irq_affinity_policy) {
|
||||
case IRQ_AFFINITY_POLICY_SINGLE:
|
||||
return link_apply_irq_affinity_single(link, effective_cpus.set ? &effective_cpus : NULL);
|
||||
case IRQ_AFFINITY_POLICY_SPREAD:
|
||||
return link_apply_irq_affinity_spread(link, effective_cpus.set ? &effective_cpus : NULL);
|
||||
default:
|
||||
assert_not_reached();
|
||||
}
|
||||
}
|
||||
|
||||
static int link_apply_rps_cpu_mask(Link *link) {
|
||||
_cleanup_free_ char *mask_str = NULL;
|
||||
LinkConfig *config;
|
||||
@@ -1051,6 +1794,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 +2147,61 @@ 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);
|
||||
|
||||
int config_parse_irq_affinity_numa(
|
||||
const char *unit,
|
||||
const char *filename,
|
||||
unsigned line,
|
||||
const char *section,
|
||||
unsigned section_line,
|
||||
const char *lvalue,
|
||||
int ltype,
|
||||
const char *rvalue,
|
||||
void *data,
|
||||
void *userdata) {
|
||||
|
||||
unsigned tmp, *numa = ASSERT_PTR(data);
|
||||
int r;
|
||||
|
||||
assert(filename);
|
||||
assert(lvalue);
|
||||
assert(rvalue);
|
||||
|
||||
if (isempty(rvalue)) {
|
||||
*numa = IRQ_AFFINITY_NUMA_UNSET;
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (streq(rvalue, "local")) {
|
||||
*numa = IRQ_AFFINITY_NUMA_LOCAL;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Parse as NUMA node number */
|
||||
r = safe_atou(rvalue, &tmp);
|
||||
if (r < 0)
|
||||
return log_syntax_parse_error(unit, filename, line, r, lvalue, rvalue);
|
||||
|
||||
/* UINT_MAX and UINT_MAX-1 are used to flag "unset" and "local NUMA node" respectively. */
|
||||
if (tmp >= IRQ_AFFINITY_NUMA_LOCAL) {
|
||||
log_syntax(unit, LOG_WARNING, filename, line, 0,
|
||||
"Invalid NUMA node number %u, ignoring assignment: %s", tmp, rvalue);
|
||||
return 0;
|
||||
}
|
||||
|
||||
*numa = tmp;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const char* const irq_affinity_policy_table[_IRQ_AFFINITY_POLICY_MAX] = {
|
||||
[IRQ_AFFINITY_POLICY_SINGLE] = "single",
|
||||
[IRQ_AFFINITY_POLICY_SPREAD] = "spread",
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
/* SPDX-License-Identifier: LGPL-2.1-or-later */
|
||||
#pragma once
|
||||
|
||||
#include <limits.h>
|
||||
|
||||
#include "sd-device.h"
|
||||
|
||||
#include "cpu-set-util.h"
|
||||
@@ -22,6 +24,17 @@ typedef enum MACAddressPolicy {
|
||||
_MAC_ADDRESS_POLICY_INVALID = -EINVAL,
|
||||
} MACAddressPolicy;
|
||||
|
||||
typedef enum IRQAffinityPolicy {
|
||||
IRQ_AFFINITY_POLICY_SINGLE,
|
||||
IRQ_AFFINITY_POLICY_SPREAD,
|
||||
_IRQ_AFFINITY_POLICY_MAX,
|
||||
_IRQ_AFFINITY_POLICY_INVALID = -EINVAL,
|
||||
} IRQAffinityPolicy;
|
||||
|
||||
/* Special values for IRQAffinityNUMA= */
|
||||
#define IRQ_AFFINITY_NUMA_UNSET UINT_MAX
|
||||
#define IRQ_AFFINITY_NUMA_LOCAL (IRQ_AFFINITY_NUMA_UNSET - 1)
|
||||
|
||||
typedef struct Link {
|
||||
UdevEvent *event;
|
||||
LinkConfig *config;
|
||||
@@ -113,6 +126,11 @@ struct LinkConfig {
|
||||
/* Rx RPS CPU mask */
|
||||
CPUSet rps_cpu_mask;
|
||||
|
||||
/* IRQ affinity */
|
||||
IRQAffinityPolicy irq_affinity_policy;
|
||||
CPUSet irq_affinity_cpus;
|
||||
unsigned irq_affinity_numa;
|
||||
|
||||
/* SR-IOV */
|
||||
uint32_t sr_iov_num_vfs;
|
||||
OrderedHashmap *sr_iov_by_section;
|
||||
@@ -136,6 +154,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 +169,5 @@ 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);
|
||||
CONFIG_PARSER_PROTOTYPE(config_parse_irq_affinity_numa);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Executable
+429
@@ -0,0 +1,429 @@
|
||||
#!/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 <<EOF
|
||||
[Match]
|
||||
MACAddress=$mac
|
||||
|
||||
[Link]
|
||||
IRQAffinityPolicy=single
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
|
||||
# Trigger udev to re-apply the link configuration
|
||||
udevadm trigger --action=add "/sys/class/net/$iface"
|
||||
udevadm settle --timeout=30
|
||||
|
||||
# Verify the link file was applied
|
||||
output=$(udevadm info --query property "/sys/class/net/$iface")
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/00-test-irq-affinity.link" "$output"
|
||||
|
||||
# Verify IRQ affinity was actually set to CPU 0 (mask "1")
|
||||
for irq in $irqs; do
|
||||
if check_irq_affinity "$irq" "1"; then
|
||||
echo "IRQ $irq correctly pinned to CPU 0"
|
||||
else
|
||||
actual=$(cat "/proc/irq/$irq/smp_affinity")
|
||||
echo "IRQ $irq affinity is '$actual', expected '1' (CPU 0)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Test 1b: test spread policy on the same interface
|
||||
cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
|
||||
[Match]
|
||||
MACAddress=$mac
|
||||
|
||||
[Link]
|
||||
IRQAffinityPolicy=spread
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
udevadm trigger --action=add "/sys/class/net/$iface"
|
||||
udevadm settle --timeout=30
|
||||
|
||||
# Get the number of online CPUs
|
||||
n_cpus=$(nproc)
|
||||
irq_count=$(echo "$irqs" | wc -w)
|
||||
|
||||
echo "System has $n_cpus CPUs, interface has $irq_count IRQs"
|
||||
|
||||
# Verify IRQs are spread (not all on CPU 0)
|
||||
# With spread policy, if we have more than 1 CPU and more than 1 IRQ,
|
||||
# at least some IRQs should be on different CPUs
|
||||
if [[ "$n_cpus" -gt 1 ]] && [[ "$irq_count" -gt 1 ]]; then
|
||||
cpu_set=()
|
||||
for irq in $irqs; do
|
||||
affinity=$(cat "/proc/irq/$irq/smp_affinity_list")
|
||||
echo "IRQ $irq is on CPU(s): $affinity"
|
||||
cpu_set+=("$affinity")
|
||||
done
|
||||
|
||||
# Check that we have at least 2 different CPU assignments
|
||||
unique_cpus=$(printf '%s\n' "${cpu_set[@]}" | sort -u | wc -l)
|
||||
if [[ "$unique_cpus" -lt 2 ]]; then
|
||||
echo "ERROR: spread policy should distribute IRQs across CPUs, but all are on same CPU"
|
||||
exit 1
|
||||
fi
|
||||
echo "IRQ affinity policy 'spread' successfully distributed IRQs across $unique_cpus CPUs"
|
||||
else
|
||||
echo "Skipping spread verification (need >1 CPU and >1 IRQ)"
|
||||
fi
|
||||
|
||||
# Test 1c: Test IRQAffinity= CPU filtering with single policy
|
||||
# Pin to CPU 1 instead of default CPU 0
|
||||
cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
|
||||
[Match]
|
||||
MACAddress=$mac
|
||||
|
||||
[Link]
|
||||
IRQAffinityPolicy=single
|
||||
IRQAffinity=1
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
udevadm trigger --action=add "/sys/class/net/$iface"
|
||||
udevadm settle --timeout=30
|
||||
|
||||
if [[ "$n_cpus" -gt 1 ]]; then
|
||||
for irq in $irqs; do
|
||||
echo "Checking IRQ $irq affinity with IRQAffinity=1..."
|
||||
if check_irq_affinity "$irq" "2"; then
|
||||
echo "IRQ $irq correctly pinned to CPU 1"
|
||||
else
|
||||
actual=$(cat "/proc/irq/$irq/smp_affinity")
|
||||
echo "IRQ $irq affinity is '$actual', expected '2' (CPU 1)"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "IRQAffinity= filtering with single policy works correctly"
|
||||
else
|
||||
echo "Skipping IRQAffinity= test (need >1 CPU)"
|
||||
fi
|
||||
|
||||
# Test 1d: Test IRQAffinity= with spread policy (restrict to subset of CPUs)
|
||||
if [[ "$n_cpus" -ge 4 ]] && [[ "$irq_count" -gt 1 ]]; then
|
||||
cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
|
||||
[Match]
|
||||
MACAddress=$mac
|
||||
|
||||
[Link]
|
||||
IRQAffinityPolicy=spread
|
||||
IRQAffinity=0-1
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
udevadm trigger --action=add "/sys/class/net/$iface"
|
||||
udevadm settle --timeout=30
|
||||
|
||||
# Verify all IRQs are on CPU 0 or 1 only
|
||||
for irq in $irqs; do
|
||||
affinity_list=$(cat "/proc/irq/$irq/smp_affinity_list")
|
||||
echo "IRQ $irq is on CPU(s): $affinity_list"
|
||||
# Check that affinity is 0 or 1 (not higher CPUs)
|
||||
if [[ "$affinity_list" =~ ^[01]$ ]]; then
|
||||
echo "IRQ $irq correctly restricted to CPUs 0-1"
|
||||
else
|
||||
echo "ERROR: IRQ $irq is on CPU $affinity_list, expected 0 or 1"
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
echo "IRQAffinity= filtering with spread policy works correctly"
|
||||
else
|
||||
echo "Skipping IRQAffinity= spread test (need >=4 CPUs and >1 IRQ)"
|
||||
fi
|
||||
|
||||
# Test 1e: Test IRQAffinityNUMA= if NUMA is available
|
||||
if [[ -d /sys/devices/system/node/node0 ]]; then
|
||||
# Get CPUs on NUMA node 0
|
||||
numa0_cpus=$(cat /sys/devices/system/node/node0/cpulist)
|
||||
echo "NUMA node 0 has CPUs: $numa0_cpus"
|
||||
|
||||
cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
|
||||
[Match]
|
||||
MACAddress=$mac
|
||||
|
||||
[Link]
|
||||
IRQAffinityPolicy=spread
|
||||
IRQAffinityNUMA=0
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
udevadm trigger --action=add "/sys/class/net/$iface"
|
||||
udevadm settle --timeout=30
|
||||
|
||||
# Verify IRQs are on NUMA node 0 CPUs
|
||||
# Parse the cpulist to get valid CPUs
|
||||
for irq in $irqs; do
|
||||
affinity_list=$(cat "/proc/irq/$irq/smp_affinity_list")
|
||||
echo "IRQ $irq is on CPU(s): $affinity_list (NUMA 0 CPUs: $numa0_cpus)"
|
||||
done
|
||||
echo "IRQAffinityNUMA= configuration applied"
|
||||
else
|
||||
echo "Skipping IRQAffinityNUMA= test (no NUMA available)"
|
||||
fi
|
||||
|
||||
# Test 1f: Test empty intersection error case
|
||||
# This should log an error and skip affinity configuration
|
||||
if [[ -d /sys/devices/system/node/node0 ]] && [[ -d /sys/devices/system/node/node1 ]]; then
|
||||
# Get first CPU from node 0 that is NOT in node 1
|
||||
first_numa0_cpu=$(cut -d',' -f1 /sys/devices/system/node/node0/cpulist | cut -d'-' -f1)
|
||||
|
||||
cat >/run/systemd/network/00-test-irq-affinity.link <<EOF
|
||||
[Match]
|
||||
MACAddress=$mac
|
||||
|
||||
[Link]
|
||||
IRQAffinityPolicy=spread
|
||||
IRQAffinity=$first_numa0_cpu
|
||||
IRQAffinityNUMA=1
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
udevadm trigger --action=add "/sys/class/net/$iface"
|
||||
udevadm settle --timeout=30
|
||||
|
||||
# The configuration should be applied but IRQ affinity skipped due to empty intersection
|
||||
# Check journal for the error message
|
||||
if journalctl -u systemd-udevd --since="1 minute ago" | grep "intersection is empty" >/dev/null; then
|
||||
echo "Empty intersection correctly detected and logged"
|
||||
else
|
||||
echo "Note: Empty intersection test - check journal for error message"
|
||||
fi
|
||||
else
|
||||
echo "Skipping empty intersection test (need 2 NUMA nodes)"
|
||||
fi
|
||||
|
||||
# Cleanup
|
||||
rm -f /run/systemd/network/00-test-irq-affinity.link
|
||||
udevadm control --reload
|
||||
else
|
||||
echo "No interface with MSI IRQs found, skipping actual IRQ affinity test"
|
||||
fi
|
||||
|
||||
# Test 2: Config parsing with dummy interfaces (no MSI IRQs)
|
||||
mkdir -p /run/systemd/network/
|
||||
cat >/run/systemd/network/10-test-irq.link <<EOF
|
||||
[Match]
|
||||
Kind=dummy
|
||||
MACAddress=00:50:56:c0:00:20
|
||||
|
||||
[Link]
|
||||
Name=testirq0
|
||||
IRQAffinityPolicy=single
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
|
||||
# Create a dummy interface
|
||||
ip link add address 00:50:56:c0:00:20 type dummy
|
||||
udevadm wait --settle --timeout=30 /sys/class/net/testirq0
|
||||
|
||||
# Check that the link file was applied
|
||||
output=$(udevadm info --query property /sys/class/net/testirq0)
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq.link" "$output"
|
||||
assert_in "ID_NET_NAME=testirq0" "$output"
|
||||
|
||||
# Test that udevadm test-builtin parses the config correctly
|
||||
output=$(udevadm test-builtin --action add net_setup_link /sys/class/net/testirq0 2>&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 <<EOF
|
||||
[Match]
|
||||
Kind=dummy
|
||||
MACAddress=00:50:56:c0:00:21
|
||||
|
||||
[Link]
|
||||
Name=testirq1
|
||||
IRQAffinityPolicy=invalid_policy
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
|
||||
# Create another dummy interface - invalid policy should be ignored
|
||||
ip link add address 00:50:56:c0:00:21 type dummy
|
||||
udevadm wait --settle --timeout=30 /sys/class/net/testirq1
|
||||
|
||||
# Check that the link file was still applied (invalid policy is just ignored/warned)
|
||||
output=$(udevadm info --query property /sys/class/net/testirq1)
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-invalid.link" "$output"
|
||||
|
||||
# Test 4: Empty policy (reset/disable)
|
||||
cat >/run/systemd/network/10-test-irq-empty.link <<EOF
|
||||
[Match]
|
||||
Kind=dummy
|
||||
MACAddress=00:50:56:c0:00:22
|
||||
|
||||
[Link]
|
||||
Name=testirq2
|
||||
IRQAffinityPolicy=
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
|
||||
ip link add address 00:50:56:c0:00:22 type dummy
|
||||
udevadm wait --settle --timeout=30 /sys/class/net/testirq2
|
||||
|
||||
output=$(udevadm info --query property /sys/class/net/testirq2)
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-empty.link" "$output"
|
||||
|
||||
# Test 5: IRQAffinity= config parsing
|
||||
cat >/run/systemd/network/10-test-irq-affinity-cpus.link <<EOF
|
||||
[Match]
|
||||
Kind=dummy
|
||||
MACAddress=00:50:56:c0:00:23
|
||||
|
||||
[Link]
|
||||
Name=testirq3
|
||||
IRQAffinityPolicy=spread
|
||||
IRQAffinity=0-3,8-11
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
|
||||
ip link add address 00:50:56:c0:00:23 type dummy
|
||||
udevadm wait --settle --timeout=30 /sys/class/net/testirq3
|
||||
|
||||
output=$(udevadm info --query property /sys/class/net/testirq3)
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-cpus.link" "$output"
|
||||
|
||||
# Test that udevadm test-builtin parses the IRQAffinity= config correctly
|
||||
output=$(udevadm test-builtin --action add net_setup_link /sys/class/net/testirq3 2>&1)
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-cpus.link" "$output"
|
||||
|
||||
# Test 6: IRQAffinityNUMA= config parsing
|
||||
cat >/run/systemd/network/10-test-irq-affinity-numa.link <<EOF
|
||||
[Match]
|
||||
Kind=dummy
|
||||
MACAddress=00:50:56:c0:00:24
|
||||
|
||||
[Link]
|
||||
Name=testirq4
|
||||
IRQAffinityPolicy=spread
|
||||
IRQAffinityNUMA=local
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
|
||||
ip link add address 00:50:56:c0:00:24 type dummy
|
||||
udevadm wait --settle --timeout=30 /sys/class/net/testirq4
|
||||
|
||||
output=$(udevadm info --query property /sys/class/net/testirq4)
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-numa.link" "$output"
|
||||
|
||||
# Test 7: IRQAffinityNUMA= with explicit node number
|
||||
cat >/run/systemd/network/10-test-irq-affinity-numa-explicit.link <<EOF
|
||||
[Match]
|
||||
Kind=dummy
|
||||
MACAddress=00:50:56:c0:00:25
|
||||
|
||||
[Link]
|
||||
Name=testirq5
|
||||
IRQAffinityPolicy=single
|
||||
IRQAffinityNUMA=0
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
|
||||
ip link add address 00:50:56:c0:00:25 type dummy
|
||||
udevadm wait --settle --timeout=30 /sys/class/net/testirq5
|
||||
|
||||
output=$(udevadm info --query property /sys/class/net/testirq5)
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-numa-explicit.link" "$output"
|
||||
|
||||
# Test 8: Combined IRQAffinity= and IRQAffinityNUMA=
|
||||
cat >/run/systemd/network/10-test-irq-affinity-combined.link <<EOF
|
||||
[Match]
|
||||
Kind=dummy
|
||||
MACAddress=00:50:56:c0:00:26
|
||||
|
||||
[Link]
|
||||
Name=testirq6
|
||||
IRQAffinityPolicy=spread
|
||||
IRQAffinity=0-7
|
||||
IRQAffinityNUMA=0
|
||||
EOF
|
||||
|
||||
udevadm control --reload
|
||||
|
||||
ip link add address 00:50:56:c0:00:26 type dummy
|
||||
udevadm wait --settle --timeout=30 /sys/class/net/testirq6
|
||||
|
||||
output=$(udevadm info --query property /sys/class/net/testirq6)
|
||||
assert_in "ID_NET_LINK_FILE=/run/systemd/network/10-test-irq-affinity-combined.link" "$output"
|
||||
|
||||
# Cleanup
|
||||
ip link del dev testirq0
|
||||
ip link del dev testirq1
|
||||
ip link del dev testirq2
|
||||
ip link del dev testirq3
|
||||
ip link del dev testirq4
|
||||
ip link del dev testirq5
|
||||
ip link del dev testirq6
|
||||
|
||||
rm -f /run/systemd/network/10-test-irq.link
|
||||
rm -f /run/systemd/network/10-test-irq-invalid.link
|
||||
rm -f /run/systemd/network/10-test-irq-empty.link
|
||||
rm -f /run/systemd/network/10-test-irq-affinity-cpus.link
|
||||
rm -f /run/systemd/network/10-test-irq-affinity-numa.link
|
||||
rm -f /run/systemd/network/10-test-irq-affinity-numa-explicit.link
|
||||
rm -f /run/systemd/network/10-test-irq-affinity-combined.link
|
||||
|
||||
exit 0
|
||||
Reference in New Issue
Block a user