Linux kernel mirror (for testing) git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
kernel os linux

selftests: net: tc_taprio: new test

Add a forwarding path test for tc-taprio, based on isochron. This is
specifically intended for NICs with an offloaded data path (switchdev/DSA)
and requires taprio 'flags 2'. Also, $h1 and $h2 must support hardware
timestamping, and $h1 tc-etf offload, for isochron to work.

Packets received by a switch while the egress port has a taprio schedule
with an open gate for the traffic class must be sent right away.

Packets received by the switch while the traffic class gate must be
delayed until it opens.

Packets received by the switch must be dropped if the gate for the
traffic class never opens.

Packets should pass if the maximum SDU for the traffic class allows it,
and should be dropped otherwise.

The schedule should auto-update itself if clock jumps take place while
taprio is installed. Repeat most of the above tests after forcing two
clock jumps, one backwards (in Jan 1970) and one back into the present.

Symlink it from tools/testing/selftests/drivers/net/dsa, because usually
DSA ports have the same MAC address, and we need STABLE_MAC_ADDRS=yes
from its forwarding.config for the test to run successfully.

Signed-off-by: Vladimir Oltean <vladimir.oltean@nxp.com>
Link: https://patch.msgid.link/20250426144859.3128352-5-vladimir.oltean@nxp.com
Signed-off-by: Jakub Kicinski <kuba@kernel.org>

authored by

Vladimir Oltean and committed by
Jakub Kicinski
4eb9da05 f52fe6ef

