1# SPDX-License-Identifier: GPL-2.0 2# pylint: disable=invalid-name,too-many-arguments 3"""Shared helpers for devmem TCP selftests.""" 4 5import os 6import re 7 8from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen, 9 ksft_eq, KsftSkipEx, NetNSEnter, EthtoolFamily, 10 NetdevFamily) 11 12 13RX_PAGE_SIZE_DEFAULT = 0 14RX_PAGE_SIZE_16K = 16384 15 16PROBE_RX_PAGE_SIZES = (RX_PAGE_SIZE_DEFAULT, RX_PAGE_SIZE_16K) 17 18NR_HUGEPAGES_FILE = "/proc/sys/vm/nr_hugepages" 19 20 21def _is_aligned(value, alignment): 22 """Equivalent of the kernel IS_ALIGNED(value, alignment). 23 24 alignment must be a power of two. 25 """ 26 return (value & (alignment - 1)) == 0 27 28 29def _restore_nr_hugepages(nr_hugepages): 30 with open(NR_HUGEPAGES_FILE, 'w', encoding='utf-8') as f: 31 f.write(str(nr_hugepages)) 32 33 34def _reserve_hugepages(want=64): 35 """Raise nr_hugepages to @want and arrange for it to be restored.""" 36 with open(NR_HUGEPAGES_FILE, 'r+', encoding='utf-8') as f: 37 nr_hugepages = int(f.read().strip()) 38 if nr_hugepages >= want: 39 return 40 f.seek(0) 41 f.write(str(want)) 42 defer(_restore_nr_hugepages, nr_hugepages) 43 44 45def _probe_devmem(cfg, rx_page_size): 46 """Return True if ncdevmem can bind cfg.ifname at @rx_page_size.""" 47 probe_command = f"{cfg.bin_local} -f {cfg.ifname}" 48 if rx_page_size != RX_PAGE_SIZE_DEFAULT: 49 probe_command += f" -b {rx_page_size}" 50 return cmd(probe_command, fail=False, shell=True).ret == 0 51 52 53def require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_DEFAULT): 54 """Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported.""" 55 if rx_page_size not in PROBE_RX_PAGE_SIZES: 56 raise RuntimeError( 57 f"rx-page-size={rx_page_size} is missing from " 58 f"PROBE_RX_PAGE_SIZES, so it was never probed.") 59 60 if not hasattr(cfg, "devmem_supported"): 61 _reserve_hugepages() 62 # Probe every size upfront: in nk tests a leased queue may land in 63 # ncdevmem's queue range and cause the probe to fail. 64 cfg.devmem_supported = {size: _probe_devmem(cfg, size) 65 for size in PROBE_RX_PAGE_SIZES} 66 67 if not cfg.devmem_supported[RX_PAGE_SIZE_DEFAULT]: 68 raise KsftSkipEx("Test requires devmem support") 69 70 if rx_page_size != RX_PAGE_SIZE_DEFAULT: 71 page_size = os.sysconf("SC_PAGE_SIZE") 72 if not _is_aligned(rx_page_size, page_size): 73 raise KsftSkipEx( 74 f"rx-page-size={rx_page_size} is invalid for this platform " 75 f"(must be a multiple of PAGE_SIZE={page_size})") 76 77 if not cfg.devmem_supported[rx_page_size]: 78 raise KsftSkipEx( 79 f"Test requires devmem rx-page-size={rx_page_size} support") 80 81 82def configure_nic(cfg): 83 """Channels, rings, RSS, queue lease for netkit devmem.""" 84 if not hasattr(cfg, "devmem_supported"): 85 raise RuntimeError( 86 "require_devmem() must be called before configure_nic(), which " 87 "may lease a queue away and make later probes fail.") 88 89 if not hasattr(cfg, 'netns'): 90 return 91 92 cfg.require_ipver('6') 93 ethnl = EthtoolFamily() 94 95 channels = ethnl.channels_get({'header': {'dev-index': cfg.ifindex}}) 96 channels = channels['combined-count'] 97 if channels < 2: 98 raise KsftSkipEx( 99 'Test requires NETIF with at least 2 combined channels' 100 ) 101 102 rings = ethnl.rings_get({'header': {'dev-index': cfg.ifindex}}) 103 orig_rx_rings = rings['rx'] 104 orig_hds_thresh = rings.get('hds-thresh', 0) 105 106 ethnl.rings_set({'header': {'dev-index': cfg.ifindex}, 107 'tcp-data-split': 'enabled', 108 'hds-thresh': 0, 109 'rx': min(64, orig_rx_rings)}) 110 defer(ethnl.rings_set, {'header': {'dev-index': cfg.ifindex}, 111 'tcp-data-split': 'unknown', 112 'hds-thresh': orig_hds_thresh, 113 'rx': orig_rx_rings}) 114 115 cfg.src_queue = channels - 1 116 ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}") 117 defer(ethtool, f"-X {cfg.ifname} default") 118 119 if not hasattr(cfg, 'nk_queue'): 120 with NetNSEnter(str(cfg.netns)): 121 netdevnl = NetdevFamily() 122 lease_result = netdevnl.queue_create({ 123 "ifindex": cfg.nk_guest_ifindex, 124 "type": "rx", 125 "lease": { 126 "ifindex": cfg.ifindex, 127 "queue": {"id": cfg.src_queue, "type": "rx"}, 128 "netns-id": 0, 129 }, 130 }) 131 cfg.nk_queue = lease_result['id'] 132 133 134def set_flow_rule(cfg, port): 135 """Install a flow rule steering to src_queue and return the flow rule ID.""" 136 output = ethtool( 137 f"-N {cfg.ifname} flow-type tcp6 dst-port {port}" 138 f" action {cfg.src_queue}" 139 ).stdout 140 return int(re.search(r'ID (\d+)', output).group(1)) 141 142 143def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False, 144 rx_page_size=RX_PAGE_SIZE_DEFAULT): 145 """Build the ncdevmem RX listener command.""" 146 if hasattr(cfg, 'netns'): 147 flow_rule_id = set_flow_rule(cfg, port) 148 defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}") 149 150 ifname = cfg.nk_guest_ifname 151 addr = cfg.nk_guest_ipv6 152 extras = [f"-t {cfg.nk_queue}", "-q 1", "-n"] 153 else: 154 ifname = cfg.ifname 155 addr = cfg.addr 156 extras = [] 157 if flow_steer: 158 extras.append(f"-c {cfg.remote_addr}") 159 160 if verify: 161 extras.append("-v 7") 162 if fail_on_linear: 163 extras.append("-L") 164 if rx_page_size != RX_PAGE_SIZE_DEFAULT: 165 extras.append(f"-b {rx_page_size}") 166 167 parts = [cfg.bin_local, "-l", f"-f {ifname}", f"-s {addr}", 168 f"-p {port}", *extras] 169 return " ".join(parts) 170 171 172def ncdevmem_tx(cfg, port, chunk_size=0): 173 """Build the ncdevmem TX send command.""" 174 if hasattr(cfg, 'netns'): 175 ifname = cfg.nk_guest_ifname 176 addr = cfg.remote_addr_v['6'] 177 extras = ["-t 0", "-q 1", "-n"] 178 else: 179 ifname = cfg.ifname 180 addr = cfg.remote_addr 181 extras = [] 182 183 if chunk_size: 184 extras.append(f"-z {chunk_size}") 185 186 parts = [cfg.bin_local, f"-f {ifname}", f"-s {addr}", 187 f"-p {port}", *extras] 188 return " ".join(parts) 189 190 191def socat_send(cfg, port, buf_size=0): 192 """Socat command for sending to the devmem listener. 193 194 When buf_size > 0, force one TCP segment per write of exactly that size by 195 setting socat's buffer (-b) and disabling Nagle (TCP_NODELAY). 196 """ 197 proto = f"TCP{cfg.addr_ipver}" 198 199 if hasattr(cfg, 'netns'): 200 addr = f"[{cfg.nk_guest_ipv6}]" 201 else: 202 addr = cfg.baddr 203 204 suffix = f",bind={cfg.remote_baddr}:{port}" 205 206 buf = "" 207 if buf_size: 208 buf = f"-b {buf_size}" 209 suffix += ",nodelay" 210 211 return f"socat {buf} -u - {proto}:{addr}:{port}{suffix}" 212 213 214def socat_listen(cfg, port): 215 """Socat listen command for TX tests.""" 216 return f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}" 217 218 219def setup_test(cfg, bin_local): 220 """Stash the local ncdevmem path on cfg and deploy it to the remote.""" 221 cfg.bin_local = bin_local 222 cfg.bin_remote = cfg.remote.deploy(cfg.bin_local) 223 224 225def run_rx(cfg): 226 """Run the devmem RX test.""" 227 require_devmem(cfg) 228 configure_nic(cfg) 229 port = rand_port() 230 socat = socat_send(cfg, port) 231 data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | head -c 1K" 232 f" | {socat}") 233 netns = getattr(cfg, "netns", None) 234 235 listen_cmd = ncdevmem_rx(cfg, port, flow_steer=not hasattr(cfg, 'netns')) 236 with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem: 237 wait_port_listen(port, proto="tcp", ns=netns) 238 cmd(data_pipe, host=cfg.remote, shell=True) 239 ksft_eq(ncdevmem.ret, 0) 240 241 242def run_tx(cfg): 243 """Run the devmem TX test.""" 244 require_devmem(cfg) 245 configure_nic(cfg) 246 netns = getattr(cfg, "netns", None) 247 port = rand_port() 248 tx_cmd = ncdevmem_tx(cfg, port) 249 listen_cmd = socat_listen(cfg, port) 250 251 with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat: 252 wait_port_listen(port, host=cfg.remote) 253 cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True) 254 ksft_eq(socat.stdout.strip(), "hello\nworld") 255 256 257def run_tx_chunks(cfg): 258 """Run the devmem TX chunking test.""" 259 require_devmem(cfg) 260 configure_nic(cfg) 261 netns = getattr(cfg, "netns", None) 262 port = rand_port() 263 tx_cmd = ncdevmem_tx(cfg, port, chunk_size=3) 264 listen_cmd = socat_listen(cfg, port) 265 266 with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat: 267 wait_port_listen(port, host=cfg.remote) 268 cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True) 269 ksft_eq(socat.stdout.strip(), "hello\nworld") 270 271 272def run_rx_large_niov(cfg): 273 """Run the devmem RX test with a large niov (rx-page-size > PAGE_SIZE). 274 275 Sweep payload sizes that straddle the niov boundary: below, equal to, 276 and above rx_page_size, to exercise sub-niov, exact-niov, and multi-niov 277 RX paths. 278 """ 279 require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_16K) 280 _reserve_hugepages() 281 configure_nic(cfg) 282 netns = getattr(cfg, "netns", None) 283 284 for size in [1024, 4096, 8192, 16384, 32768, 65536]: 285 port = rand_port() 286 socat = socat_send(cfg, port) 287 listen_cmd = ncdevmem_rx(cfg, port, 288 flow_steer=not netns, 289 rx_page_size=RX_PAGE_SIZE_16K) 290 data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | " 291 f"head -c {size} | {socat}") 292 with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem: 293 wait_port_listen(port, proto="tcp", ns=netns) 294 cmd(data_pipe, host=cfg.remote, shell=True) 295 ksft_eq(ncdevmem.ret, 0, 296 f"large-niov failed for payload size {size}") 297 298 299def run_rx_hds(cfg): 300 """Run the HDS test by running devmem RX across a segment size sweep.""" 301 require_devmem(cfg) 302 configure_nic(cfg) 303 netns = getattr(cfg, "netns", None) 304 305 for size in [1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]: 306 port = rand_port() 307 308 listen_cmd = ncdevmem_rx(cfg, port, verify=False, 309 fail_on_linear=True) 310 socat = socat_send(cfg, port, buf_size=size) 311 312 with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem: 313 wait_port_listen(port, proto="tcp", ns=netns) 314 cmd(f"dd if=/dev/zero bs={size} count=1 2>/dev/null | " 315 f"{socat}", host=cfg.remote, shell=True) 316 ksft_eq(ncdevmem.ret, 0, f"HDS failed for payload size {size}") 317