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"""
5Driver-related behavior tests for RSS.
6"""
7
8from lib.py import ksft_run, ksft_exit, ksft_eq, ksft_ge
9from lib.py import ksft_variants, KsftNamedVariant, KsftSkipEx, ksft_raises
10from lib.py import defer, ethtool, CmdExitFailure
11from lib.py import EthtoolFamily, NlError
12from lib.py import NetDrvEnv
13
14
15def _is_power_of_two(n):
16 return n > 0 and (n & (n - 1)) == 0
17
18
19def _get_rss(cfg, context=0):
20 return ethtool(f"-x {cfg.ifname} context {context}", json=True)[0]
21
22
23def _test_rss_indir_size(cfg, qcnt, context=0):
24 """Test that indirection table size is at least 4x queue count."""
25 ethtool(f"-L {cfg.ifname} combined {qcnt}")
26
27 rss = _get_rss(cfg, context=context)
28 indir = rss['rss-indirection-table']
29 ksft_ge(len(indir), 4 * qcnt, "Table smaller than 4x")
30 return len(indir)
31
32
33def _maybe_create_context(cfg, create_context):
34 """ Either create a context and return its ID or return 0 for main ctx """
35 if not create_context:
36 return 0
37 try:
38 ctx = cfg.ethnl.rss_create_act({'header': {'dev-index': cfg.ifindex}})
39 ctx_id = ctx['context']
40 defer(cfg.ethnl.rss_delete_act,
41 {'header': {'dev-index': cfg.ifindex}, 'context': ctx_id})
42 except NlError:
43 raise KsftSkipEx("Device does not support additional RSS contexts")
44
45 return ctx_id
46
47
48def _require_dynamic_indir_size(cfg, ch_max):
49 """Skip if the device does not dynamically size its indirection table."""
50 ethtool(f"-X {cfg.ifname} default")
51 ethtool(f"-L {cfg.ifname} combined 2")
52 small = len(_get_rss(cfg)['rss-indirection-table'])
53 ethtool(f"-L {cfg.ifname} combined {ch_max}")
54 large = len(_get_rss(cfg)['rss-indirection-table'])
55
56 if small == large:
57 raise KsftSkipEx("Device does not dynamically size indirection table")
58
59
60@ksft_variants([
61 KsftNamedVariant("main", False),
62 KsftNamedVariant("ctx", True),
63])
64def indir_size_4x(cfg, create_context):
65 """
66 Test that the indirection table has at least 4 entries per queue.
67 Empirically network-heavy workloads like memcache suffer with the 33%
68 imbalance of a 2x indirection table size.
69 4x table translates to a 16% imbalance.
70 """
71 channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
72 ch_max = channels.get('combined-max', 0)
73 qcnt = channels['combined-count']
74
75 if ch_max < 3:
76 raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
77
78 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
79 ethtool(f"-L {cfg.ifname} combined 3")
80
81 ctx_id = _maybe_create_context(cfg, create_context)
82
83 indir_sz = _test_rss_indir_size(cfg, 3, context=ctx_id)
84
85 # Test with max queue count (max - 1 if max is a power of two)
86 test_max = ch_max - 1 if _is_power_of_two(ch_max) else ch_max
87 if test_max > 3 and indir_sz < test_max * 4:
88 _test_rss_indir_size(cfg, test_max, context=ctx_id)
89
90
91@ksft_variants([
92 KsftNamedVariant("main", False),
93 KsftNamedVariant("ctx", True),
94])
95def resize_periodic(cfg, create_context):
96 """Test that a periodic indirection table survives channel changes.
97
98 Set a non-default periodic table ([3, 2, 1, 0] x N) via netlink,
99 reduce channels to trigger a fold, then increase to trigger an
100 unfold. Using a reversed pattern (instead of [0, 1, 2, 3]) ensures
101 the test can distinguish a correct fold from a driver that silently
102 resets the table to defaults. Verify the exact pattern is preserved
103 and the size tracks the channel count.
104 """
105 channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
106 ch_max = channels.get('combined-max', 0)
107 qcnt = channels['combined-count']
108
109 if ch_max < 4:
110 raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
111
112 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
113
114 _require_dynamic_indir_size(cfg, ch_max)
115
116 ctx_id = _maybe_create_context(cfg, create_context)
117
118 # Set a non-default periodic pattern via netlink.
119 # Send only 4 entries (user_size=4) so the kernel replicates it
120 # to fill the device table. This allows folding down to 4 entries.
121 rss = _get_rss(cfg, context=ctx_id)
122 orig_size = len(rss['rss-indirection-table'])
123 pattern = [3, 2, 1, 0]
124 req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
125 if ctx_id:
126 req['context'] = ctx_id
127 else:
128 defer(ethtool, f"-X {cfg.ifname} default")
129 cfg.ethnl.rss_set(req)
130
131 # Shrink — should fold
132 ethtool(f"-L {cfg.ifname} combined 4")
133 rss = _get_rss(cfg, context=ctx_id)
134 indir = rss['rss-indirection-table']
135
136 ksft_ge(orig_size, len(indir), "Table did not shrink")
137 ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
138 "Folded table has wrong pattern")
139
140 # Grow back — should unfold
141 ethtool(f"-L {cfg.ifname} combined {ch_max}")
142 rss = _get_rss(cfg, context=ctx_id)
143 indir = rss['rss-indirection-table']
144
145 ksft_eq(len(indir), orig_size, "Table size not restored")
146 ksft_eq(indir, [3, 2, 1, 0] * (len(indir) // 4),
147 "Unfolded table has wrong pattern")
148
149
150@ksft_variants([
151 KsftNamedVariant("main", False),
152 KsftNamedVariant("ctx", True),
153])
154def resize_below_user_size_reject(cfg, create_context):
155 """Test that shrinking below user_size is rejected.
156
157 Send a table via netlink whose size (user_size) sits between
158 the small and large device table sizes. The table is periodic,
159 so folding would normally succeed, but the user_size floor must
160 prevent it.
161 """
162 channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
163 ch_max = channels.get('combined-max', 0)
164 qcnt = channels['combined-count']
165
166 if ch_max < 4:
167 raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
168
169 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
170
171 _require_dynamic_indir_size(cfg, ch_max)
172
173 ctx_id = _maybe_create_context(cfg, create_context)
174
175 # Measure the table size at max channels
176 rss = _get_rss(cfg, context=ctx_id)
177 big_size = len(rss['rss-indirection-table'])
178
179 # Measure the table size at reduced channels
180 ethtool(f"-L {cfg.ifname} combined 4")
181 rss = _get_rss(cfg, context=ctx_id)
182 small_size = len(rss['rss-indirection-table'])
183 ethtool(f"-L {cfg.ifname} combined {ch_max}")
184
185 if small_size >= big_size:
186 raise KsftSkipEx("Table did not shrink at reduced channels")
187
188 # Find a user_size
189 user_size = None
190 for div in [2, 4]:
191 candidate = big_size // div
192 if candidate > small_size and big_size % candidate == 0:
193 user_size = candidate
194 break
195 if user_size is None:
196 raise KsftSkipEx("No suitable user_size between small and big table")
197
198 # Send a periodic sub-table of exactly user_size entries.
199 # Pattern safe for 4 channels.
200 pattern = [0, 1, 2, 3] * (user_size // 4)
201 if len(pattern) != user_size:
202 raise KsftSkipEx(f"user_size ({user_size}) not divisible by 4")
203 req = {'header': {'dev-index': cfg.ifindex}, 'indir': pattern}
204 if ctx_id:
205 req['context'] = ctx_id
206 else:
207 defer(ethtool, f"-X {cfg.ifname} default")
208 cfg.ethnl.rss_set(req)
209
210 # Shrink channels — table would go to small_size < user_size.
211 # The table is periodic so folding would work, but user_size
212 # floor must reject it.
213 with ksft_raises(CmdExitFailure):
214 ethtool(f"-L {cfg.ifname} combined 4")
215
216
217@ksft_variants([
218 KsftNamedVariant("main", False),
219 KsftNamedVariant("ctx", True),
220])
221def resize_nonperiodic_reject(cfg, create_context):
222 """Test that a non-periodic table blocks channel reduction.
223
224 Set equal weight across all queues so the table is not periodic
225 at any smaller size, then verify channel reduction is rejected.
226 An additional context with a periodic table is created to verify
227 that validation catches the non-periodic one even when others
228 are fine.
229 """
230 channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
231 ch_max = channels.get('combined-max', 0)
232 qcnt = channels['combined-count']
233
234 if ch_max < 4:
235 raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
236
237 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
238
239 _require_dynamic_indir_size(cfg, ch_max)
240
241 ctx_id = _maybe_create_context(cfg, create_context)
242 ctx_ref = f"context {ctx_id}" if ctx_id else ""
243
244 # Create an extra context with a periodic (foldable) table so that
245 # the validation must iterate all contexts to find the bad one.
246 extra_ctx = _maybe_create_context(cfg, True)
247 ethtool(f"-X {cfg.ifname} context {extra_ctx} equal 2")
248
249 ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
250 if not create_context:
251 defer(ethtool, f"-X {cfg.ifname} default")
252
253 with ksft_raises(CmdExitFailure):
254 ethtool(f"-L {cfg.ifname} combined 2")
255
256
257@ksft_variants([
258 KsftNamedVariant("main", False),
259 KsftNamedVariant("ctx", True),
260])
261def resize_nonperiodic_no_corruption(cfg, create_context):
262 """Test that a failed resize does not corrupt table or channel count.
263
264 Set a non-periodic table, attempt a channel reduction (which must
265 fail), then verify both the indirection table contents and the
266 channel count are unchanged.
267 """
268 channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
269 ch_max = channels.get('combined-max', 0)
270 qcnt = channels['combined-count']
271
272 if ch_max < 4:
273 raise KsftSkipEx(f"Not enough queues for the test: max={ch_max}")
274
275 defer(ethtool, f"-L {cfg.ifname} combined {qcnt}")
276
277 _require_dynamic_indir_size(cfg, ch_max)
278
279 ctx_id = _maybe_create_context(cfg, create_context)
280 ctx_ref = f"context {ctx_id}" if ctx_id else ""
281
282 ethtool(f"-X {cfg.ifname} {ctx_ref} equal {ch_max}")
283 if not create_context:
284 defer(ethtool, f"-X {cfg.ifname} default")
285
286 rss_before = _get_rss(cfg, context=ctx_id)
287
288 with ksft_raises(CmdExitFailure):
289 ethtool(f"-L {cfg.ifname} combined 2")
290
291 rss_after = _get_rss(cfg, context=ctx_id)
292 ksft_eq(rss_after['rss-indirection-table'],
293 rss_before['rss-indirection-table'],
294 "Indirection table corrupted after failed resize")
295
296 channels = cfg.ethnl.channels_get({'header': {'dev-index': cfg.ifindex}})
297 ksft_eq(channels['combined-count'], ch_max,
298 "Channel count changed after failed resize")
299
300
301def main() -> None:
302 """ Ksft boiler plate main """
303 with NetDrvEnv(__file__) as cfg:
304 cfg.ethnl = EthtoolFamily()
305 ksft_run([indir_size_4x, resize_periodic,
306 resize_below_user_size_reject,
307 resize_nonperiodic_reject,
308 resize_nonperiodic_no_corruption], args=(cfg, ))
309 ksft_exit()
310
311
312if __name__ == "__main__":
313 main()