mirror of
https://github.com/torvalds/linux.git
synced 2026-09-27 19:42:03 +02:00
Add a new devmem test case for binding the dmabuf with rx-page-size=16K. The test sweeps RX payload sizes straddling the niov boundary to cover the sub-niov, exact-niov, and multi-niov RX paths. Silence pylint invalid-name (`with open() as f`) and too-many-arguments (ncdevmem_rx grew to 6 args) at file scope. Acked-by: Stanislav Fomichev <sdf@fomichev.me> Reviewed-by: Nikolay Aleksandrov <razor@blackwall.org> Signed-off-by: Bobby Eshleman <bobbyeshleman@meta.com> Link: https://patch.msgid.link/20260805-tcpdm-large-niovs-v8-3-3e0225e2808c@meta.com Signed-off-by: Jakub Kicinski <kuba@kernel.org>
317 lines
10 KiB
Python
317 lines
10 KiB
Python
# SPDX-License-Identifier: GPL-2.0
|
|
# pylint: disable=invalid-name,too-many-arguments
|
|
"""Shared helpers for devmem TCP selftests."""
|
|
|
|
import os
|
|
import re
|
|
|
|
from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen,
|
|
ksft_eq, KsftSkipEx, NetNSEnter, EthtoolFamily,
|
|
NetdevFamily)
|
|
|
|
|
|
RX_PAGE_SIZE_DEFAULT = 0
|
|
RX_PAGE_SIZE_16K = 16384
|
|
|
|
PROBE_RX_PAGE_SIZES = (RX_PAGE_SIZE_DEFAULT, RX_PAGE_SIZE_16K)
|
|
|
|
NR_HUGEPAGES_FILE = "/proc/sys/vm/nr_hugepages"
|
|
|
|
|
|
def _is_aligned(value, alignment):
|
|
"""Equivalent of the kernel IS_ALIGNED(value, alignment).
|
|
|
|
alignment must be a power of two.
|
|
"""
|
|
return (value & (alignment - 1)) == 0
|
|
|
|
|
|
def _restore_nr_hugepages(nr_hugepages):
|
|
with open(NR_HUGEPAGES_FILE, 'w', encoding='utf-8') as f:
|
|
f.write(str(nr_hugepages))
|
|
|
|
|
|
def _reserve_hugepages(want=64):
|
|
"""Raise nr_hugepages to @want and arrange for it to be restored."""
|
|
with open(NR_HUGEPAGES_FILE, 'r+', encoding='utf-8') as f:
|
|
nr_hugepages = int(f.read().strip())
|
|
if nr_hugepages >= want:
|
|
return
|
|
f.seek(0)
|
|
f.write(str(want))
|
|
defer(_restore_nr_hugepages, nr_hugepages)
|
|
|
|
|
|
def _probe_devmem(cfg, rx_page_size):
|
|
"""Return True if ncdevmem can bind cfg.ifname at @rx_page_size."""
|
|
probe_command = f"{cfg.bin_local} -f {cfg.ifname}"
|
|
if rx_page_size != RX_PAGE_SIZE_DEFAULT:
|
|
probe_command += f" -b {rx_page_size}"
|
|
return cmd(probe_command, fail=False, shell=True).ret == 0
|
|
|
|
|
|
def require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_DEFAULT):
|
|
"""Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported."""
|
|
if rx_page_size not in PROBE_RX_PAGE_SIZES:
|
|
raise RuntimeError(
|
|
f"rx-page-size={rx_page_size} is missing from "
|
|
f"PROBE_RX_PAGE_SIZES, so it was never probed.")
|
|
|
|
if not hasattr(cfg, "devmem_supported"):
|
|
_reserve_hugepages()
|
|
# Probe every size upfront: in nk tests a leased queue may land in
|
|
# ncdevmem's queue range and cause the probe to fail.
|
|
cfg.devmem_supported = {size: _probe_devmem(cfg, size)
|
|
for size in PROBE_RX_PAGE_SIZES}
|
|
|
|
if not cfg.devmem_supported[RX_PAGE_SIZE_DEFAULT]:
|
|
raise KsftSkipEx("Test requires devmem support")
|
|
|
|
if rx_page_size != RX_PAGE_SIZE_DEFAULT:
|
|
page_size = os.sysconf("SC_PAGE_SIZE")
|
|
if not _is_aligned(rx_page_size, page_size):
|
|
raise KsftSkipEx(
|
|
f"rx-page-size={rx_page_size} is invalid for this platform "
|
|
f"(must be a multiple of PAGE_SIZE={page_size})")
|
|
|
|
if not cfg.devmem_supported[rx_page_size]:
|
|
raise KsftSkipEx(
|
|
f"Test requires devmem rx-page-size={rx_page_size} support")
|
|
|
|
|
|
def configure_nic(cfg):
|
|
"""Channels, rings, RSS, queue lease for netkit devmem."""
|
|
if not hasattr(cfg, "devmem_supported"):
|
|
raise RuntimeError(
|
|
"require_devmem() must be called before configure_nic(), which "
|
|
"may lease a queue away and make later probes fail.")
|
|
|
|
if not hasattr(cfg, 'netns'):
|
|
return
|
|
|
|
cfg.require_ipver('6')
|
|
ethnl = EthtoolFamily()
|
|
|
|
channels = ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
|
|
channels = channels['combined-count']
|
|
if channels < 2:
|
|
raise KsftSkipEx(
|
|
'Test requires NETIF with at least 2 combined channels'
|
|
)
|
|
|
|
rings = ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
|
|
orig_rx_rings = rings['rx']
|
|
orig_hds_thresh = rings.get('hds-thresh', 0)
|
|
|
|
ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
|
|
'tcp-data-split': 'enabled',
|
|
'hds-thresh': 0,
|
|
'rx': min(64, orig_rx_rings)})
|
|
defer(ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
|
|
'tcp-data-split': 'unknown',
|
|
'hds-thresh': orig_hds_thresh,
|
|
'rx': orig_rx_rings})
|
|
|
|
cfg.src_queue = channels - 1
|
|
ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")
|
|
defer(ethtool, f"-X {cfg.ifname} default")
|
|
|
|
if not hasattr(cfg, 'nk_queue'):
|
|
with NetNSEnter(str(cfg.netns)):
|
|
netdevnl = NetdevFamily()
|
|
lease_result = netdevnl.queue_create({
|
|
"ifindex": cfg.nk_guest_ifindex,
|
|
"type": "rx",
|
|
"lease": {
|
|
"ifindex": cfg.ifindex,
|
|
"queue": {"id": cfg.src_queue, "type": "rx"},
|
|
"netns-id": 0,
|
|
},
|
|
})
|
|
cfg.nk_queue = lease_result['id']
|
|
|
|
|
|
def set_flow_rule(cfg, port):
|
|
"""Install a flow rule steering to src_queue and return the flow rule ID."""
|
|
output = ethtool(
|
|
f"-N {cfg.ifname} flow-type tcp6 dst-port {port}"
|
|
f" action {cfg.src_queue}"
|
|
).stdout
|
|
return int(re.search(r'ID (\d+)', output).group(1))
|
|
|
|
|
|
def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False,
|
|
rx_page_size=RX_PAGE_SIZE_DEFAULT):
|
|
"""Build the ncdevmem RX listener command."""
|
|
if hasattr(cfg, 'netns'):
|
|
flow_rule_id = set_flow_rule(cfg, port)
|
|
defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
|
|
|
|
ifname = cfg.nk_guest_ifname
|
|
addr = cfg.nk_guest_ipv6
|
|
extras = [f"-t {cfg.nk_queue}", "-q 1", "-n"]
|
|
else:
|
|
ifname = cfg.ifname
|
|
addr = cfg.addr
|
|
extras = []
|
|
if flow_steer:
|
|
extras.append(f"-c {cfg.remote_addr}")
|
|
|
|
if verify:
|
|
extras.append("-v 7")
|
|
if fail_on_linear:
|
|
extras.append("-L")
|
|
if rx_page_size != RX_PAGE_SIZE_DEFAULT:
|
|
extras.append(f"-b {rx_page_size}")
|
|
|
|
parts = [cfg.bin_local, "-l", f"-f {ifname}", f"-s {addr}",
|
|
f"-p {port}", *extras]
|
|
return " ".join(parts)
|
|
|
|
|
|
def ncdevmem_tx(cfg, port, chunk_size=0):
|
|
"""Build the ncdevmem TX send command."""
|
|
if hasattr(cfg, 'netns'):
|
|
ifname = cfg.nk_guest_ifname
|
|
addr = cfg.remote_addr_v['6']
|
|
extras = ["-t 0", "-q 1", "-n"]
|
|
else:
|
|
ifname = cfg.ifname
|
|
addr = cfg.remote_addr
|
|
extras = []
|
|
|
|
if chunk_size:
|
|
extras.append(f"-z {chunk_size}")
|
|
|
|
parts = [cfg.bin_local, f"-f {ifname}", f"-s {addr}",
|
|
f"-p {port}", *extras]
|
|
return " ".join(parts)
|
|
|
|
|
|
def socat_send(cfg, port, buf_size=0):
|
|
"""Socat command for sending to the devmem listener.
|
|
|
|
When buf_size > 0, force one TCP segment per write of exactly that size by
|
|
setting socat's buffer (-b) and disabling Nagle (TCP_NODELAY).
|
|
"""
|
|
proto = f"TCP{cfg.addr_ipver}"
|
|
|
|
if hasattr(cfg, 'netns'):
|
|
addr = f"[{cfg.nk_guest_ipv6}]"
|
|
else:
|
|
addr = cfg.baddr
|
|
|
|
suffix = f",bind={cfg.remote_baddr}:{port}"
|
|
|
|
buf = ""
|
|
if buf_size:
|
|
buf = f"-b {buf_size}"
|
|
suffix += ",nodelay"
|
|
|
|
return f"socat {buf} -u - {proto}:{addr}:{port}{suffix}"
|
|
|
|
|
|
def socat_listen(cfg, port):
|
|
"""Socat listen command for TX tests."""
|
|
return f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}"
|
|
|
|
|
|
def setup_test(cfg, bin_local):
|
|
"""Stash the local ncdevmem path on cfg and deploy it to the remote."""
|
|
cfg.bin_local = bin_local
|
|
cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
|
|
|
|
|
|
def run_rx(cfg):
|
|
"""Run the devmem RX test."""
|
|
require_devmem(cfg)
|
|
configure_nic(cfg)
|
|
port = rand_port()
|
|
socat = socat_send(cfg, port)
|
|
data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | head -c 1K"
|
|
f" | {socat}")
|
|
netns = getattr(cfg, "netns", None)
|
|
|
|
listen_cmd = ncdevmem_rx(cfg, port, flow_steer=not hasattr(cfg, 'netns'))
|
|
with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
|
|
wait_port_listen(port, proto="tcp", ns=netns)
|
|
cmd(data_pipe, host=cfg.remote, shell=True)
|
|
ksft_eq(ncdevmem.ret, 0)
|
|
|
|
|
|
def run_tx(cfg):
|
|
"""Run the devmem TX test."""
|
|
require_devmem(cfg)
|
|
configure_nic(cfg)
|
|
netns = getattr(cfg, "netns", None)
|
|
port = rand_port()
|
|
tx_cmd = ncdevmem_tx(cfg, port)
|
|
listen_cmd = socat_listen(cfg, port)
|
|
|
|
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
|
|
wait_port_listen(port, host=cfg.remote)
|
|
cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
|
|
ksft_eq(socat.stdout.strip(), "hello\nworld")
|
|
|
|
|
|
def run_tx_chunks(cfg):
|
|
"""Run the devmem TX chunking test."""
|
|
require_devmem(cfg)
|
|
configure_nic(cfg)
|
|
netns = getattr(cfg, "netns", None)
|
|
port = rand_port()
|
|
tx_cmd = ncdevmem_tx(cfg, port, chunk_size=3)
|
|
listen_cmd = socat_listen(cfg, port)
|
|
|
|
with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
|
|
wait_port_listen(port, host=cfg.remote)
|
|
cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
|
|
ksft_eq(socat.stdout.strip(), "hello\nworld")
|
|
|
|
|
|
def run_rx_large_niov(cfg):
|
|
"""Run the devmem RX test with a large niov (rx-page-size > PAGE_SIZE).
|
|
|
|
Sweep payload sizes that straddle the niov boundary: below, equal to,
|
|
and above rx_page_size, to exercise sub-niov, exact-niov, and multi-niov
|
|
RX paths.
|
|
"""
|
|
require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_16K)
|
|
_reserve_hugepages()
|
|
configure_nic(cfg)
|
|
netns = getattr(cfg, "netns", None)
|
|
|
|
for size in [1024, 4096, 8192, 16384, 32768, 65536]:
|
|
port = rand_port()
|
|
socat = socat_send(cfg, port)
|
|
listen_cmd = ncdevmem_rx(cfg, port,
|
|
flow_steer=not netns,
|
|
rx_page_size=RX_PAGE_SIZE_16K)
|
|
data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | "
|
|
f"head -c {size} | {socat}")
|
|
with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
|
|
wait_port_listen(port, proto="tcp", ns=netns)
|
|
cmd(data_pipe, host=cfg.remote, shell=True)
|
|
ksft_eq(ncdevmem.ret, 0,
|
|
f"large-niov failed for payload size {size}")
|
|
|
|
|
|
def run_rx_hds(cfg):
|
|
"""Run the HDS test by running devmem RX across a segment size sweep."""
|
|
require_devmem(cfg)
|
|
configure_nic(cfg)
|
|
netns = getattr(cfg, "netns", None)
|
|
|
|
for size in [1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]:
|
|
port = rand_port()
|
|
|
|
listen_cmd = ncdevmem_rx(cfg, port, verify=False,
|
|
fail_on_linear=True)
|
|
socat = socat_send(cfg, port, buf_size=size)
|
|
|
|
with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
|
|
wait_port_listen(port, proto="tcp", ns=netns)
|
|
cmd(f"dd if=/dev/zero bs={size} count=1 2>/dev/null | "
|
|
f"{socat}", host=cfg.remote, shell=True)
|
|
ksft_eq(ncdevmem.ret, 0, f"HDS failed for payload size {size}")
|