xref: /linux/tools/testing/selftests/drivers/net/hw/toeplitz.py (revision 91ec2035134982b98fab0609a9fd8480e8217dc1)
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3
4"""
5Toeplitz Rx hashing test:
6 - rxhash (the hash value calculation itself);
7 - RSS mapping from rxhash to rx queue;
8 - RPS mapping from rxhash to cpu.
9"""
10
11import glob
12import os
13import socket
14from lib.py import ksft_run, ksft_exit, ksft_pr
15from lib.py import NetDrvEpEnv, EthtoolFamily, NetdevFamily
16from lib.py import cmd, bkg, rand_port, defer
17from lib.py import ksft_in
18from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, KsftFailEx
19
20# "define" for the ID of the Toeplitz hash function
21ETH_RSS_HASH_TOP = 1
22# Must match RPS_MAX_CPUS in toeplitz.c
23RPS_MAX_CPUS = 16
24# Cap Rx queues so IRQ pinning leaves free CPUs in the RPS_MAX_CPUS range
25QUEUE_CAP = 8
26
27
28def _check_rps_and_rfs_not_configured(cfg):
29    """Verify that RPS is not already configured."""
30
31    for rps_file in glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*/rps_cpus"):
32        with open(rps_file, "r", encoding="utf-8") as fp:
33            val = fp.read().strip()
34            if set(val) - {"0", ","}:
35                raise KsftSkipEx(f"RPS already configured on {rps_file}: {val}")
36
37    rfs_file = "/proc/sys/net/core/rps_sock_flow_entries"
38    with open(rfs_file, "r", encoding="utf-8") as fp:
39        val = fp.read().strip()
40        if val != "0":
41            raise KsftSkipEx(f"RFS already configured {rfs_file}: {val}")
42
43
44def _get_cpu_for_irq(irq):
45    with open(f"/proc/irq/{irq}/smp_affinity_list", "r",
46              encoding="utf-8") as fp:
47        data = fp.read().strip()
48        if "," in data or "-" in data:
49            raise KsftFailEx(f"IRQ{irq} is not mapped to a single core: {data}")
50        return int(data)
51
52
53def _cap_queue_count(cfg):
54    ehdr = {"header": {"dev-index": cfg.ifindex}}
55    chans = cfg.ethnl.channels_get(ehdr)
56
57    config = {}
58    restore = {}
59    for key in ("combined-count", "rx-count"):
60        cur = chans.get(key, 0)
61        if cur > QUEUE_CAP:
62            config[key] = QUEUE_CAP
63            restore[key] = cur
64
65    if not config:
66        return
67
68    cfg.ethnl.channels_set(ehdr | config)
69    defer(cfg.ethnl.channels_set, ehdr | restore)
70
71
72def _get_irq_cpus(cfg):
73    """
74    Read the list of IRQs for the device Rx queues.
75    """
76    queues = cfg.netnl.queue_get({"ifindex": cfg.ifindex}, dump=True)
77    napis = cfg.netnl.napi_get({"ifindex": cfg.ifindex}, dump=True)
78
79    # Remap into ID-based dicts
80    napis = {n["id"]: n for n in napis}
81    queues = {f"{q['type']}{q['id']}": q for q in queues}
82
83    cpus = []
84    for rx in range(9999):
85        name = f"rx{rx}"
86        if name not in queues:
87            break
88        cpus.append(_get_cpu_for_irq(napis[queues[name]["napi-id"]]["irq"]))
89
90    return cpus
91
92
93def _get_unused_rps_cpus(cfg, count=2):
94    """
95    Get CPUs that are not used by Rx queues for RPS.
96    Returns a list of at least 'count' CPU numbers within
97    the RPS_MAX_CPUS supported range.
98    """
99
100    # Get CPUs used by Rx queues
101    rx_cpus = set(_get_irq_cpus(cfg))
102
103    # Get total number of CPUs, capped by RPS_MAX_CPUS
104    num_cpus = min(os.cpu_count(), RPS_MAX_CPUS)
105
106    # Find unused CPUs
107    unused_cpus = [cpu for cpu in range(num_cpus) if cpu not in rx_cpus]
108
109    if len(unused_cpus) < count:
110        raise KsftSkipEx(f"Need at least {count} CPUs in range 0..{num_cpus - 1} not used by Rx queues, found {len(unused_cpus)}")
111
112    return unused_cpus[:count]
113
114
115def _configure_rps(cfg, rps_cpus):
116    """Configure RPS for all Rx queues."""
117
118    mask = 0
119    for cpu in rps_cpus:
120        mask |= (1 << cpu)
121
122    mask = hex(mask)
123
124    # Set RPS bitmap for all rx queues
125    for rps_file in glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*/rps_cpus"):
126        with open(rps_file, "w", encoding="utf-8") as fp:
127            # sysfs expects hex without '0x' prefix, toeplitz.c needs the prefix
128            fp.write(mask[2:])
129
130    return mask
131
132
133def _send_traffic(cfg, proto_flag, ipver, port):
134    """Send 20 packets of requested type."""
135
136    # Determine protocol and IP version for socat
137    if proto_flag == "-u":
138        proto = "UDP"
139    else:
140        proto = "TCP"
141
142    baddr = f"[{cfg.addr_v['6']}]" if ipver == "6" else cfg.addr_v["4"]
143
144    # Run socat in a loop to send traffic periodically
145    # Use sh -c with a loop similar to toeplitz_client.sh
146    socat_cmd = f"""
147    for i in `seq 20`; do
148        echo "msg $i" | socat -{ipver} -t 0.1 - {proto}:{baddr}:{port};
149        sleep 0.001;
150    done
151    """
152
153    cmd(socat_cmd, shell=True, host=cfg.remote)
154
155
156def _test_variants():
157    for grp in ["", "rss", "rps"]:
158        for l4 in ["tcp", "udp"]:
159            for l3 in ["4", "6"]:
160                name = f"{l4}_ipv{l3}"
161                if grp:
162                    name = f"{grp}_{name}"
163                yield KsftNamedVariant(name, "-" + l4[0], l3, grp)
164
165
166@ksft_variants(_test_variants())
167def test(cfg, proto_flag, ipver, grp):
168    """Run a single toeplitz test."""
169
170    cfg.require_ipver(ipver)
171
172    # Check that rxhash is enabled
173    ksft_in("receive-hashing: on", cmd(f"ethtool -k {cfg.ifname}").stdout)
174
175    rss = cfg.ethnl.rss_get({"header": {"dev-index": cfg.ifindex}})
176    # Make sure NIC is configured to use Toeplitz hash, and no key xfrm.
177    if rss.get('hfunc') != ETH_RSS_HASH_TOP or rss.get('input-xfrm'):
178        cfg.ethnl.rss_set({"header": {"dev-index": cfg.ifindex},
179                           "hfunc": ETH_RSS_HASH_TOP,
180                           "input-xfrm": {}})
181        defer(cfg.ethnl.rss_set, {"header": {"dev-index": cfg.ifindex},
182                                  "hfunc": rss.get('hfunc'),
183                                  "input-xfrm": rss.get('input-xfrm', {})
184                                  })
185
186    port = rand_port(socket.SOCK_DGRAM)
187
188    toeplitz_path = cfg.test_dir / "toeplitz"
189    rx_cmd = [
190        str(toeplitz_path),
191        "-" + ipver,
192        proto_flag,
193        "-d", str(port),
194        "-i", cfg.ifname,
195        "-T", "4000",
196        "-s",
197        "-v"
198    ]
199
200    if grp:
201        _cap_queue_count(cfg)
202        _check_rps_and_rfs_not_configured(cfg)
203    if grp == "rss":
204        irq_cpus = ",".join([str(x) for x in _get_irq_cpus(cfg)])
205        rx_cmd += ["-C", irq_cpus]
206        ksft_pr(f"RSS using CPUs: {irq_cpus}")
207    elif grp == "rps":
208        # Get CPUs not used by Rx queues and configure them for RPS
209        rps_cpus = _get_unused_rps_cpus(cfg, count=2)
210        rps_mask = _configure_rps(cfg, rps_cpus)
211        defer(_configure_rps, cfg, [])
212        rx_cmd += ["-r", rps_mask]
213        ksft_pr(f"RPS using CPUs: {rps_cpus}, mask: {rps_mask}")
214
215    # Run rx in background, it will exit once it has seen enough packets
216    with bkg(" ".join(rx_cmd), ksft_ready=True, exit_wait=True) as rx_proc:
217        while rx_proc.proc.poll() is None:
218            _send_traffic(cfg, proto_flag, ipver, port)
219
220    # Check rx result
221    ksft_pr("Receiver output:")
222    ksft_pr(rx_proc.stdout.strip().replace('\n', '\n# '))
223    if rx_proc.stderr:
224        ksft_pr(rx_proc.stderr.strip().replace('\n', '\n# '))
225
226
227def main() -> None:
228    """Ksft boilerplate main."""
229
230    with NetDrvEpEnv(__file__) as cfg:
231        cfg.ethnl = EthtoolFamily()
232        cfg.netnl = NetdevFamily()
233        ksft_run(cases=[test], args=(cfg,))
234    ksft_exit()
235
236
237if __name__ == "__main__":
238    main()
239