+431
+421
tools/testing/selftests/net/forwarding/tc_taprio.sh
··· 1 + #!/bin/bash 2 + # SPDX-License-Identifier: GPL-2.0 3 + 4 + ALL_TESTS=" \ 5 + test_clock_jump_backward \ 6 + test_taprio_after_ptp \ 7 + test_max_sdu \ 8 + test_clock_jump_backward_forward \ 9 + " 10 + NUM_NETIFS=4 11 + source tc_common.sh 12 + source lib.sh 13 + source tsn_lib.sh 14 + 15 + require_command python3 16 + 17 + # The test assumes the usual topology from the README, where h1 is connected to 18 + # swp1, h2 to swp2, and swp1 and swp2 are together in a bridge. 19 + # Additional assumption: h1 and h2 use the same PHC, and so do swp1 and swp2. 20 + # By synchronizing h1 to swp1 via PTP, h2 is also implicitly synchronized to 21 + # swp1 (and both to CLOCK_REALTIME). 22 + h1=${NETIFS[p1]} 23 + swp1=${NETIFS[p2]} 24 + swp2=${NETIFS[p3]} 25 + h2=${NETIFS[p4]} 26 + 27 + UDS_ADDRESS_H1="/var/run/ptp4l_h1" 28 + UDS_ADDRESS_SWP1="/var/run/ptp4l_swp1" 29 + 30 + H1_IPV4="192.0.2.1" 31 + H2_IPV4="192.0.2.2" 32 + H1_IPV6="2001:db8:1::1" 33 + H2_IPV6="2001:db8:1::2" 34 + 35 + # Tunables 36 + NUM_PKTS=100 37 + STREAM_VID=10 38 + STREAM_PRIO_1=6 39 + STREAM_PRIO_2=5 40 + STREAM_PRIO_3=4 41 + # PTP uses TC 0 42 + ALL_GATES=$((1 << 0 | 1 << STREAM_PRIO_1 | 1 << STREAM_PRIO_2)) 43 + # Use a conservative cycle of 10 ms to allow the test to still pass when the 44 + # kernel has some extra overhead like lockdep etc 45 + CYCLE_TIME_NS=10000000 46 + # Create two Gate Control List entries, one OPEN and one CLOSE, of equal 47 + # durations 48 + GATE_DURATION_NS=$((CYCLE_TIME_NS / 2)) 49 + # Give 2/3 of the cycle time to user space and 1/3 to the kernel 50 + FUDGE_FACTOR=$((CYCLE_TIME_NS / 3)) 51 + # Shift the isochron base time by half the gate time, so that packets are 52 + # always received by swp1 close to the middle of the time slot, to minimize 53 + # inaccuracies due to network sync 54 + SHIFT_TIME_NS=$((GATE_DURATION_NS / 2)) 55 + 56 + path_delay= 57 + 58 + h1_create() 59 + { 60 + simple_if_init $h1 $H1_IPV4/24 $H1_IPV6/64 61 + } 62 + 63 + h1_destroy() 64 + { 65 + simple_if_fini $h1 $H1_IPV4/24 $H1_IPV6/64 66 + } 67 + 68 + h2_create() 69 + { 70 + simple_if_init $h2 $H2_IPV4/24 $H2_IPV6/64 71 + } 72 + 73 + h2_destroy() 74 + { 75 + simple_if_fini $h2 $H2_IPV4/24 $H2_IPV6/64 76 + } 77 + 78 + switch_create() 79 + { 80 + local h2_mac_addr=$(mac_get $h2) 81 + 82 + ip link set $swp1 up 83 + ip link set $swp2 up 84 + 85 + ip link add br0 type bridge vlan_filtering 1 86 + ip link set $swp1 master br0 87 + ip link set $swp2 master br0 88 + ip link set br0 up 89 + 90 + bridge vlan add dev $swp2 vid $STREAM_VID 91 + bridge vlan add dev $swp1 vid $STREAM_VID 92 + bridge fdb add dev $swp2 \ 93 + $h2_mac_addr vlan $STREAM_VID static master 94 + } 95 + 96 + switch_destroy() 97 + { 98 + ip link del br0 99 + } 100 + 101 + ptp_setup() 102 + { 103 + # Set up swp1 as a master PHC for h1, synchronized to the local 104 + # CLOCK_REALTIME. 105 + phc2sys_start $UDS_ADDRESS_SWP1 106 + ptp4l_start $h1 true $UDS_ADDRESS_H1 107 + ptp4l_start $swp1 false $UDS_ADDRESS_SWP1 108 + } 109 + 110 + ptp_cleanup() 111 + { 112 + ptp4l_stop $swp1 113 + ptp4l_stop $h1 114 + phc2sys_stop 115 + } 116 + 117 + txtime_setup() 118 + { 119 + local if_name=$1 120 + 121 + tc qdisc add dev $if_name clsact 122 + # Classify PTP on TC 7 and isochron on TC 6 123 + tc filter add dev $if_name egress protocol 0x88f7 \ 124 + flower action skbedit priority 7 125 + tc filter add dev $if_name egress protocol 802.1Q \ 126 + flower vlan_ethtype 0xdead action skbedit priority 6 127 + tc qdisc add dev $if_name handle 100: parent root mqprio num_tc 8 \ 128 + queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 \ 129 + map 0 1 2 3 4 5 6 7 \ 130 + hw 1 131 + # Set up TC 5, 6, 7 for SO_TXTIME. tc-mqprio queues count from 1. 132 + tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_1 + 1)) etf \ 133 + clockid CLOCK_TAI offload delta $FUDGE_FACTOR 134 + tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_2 + 1)) etf \ 135 + clockid CLOCK_TAI offload delta $FUDGE_FACTOR 136 + tc qdisc replace dev $if_name parent 100:$((STREAM_PRIO_3 + 1)) etf \ 137 + clockid CLOCK_TAI offload delta $FUDGE_FACTOR 138 + } 139 + 140 + txtime_cleanup() 141 + { 142 + local if_name=$1 143 + 144 + tc qdisc del dev $if_name clsact 145 + tc qdisc del dev $if_name root 146 + } 147 + 148 + taprio_replace() 149 + { 150 + local if_name="$1"; shift 151 + local extra_args="$1"; shift 152 + 153 + # STREAM_PRIO_1 always has an open gate. 154 + # STREAM_PRIO_2 has a gate open for GATE_DURATION_NS (half the cycle time) 155 + # STREAM_PRIO_3 always has a closed gate. 156 + tc qdisc replace dev $if_name root stab overhead 24 taprio num_tc 8 \ 157 + queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 \ 158 + map 0 1 2 3 4 5 6 7 \ 159 + sched-entry S $(printf "%x" $ALL_GATES) $GATE_DURATION_NS \ 160 + sched-entry S $(printf "%x" $((ALL_GATES & ~(1 << STREAM_PRIO_2)))) $GATE_DURATION_NS \ 161 + base-time 0 flags 0x2 $extra_args 162 + taprio_wait_for_admin $if_name 163 + } 164 + 165 + taprio_cleanup() 166 + { 167 + local if_name=$1 168 + 169 + tc qdisc del dev $if_name root 170 + } 171 + 172 + probe_path_delay() 173 + { 174 + local isochron_dat="$(mktemp)" 175 + local received 176 + 177 + log_info "Probing path delay" 178 + 179 + isochron_do "$h1" "$h2" "$UDS_ADDRESS_H1" "" 0 \ 180 + "$CYCLE_TIME_NS" "" "" "$NUM_PKTS" \ 181 + "$STREAM_VID" "$STREAM_PRIO_1" "" "$isochron_dat" 182 + 183 + received=$(isochron_report_num_received "$isochron_dat") 184 + if [ "$received" != "$NUM_PKTS" ]; then 185 + echo "Cannot establish basic data path between $h1 and $h2" 186 + exit $ksft_fail 187 + fi 188 + 189 + printf "pdelay = {}\n" > isochron_data.py 190 + isochron report --input-file "$isochron_dat" \ 191 + --printf-format "pdelay[%u] = %d - %d\n" \ 192 + --printf-args "qRT" \ 193 + >> isochron_data.py 194 + cat <<-'EOF' > isochron_postprocess.py 195 + #!/usr/bin/env python3 196 + 197 + from isochron_data import pdelay 198 + import numpy as np 199 + 200 + w = np.array(list(pdelay.values())) 201 + print("{}".format(np.max(w))) 202 + EOF 203 + path_delay=$(python3 ./isochron_postprocess.py) 204 + 205 + log_info "Path delay from $h1 to $h2 estimated at $path_delay ns" 206 + 207 + if [ "$path_delay" -gt "$GATE_DURATION_NS" ]; then 208 + echo "Path delay larger than gate duration, aborting" 209 + exit $ksft_fail 210 + fi 211 + 212 + rm -f ./isochron_data.py 2> /dev/null 213 + rm -f ./isochron_postprocess.py 2> /dev/null 214 + rm -f "$isochron_dat" 2> /dev/null 215 + } 216 + 217 + setup_prepare() 218 + { 219 + vrf_prepare 220 + 221 + h1_create 222 + h2_create 223 + switch_create 224 + 225 + txtime_setup $h1 226 + 227 + # Temporarily set up PTP just to probe the end-to-end path delay. 228 + ptp_setup 229 + probe_path_delay 230 + ptp_cleanup 231 + } 232 + 233 + cleanup() 234 + { 235 + pre_cleanup 236 + 237 + isochron_recv_stop 238 + txtime_cleanup $h1 239 + 240 + switch_destroy 241 + h2_destroy 242 + h1_destroy 243 + 244 + vrf_cleanup 245 + } 246 + 247 + run_test() 248 + { 249 + local base_time=$1; shift 250 + local stream_prio=$1; shift 251 + local expected_delay=$1; shift 252 + local should_fail=$1; shift 253 + local test_name=$1; shift 254 + local isochron_dat="$(mktemp)" 255 + local received 256 + local median_delay 257 + 258 + RET=0 259 + 260 + # Set the shift time equal to the cycle time, which effectively 261 + # cancels the default advance time. Packets won't be sent early in 262 + # software, which ensures that they won't prematurely enter through 263 + # the open gate in __test_out_of_band(). Also, the gate is open for 264 + # long enough that this won't cause a problem in __test_in_band(). 265 + isochron_do "$h1" "$h2" "$UDS_ADDRESS_H1" "" "$base_time" \ 266 + "$CYCLE_TIME_NS" "$SHIFT_TIME_NS" "$GATE_DURATION_NS" \ 267 + "$NUM_PKTS" "$STREAM_VID" "$stream_prio" "" "$isochron_dat" 268 + 269 + received=$(isochron_report_num_received "$isochron_dat") 270 + [ "$received" = "$NUM_PKTS" ] 271 + check_err_fail $should_fail $? "Reception of $NUM_PKTS packets" 272 + 273 + if [ $should_fail = 0 ] && [ "$received" = "$NUM_PKTS" ]; then 274 + printf "pdelay = {}\n" > isochron_data.py 275 + isochron report --input-file "$isochron_dat" \ 276 + --printf-format "pdelay[%u] = %d - %d\n" \ 277 + --printf-args "qRT" \ 278 + >> isochron_data.py 279 + cat <<-'EOF' > isochron_postprocess.py 280 + #!/usr/bin/env python3 281 + 282 + from isochron_data import pdelay 283 + import numpy as np 284 + 285 + w = np.array(list(pdelay.values())) 286 + print("{}".format(int(np.median(w)))) 287 + EOF 288 + median_delay=$(python3 ./isochron_postprocess.py) 289 + 290 + # If the condition below is true, packets were delayed by a closed gate 291 + [ "$median_delay" -gt $((path_delay + expected_delay)) ] 292 + check_fail $? "Median delay $median_delay is greater than expected delay $expected_delay plus path delay $path_delay" 293 + 294 + # If the condition below is true, packets were sent expecting them to 295 + # hit a closed gate in the switch, but were not delayed 296 + [ "$expected_delay" -gt 0 ] && [ "$median_delay" -lt "$expected_delay" ] 297 + check_fail $? "Median delay $median_delay is less than expected delay $expected_delay" 298 + fi 299 + 300 + log_test "$test_name" 301 + 302 + rm -f ./isochron_data.py 2> /dev/null 303 + rm -f ./isochron_postprocess.py 2> /dev/null 304 + rm -f "$isochron_dat" 2> /dev/null 305 + } 306 + 307 + __test_always_open() 308 + { 309 + run_test 0.000000000 $STREAM_PRIO_1 0 0 "Gate always open" 310 + } 311 + 312 + __test_always_closed() 313 + { 314 + run_test 0.000000000 $STREAM_PRIO_3 0 1 "Gate always closed" 315 + } 316 + 317 + __test_in_band() 318 + { 319 + # Send packets in-band with the OPEN gate entry 320 + run_test 0.000000000 $STREAM_PRIO_2 0 0 "In band with gate" 321 + } 322 + 323 + __test_out_of_band() 324 + { 325 + # Send packets in-band with the CLOSE gate entry 326 + run_test 0.005000000 $STREAM_PRIO_2 \ 327 + $((GATE_DURATION_NS - SHIFT_TIME_NS)) 0 \ 328 + "Out of band with gate" 329 + } 330 + 331 + run_subtests() 332 + { 333 + __test_always_open 334 + __test_always_closed 335 + __test_in_band 336 + __test_out_of_band 337 + } 338 + 339 + test_taprio_after_ptp() 340 + { 341 + log_info "Setting up taprio after PTP" 342 + ptp_setup 343 + taprio_replace $swp2 344 + run_subtests 345 + taprio_cleanup $swp2 346 + ptp_cleanup 347 + } 348 + 349 + __test_under_max_sdu() 350 + { 351 + # Limit max-sdu for STREAM_PRIO_1 352 + taprio_replace "$swp2" "max-sdu 0 0 0 0 0 0 100 0" 353 + run_test 0.000000000 $STREAM_PRIO_1 0 0 "Under maximum SDU" 354 + } 355 + 356 + __test_over_max_sdu() 357 + { 358 + # Limit max-sdu for STREAM_PRIO_1 359 + taprio_replace "$swp2" "max-sdu 0 0 0 0 0 0 20 0" 360 + run_test 0.000000000 $STREAM_PRIO_1 0 1 "Over maximum SDU" 361 + } 362 + 363 + test_max_sdu() 364 + { 365 + ptp_setup 366 + __test_under_max_sdu 367 + __test_over_max_sdu 368 + taprio_cleanup $swp2 369 + ptp_cleanup 370 + } 371 + 372 + # Perform a clock jump in the past without synchronization running, so that the 373 + # time base remains where it was set by phc_ctl. 374 + test_clock_jump_backward() 375 + { 376 + # This is a more complex schedule specifically crafted in a way that 377 + # has been problematic on NXP LS1028A. Not much to test with it other 378 + # than the fact that it passes traffic. 379 + tc qdisc replace dev $swp2 root stab overhead 24 taprio num_tc 8 \ 380 + queues 1@0 1@1 1@2 1@3 1@4 1@5 1@6 1@7 map 0 1 2 3 4 5 6 7 \ 381 + base-time 0 sched-entry S 20 300000 sched-entry S 10 200000 \ 382 + sched-entry S 20 300000 sched-entry S 48 200000 \ 383 + sched-entry S 20 300000 sched-entry S 83 200000 \ 384 + sched-entry S 40 300000 sched-entry S 00 200000 flags 2 385 + 386 + log_info "Forcing a backward clock jump" 387 + phc_ctl $swp1 set 0 388 + 389 + ping_test $h1 192.0.2.2 390 + taprio_cleanup $swp2 391 + } 392 + 393 + # Test that taprio tolerates clock jumps. 394 + # Since ptp4l and phc2sys are running, it is expected for the time to 395 + # eventually recover (through yet another clock jump). Isochron waits 396 + # until that is the case. 397 + test_clock_jump_backward_forward() 398 + { 399 + log_info "Forcing a backward and a forward clock jump" 400 + taprio_replace $swp2 401 + phc_ctl $swp1 set 0 402 + ptp_setup 403 + ping_test $h1 192.0.2.2 404 + run_subtests 405 + ptp_cleanup 406 + taprio_cleanup $swp2 407 + } 408 + 409 + tc_offload_check 410 + if [[ $? -ne 0 ]]; then 411 + log_test_skip "Could not test offloaded functionality" 412 + exit $EXIT_STATUS 413 + fi 414 + 415 + trap cleanup EXIT 416 + 417 + setup_prepare 418 + setup_wait 419 + tests_run 420 + 421 + exit $EXIT_STATUS
+10
tools/testing/selftests/net/forwarding/tsn_lib.sh
··· 2 2 # SPDX-License-Identifier: GPL-2.0 3 3 # Copyright 2021-2022 NXP 4 4 5 + tc_testing_scripts_dir=$(dirname $0)/../../tc-testing/scripts 6 + 5 7 REQUIRE_ISOCHRON=${REQUIRE_ISOCHRON:=yes} 6 8 REQUIRE_LINUXPTP=${REQUIRE_LINUXPTP:=yes} 7 9 ··· 20 18 if [[ "$REQUIRE_LINUXPTP" = "yes" ]]; then 21 19 require_command phc2sys 22 20 require_command ptp4l 21 + require_command phc_ctl 23 22 fi 24 23 25 24 phc2sys_start() ··· 265 262 --input-file "${isochron_dat}" \ 266 263 --printf-format "%u\n" --printf-args "R" | \ 267 264 grep -w -v '0' | wc -l 265 + } 266 + 267 + taprio_wait_for_admin() 268 + { 269 + local if_name="$1"; shift 270 + 271 + "$tc_testing_scripts_dir/taprio_wait_for_admin.sh" "$(which tc)" "$if_name" 268 272 }