mirror of
https://github.com/torvalds/linux.git
synced 2026-11-07 06:17:02 +01:00
Merge branch 'net-ethtool-make-netdev_rss_key_fill-spread-flows-over-all-queues'
Eric Dumazet says: ==================== net: ethtool: make netdev_rss_key_fill() spread flows over all queues netdev_rss_key_fill() hands drivers a uniformly random key. Because the Toeplitz hash is linear over GF(2), a random key is singular for a given header field and queue count with probability 1/2: flows differing only in the low order bits of that field (such as a burst of consecutive ephemeral ports) then cannot reach all RX queues. On one affected 16-queue host, only 4 queues received traffic until the key was replaced. Patch 1 synchronizes proc_do_rss_key() with netdev_rss_key_fill() using an smp_wmb()/smp_rmb() pair and a boolean flag so readers of /proc/sys/net/core/netdev_rss_key print zero bytes until the key is fully populated. Patch 2 keeps the key random but constrains 1008 of its 2048 bits so that, at every 16-bit aligned position and for every power-of-two queue count up to 256, consecutive values of a field ending there visit every queue once. Applying the fixup on the 16-bit grid covers standard 2-tuple/4-tuple hashes as well as encapsulated or bitmap-selected layouts (such as PSP inner TCP ports at byte 78) without enumerating them in the core. The fixup is a bijection onto the set of valid keys, and redraws in the ~1 in 5 case where two 32-bit windows a multiple of 8 bits apart alias. Patch 3 adds RSS key, indirection table, and flow-hash reporting to netdevsim so the selftest can exercise both the host key and ethtool netlink RSS reporting in a virtual machine without hardware. Patch 4 adds a selftest under tools/testing/selftests/drivers/net/rss_key.py checking /proc/sys/net/core/netdev_rss_key and the live RSS key, indirection table, and flow-hash layouts reported by a device via ethtool netlink. ==================== Link: https://patch.msgid.link/20260922163458.3900996-1-edumazet@google.com Signed-off-by: Jakub Kicinski <kuba@kernel.org>
This commit is contained in:
commit
cee5db680d
|
|
@ -48,6 +48,15 @@ count is not a power of two. NICs should provide an indirection table
|
|||
at least 4 times larger than the queue count. 4x table results in ~16%
|
||||
imbalance between the queues, which is acceptable for most applications.
|
||||
|
||||
The Toeplitz hash is linear over GF(2), so the quality of the hash key
|
||||
matters as much as its randomness. For a key made of uniformly random
|
||||
bytes, the q lowest order bits of a given header field fail to spread
|
||||
flows over all 2^q queues with probability 1/2, and a burst of connections
|
||||
picking nearly consecutive ephemeral ports then lands on a fraction of the
|
||||
queues while the others stay idle. The netdev_rss_key_fill() helper draws
|
||||
a random key that is free of this defect; drivers should use it rather
|
||||
than seeding a key of their own.
|
||||
|
||||
Some NICs support symmetric RSS hashing where, if the IP (source address,
|
||||
destination address) and TCP/UDP (source port, destination port) tuples
|
||||
are swapped, the computed hash is the same. This is beneficial in some
|
||||
|
|
|
|||
|
|
@ -205,7 +205,59 @@ static int nsim_get_ts_info(struct net_device *dev,
|
|||
return 0;
|
||||
}
|
||||
|
||||
static u32 nsim_get_rxfh_key_size(struct net_device *dev)
|
||||
{
|
||||
return NETDEV_RSS_KEY_LEN;
|
||||
}
|
||||
|
||||
static u32 nsim_get_rxfh_indir_size(struct net_device *dev)
|
||||
{
|
||||
return NSIM_RSS_INDIR_SIZE;
|
||||
}
|
||||
|
||||
static int nsim_get_rxfh(struct net_device *dev,
|
||||
struct ethtool_rxfh_param *rxfh)
|
||||
{
|
||||
struct netdevsim *ns = netdev_priv(dev);
|
||||
u32 i;
|
||||
|
||||
rxfh->hfunc = ETH_RSS_HASH_TOP;
|
||||
if (rxfh->indir) {
|
||||
for (i = 0; i < NSIM_RSS_INDIR_SIZE; i++)
|
||||
rxfh->indir[i] = ethtool_rxfh_indir_default(i, ns->ethtool.channels);
|
||||
}
|
||||
if (rxfh->key)
|
||||
memcpy(rxfh->key, ns->ethtool.rss_key,
|
||||
sizeof(ns->ethtool.rss_key));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int nsim_get_rxfh_fields(struct net_device *dev,
|
||||
struct ethtool_rxfh_fields *info)
|
||||
{
|
||||
switch (info->flow_type) {
|
||||
case TCP_V4_FLOW:
|
||||
case UDP_V4_FLOW:
|
||||
case TCP_V6_FLOW:
|
||||
case UDP_V6_FLOW:
|
||||
info->data = RXH_IP_SRC | RXH_IP_DST |
|
||||
RXH_L4_B_0_1 | RXH_L4_B_2_3;
|
||||
return 0;
|
||||
case IPV4_FLOW:
|
||||
case IPV6_FLOW:
|
||||
info->data = RXH_IP_SRC | RXH_IP_DST;
|
||||
return 0;
|
||||
default:
|
||||
return -EOPNOTSUPP;
|
||||
}
|
||||
}
|
||||
|
||||
static const struct ethtool_ops nsim_ethtool_ops = {
|
||||
.get_rxfh_key_size = nsim_get_rxfh_key_size,
|
||||
.get_rxfh_indir_size = nsim_get_rxfh_indir_size,
|
||||
.get_rxfh = nsim_get_rxfh,
|
||||
.get_rxfh_fields = nsim_get_rxfh_fields,
|
||||
.supported_coalesce_params = ETHTOOL_COALESCE_ALL_PARAMS,
|
||||
.supported_ring_params = ETHTOOL_RING_USE_TCP_DATA_SPLIT |
|
||||
ETHTOOL_RING_USE_HDS_THRS,
|
||||
|
|
@ -250,6 +302,7 @@ void nsim_ethtool_init(struct netdevsim *ns)
|
|||
ns->ethtool.fec.active_fec = ETHTOOL_FEC_NONE;
|
||||
|
||||
ns->ethtool.channels = ns->nsim_bus_dev->num_queues;
|
||||
netdev_rss_key_fill(ns->ethtool.rss_key, sizeof(ns->ethtool.rss_key));
|
||||
|
||||
ethtool = debugfs_create_dir("ethtool", ns->nsim_dev_port->ddir);
|
||||
ns->ethtool_ddir = ethtool;
|
||||
|
|
|
|||
|
|
@ -87,10 +87,13 @@ struct nsim_ethtool_pauseparam {
|
|||
bool report_stats_tx;
|
||||
};
|
||||
|
||||
#define NSIM_RSS_INDIR_SIZE 128
|
||||
|
||||
struct nsim_ethtool {
|
||||
u32 get_err;
|
||||
u32 set_err;
|
||||
u32 channels;
|
||||
u8 rss_key[NETDEV_RSS_KEY_LEN];
|
||||
struct nsim_ethtool_pauseparam pauseparam;
|
||||
struct ethtool_coalesce coalesce;
|
||||
struct ethtool_ringparam ring;
|
||||
|
|
|
|||
|
|
@ -5323,7 +5323,6 @@ void netdev_lower_state_changed(struct net_device *lower_dev,
|
|||
void *lower_state_info);
|
||||
|
||||
#define NETDEV_RSS_KEY_LEN 256
|
||||
extern u8 netdev_rss_key[NETDEV_RSS_KEY_LEN] __read_mostly;
|
||||
void netdev_rss_key_fill(void *buffer, size_t len);
|
||||
|
||||
int skb_checksum_help(struct sk_buff *skb);
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ extern int netdev_unregister_timeout_secs;
|
|||
extern int weight_p;
|
||||
extern int dev_weight_rx_bias;
|
||||
extern int dev_weight_tx_bias;
|
||||
extern u8 netdev_rss_key[NETDEV_RSS_KEY_LEN] __read_mostly;
|
||||
extern bool netdev_rss_key_initialized;
|
||||
|
||||
extern struct rw_semaphore dev_addr_sem;
|
||||
|
||||
|
|
|
|||
|
|
@ -337,10 +337,15 @@ static int proc_do_rss_key(const struct ctl_table *table, int write,
|
|||
{
|
||||
char buf[NETDEV_RSS_KEY_LEN * 3];
|
||||
struct ctl_table fake_table;
|
||||
bool initialized;
|
||||
char *pos = buf;
|
||||
|
||||
initialized = READ_ONCE(netdev_rss_key_initialized);
|
||||
/* Pair with smp_wmb() in netdev_rss_key_fill(). */
|
||||
smp_rmb();
|
||||
|
||||
for (int i = 0; i < NETDEV_RSS_KEY_LEN; i++) {
|
||||
pos = hex_byte_pack(pos, netdev_rss_key[i]);
|
||||
pos = hex_byte_pack(pos, initialized ? netdev_rss_key[i] : 0);
|
||||
*pos++ = ':';
|
||||
}
|
||||
*(--pos) = 0;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@
|
|||
|
||||
#include <linux/ethtool_netlink.h>
|
||||
#include <linux/net_tstamp.h>
|
||||
#include <linux/once.h>
|
||||
#include <linux/random.h>
|
||||
#include <linux/unaligned.h>
|
||||
#include <linux/phy.h>
|
||||
#include <linux/rtnetlink.h>
|
||||
#include <linux/ptp_clock_kernel.h>
|
||||
|
|
@ -1400,3 +1403,174 @@ enum ethtool_link_medium ethtool_str_to_medium(const char *str)
|
|||
return ETHTOOL_LINK_MEDIUM_NONE;
|
||||
}
|
||||
EXPORT_SYMBOL_GPL(ethtool_str_to_medium);
|
||||
|
||||
u8 netdev_rss_key[NETDEV_RSS_KEY_LEN] __read_mostly;
|
||||
bool netdev_rss_key_initialized __read_mostly;
|
||||
|
||||
/* Toeplitz is linear over GF(2): the hash is the XOR of the 32-bit key
|
||||
* windows selected by the set bits of the input, and hardware indexes the
|
||||
* indirection table with the low order bits of the hash. Only the tail of
|
||||
* each window therefore matters for queue selection:
|
||||
*
|
||||
* v(i) = key bits [i + 32 - q .. i + 31]
|
||||
*
|
||||
* for input bit @i, with q = log2(number of RX queues). Consecutive input
|
||||
* bits give windows overlapping in q - 1 positions, so the matrix formed by
|
||||
* the windows of the q lowest bits of a header field is a Toeplitz matrix
|
||||
* built from 2 * q - 1 key bits, not from q * q independent ones. A random
|
||||
* Toeplitz matrix over GF(2) is singular with probability 1/2, whatever its
|
||||
* size, and when it is singular the flows of a burst differing only in the
|
||||
* low order bits of that field (consecutive ephemeral ports, typically)
|
||||
* cannot reach all the RX queues no matter how many of them are configured.
|
||||
*
|
||||
* Keep the key random, but constrain the few bits that decide this. The
|
||||
* fixup is applied at every 16-bit aligned position of the key, rather than
|
||||
* at the offsets of the one hash input layout the software happens to know
|
||||
* about: hardware is free to hash whatever it wants, but the fields it picks
|
||||
* are 16 bits wide at the smallest and are not expected to straddle that
|
||||
* grid, so an encapsulated or offloaded layout is covered like the usual
|
||||
* 2-tuple and 4-tuple ones. Each position constrains 2 * q - 1 bits, so
|
||||
* NETDEV_RSS_KEY_QMAX is both enough for 256 queues and the largest value
|
||||
* keeping the ranges of two adjacent positions disjoint.
|
||||
*/
|
||||
#define NETDEV_RSS_KEY_QMAX 8
|
||||
#define NETDEV_RSS_KEY_SPAN (2 * NETDEV_RSS_KEY_QMAX - 1)
|
||||
|
||||
static bool netdev_rss_key_bit(const u8 *key, unsigned int bit)
|
||||
{
|
||||
return key[bit / BITS_PER_BYTE] & (0x80 >> (bit % BITS_PER_BYTE));
|
||||
}
|
||||
|
||||
static void netdev_rss_key_assign_bit(u8 *key, unsigned int bit, bool value)
|
||||
{
|
||||
u8 mask = 0x80 >> (bit % BITS_PER_BYTE);
|
||||
|
||||
if (value)
|
||||
key[bit / BITS_PER_BYTE] |= mask;
|
||||
else
|
||||
key[bit / BITS_PER_BYTE] &= ~mask;
|
||||
}
|
||||
|
||||
/* Writing d[t] for key bit (@lsb + 31 - t), the matrices of all the q values
|
||||
* up to NETDEV_RSS_KEY_QMAX are non singular if and only if
|
||||
*
|
||||
* d[2 * i] = 1 ^ d[i] ^ d[i + 1] ^ ... ^ d[2 * i - 1]
|
||||
*
|
||||
* The odd positions stay free, so this is a one pass fixup rather than a
|
||||
* search. It is also a bijection onto the set of the values having the
|
||||
* property, so the key stays uniformly distributed over that set. It costs
|
||||
* NETDEV_RSS_KEY_QMAX bits of entropy per position.
|
||||
*/
|
||||
static void netdev_rss_key_fixup_field(u8 *key, unsigned int lsb)
|
||||
{
|
||||
bool d[NETDEV_RSS_KEY_SPAN];
|
||||
unsigned int i, t;
|
||||
|
||||
for (t = 0; t < NETDEV_RSS_KEY_SPAN; t++)
|
||||
d[t] = netdev_rss_key_bit(key, lsb + 31 - t);
|
||||
|
||||
for (i = 0; 2 * i < NETDEV_RSS_KEY_SPAN; i++) {
|
||||
bool value = true;
|
||||
|
||||
for (t = i; t < 2 * i; t++)
|
||||
value ^= d[t];
|
||||
|
||||
d[2 * i] = value;
|
||||
}
|
||||
|
||||
for (t = 0; t < NETDEV_RSS_KEY_SPAN; t++)
|
||||
netdev_rss_key_assign_bit(key, lsb + 31 - t, d[t]);
|
||||
}
|
||||
|
||||
/* The 32 key bits starting at @bit, which is what input bit @bit contributes
|
||||
* to the hash. @bit + 32 must fit in the key.
|
||||
*/
|
||||
static u32 netdev_rss_key_window(const u8 *key, unsigned int bit)
|
||||
{
|
||||
unsigned int byte = bit / BITS_PER_BYTE;
|
||||
unsigned int shift = bit % BITS_PER_BYTE;
|
||||
u32 window = get_unaligned_be32(key + byte);
|
||||
|
||||
if (shift)
|
||||
window = (window << shift) |
|
||||
(key[byte + 4] >> (BITS_PER_BYTE - shift));
|
||||
|
||||
return window;
|
||||
}
|
||||
|
||||
/* Two input bits contributing the same window are indistinguishable to the
|
||||
* hash, since flipping both of them leaves it unchanged. For a uniformly
|
||||
* random key that is a 2 ** -32 event per pair of positions, but the fixup
|
||||
* makes it likelier: it derives 8 of every 16 bits from the 8 others, so a
|
||||
* 16-bit aligned word only takes 2 ** 8 values and two windows a whole
|
||||
* number of words apart collide with probability 2 ** -16 instead. Half a
|
||||
* word apart is less affected but still well clear of the random odds, so
|
||||
* cover every distance that is a multiple of 8. What is left after that is
|
||||
* below what a plain random key gives.
|
||||
*
|
||||
* Two windows at a distance that is a multiple of 8 are two windows at the
|
||||
* same offset modulo 8. Caching a whole class would put 253 u32 on the
|
||||
* stack, so cache one class modulo 16 and stream the class 8 bits above it
|
||||
* against it.
|
||||
*/
|
||||
static bool netdev_rss_key_aliases(const u8 *key, unsigned int bits)
|
||||
{
|
||||
u32 windows[NETDEV_RSS_KEY_LEN * BITS_PER_BYTE / 16];
|
||||
unsigned int i, j, n, r;
|
||||
|
||||
for (r = 0; r < 16; r++) {
|
||||
n = 0;
|
||||
for (i = r; i + 32 <= bits; i += 16)
|
||||
windows[n++] = netdev_rss_key_window(key, i);
|
||||
|
||||
for (i = 0; i < n; i++)
|
||||
for (j = i + 1; j < n; j++)
|
||||
if (windows[i] == windows[j])
|
||||
return true;
|
||||
|
||||
if (r >= 8)
|
||||
continue;
|
||||
|
||||
for (i = r + 8; i + 32 <= bits; i += 16) {
|
||||
u32 window = netdev_rss_key_window(key, i);
|
||||
|
||||
for (j = 0; j < n; j++)
|
||||
if (windows[j] == window)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void netdev_rss_key_init(u8 *key, size_t len)
|
||||
{
|
||||
unsigned int lsb, bits = len * BITS_PER_BYTE;
|
||||
|
||||
/* Four keys out of five come out of the fixup free of aliases, so
|
||||
* drawing another one is both simpler and cheaper than repairing.
|
||||
*/
|
||||
do {
|
||||
get_random_bytes(key, len);
|
||||
|
||||
/* A field ending at bit @lsb uses key bits [.. , @lsb + 31],
|
||||
* so stop as soon as a 32-bit window no longer fits in the
|
||||
* key.
|
||||
*/
|
||||
for (lsb = 15; lsb + 32 <= bits; lsb += 16)
|
||||
netdev_rss_key_fixup_field(key, lsb);
|
||||
} while (netdev_rss_key_aliases(key, bits));
|
||||
|
||||
/* Pair with smp_rmb() in proc_do_rss_key(). */
|
||||
smp_wmb();
|
||||
WRITE_ONCE(netdev_rss_key_initialized, true);
|
||||
}
|
||||
|
||||
void netdev_rss_key_fill(void *buffer, size_t len)
|
||||
{
|
||||
if (WARN_ON_ONCE(len > sizeof(netdev_rss_key)))
|
||||
len = sizeof(netdev_rss_key);
|
||||
DO_ONCE(netdev_rss_key_init, netdev_rss_key, sizeof(netdev_rss_key));
|
||||
memcpy(buffer, netdev_rss_key, len);
|
||||
}
|
||||
EXPORT_SYMBOL(netdev_rss_key_fill);
|
||||
|
|
|
|||
|
|
@ -1301,15 +1301,6 @@ static int ethtool_copy_validate_indir(u32 *indir, void __user *useraddr,
|
|||
return 0;
|
||||
}
|
||||
|
||||
u8 netdev_rss_key[NETDEV_RSS_KEY_LEN] __read_mostly;
|
||||
|
||||
void netdev_rss_key_fill(void *buffer, size_t len)
|
||||
{
|
||||
BUG_ON(len > sizeof(netdev_rss_key));
|
||||
net_get_random_once(netdev_rss_key, sizeof(netdev_rss_key));
|
||||
memcpy(buffer, netdev_rss_key, len);
|
||||
}
|
||||
EXPORT_SYMBOL(netdev_rss_key_fill);
|
||||
|
||||
static noinline_for_stack int ethtool_get_rxfh_indir(struct net_device *dev,
|
||||
void __user *useraddr)
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ TEST_PROGS := \
|
|||
psp.py \
|
||||
queues.py \
|
||||
ring_reconfig.py \
|
||||
rss_key.py \
|
||||
shaper.py \
|
||||
so_txtime.py \
|
||||
stats.py \
|
||||
|
|
|
|||
343
tools/testing/selftests/drivers/net/rss_key.py
Executable file
343
tools/testing/selftests/drivers/net/rss_key.py
Executable file
|
|
@ -0,0 +1,343 @@
|
|||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: GPL-2.0
|
||||
|
||||
"""
|
||||
Check the quality of the host RSS key (/proc/sys/net/core/netdev_rss_key)
|
||||
and that the RSS key a device actually uses spreads flows over all of the
|
||||
entries of its indirection table.
|
||||
|
||||
The Toeplitz hash is linear over GF(2): the hash is the XOR of the 32 bit key
|
||||
windows selected by the set bits of the input, and hardware indexes the
|
||||
indirection table with the low order bits of the hash. The windows belonging
|
||||
to the q lowest bits of a header field therefore form a Toeplitz matrix, and
|
||||
when that matrix is singular the flows of a burst differing only in those
|
||||
bits, consecutive ephemeral ports typically, can not reach all of the 2 ** q
|
||||
entries of the table. A key drawn uniformly at random is singular for a given
|
||||
field and a given q with probability 1/2.
|
||||
|
||||
netdev_rss_key_fill() generates keys that are non singular for every field of
|
||||
the hash input and every q up to RSS_KEY_QMAX.
|
||||
"""
|
||||
|
||||
import errno
|
||||
import random
|
||||
|
||||
from lib.py import ksft_run, ksft_exit, ksft_pr
|
||||
from lib.py import ksft_eq, ksft_ge
|
||||
from lib.py import KsftSkipEx
|
||||
from lib.py import NetDrvEnv, EthtoolFamily, NlError
|
||||
|
||||
KEY_PATH = "/proc/sys/net/core/netdev_rss_key"
|
||||
|
||||
# Shortest key able to hash an IPv6 4-tuple.
|
||||
MIN_KEY_LEN = 40
|
||||
|
||||
# Matches NETDEV_RSS_KEY_QMAX, that is up to 256 entries of the table.
|
||||
RSS_KEY_QMAX = 8
|
||||
|
||||
# "define" for the ID of the Toeplitz hash function
|
||||
ETH_RSS_HASH_TOP = 1
|
||||
|
||||
FLOW_TYPES = ("tcp4", "udp4", "tcp6", "udp6")
|
||||
|
||||
# Map ethtool netlink rxfh-fields flag names to rss_key_layout() codes.
|
||||
FIELD_NAMES = {
|
||||
"ip-src": "s",
|
||||
"ip-dst": "d",
|
||||
"l3-proto": "t",
|
||||
"l4-b-0-1": "f",
|
||||
"l4-b-2-3": "n",
|
||||
"ip6-fl": "l",
|
||||
}
|
||||
|
||||
|
||||
def rss_key_bit(buf, bit):
|
||||
"""Bit @bit of @buf, counting from the most significant bit of byte 0."""
|
||||
return (buf[bit // 8] >> (7 - bit % 8)) & 1
|
||||
|
||||
|
||||
def rss_key_assign_bit(buf, bit, value):
|
||||
mask = 0x80 >> (bit % 8)
|
||||
|
||||
if value:
|
||||
buf[bit // 8] |= mask
|
||||
else:
|
||||
buf[bit // 8] &= ~mask
|
||||
|
||||
|
||||
def rss_key_window(key, bit):
|
||||
"""The 32 key bits starting at @bit, what input bit @bit contributes."""
|
||||
value = 0
|
||||
|
||||
for i in range(32):
|
||||
value = (value << 1) | rss_key_bit(key, bit + i)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def rss_key_toeplitz(key, inp, nbits):
|
||||
"""The Toeplitz hash of the @nbits long input @inp under @key."""
|
||||
value = 0
|
||||
|
||||
for i in range(nbits):
|
||||
if rss_key_bit(inp, i):
|
||||
value ^= rss_key_window(key, i)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def rss_key_full_rank(key, lsb, q):
|
||||
"""Do the q low order bits of the field at @lsb reach all 2 ** q entries?
|
||||
|
||||
Gaussian elimination over GF(2) on the q windows involved, reduced to
|
||||
their q low order bits, which are the ones indexing the table.
|
||||
"""
|
||||
basis = {}
|
||||
|
||||
for j in range(q):
|
||||
vector = rss_key_window(key, lsb - j) & ((1 << q) - 1)
|
||||
|
||||
while vector:
|
||||
low = vector & -vector
|
||||
if low not in basis:
|
||||
basis[low] = vector
|
||||
break
|
||||
vector ^= basis[low]
|
||||
|
||||
if not vector:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def rss_key_layout(fields, ipv6):
|
||||
"""Describe the hash input built from @fields.
|
||||
|
||||
@fields is the flow hash configuration, "sdfn" for a 4-tuple or "sd" for
|
||||
a 2-tuple. Returns the list of (name, position of the least significant
|
||||
bit) and the length of the input in bits, or None if the layout involves
|
||||
something this does not know how to place.
|
||||
"""
|
||||
addr_bits = 128 if ipv6 else 32
|
||||
known = (("s", "saddr", addr_bits),
|
||||
("d", "daddr", addr_bits),
|
||||
("f", "sport", 16),
|
||||
("n", "dport", 16))
|
||||
|
||||
if set(fields) - {flag for flag, _, _ in known}:
|
||||
return None, 0
|
||||
|
||||
layout = []
|
||||
nbits = 0
|
||||
|
||||
for flag, name, width in known:
|
||||
if flag not in fields:
|
||||
continue
|
||||
nbits += width
|
||||
layout.append((name, nbits - 1))
|
||||
|
||||
return layout, nbits
|
||||
|
||||
|
||||
def _read_host_key():
|
||||
"""Return the host RSS key, skipping if it has not been generated."""
|
||||
try:
|
||||
with open(KEY_PATH, "r", encoding="ascii") as fp:
|
||||
text = fp.read().strip()
|
||||
except FileNotFoundError as exc:
|
||||
raise KsftSkipEx(f"{KEY_PATH} is not available") from exc
|
||||
|
||||
key = bytes(int(byte, 16) for byte in text.split(":")) if text else b""
|
||||
|
||||
if not any(key):
|
||||
raise KsftSkipEx("the host RSS key has not been generated yet, "
|
||||
"no driver has called netdev_rss_key_fill()")
|
||||
|
||||
return key
|
||||
|
||||
|
||||
def _get_rss(cfg):
|
||||
"""The key, indirection table, and flow-hash config of @cfg's device."""
|
||||
try:
|
||||
rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
|
||||
except NlError as exc:
|
||||
if exc.error == errno.EOPNOTSUPP:
|
||||
raise KsftSkipEx(f"{cfg.ifname} does not support RSS") from exc
|
||||
raise
|
||||
|
||||
hkey = rss.get("hkey")
|
||||
if not hkey or not any(hkey):
|
||||
raise KsftSkipEx(f"{cfg.ifname} does not report an RSS key")
|
||||
|
||||
if rss.get("hfunc") != ETH_RSS_HASH_TOP:
|
||||
raise KsftSkipEx(f"{cfg.ifname} does not use the Toeplitz hash")
|
||||
|
||||
if rss.get("input-xfrm"):
|
||||
raise KsftSkipEx(f"{cfg.ifname} transforms the hash input")
|
||||
|
||||
indir = rss.get("indir")
|
||||
if not indir:
|
||||
raise KsftSkipEx(f"{cfg.ifname} does not report an indirection table")
|
||||
|
||||
if len(indir) & (len(indir) - 1):
|
||||
raise KsftSkipEx(f"{cfg.ifname} has {len(indir)} indirection table "
|
||||
"entries, which is not a power of two")
|
||||
|
||||
return bytes(hkey), indir, rss.get("flow-hash", {})
|
||||
|
||||
|
||||
def _get_layouts(flow_hash):
|
||||
"""The hash input layouts in use, mapped to the flow types sharing them."""
|
||||
layouts = {}
|
||||
|
||||
for fl_type in FLOW_TYPES:
|
||||
nl_fields = flow_hash.get(fl_type)
|
||||
if not nl_fields:
|
||||
continue
|
||||
|
||||
fields = "".join(FIELD_NAMES.get(name, "?") for name in nl_fields)
|
||||
layout, nbits = rss_key_layout(fields, fl_type.endswith("6"))
|
||||
if layout is None:
|
||||
ksft_pr(f"{fl_type}: not checked, hashes fields we can not place "
|
||||
f"({nl_fields})")
|
||||
continue
|
||||
|
||||
layouts.setdefault((tuple(layout), nbits), []).append(fl_type)
|
||||
|
||||
if not layouts:
|
||||
raise KsftSkipEx("no flow type with a hash input we can describe")
|
||||
|
||||
return layouts
|
||||
|
||||
|
||||
def test_host_rss_key_length(cfg) -> None:
|
||||
key = _read_host_key()
|
||||
|
||||
ksft_pr(f"host RSS key is {len(key)} bytes")
|
||||
ksft_ge(len(key), MIN_KEY_LEN, "key too short to hash an IPv6 4-tuple")
|
||||
|
||||
|
||||
def test_host_rss_key_spread(cfg) -> None:
|
||||
key = _read_host_key()
|
||||
degenerate = []
|
||||
|
||||
for ipv6 in (False, True):
|
||||
layout, _ = rss_key_layout("sdfn", ipv6)
|
||||
family = "IPv6" if ipv6 else "IPv4"
|
||||
|
||||
for name, lsb in layout:
|
||||
if lsb + 32 > len(key) * 8:
|
||||
continue
|
||||
|
||||
for q in range(1, RSS_KEY_QMAX + 1):
|
||||
if not rss_key_full_rank(key, lsb, q):
|
||||
degenerate.append(f"{family} {name} over {1 << q} queues")
|
||||
|
||||
for bad in degenerate:
|
||||
ksft_pr(f"degenerate: {bad}")
|
||||
|
||||
ksft_eq(len(degenerate), 0,
|
||||
"the host RSS key does not spread flows over all the queues")
|
||||
|
||||
|
||||
def test_host_rss_key_grid(cfg) -> None:
|
||||
"""Sweep the whole key, not only the fields of the usual layouts."""
|
||||
key = _read_host_key()
|
||||
bits = len(key) * 8
|
||||
positions = 0
|
||||
degenerate = []
|
||||
|
||||
for lsb in range(15, bits - 31, 16):
|
||||
positions += 1
|
||||
|
||||
for q in range(1, RSS_KEY_QMAX + 1):
|
||||
if not rss_key_full_rank(key, lsb, q):
|
||||
degenerate.append(f"field ending at bit {lsb} "
|
||||
f"over {1 << q} queues")
|
||||
|
||||
ksft_pr(f"checked {positions} positions of the {len(key)} byte key")
|
||||
|
||||
for bad in degenerate[:8]:
|
||||
ksft_pr(f"degenerate: {bad}")
|
||||
|
||||
ksft_eq(len(degenerate), 0,
|
||||
"the host RSS key does not spread flows over all the queues "
|
||||
"at every 16-bit aligned position")
|
||||
|
||||
|
||||
def test_dev_rss_key_rank(cfg) -> None:
|
||||
"""The key has to be non singular for the size of the table."""
|
||||
hkey, indir, flow_hash = _get_rss(cfg)
|
||||
q = min((len(indir) - 1).bit_length(), RSS_KEY_QMAX)
|
||||
degenerate = []
|
||||
|
||||
if not q:
|
||||
raise KsftSkipEx("the indirection table has a single entry")
|
||||
|
||||
for (layout, _), fl_types in _get_layouts(flow_hash).items():
|
||||
for name, lsb in layout:
|
||||
if lsb + 32 > len(hkey) * 8:
|
||||
ksft_pr(f"{name}: not checked, the key is {len(hkey)} bytes")
|
||||
continue
|
||||
|
||||
if not rss_key_full_rank(hkey, lsb, q):
|
||||
degenerate.append(f"{'/'.join(fl_types)} {name}")
|
||||
|
||||
for bad in degenerate:
|
||||
ksft_pr(f"degenerate: {bad}")
|
||||
|
||||
ksft_eq(len(degenerate), 0,
|
||||
f"the key of {cfg.ifname} does not spread flows over the "
|
||||
f"{1 << q} entries of its indirection table")
|
||||
|
||||
|
||||
def test_dev_rss_key_spread(cfg) -> None:
|
||||
"""Hash bursts differing in one field only, and place them in the table."""
|
||||
hkey, indir, flow_hash = _get_rss(cfg)
|
||||
q = (len(indir) - 1).bit_length()
|
||||
collisions = []
|
||||
|
||||
if q > RSS_KEY_QMAX:
|
||||
raise KsftSkipEx(f"{len(indir)} indirection table entries is more "
|
||||
"than the kernel guarantees")
|
||||
if not q:
|
||||
raise KsftSkipEx("the indirection table has a single entry")
|
||||
|
||||
for (layout, nbits), fl_types in _get_layouts(flow_hash).items():
|
||||
if nbits + 31 > len(hkey) * 8:
|
||||
ksft_pr(f"{'/'.join(fl_types)}: not checked, the key is "
|
||||
f"{len(hkey)} bytes, input needs {(nbits + 31 + 7) // 8}")
|
||||
continue
|
||||
|
||||
for name, lsb in layout:
|
||||
inp = bytearray(random.randbytes(nbits // 8))
|
||||
entries = set()
|
||||
for value in range(1 << q):
|
||||
for bit in range(q):
|
||||
rss_key_assign_bit(inp, lsb - bit, value & (1 << bit))
|
||||
hash_ = rss_key_toeplitz(hkey, inp, nbits)
|
||||
entries.add(hash_ & (len(indir) - 1))
|
||||
|
||||
if len(entries) != 1 << q:
|
||||
collisions.append(f"{'/'.join(fl_types)} {name} reaches "
|
||||
f"{len(entries)} of the {1 << q} entries")
|
||||
|
||||
for bad in collisions:
|
||||
ksft_pr(bad)
|
||||
|
||||
ksft_eq(len(collisions), 0,
|
||||
f"flows differing in one field only do not fill the "
|
||||
f"indirection table of {cfg.ifname}")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
""" Ksft boiler plate main """
|
||||
|
||||
with NetDrvEnv(__file__, queue_count=4) as cfg:
|
||||
cfg.ethnl = EthtoolFamily()
|
||||
ksft_run(globs=globals(), case_pfx={"test_"}, args=(cfg, ))
|
||||
ksft_exit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user