Linux kernel mirror (for testing)
git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel
os
linux
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()