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