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"""
5HW GRO tests focusing on device machinery like stats, rather than protocol
6processing.
7"""
8
9import glob
10import re
11
12from lib.py import ksft_run, ksft_exit, ksft_pr
13from lib.py import ksft_eq, ksft_ge, ksft_variants
14from lib.py import NetDrvEpEnv, NetdevFamily
15from lib.py import KsftSkipEx
16from lib.py import bkg, cmd, defer, ethtool, ip
17
18
19# gro.c uses hardcoded DPORT=8000
20GRO_DPORT = 8000
21
22
23def _get_queue_stats(cfg, queue_id):
24 """Get stats for a specific Rx queue."""
25 cfg.wait_hw_stats_settle()
26 data = cfg.netnl.qstats_get({"ifindex": cfg.ifindex, "scope": ["queue"]},
27 dump=True)
28 for q in data:
29 if q.get('queue-type') == 'rx' and q.get('queue-id') == queue_id:
30 return q
31 return {}
32
33
34def _resolve_dmac(cfg, ipver):
35 """Find the destination MAC address for sending packets."""
36 attr = "dmac" + ipver
37 if hasattr(cfg, attr):
38 return getattr(cfg, attr)
39
40 route = ip(f"-{ipver} route get {cfg.addr_v[ipver]}",
41 json=True, host=cfg.remote)[0]
42 gw = route.get("gateway")
43 if not gw:
44 setattr(cfg, attr, cfg.dev['address'])
45 return getattr(cfg, attr)
46
47 cmd(f"ping -c1 -W0 -I{cfg.remote_ifname} {gw}", host=cfg.remote)
48 neigh = ip(f"neigh get {gw} dev {cfg.remote_ifname}",
49 json=True, host=cfg.remote)[0]
50 setattr(cfg, attr, neigh['lladdr'])
51 return getattr(cfg, attr)
52
53
54def _setup_isolated_queue(cfg):
55 """Set up an isolated queue for testing using ntuple filter.
56
57 Remove queue 1 from the default RSS context and steer test traffic to it.
58 """
59 test_queue = 1
60
61 qcnt = len(glob.glob(f"/sys/class/net/{cfg.ifname}/queues/rx-*"))
62 if qcnt < 2:
63 raise KsftSkipEx(f"Need at least 2 queues, have {qcnt}")
64
65 # Remove queue 1 from default RSS context by setting its weight to 0
66 weights = ["1"] * qcnt
67 weights[test_queue] = "0"
68 ethtool(f"-X {cfg.ifname} weight " + " ".join(weights))
69 defer(ethtool, f"-X {cfg.ifname} default")
70
71 # Set up ntuple filter to steer our test traffic to the isolated queue
72 flow = f"flow-type tcp{cfg.addr_ipver} "
73 flow += f"dst-ip {cfg.addr} dst-port {GRO_DPORT} action {test_queue}"
74 output = ethtool(f"-N {cfg.ifname} {flow}").stdout
75 ntuple_id = int(output.split()[-1])
76 defer(ethtool, f"-N {cfg.ifname} delete {ntuple_id}")
77
78 return test_queue
79
80
81def _run_gro_test(cfg, test_name, num_flows=None, ignore_fail=False,
82 order_check=False):
83 """Run gro binary with given test and return output."""
84 if not hasattr(cfg, "bin_remote"):
85 cfg.bin_local = cfg.net_lib_dir / "gro"
86 cfg.bin_remote = cfg.remote.deploy(cfg.bin_local)
87
88 ipver = cfg.addr_ipver
89 protocol = f"--ipv{ipver}"
90 dmac = _resolve_dmac(cfg, ipver)
91
92 base_args = [
93 protocol,
94 f"--dmac {dmac}",
95 f"--smac {cfg.remote_dev['address']}",
96 f"--daddr {cfg.addr}",
97 f"--saddr {cfg.remote_addr_v[ipver]}",
98 f"--test {test_name}",
99 ]
100 if num_flows:
101 base_args.append(f"--num-flows {num_flows}")
102 if order_check:
103 base_args.append("--order-check")
104
105 args = " ".join(base_args)
106
107 rx_cmd = f"{cfg.bin_local} {args} --rx --iface {cfg.ifname}"
108 tx_cmd = f"{cfg.bin_remote} {args} --iface {cfg.remote_ifname}"
109
110 with bkg(rx_cmd, ksft_ready=True, exit_wait=True, fail=False) as rx_proc:
111 cmd(tx_cmd, host=cfg.remote)
112
113 if not ignore_fail:
114 ksft_eq(rx_proc.ret, 0)
115 if rx_proc.ret != 0:
116 ksft_pr(rx_proc)
117
118 return rx_proc.stdout
119
120
121def _require_hw_gro_stats(cfg, queue_id):
122 """Check if device reports HW GRO stats for the queue."""
123 stats = _get_queue_stats(cfg, queue_id)
124 required = ['rx-packets', 'rx-hw-gro-packets', 'rx-hw-gro-wire-packets']
125 for stat in required:
126 if stat not in stats:
127 raise KsftSkipEx(f"Driver does not report '{stat}' via qstats")
128
129
130def _set_ethtool_feat(cfg, current, feats):
131 """Set ethtool features with defer to restore original state."""
132 s2n = {True: "on", False: "off"}
133
134 new = ["-K", cfg.ifname]
135 old = ["-K", cfg.ifname]
136 no_change = True
137 for name, state in feats.items():
138 new += [name, s2n[state]]
139 old += [name, s2n[current[name]["active"]]]
140
141 if current[name]["active"] != state:
142 no_change = False
143 if current[name]["fixed"]:
144 raise KsftSkipEx(f"Device does not support {name}")
145 if no_change:
146 return
147
148 eth_cmd = ethtool(" ".join(new))
149 defer(ethtool, " ".join(old))
150
151 # If ethtool printed something kernel must have modified some features
152 if eth_cmd.stdout:
153 ksft_pr(eth_cmd)
154
155
156def _setup_hw_gro(cfg):
157 """Enable HW GRO on the device, disabling SW GRO."""
158 feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
159
160 # Try to disable SW GRO and enable HW GRO
161 _set_ethtool_feat(cfg, feat,
162 {"generic-receive-offload": False,
163 "rx-gro-hw": True,
164 "large-receive-offload": False})
165
166 # Some NICs treat HW GRO as a GRO sub-feature so disabling GRO
167 # will also clear HW GRO. Use a hack of installing XDP generic
168 # to skip SW GRO, even when enabled.
169 feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
170 if not feat["rx-gro-hw"]["active"]:
171 ksft_pr("Driver clears HW GRO when SW GRO is cleared, using generic XDP workaround")
172 prog = cfg.net_lib_dir / "xdp_dummy.bpf.o"
173 ip(f"link set dev {cfg.ifname} xdpgeneric obj {prog} sec xdp")
174 defer(ip, f"link set dev {cfg.ifname} xdpgeneric off")
175
176 # Attaching XDP may change features, fetch the latest state
177 feat = ethtool(f"-k {cfg.ifname}", json=True)[0]
178
179 _set_ethtool_feat(cfg, feat,
180 {"generic-receive-offload": True,
181 "rx-gro-hw": True,
182 "large-receive-offload": False})
183
184
185def _check_gro_stats(cfg, test_queue, stats_before,
186 expect_rx, expect_gro, expect_wire):
187 """Validate GRO stats against expected values."""
188 stats_after = _get_queue_stats(cfg, test_queue)
189
190 rx_delta = (stats_after.get('rx-packets', 0) -
191 stats_before.get('rx-packets', 0))
192 gro_delta = (stats_after.get('rx-hw-gro-packets', 0) -
193 stats_before.get('rx-hw-gro-packets', 0))
194 wire_delta = (stats_after.get('rx-hw-gro-wire-packets', 0) -
195 stats_before.get('rx-hw-gro-wire-packets', 0))
196
197 ksft_eq(rx_delta, expect_rx, comment="rx-packets")
198 ksft_eq(gro_delta, expect_gro, comment="rx-hw-gro-packets")
199 ksft_eq(wire_delta, expect_wire, comment="rx-hw-gro-wire-packets")
200
201
202def test_gro_stats_single(cfg):
203 """
204 Test that a single packet doesn't affect GRO stats.
205
206 Send a single packet that cannot be coalesced (nothing to coalesce with).
207 GRO stats should not increase since no coalescing occurred.
208 rx-packets should increase by 2 (1 data + 1 FIN).
209 """
210 _setup_hw_gro(cfg)
211
212 test_queue = _setup_isolated_queue(cfg)
213 _require_hw_gro_stats(cfg, test_queue)
214
215 stats_before = _get_queue_stats(cfg, test_queue)
216
217 _run_gro_test(cfg, "single")
218
219 # 1 data + 1 FIN = 2 rx-packets, no coalescing
220 _check_gro_stats(cfg, test_queue, stats_before,
221 expect_rx=2, expect_gro=0, expect_wire=0)
222
223
224def test_gro_stats_full(cfg):
225 """
226 Test GRO stats when overwhelming HW GRO capacity.
227
228 Send 500 flows to exceed HW GRO flow capacity on a single queue.
229 This should result in some packets not being coalesced.
230 Validate that qstats match what gro.c observed.
231 """
232 _setup_hw_gro(cfg)
233
234 test_queue = _setup_isolated_queue(cfg)
235 _require_hw_gro_stats(cfg, test_queue)
236
237 num_flows = 500
238 stats_before = _get_queue_stats(cfg, test_queue)
239
240 # Run capacity test - will likely fail because not all packets coalesce
241 output = _run_gro_test(cfg, "capacity", num_flows=num_flows,
242 ignore_fail=True)
243
244 # Parse gro.c output: "STATS: received=X wire=Y coalesced=Z"
245 match = re.search(r'STATS: received=(\d+) wire=(\d+) coalesced=(\d+)',
246 output)
247 if not match:
248 raise KsftSkipEx(f"Could not parse gro.c output: {output}")
249
250 rx_frames = int(match.group(2))
251 gro_coalesced = int(match.group(3))
252
253 ksft_ge(gro_coalesced, 1,
254 comment="At least some packets should coalesce")
255
256 # received + 1 FIN, coalesced super-packets, coalesced * 2 wire packets
257 _check_gro_stats(cfg, test_queue, stats_before,
258 expect_rx=rx_frames + 1,
259 expect_gro=gro_coalesced,
260 expect_wire=gro_coalesced * 2)
261
262
263@ksft_variants([4, 32, 512])
264def test_gro_order(cfg, num_flows):
265 """
266 Test that HW GRO preserves packet ordering between flows.
267
268 Packets may get delayed until the aggregate is released,
269 but reordering between aggregates and packet terminating
270 the aggregate and normal packets should not happen.
271
272 Note that this test is stricter than truly required.
273 Reordering packets between flows should not cause issues.
274 This test will also fail if traffic is run over an ECMP fabric.
275 """
276 _setup_hw_gro(cfg)
277 _setup_isolated_queue(cfg)
278
279 _run_gro_test(cfg, "capacity", num_flows=num_flows, order_check=True)
280
281
282def main() -> None:
283 """ Ksft boiler plate main """
284
285 with NetDrvEpEnv(__file__, nsim_test=False) as cfg:
286 cfg.netnl = NetdevFamily()
287 ksft_run([test_gro_stats_single,
288 test_gro_stats_full,
289 test_gro_order], args=(cfg,))
290 ksft_exit()
291
292
293if __name__ == "__main__":
294 main()