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