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