1#!/usr/bin/env python3 2# SPDX-License-Identifier: GPL-2.0 3 4""" 5Test channel and ring size configuration via ethtool (-L / -G). 6""" 7 8import socket 9import struct 10import time 11 12from lib.py import ksft_run, ksft_exit, ksft_pr 13from lib.py import ksft_eq 14from lib.py import KsftSkipEx, KsftXfailEx 15from lib.py import NetDrvEpEnv, EthtoolFamily, GenerateTraffic 16from lib.py import cmd, defer, rand_port, tc, NlError 17 18# Added in Python 3.13; fallback to 61 for x86/ARM/MIPS 19SO_TXTIME = getattr(socket, "SO_TXTIME", 61) 20 21# Not always exported by the socket module; asm-generic value (x86/ARM/MIPS). 22SO_SNDBUFFORCE = getattr(socket, "SO_SNDBUFFORCE", 32) 23 24# TX ring size the test shrinks to so the ring fills quickly. 25MIN_TX_RING = 32 26MAX_TX_RING = 1024 27 28 29def channels(cfg) -> None: 30 """ 31 Twiddle channel counts in various combinations of parameters. 32 We're only looking for driver adhering to the requested config 33 if the config is accepted and crashes. 34 """ 35 ehdr = {'header':{'dev-index': cfg.ifindex}} 36 chans = cfg.eth.channels_get(ehdr) 37 38 all_keys = ["rx", "tx", "combined"] 39 mixes = [{"combined"}, {"rx", "tx"}, {"rx", "combined"}, {"tx", "combined"}, 40 {"rx", "tx", "combined"},] 41 42 # Get the set of keys that device actually supports 43 restore = {} 44 supported = set() 45 for key in all_keys: 46 if key + "-max" in chans: 47 supported.add(key) 48 restore |= {key + "-count": chans[key + "-count"]} 49 50 defer(cfg.eth.channels_set, ehdr | restore) 51 52 def test_config(config): 53 try: 54 cfg.eth.channels_set(ehdr | config) 55 get = cfg.eth.channels_get(ehdr) 56 for k, v in config.items(): 57 ksft_eq(get.get(k, 0), v) 58 except NlError as e: 59 failed.append(mix) 60 ksft_pr("Can't set", config, e) 61 else: 62 ksft_pr("Okay", config) 63 64 failed = [] 65 for mix in mixes: 66 if not mix.issubset(supported): 67 continue 68 69 # Set all the values in the mix to 1, other supported to 0 70 config = {} 71 for key in all_keys: 72 config[key + "-count"] = 1 if key in mix else 0 73 test_config(config) 74 75 for mix in mixes: 76 if not mix.issubset(supported): 77 continue 78 if mix in failed: 79 continue 80 81 # Set all the values in the mix to max, other supported to 0 82 config = {} 83 for key in all_keys: 84 config[key + "-count"] = chans[key + '-max'] if key in mix else 0 85 test_config(config) 86 87 88def _configure_min_ring_cnt(cfg) -> None: 89 """ Try to configure a single Rx/Tx ring. """ 90 ehdr = {'header':{'dev-index': cfg.ifindex}} 91 chans = cfg.eth.channels_get(ehdr) 92 93 all_keys = ["rx-count", "tx-count", "combined-count"] 94 restore = {} 95 config = {} 96 for key in all_keys: 97 if key in chans: 98 restore[key] = chans[key] 99 config[key] = 0 100 101 if chans.get('combined-count', 0) > 1: 102 config['combined-count'] = 1 103 elif chans.get('rx-count', 0) > 1 and chans.get('tx-count', 0) > 1: 104 config['tx-count'] = 1 105 config['rx-count'] = 1 106 else: 107 # looks like we're already on 1 channel 108 return 109 110 cfg.eth.channels_set(ehdr | config) 111 defer(cfg.eth.channels_set, ehdr | restore) 112 113 114def ringparam(cfg) -> None: 115 """ 116 Tweak the ringparam configuration. Try to run some traffic over min 117 ring size to make sure it actually functions. 118 """ 119 ehdr = {'header':{'dev-index': cfg.ifindex}} 120 rings = cfg.eth.rings_get(ehdr) 121 122 restore = {} 123 maxes = {} 124 params = set() 125 for key in rings.keys(): 126 if 'max' in key: 127 param = key[:-4] 128 maxes[param] = rings[key] 129 params.add(param) 130 restore[param] = rings[param] 131 132 defer(cfg.eth.rings_set, ehdr | restore) 133 134 # Speed up the reconfig by configuring just one ring 135 _configure_min_ring_cnt(cfg) 136 137 # Try to reach min on all settings 138 for param in params: 139 val = rings[param] 140 while True: 141 try: 142 cfg.eth.rings_set({'header':{'dev-index': cfg.ifindex}, 143 param: val // 2}) 144 if val == 0: 145 break 146 val //= 2 147 except NlError: 148 break 149 150 get = cfg.eth.rings_get(ehdr) 151 ksft_eq(get[param], val) 152 153 ksft_pr(f"Reached min for '{param}' at {val} (max {rings[param]})") 154 155 GenerateTraffic(cfg).wait_pkts_and_stop(10000) 156 157 # Try max across all params, if the driver supports large rings 158 # this may OOM so we ignore errors 159 try: 160 ksft_pr("Applying max settings") 161 config = {p: maxes[p] for p in params} 162 cfg.eth.rings_set(ehdr | config) 163 except NlError as e: 164 ksft_pr("Can't set max params", config, e) 165 else: 166 GenerateTraffic(cfg).wait_pkts_and_stop(10000) 167 168 169def _write_file(path, val): 170 """Write val to a file.""" 171 with open(path, "w", encoding="utf-8") as fp: 172 fp.write(str(val)) 173 174 175def _write_sysfs(path, val): 176 """Write val to a sysfs file, restoring the original value on exit.""" 177 with open(path, "r", encoding="utf-8") as fp: 178 orig_val = fp.read().strip() 179 if str(val) == orig_val: 180 return 181 _write_file(path, val) 182 defer(_write_file, path, orig_val) 183 184 185def _get_qdisc_backlog(cfg, mq_handle, queue): 186 """Return the qdisc backlog (bytes) for the given TX queue's leaf.""" 187 target_parent = f"{mq_handle}{queue + 1:x}" 188 for q in tc(f"-s qdisc show dev {cfg.ifname}", json=True): 189 if q.get("parent", "") == target_parent: 190 return q.get("backlog") or 0 191 return 0 192 193 194def _setup_fq_qdisc(cfg, port, target_queue, other_queue, flow_limit): 195 """Put an fq qdisc on target_queue's leaf and return the mq handle in use. 196 197 We must not disturb the device's existing TX/RX qdisc policy. On a real 198 NIC the root mq already has an addressable handle, so we leave the root 199 and every other queue alone and only swap this one leaf, restoring its 200 original qdisc afterwards. 201 202 @flow_limit raises fq's per-flow packet limit (default 100) so a single 203 flow can back up more packets than the Tx ring holds and thus overflow it. 204 """ 205 qdiscs = tc(f"qdisc show dev {cfg.ifname}", json=True) 206 root = next((q for q in qdiscs if q.get("root")), None) 207 208 if root and root["kind"] == "mq" and root["handle"] != "0:": 209 # Addressable mq (previously-configured): touch only the target queue's 210 # leaf and restore its original qdisc afterwards. 211 mq_handle = root["handle"] 212 parent = f"{mq_handle}{target_queue + 1:x}" 213 orig = next((q for q in qdiscs if q.get("parent") == parent), None) 214 orig_kind = orig["kind"] if orig else \ 215 cmd("sysctl -n net.core.default_qdisc").stdout.strip() 216 defer(tc, f"qdisc replace dev {cfg.ifname} parent {parent} {orig_kind}") 217 elif root is None or root["kind"] in ("mq", "noqueue"): 218 # The auto-attached root mq has handle 0: on any device (real or sim), 219 # which the kernel rejects as a qdisc parent. A 0: handle means the mq 220 # is the untouched kernel default - no custom child qdiscs can hang off 221 # an unaddressable parent - so installing a real handle and restoring 222 # the default mq on exit preserves the device's effective policy. 223 mq_handle = "1:" 224 tc(f"qdisc replace dev {cfg.ifname} root handle {mq_handle} mq") 225 defer(tc, f"qdisc replace dev {cfg.ifname} root mq") 226 parent = f"{mq_handle}{target_queue + 1:x}" 227 else: 228 raise KsftSkipEx(f"root qdisc '{root['kind']}' is not mq; " 229 "refusing to disturb existing qdisc policy") 230 231 try: 232 tc(f"qdisc replace dev {cfg.ifname} parent {parent} fq " 233 f"flow_limit {flow_limit} limit {flow_limit * 2}") 234 except Exception as exc: 235 raise KsftSkipEx( 236 f"fq not available (CONFIG_NET_SCH_FQ): {exc}") from exc 237 238 qdisc_j = tc(f"qdisc show dev {cfg.ifname}", json=True) 239 has_clsact = any(q['kind'] == 'clsact' for q in qdisc_j) 240 if not has_clsact: 241 tc(f"qdisc add dev {cfg.ifname} clsact") 242 defer(tc, f"qdisc del dev {cfg.ifname} clsact") 243 244 proto = "ipv6" if int(cfg.addr_ipver) == 6 else "ip" 245 try: 246 tc(f"filter add dev {cfg.ifname} egress protocol {proto} " 247 f"pref 1 flower ip_proto udp dst_port {port} " 248 f"action skbedit queue_mapping {target_queue}") 249 except Exception as exc: 250 raise KsftSkipEx("tc flower/act_skbedit not available") from exc 251 defer(tc, f"filter del dev {cfg.ifname} egress pref 1") 252 253 tc(f"filter add dev {cfg.ifname} egress pref 101 " 254 f"matchall action skbedit queue_mapping {other_queue}") 255 defer(tc, f"filter del dev {cfg.ifname} egress pref 101") 256 257 return mq_handle 258 259 260def _create_sotxtime_socket(cfg, sndbuf): 261 """Create a UDP socket with SO_TXTIME enabled, bound to the test device.""" 262 sock = socket.socket(socket.AF_INET6 if cfg.addr_ipver == "6" 263 else socket.AF_INET, socket.SOCK_DGRAM) 264 try: 265 sock.setsockopt(socket.SOL_SOCKET, SO_TXTIME, struct.pack("Ii", 1, 0)) 266 except OSError as exc: 267 sock.close() 268 raise KsftSkipEx("SO_TXTIME not supported") from exc 269 sock.setsockopt(socket.SOL_SOCKET, socket.SO_BINDTODEVICE, 270 cfg.ifname.encode()) 271 # Deferred completions keep every in-flight skb charged to the socket, so 272 # size the send buffer to hold the whole burst. SO_SNDBUFFORCE bypasses 273 # net.core.wmem_max (the test runs as root). 274 try: 275 sock.setsockopt(socket.SOL_SOCKET, SO_SNDBUFFORCE, sndbuf) 276 except OSError: 277 sock.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, sndbuf) 278 return sock 279 280 281def _send_sotxtime_burst(cfg, sock, port, count, delay_ns, pkt_size): 282 """Send count UDP packets scheduled delay_ns ahead using SO_TXTIME.""" 283 payload = b'\x00' * pkt_size 284 txtime_ns = time.clock_gettime_ns(time.CLOCK_MONOTONIC) + delay_ns 285 286 ancdata = [(socket.SOL_SOCKET, SO_TXTIME, struct.pack("Q", txtime_ns))] 287 if int(cfg.addr_ipver) == 6: 288 dest = (cfg.remote_addr, port, 0, 0) 289 else: 290 dest = (cfg.remote_addr, port) 291 for _ in range(count): 292 sock.sendmsg([payload], ancdata, 0, dest) 293 294 295def _set_small_tx_ring(cfg, ehdr): 296 """Set the Tx ring to the smallest size the driver accepts. 297 298 Start at 32 so the ring fills quickly, then grow exponentially (64, 299 128, 256, ...) up to 1024. Some drivers enforce a minimum well above 32 300 (e.g. bnxt needs a large ring for software UDP segmentation), so raise 301 the lower bound until the driver accepts it, giving up past 1024. 302 """ 303 size = MIN_TX_RING 304 while size <= MAX_TX_RING: 305 try: 306 cfg.eth.rings_set(ehdr | {'tx': size}) 307 return size 308 except NlError: 309 size = size * 2 310 continue 311 raise KsftSkipEx("driver rejects all tx ring sizes up to 1024") 312 313 314def reconfig_tx_stall(cfg) -> None: 315 """Test that qdisc backlog drains after ring reconfiguration.""" 316 target_queue = 1 317 other_queue = 0 318 319 ehdr = {'header': {'dev-index': cfg.ifindex}} 320 chans = cfg.eth.channels_get(ehdr) 321 322 if "combined-max" not in chans: 323 raise KsftSkipEx("device does not support combined channels") 324 if chans.get("combined-max", 0) < 2: 325 raise KsftSkipEx("device does not support 2+ combined channels") 326 if chans["combined-count"] < 2: 327 defer(cfg.eth.channels_set, 328 ehdr | {"combined-count": chans["combined-count"]}) 329 cfg.eth.channels_set(ehdr | {"combined-count": 2}) 330 331 rings = cfg.eth.rings_get(ehdr) 332 if 'rx' not in rings or 'tx' not in rings: 333 raise KsftSkipEx("device does not expose rx/tx ring params") 334 tx_cur = rings['tx'] 335 if tx_cur <= MIN_TX_RING: 336 raise KsftSkipEx("tx ring size already at minimum") 337 defer(cfg.eth.rings_set, ehdr | {'tx': tx_cur}) 338 339 # Use the smallest Tx ring the driver accepts (32, growing to 1024). 340 tx_ring = _set_small_tx_ring(cfg, ehdr) 341 342 # Slow completions so the ring stays full after FQ releases packets 343 napi_defer = f"/sys/class/net/{cfg.ifname}/napi_defer_hard_irqs" 344 gro_timeout = f"/sys/class/net/{cfg.ifname}/gro_flush_timeout" 345 _write_sysfs(napi_defer, 100) 346 _write_sysfs(gro_timeout, 1000000000) 347 348 port = rand_port() 349 # A single flow must overflow the ring, so send twice the ring depth and 350 # let fq hold that many packets for the flow. 351 pkt_count = tx_ring * 2 352 mq_handle = _setup_fq_qdisc(cfg, port, target_queue, other_queue, 353 tx_ring * 2) 354 355 # Size each packet to one MTU (less L3/L4 headers to avoid fragmentation). 356 pkt_size = cfg.dev['mtu'] - (48 if int(cfg.addr_ipver) == 6 else 28) 357 358 # Each queued skb charges the socket its truesize (~2x the payload), so 359 # budget the send buffer for the whole in-flight burst. 360 sock = _create_sotxtime_socket(cfg, pkt_count * pkt_size * 2) 361 defer(sock.close) 362 363 for delay_ms in [100, 200, 500]: 364 _send_sotxtime_burst(cfg, sock, port, pkt_count, 365 delay_ms * 1_000_000, pkt_size) 366 ksft_pr(f"Sent {pkt_count} SO_TXTIME packets (+{delay_ms}ms)") 367 time.sleep(delay_ms / 1000 + 0.3) 368 369 backlog = _get_qdisc_backlog(cfg, mq_handle, target_queue) 370 if backlog: 371 break 372 else: 373 # A device that completes Tx synchronously (e.g. a software/virtual 374 # driver like netdevsim) never keeps the ring full long enough for a 375 # backlog to form, so the wake-vs-start behavior can't be exercised. 376 # Treat that as an expected failure rather than a hard failure. 377 raise KsftXfailEx("could not build qdisc backlog") 378 379 ksft_pr(f"Backlog before reconfig: {backlog} bytes") 380 381 # Trigger ring reconfig — driver should call wake, not just start. 382 # Grow back to the original size so the driver actually switches channels 383 # (setting the current size is a no-op the driver short-circuits). 384 cfg.eth.rings_set(ehdr | {'tx': tx_cur}) 385 386 # Let completions proceed normally 387 _write_sysfs(napi_defer, 0) 388 _write_sysfs(gro_timeout, 0) 389 390 # Poll for backlog to drain 391 for _ in range(100): 392 backlog = _get_qdisc_backlog(cfg, mq_handle, target_queue) 393 if not backlog: 394 break 395 time.sleep(0.1) 396 397 ksft_eq(0, backlog, 398 comment=f"qdisc backlog stuck on queue {target_queue} " 399 f"after ring reconfig") 400 401 402def main() -> None: 403 """ Ksft boiler plate main """ 404 405 with NetDrvEpEnv(__file__, queue_count=2) as cfg: 406 cfg.eth = EthtoolFamily() 407 408 ksft_run([channels, 409 ringparam, 410 reconfig_tx_stall], 411 args=(cfg, )) 412 ksft_exit() 413 414 415if __name__ == "__main__": 416 main() 417