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