projects/linux·blob master · refs

316 lines · 10.5 KiB

1# SPDX-License-Identifier: GPL-2.0
2# pylint: disable=invalid-name,too-many-arguments
3"""Shared helpers for devmem TCP selftests."""
4
5import os
6import re
7
8from lib.py import (bkg, cmd, defer, ethtool, rand_port, wait_port_listen,
9 ksft_eq, KsftSkipEx, NetNSEnter, EthtoolFamily,
10 NetdevFamily)
11
12
13RX_PAGE_SIZE_DEFAULT = 0
14RX_PAGE_SIZE_16K = 16384
15
16PROBE_RX_PAGE_SIZES = (RX_PAGE_SIZE_DEFAULT, RX_PAGE_SIZE_16K)
17
18NR_HUGEPAGES_FILE = "/proc/sys/vm/nr_hugepages"
19
20
21def _is_aligned(value, alignment):
22 """Equivalent of the kernel IS_ALIGNED(value, alignment).
23
24 alignment must be a power of two.
25 """
26 return (value & (alignment - 1)) == 0
27
28
29def _restore_nr_hugepages(nr_hugepages):
30 with open(NR_HUGEPAGES_FILE, 'w', encoding='utf-8') as f:
31 f.write(str(nr_hugepages))
32
33
34def _reserve_hugepages(want=64):
35 """Raise nr_hugepages to @want and arrange for it to be restored."""
36 with open(NR_HUGEPAGES_FILE, 'r+', encoding='utf-8') as f:
37 nr_hugepages = int(f.read().strip())
38 if nr_hugepages >= want:
39 return
40 f.seek(0)
41 f.write(str(want))
42 defer(_restore_nr_hugepages, nr_hugepages)
43
44
45def _probe_devmem(cfg, rx_page_size):
46 """Return True if ncdevmem can bind cfg.ifname at @rx_page_size."""
47 probe_command = f"{cfg.bin_local} -f {cfg.ifname}"
48 if rx_page_size != RX_PAGE_SIZE_DEFAULT:
49 probe_command += f" -b {rx_page_size}"
50 return cmd(probe_command, fail=False, shell=True).ret == 0
51
52
53def require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_DEFAULT):
54 """Probe ncdevmem on cfg.ifname and SKIP the test if devmem isn't supported."""
55 if rx_page_size not in PROBE_RX_PAGE_SIZES:
56 raise RuntimeError(
57 f"rx-page-size={rx_page_size} is missing from "
58 f"PROBE_RX_PAGE_SIZES, so it was never probed.")
59
60 if not hasattr(cfg, "devmem_supported"):
61 _reserve_hugepages()
62 # Probe every size upfront: in nk tests a leased queue may land in
63 # ncdevmem's queue range and cause the probe to fail.
64 cfg.devmem_supported = {size: _probe_devmem(cfg, size)
65 for size in PROBE_RX_PAGE_SIZES}
66
67 if not cfg.devmem_supported[RX_PAGE_SIZE_DEFAULT]:
68 raise KsftSkipEx("Test requires devmem support")
69
70 if rx_page_size != RX_PAGE_SIZE_DEFAULT:
71 page_size = os.sysconf("SC_PAGE_SIZE")
72 if not _is_aligned(rx_page_size, page_size):
73 raise KsftSkipEx(
74 f"rx-page-size={rx_page_size} is invalid for this platform "
75 f"(must be a multiple of PAGE_SIZE={page_size})")
76
77 if not cfg.devmem_supported[rx_page_size]:
78 raise KsftSkipEx(
79 f"Test requires devmem rx-page-size={rx_page_size} support")
80
81
82def configure_nic(cfg):
83 """Channels, rings, RSS, queue lease for netkit devmem."""
84 if not hasattr(cfg, "devmem_supported"):
85 raise RuntimeError(
86 "require_devmem() must be called before configure_nic(), which "
87 "may lease a queue away and make later probes fail.")
88
89 if not hasattr(cfg, 'netns'):
90 return
91
92 cfg.require_ipver('6')
93 ethnl = EthtoolFamily()
94
95 channels = ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
96 channels = channels['combined-count']
97 if channels < 2:
98 raise KsftSkipEx(
99 'Test requires NETIF with at least 2 combined channels'
100 )
101
102 rings = ethnl.rings_get({'header': {'dev-index': cfg.ifindex}})
103 orig_rx_rings = rings['rx']
104 orig_hds_thresh = rings.get('hds-thresh', 0)
105
106 ethnl.rings_set({'header': {'dev-index': cfg.ifindex},
107 'tcp-data-split': 'enabled',
108 'hds-thresh': 0,
109 'rx': min(64, orig_rx_rings)})
110 defer(ethnl.rings_set, {'header': {'dev-index': cfg.ifindex},
111 'tcp-data-split': 'unknown',
112 'hds-thresh': orig_hds_thresh,
113 'rx': orig_rx_rings})
114
115 cfg.src_queue = channels - 1
116 ethtool(f"-X {cfg.ifname} equal {cfg.src_queue}")
117 defer(ethtool, f"-X {cfg.ifname} default")
118
119 if not hasattr(cfg, 'nk_queue'):
120 with NetNSEnter(str(cfg.netns)):
121 netdevnl = NetdevFamily()
122 lease_result = netdevnl.queue_create({
123 "ifindex": cfg.nk_guest_ifindex,
124 "type": "rx",
125 "lease": {
126 "ifindex": cfg.ifindex,
127 "queue": {"id": cfg.src_queue, "type": "rx"},
128 "netns-id": 0,
129 },
130 })
131 cfg.nk_queue = lease_result['id']
132
133
134def set_flow_rule(cfg, port):
135 """Install a flow rule steering to src_queue and return the flow rule ID."""
136 output = ethtool(
137 f"-N {cfg.ifname} flow-type tcp6 dst-port {port}"
138 f" action {cfg.src_queue}"
139 ).stdout
140 return int(re.search(r'ID (\d+)', output).group(1))
141
142
143def ncdevmem_rx(cfg, port, verify=True, fail_on_linear=False, flow_steer=False,
144 rx_page_size=RX_PAGE_SIZE_DEFAULT):
145 """Build the ncdevmem RX listener command."""
146 if hasattr(cfg, 'netns'):
147 flow_rule_id = set_flow_rule(cfg, port)
148 defer(ethtool, f"-N {cfg.ifname} delete {flow_rule_id}")
149
150 ifname = cfg.nk_guest_ifname
151 addr = cfg.nk_guest_ipv6
152 extras = [f"-t {cfg.nk_queue}", "-q 1", "-n"]
153 else:
154 ifname = cfg.ifname
155 addr = cfg.addr
156 extras = []
157 if flow_steer:
158 extras.append(f"-c {cfg.remote_addr}")
159
160 if verify:
161 extras.append("-v 7")
162 if fail_on_linear:
163 extras.append("-L")
164 if rx_page_size != RX_PAGE_SIZE_DEFAULT:
165 extras.append(f"-b {rx_page_size}")
166
167 parts = [cfg.bin_local, "-l", f"-f {ifname}", f"-s {addr}",
168 f"-p {port}", *extras]
169 return " ".join(parts)
170
171
172def ncdevmem_tx(cfg, port, chunk_size=0):
173 """Build the ncdevmem TX send command."""
174 if hasattr(cfg, 'netns'):
175 ifname = cfg.nk_guest_ifname
176 addr = cfg.remote_addr_v['6']
177 extras = ["-t 0", "-q 1", "-n"]
178 else:
179 ifname = cfg.ifname
180 addr = cfg.remote_addr
181 extras = []
182
183 if chunk_size:
184 extras.append(f"-z {chunk_size}")
185
186 parts = [cfg.bin_local, f"-f {ifname}", f"-s {addr}",
187 f"-p {port}", *extras]
188 return " ".join(parts)
189
190
191def socat_send(cfg, port, buf_size=0):
192 """Socat command for sending to the devmem listener.
193
194 When buf_size > 0, force one TCP segment per write of exactly that size by
195 setting socat's buffer (-b) and disabling Nagle (TCP_NODELAY).
196 """
197 proto = f"TCP{cfg.addr_ipver}"
198
199 if hasattr(cfg, 'netns'):
200 addr = f"[{cfg.nk_guest_ipv6}]"
201 else:
202 addr = cfg.baddr
203
204 suffix = f",bind={cfg.remote_baddr}:{port}"
205
206 buf = ""
207 if buf_size:
208 buf = f"-b {buf_size}"
209 suffix += ",nodelay"
210
211 return f"socat {buf} -u - {proto}:{addr}:{port}{suffix}"
212
213
214def socat_listen(cfg, port):
215 """Socat listen command for TX tests."""
216 return f"socat -U - TCP{cfg.addr_ipver}-LISTEN:{port}"
217
218
219def setup_test(cfg, bin_local):
220 """Stash the local ncdevmem path on cfg and deploy it to the remote."""
221 cfg.bin_local = bin_local
222 cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
223
224
225def run_rx(cfg):
226 """Run the devmem RX test."""
227 require_devmem(cfg)
228 configure_nic(cfg)
229 port = rand_port()
230 socat = socat_send(cfg, port)
231 data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | head -c 1K"
232 f" | {socat}")
233 netns = getattr(cfg, "netns", None)
234
235 listen_cmd = ncdevmem_rx(cfg, port, flow_steer=not hasattr(cfg, 'netns'))
236 with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
237 wait_port_listen(port, proto="tcp", ns=netns)
238 cmd(data_pipe, host=cfg.remote, shell=True)
239 ksft_eq(ncdevmem.ret, 0)
240
241
242def run_tx(cfg):
243 """Run the devmem TX test."""
244 require_devmem(cfg)
245 configure_nic(cfg)
246 netns = getattr(cfg, "netns", None)
247 port = rand_port()
248 tx_cmd = ncdevmem_tx(cfg, port)
249 listen_cmd = socat_listen(cfg, port)
250
251 with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
252 wait_port_listen(port, host=cfg.remote)
253 cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
254 ksft_eq(socat.stdout.strip(), "hello\nworld")
255
256
257def run_tx_chunks(cfg):
258 """Run the devmem TX chunking test."""
259 require_devmem(cfg)
260 configure_nic(cfg)
261 netns = getattr(cfg, "netns", None)
262 port = rand_port()
263 tx_cmd = ncdevmem_tx(cfg, port, chunk_size=3)
264 listen_cmd = socat_listen(cfg, port)
265
266 with bkg(listen_cmd, host=cfg.remote, exit_wait=True) as socat:
267 wait_port_listen(port, host=cfg.remote)
268 cmd(f"bash -c 'echo -e \"hello\\nworld\" | {tx_cmd}'", ns=netns, shell=True)
269 ksft_eq(socat.stdout.strip(), "hello\nworld")
270
271
272def run_rx_large_niov(cfg):
273 """Run the devmem RX test with a large niov (rx-page-size > PAGE_SIZE).
274
275 Sweep payload sizes that straddle the niov boundary: below, equal to,
276 and above rx_page_size, to exercise sub-niov, exact-niov, and multi-niov
277 RX paths.
278 """
279 require_devmem(cfg, rx_page_size=RX_PAGE_SIZE_16K)
280 _reserve_hugepages()
281 configure_nic(cfg)
282 netns = getattr(cfg, "netns", None)
283
284 for size in [1024, 4096, 8192, 16384, 32768, 65536]:
285 port = rand_port()
286 socat = socat_send(cfg, port)
287 listen_cmd = ncdevmem_rx(cfg, port,
288 flow_steer=not netns,
289 rx_page_size=RX_PAGE_SIZE_16K)
290 data_pipe = (f"yes $(echo -e \x01\x02\x03\x04\x05\x06) | "
291 f"head -c {size} | {socat}")
292 with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
293 wait_port_listen(port, proto="tcp", ns=netns)
294 cmd(data_pipe, host=cfg.remote, shell=True)
295 ksft_eq(ncdevmem.ret, 0,
296 f"large-niov failed for payload size {size}")
297
298
299def run_rx_hds(cfg):
300 """Run the HDS test by running devmem RX across a segment size sweep."""
301 require_devmem(cfg)
302 configure_nic(cfg)
303 netns = getattr(cfg, "netns", None)
304
305 for size in [1, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192]:
306 port = rand_port()
307
308 listen_cmd = ncdevmem_rx(cfg, port, verify=False,
309 fail_on_linear=True)
310 socat = socat_send(cfg, port, buf_size=size)
311
312 with bkg(listen_cmd, exit_wait=True, ns=netns) as ncdevmem:
313 wait_port_listen(port, proto="tcp", ns=netns)
314 cmd(f"dd if=/dev/zero bs={size} count=1 2>/dev/null | "
315 f"{socat}", host=cfg.remote, shell=True)
316 ksft_eq(ncdevmem.ret, 0, f"HDS failed for payload size {size}")