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