xref: /linux/tools/testing/selftests/drivers/net/ring_reconfig.py (revision cf85f810f911234a06a4ef2439e8694b93b717fc)
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