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