From: hawk@kernel.org
To: netdev@vger.kernel.org
Cc: davem@davemloft.net, dsahern@kernel.org, edumazet@google.com,
kuba@kernel.org, pabeni@redhat.com, horms@kernel.org,
shuah@kernel.org, linux-kselftest@vger.kernel.org,
hawk@kernel.org, ivan@cloudflare.com, kernel-team@cloudflare.com
Subject: [RFC PATCH net-next 4/4] selftests: net: add IPv4 address lookup stress test
Date: Tue, 31 Mar 2026 23:07:39 +0200 [thread overview]
Message-ID: <20260331210739.3998753-5-hawk@kernel.org> (raw)
In-Reply-To: <20260331210739.3998753-1-hawk@kernel.org>
From: Jesper Dangaard Brouer <hawk@kernel.org>
Add a test that exercises the IPv4 local address hash table
(inet_addr_lst) insert, lookup, and remove paths under load:
- Add/remove 1000 addresses to trigger rhltable growth and shrinking
- Unconnected UDP sendmsg stress to exercise the __ip_dev_find()
lookup hot path (each sendto triggers a hash table lookup)
- Duplicate key test: same IP on two different interfaces
- Address lifetime expiry via check_lifetime() work function
- Ping-based lookup verification from sampled addresses
The test uses network namespaces and veth pairs to avoid polluting the
host. A C helper (ipv4_addr_lookup_udp_sender) pre-creates sockets
during setup for low-noise measurement with per-round statistics.
Optional bpftrace integration (--bpftrace, --bpftrace-debug) provides
latency histograms and resize event tracing for A/B kernel comparison.
A virtme-ng wrapper script is included for isolated VM testing.
Signed-off-by: Jesper Dangaard Brouer <hawk@kernel.org>
---
tools/testing/selftests/net/Makefile | 4 +
.../selftests/net/ipv4_addr_lookup_test.sh | 804 ++++++++++++++++++
.../net/ipv4_addr_lookup_test_virtme.sh | 282 ++++++
.../selftests/net/ipv4_addr_lookup_trace.bt | 178 ++++
.../net/ipv4_addr_lookup_udp_sender.c | 401 +++++++++
5 files changed, 1669 insertions(+)
create mode 100755 tools/testing/selftests/net/ipv4_addr_lookup_test.sh
create mode 100755 tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh
create mode 100644 tools/testing/selftests/net/ipv4_addr_lookup_trace.bt
create mode 100644 tools/testing/selftests/net/ipv4_addr_lookup_udp_sender.c
diff --git a/tools/testing/selftests/net/Makefile b/tools/testing/selftests/net/Makefile
index 6bced3ed798b..1724d1478020 100644
--- a/tools/testing/selftests/net/Makefile
+++ b/tools/testing/selftests/net/Makefile
@@ -42,6 +42,7 @@ TEST_PROGS := \
gre_ipv6_lladdr.sh \
icmp.sh \
icmp_redirect.sh \
+ ipv4_addr_lookup_test.sh \
io_uring_zerocopy_tx.sh \
ioam6.sh \
ip6_gre_headroom.sh \
@@ -127,6 +128,8 @@ TEST_PROGS := \
# end of TEST_PROGS
TEST_PROGS_EXTENDED := \
+ ipv4_addr_lookup_test_virtme.sh \
+ ipv4_addr_lookup_trace.bt \
xfrm_policy_add_speed.sh \
# end of TEST_PROGS_EXTENDED
@@ -135,6 +138,7 @@ TEST_GEN_FILES := \
cmsg_sender \
fin_ack_lat \
hwtstamp_config \
+ ipv4_addr_lookup_udp_sender \
io_uring_zerocopy_tx \
ioam6_parser \
ip_defrag \
diff --git a/tools/testing/selftests/net/ipv4_addr_lookup_test.sh b/tools/testing/selftests/net/ipv4_addr_lookup_test.sh
new file mode 100755
index 000000000000..df9924e165af
--- /dev/null
+++ b/tools/testing/selftests/net/ipv4_addr_lookup_test.sh
@@ -0,0 +1,804 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+#
+# Stress test for IPv4 address hash table (inet_addr_lst / rhltable).
+#
+# Exercises the rhltable insert, lookup, and remove paths by:
+# 1. Adding many IPv4 addresses (triggers rhltable growth/resizing)
+# 2. Sending unconnected UDP to exercise the __ip_dev_find lookup hot path
+# 3. Removing all addresses (triggers rhltable shrinking)
+# 4. Testing duplicate keys (same IP on different devices)
+#
+# Uses veth pairs in network namespaces to avoid polluting the host.
+#
+# Options:
+# --num-addrs N Number of addresses to add (default: 1000)
+# --rounds N Measurement rounds for UDP benchmark (default: 10)
+# --duration S Seconds per measurement round (default: 3)
+# --bench-only Only run the UDP sendmsg benchmark (skip other tests)
+# --sink Use C receiver to count packets (adds CPU overhead)
+# --threaded-napi Move veth RX to separate CPU (cleaner perf profiles)
+# --verbose Show detailed output
+# --help Show usage
+
+source "$(dirname "$(readlink -e "${BASH_SOURCE[0]}")")/lib.sh"
+
+NUM_ADDRS=1000
+ROUNDS=10
+DURATION=3
+BENCH_ONLY=0
+VERBOSE=0
+USE_BPFTRACE=0
+BPFTRACE_DEBUG=0
+USE_SINK=0
+USE_THREADED_NAPI=0
+RET=0
+BPFTRACE_PID=0
+BPFTRACE_LOG=""
+
+usage() {
+ echo "Usage: $0 [OPTIONS]"
+ echo " --num-addrs N Number of IPv4 addresses to add (default: $NUM_ADDRS)"
+ echo " --rounds N Measurement rounds for benchmark (default: $ROUNDS)"
+ echo " --duration S Seconds per measurement round (default: $DURATION)"
+ echo " --bench-only Only run the UDP sendmsg benchmark"
+ echo " --verbose Show detailed output"
+ echo " --bpftrace Trace __ip_dev_find latency (minimal overhead for A/B)"
+ echo " --sink Use C receiver to count packets (adds CPU overhead)"
+ echo " --threaded-napi Move veth RX to separate CPU (cleaner perf profiles)"
+ echo " --bpftrace-debug Trace all code paths (lookup, insert, remove, resize)"
+ exit 0
+}
+
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --num-addrs) NUM_ADDRS="$2"; shift 2 ;;
+ --rounds) ROUNDS="$2"; shift 2 ;;
+ --duration) DURATION="$2"; shift 2 ;;
+ --bench-only) BENCH_ONLY=1; shift ;;
+ --verbose) VERBOSE=1; shift ;;
+ --bpftrace) USE_BPFTRACE=1; shift ;;
+ --sink) USE_SINK=1; shift ;;
+ --threaded-napi) USE_THREADED_NAPI=1; shift ;;
+ --bpftrace-debug) USE_BPFTRACE=1; BPFTRACE_DEBUG=1; shift ;;
+ --help) usage ;;
+ *) echo "Unknown option: $1"; usage ;;
+ esac
+done
+
+log() {
+ [ "$VERBOSE" -eq 1 ] && echo " $*"
+}
+
+log_config() {
+ echo " Config: $*"
+}
+
+PASS=0
+FAIL=0
+
+# ---------------------------------------------------------------------------
+# bpftrace helpers
+# ---------------------------------------------------------------------------
+
+BT_SCRIPT_GEN=""
+
+# Check if a kernel function is actually kprobe-able (not notrace)
+can_kprobe() {
+ local f="$1"
+ # available_filter_functions lists what kprobes can actually attach to
+ local aff
+ for aff in /sys/kernel/tracing/available_filter_functions \
+ /sys/kernel/debug/tracing/available_filter_functions; do
+ [ -r "$aff" ] && { grep -qw "$f" "$aff" 2>/dev/null; return; }
+ done
+ # Fallback: check kallsyms (may include notrace functions)
+ grep -q "^[0-9a-f]* [tT] ${f}$" /proc/kallsyms 2>/dev/null
+}
+
+# Build bpftrace script dynamically based on available symbols.
+# Sets NPROBES and writes to BT_SCRIPT_GEN (must be set before calling).
+bpftrace_build_script() {
+ NPROBES=0
+
+ # Resolve bucket_table_alloc (may have .isra.0 suffix from GCC)
+ local bta_sym=""
+ local aff
+ for aff in /sys/kernel/tracing/available_filter_functions \
+ /sys/kernel/debug/tracing/available_filter_functions; do
+ [ -r "$aff" ] && {
+ bta_sym=$(grep -oP 'bucket_table_alloc\S*' "$aff" 2>/dev/null | head -1)
+ break
+ }
+ done
+ [ -z "$bta_sym" ] && \
+ bta_sym=$(grep -oP '(?<= )[tT] \K(bucket_table_alloc[.\w]*)' \
+ /proc/kallsyms 2>/dev/null | head -1)
+
+ # --- BEGIN block ---
+ if [ "$BPFTRACE_DEBUG" -eq 1 ]; then
+ cat > "$BT_SCRIPT_GEN" <<'BTEOF'
+BEGIN {
+ printf("Tracing inet_addr_lst rhltable paths (debug mode)...\n\n");
+ @ipdev_count = 0; @lookup_count = 0;
+ @insert_count = 0; @insert_slow = 0; @remove_count = 0;
+ @resize_events = 0; @bucket_allocs = 0; @rehash_count = 0;
+ @tbl_size = 0; @tbl_resizes = 0;
+}
+BTEOF
+ else
+ cat > "$BT_SCRIPT_GEN" <<'BTEOF'
+BEGIN {
+ printf("Tracing inet_addr_lst rhltable paths...\n\n");
+ @ipdev_count = 0;
+}
+BTEOF
+ fi
+
+ # Detect old (hlist) vs new (rhltable) kernel:
+ # old kernel: inet_hash_insert does hlist hash+insert, visible to kprobe
+ # new kernel: inet_hash_insert wraps rhltable_insert, inlined away
+ local has_rhltable=0
+ if can_kprobe inet_hash_insert; then
+ log " detected OLD kernel (inet_hash_insert is kprobe-able)"
+ else
+ has_rhltable=1
+ log " detected NEW kernel (inet_hash_insert inlined -> rhltable)"
+ fi
+
+ # --- Core probe: __ip_dev_find (always, minimal overhead for A/B) ---
+ if can_kprobe __ip_dev_find; then
+ log " probe: __ip_dev_find (full lookup)"
+ if [ "$BPFTRACE_DEBUG" -eq 1 ] && [ "$has_rhltable" -eq 1 ]; then
+ # New kernel: read rhltable bucket count via BTF to detect resize
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:__ip_dev_find {
+ @ipdev_entry[tid] = nsecs;
+ $net = (struct net *)arg0;
+ $tbl = $net->ipv4.inet_addr_lst.ht.tbl;
+ $size = $tbl->size;
+ if ($size != @tbl_size) {
+ printf("TABLE RESIZE: buckets %lld -> %d (nelems=%d)\n",
+ @tbl_size, $size, $net->ipv4.inet_addr_lst.ht.nelems.counter);
+ @tbl_size = $size;
+ @tbl_resizes++;
+ }
+}
+BTEOF
+ else
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:__ip_dev_find { @ipdev_entry[tid] = nsecs; }
+BTEOF
+ fi
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kretprobe:__ip_dev_find /@ipdev_entry[tid]/ {
+ $dt = nsecs - @ipdev_entry[tid];
+ @ipdev_ns = hist($dt); @ipdev_stats = stats($dt); @ipdev_count++;
+ delete(@ipdev_entry[tid]);
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ # --- Debug probes (only with --bpftrace-debug) ---
+ local has_lookup=0 has_resize_wq=0 has_bta=0 has_rehash=0
+
+ if [ "$BPFTRACE_DEBUG" -eq 1 ]; then
+ log " debug mode: attaching extra probes"
+
+ if can_kprobe inet_lookup_ifaddr_rcu; then
+ has_lookup=1
+ log " probe: inet_lookup_ifaddr_rcu (inner lookup)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:inet_lookup_ifaddr_rcu { @lookup_entry[tid] = nsecs; }
+kretprobe:inet_lookup_ifaddr_rcu /@lookup_entry[tid]/ {
+ $dt = nsecs - @lookup_entry[tid];
+ @lookup_ns = hist($dt); @lookup_stats = stats($dt); @lookup_count++;
+ delete(@lookup_entry[tid]);
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe inet_hash_insert; then
+ log " probe: inet_hash_insert (old kernel insert path)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:inet_hash_insert { @insert_count++; }
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe rhashtable_insert_slow; then
+ log " probe: rhashtable_insert_slow (insert slow path)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:rhashtable_insert_slow { @insert_slow++; }
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe inet_hash_remove; then
+ log " probe: inet_hash_remove (remove)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:inet_hash_remove { @remove_count++; }
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe rht_deferred_worker; then
+ has_resize_wq=1
+ log " probe: rht_deferred_worker (resize worker)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:rht_deferred_worker {
+ @resize_wq_entry[tid] = nsecs; @resize_events++;
+ printf(">>> RESIZE #%lld: deferred_worker started\n", @resize_events);
+}
+kretprobe:rht_deferred_worker /@resize_wq_entry[tid]/ {
+ $dt = nsecs - @resize_wq_entry[tid];
+ @resize_wq_ns = hist($dt);
+ printf(" RESIZE: done in %lld us\n", $dt / 1000);
+ delete(@resize_wq_entry[tid]);
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if [ -n "$bta_sym" ] && can_kprobe "$bta_sym"; then
+ has_bta=1
+ log " probe: $bta_sym (table alloc, arg1=nbuckets)"
+ cat >> "$BT_SCRIPT_GEN" <<BTEOF
+kprobe:${bta_sym} {
+ @bucket_allocs++; @last_alloc_size = arg1;
+ printf(" RESIZE: bucket_table_alloc nbuckets=%lld\\n", arg1);
+ print(kstack(5));
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+
+ if can_kprobe rhashtable_rehash_table; then
+ has_rehash=1
+ log " probe: rhashtable_rehash_table (data migration)"
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+kprobe:rhashtable_rehash_table { @rehash_entry[tid] = nsecs; }
+kretprobe:rhashtable_rehash_table /@rehash_entry[tid]/ {
+ $dt = nsecs - @rehash_entry[tid];
+ @rehash_ns = hist($dt); @rehash_count++;
+ printf(" RESIZE: rehash done in %lld us\n", $dt / 1000);
+ delete(@rehash_entry[tid]);
+}
+BTEOF
+ NPROBES=$((NPROBES + 1))
+ fi
+ fi
+
+ # --- END block -- only reference maps that actually exist ---
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+END {
+ printf("\n========================================================\n");
+ printf(" inet_addr_lst rhltable trace summary\n");
+ printf("========================================================\n\n");
+ printf("--- __ip_dev_find latency (ns) ---\n");
+ print(@ipdev_ns);
+ printf(" stats (count/avg/total): "); print(@ipdev_stats);
+ printf("\nCOMPARISON: __ip_dev_find calls=%lld\n", @ipdev_count);
+BTEOF
+ if [ "$BPFTRACE_DEBUG" -eq 1 ]; then
+ if [ "$has_rhltable" -eq 1 ]; then
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- rhltable state (via BTF struct reads) ---\n");
+ printf(" kernel type : rhltable (new)\n");
+ printf(" final bucket count : %8lld\n", @tbl_size);
+ printf(" resize events observed : %8lld\n", @tbl_resizes);
+BTEOF
+ else
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- hash table type ---\n");
+ printf(" kernel type : hlist (old)\n");
+BTEOF
+ fi
+ [ "$has_lookup" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- inet_lookup_ifaddr_rcu latency (ns) ---\n");
+ print(@lookup_ns);
+ printf(" stats (count/avg/total): "); print(@lookup_stats);
+ printf("COMPARISON: inet_lookup_ifaddr_rcu calls=%lld\n", @lookup_count);
+ clear(@lookup_entry);
+BTEOF
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- Debug call counts ---\n");
+ printf(" inet_hash_insert : %8lld\n", @insert_count);
+ printf(" rhashtable_insert_slow : %8lld\n", @insert_slow);
+ printf(" inet_hash_remove : %8lld\n", @remove_count);
+ printf(" rht_deferred_worker : %8lld\n", @resize_events);
+ printf(" bucket_table_alloc : %8lld\n", @bucket_allocs);
+BTEOF
+ [ "$has_rehash" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf(" rhashtable_rehash : %8lld\n", @rehash_count);
+BTEOF
+ [ "$has_resize_wq" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- rht_deferred_worker duration (ns) ---\n");
+ print(@resize_wq_ns);
+ clear(@resize_wq_entry);
+BTEOF
+ [ "$has_rehash" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ printf("\n--- rhashtable_rehash_table duration (ns) ---\n");
+ print(@rehash_ns);
+ clear(@rehash_entry);
+BTEOF
+ [ "$has_bta" -eq 1 ] && cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ clear(@last_alloc_size);
+BTEOF
+ fi
+ cat >> "$BT_SCRIPT_GEN" <<'BTEOF'
+ clear(@ipdev_entry);
+}
+BTEOF
+}
+
+bpftrace_start() {
+ [ "$USE_BPFTRACE" -eq 0 ] && return
+
+ if ! command -v bpftrace >/dev/null 2>&1; then
+ echo "WARN: bpftrace not found, skipping tracing"
+ USE_BPFTRACE=0
+ return
+ fi
+
+ BT_SCRIPT_GEN=$(mktemp /tmp/rhltable_trace_XXXXXX.bt)
+ echo "Probing /proc/kallsyms for available trace points..."
+ bpftrace_build_script
+
+ if [ "$NPROBES" -eq 0 ]; then
+ echo "WARN: no kprobe-able symbols found, skipping tracing"
+ USE_BPFTRACE=0
+ rm -f "$BT_SCRIPT_GEN"
+ return
+ fi
+ echo "Built dynamic bpftrace script with $NPROBES probe groups"
+ log "Script: $BT_SCRIPT_GEN"
+
+ BPFTRACE_LOG=$(mktemp /tmp/rhltable_trace.XXXXXX)
+ bpftrace "$BT_SCRIPT_GEN" > "$BPFTRACE_LOG" 2>&1 &
+ BPFTRACE_PID=$!
+ # Give bpftrace time to attach probes
+ sleep 2
+ if ! kill -0 $BPFTRACE_PID 2>/dev/null; then
+ echo "WARN: bpftrace failed to start"
+ cat "$BPFTRACE_LOG"
+ USE_BPFTRACE=0
+ rm -f "$BT_SCRIPT_GEN"
+ return
+ fi
+ echo "bpftrace attached (pid $BPFTRACE_PID)"
+}
+
+bpftrace_stop() {
+ [ "$USE_BPFTRACE" -eq 0 ] && return
+ [ "$BPFTRACE_PID" -eq 0 ] && return
+
+ # Send INT so bpftrace prints its END summary
+ kill -INT $BPFTRACE_PID 2>/dev/null || true
+ wait $BPFTRACE_PID 2>/dev/null || true
+ BPFTRACE_PID=0
+
+ echo ""
+ echo "============================================"
+ echo "bpftrace output"
+ echo "============================================"
+ cat "$BPFTRACE_LOG"
+ echo ""
+
+ # Validate expected code paths were hit
+ local rc=0
+ if grep -q '__ip_dev_find calls=0' "$BPFTRACE_LOG" 2>/dev/null; then
+ echo "FAIL: __ip_dev_find was never called"
+ rc=1
+ elif grep -q 'COMPARISON: __ip_dev_find' "$BPFTRACE_LOG" 2>/dev/null; then
+ echo "PASS: __ip_dev_find lookup path verified"
+ fi
+ if grep -q 'TABLE RESIZE:' "$BPFTRACE_LOG" 2>/dev/null; then
+ echo "PASS: rhltable resize detected (BTF struct reads)"
+ elif grep -q 'RESIZE.*bucket_table_alloc' "$BPFTRACE_LOG" 2>/dev/null; then
+ echo "PASS: rhltable resize detected (kprobe)"
+ else
+ echo "INFO: no resize observed (use --bpftrace-debug to detect via BTF)"
+ fi
+ check_result "bpftrace code path verification" $rc
+
+ rm -f "$BPFTRACE_LOG" "$BT_SCRIPT_GEN"
+}
+
+check_result() {
+ local desc="$1"
+ local rc="$2"
+
+ if [ "$rc" -eq 0 ]; then
+ echo "PASS: $desc"
+ PASS=$((PASS + 1))
+ else
+ echo "FAIL: $desc"
+ FAIL=$((FAIL + 1))
+ RET=1
+ fi
+}
+
+cleanup() {
+ # Stop bpftrace if running
+ if [ "$BPFTRACE_PID" -ne 0 ]; then
+ kill -INT $BPFTRACE_PID 2>/dev/null || true
+ wait $BPFTRACE_PID 2>/dev/null || true
+ BPFTRACE_PID=0
+ fi
+
+ # Kill any other background jobs
+ local jobs
+ jobs="$(jobs -p 2>/dev/null)" || true
+ [ -n "$jobs" ] && kill $jobs 2>/dev/null || true
+ wait 2>/dev/null || true
+
+ cleanup_all_ns
+ [ -n "$BPFTRACE_LOG" ] && rm -f "$BPFTRACE_LOG"
+}
+
+trap cleanup EXIT
+
+# Helper: generate address from index (spreads across octets to avoid /24 limits)
+# Returns 10.B2.B3.1 where B2.B3 encodes the index
+idx_to_addr() {
+ local i=$1
+ local b2=$(( (i >> 8) & 0xff ))
+ local b3=$(( i & 0xff ))
+ echo "10.${b2}.${b3}.1"
+}
+
+# ---------------------------------------------------------------------------
+# Setup
+# ---------------------------------------------------------------------------
+
+setup() {
+ if ! setup_ns NS_SRC NS_DST; then
+ echo "SKIP: Could not create namespaces"
+ exit $ksft_skip
+ fi
+
+ # Create veth pair
+ ip link add veth_src type veth peer name veth_dst
+ ip link set veth_src netns "$NS_SRC"
+ ip link set veth_dst netns "$NS_DST"
+ ip -n "$NS_SRC" link set veth_src up
+ ip -n "$NS_DST" link set veth_dst up
+
+ if [ "$USE_THREADED_NAPI" -eq 1 ]; then
+ # Move veth RX to a separate NAPI kthread for cleaner perf profiles.
+ # Disable TSO on src so packets travel individually through the
+ # veth ptr_ring (256 entries), enable GRO on dst for NAPI polling.
+ ip netns exec "$NS_SRC" ethtool -K veth_src tso off 2>/dev/null || true
+ ip netns exec "$NS_DST" ethtool -K veth_dst gro on 2>/dev/null || true
+ ip netns exec "$NS_DST" \
+ bash -c 'echo 1 > /sys/class/net/veth_dst/threaded' 2>/dev/null || true
+ log_config "threaded-napi: veth_dst (TSO off, GRO on, NAPI kthread on CPU 0)"
+ fi
+
+ # Base addresses for connectivity
+ ip -n "$NS_SRC" addr add 192.168.1.1/24 dev veth_src
+ ip -n "$NS_DST" addr add 192.168.1.2/24 dev veth_dst
+
+ # Accept packets from any source on dst side
+ ip netns exec "$NS_DST" sysctl -wq net.ipv4.conf.all.rp_filter=0
+ ip netns exec "$NS_DST" sysctl -wq net.ipv4.conf.veth_dst.rp_filter=0
+
+ # Route the 10.0.0.0/8 range toward veth_src from dst side
+ ip -n "$NS_DST" route add 10.0.0.0/8 via 192.168.1.1
+
+ log "Namespaces: NS_SRC=$NS_SRC NS_DST=$NS_DST"
+}
+
+# ---------------------------------------------------------------------------
+# Test 1: Add many addresses (rhltable insert + resize)
+# ---------------------------------------------------------------------------
+
+test_add_many_addrs() {
+ local i addr
+ local rc=0
+
+ echo "Test: Adding $NUM_ADDRS addresses..."
+ local batch
+ batch=$(mktemp /tmp/ip_batch_add.XXXXXX)
+ for ((i = 1; i <= NUM_ADDRS; i++)); do
+ echo "addr add 10.$(( (i >> 8) & 0xff )).$(( i & 0xff )).1/32 dev veth_src"
+ done > "$batch"
+ ip -n "$NS_SRC" -batch "$batch" 2>/dev/null || true
+ rm -f "$batch"
+
+ # Verify address count
+ local count
+ count=$(ip -n "$NS_SRC" -4 addr show dev veth_src | grep -c "inet " || true)
+ log "Addresses on veth_src: $count (expected $((NUM_ADDRS + 1)))"
+
+ [ "$count" -ge "$NUM_ADDRS" ] || rc=1
+ check_result "add $NUM_ADDRS addresses" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 2: Verify lookup works (ping from specific source addresses)
+# ---------------------------------------------------------------------------
+
+test_lookup_ping() {
+ local rc=0
+
+ echo "Test: Verify address lookup via ping..."
+ # Ping dst from a few of the added addresses
+ for idx in 1 100 $((NUM_ADDRS / 2)) $NUM_ADDRS; do
+ [ "$idx" -gt "$NUM_ADDRS" ] && continue
+ local addr
+ addr=$(idx_to_addr $idx)
+ if ! ip netns exec "$NS_SRC" ping -c 1 -W 1 -I "$addr" 192.168.1.2 \
+ >/dev/null 2>&1; then
+ log "ping from $addr failed"
+ rc=1
+ else
+ log "ping from $addr OK"
+ fi
+ done
+
+ check_result "address lookup via ping" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 3: Unconnected UDP sendmsg stress (exercises __ip_dev_find hot path)
+# ---------------------------------------------------------------------------
+
+test_udp_sendmsg_stress() {
+ local rc=0
+
+ local total_time=$((ROUNDS * DURATION + 1))
+ echo "Test: UDP sendmsg bench ($NUM_ADDRS addrs, ${ROUNDS}x${DURATION}s + 1s warmup = ~${total_time}s)..."
+
+ # Locate C binary (used for both sink and sender)
+ local sender_bin=""
+ local script_dir
+ script_dir=$(dirname "$0")
+
+ if [ -x "${script_dir}/ipv4_addr_lookup_udp_sender" ]; then
+ sender_bin="${script_dir}/ipv4_addr_lookup_udp_sender"
+ elif gcc -O2 -Wall -o /tmp/udp_sender \
+ "${script_dir}/ipv4_addr_lookup_udp_sender.c" 2>/dev/null; then
+ sender_bin="/tmp/udp_sender"
+ else
+ echo "SKIP: ipv4_addr_lookup_udp_sender not found (run make first)"
+ check_result "UDP sender binary available" 1
+ return
+ fi
+
+ local sink_pid=0 sink_log=""
+
+ if [ "$USE_SINK" -eq 1 ]; then
+ # C receiver counts packets (adds CPU overhead to perf profiles)
+ log_config "sink: C receiver on CPU 0 (verifies packet counts)"
+ sink_log=$(mktemp /tmp/udp_sink.XXXXXX)
+ ip netns exec "$NS_DST" \
+ taskset -c 0 "$sender_bin" --sink > "$sink_log" 2>&1 &
+ sink_pid=$!
+ sleep 0.2
+ else
+ # Default: iptables DROP -- zero userspace overhead in perf profiles
+ ip netns exec "$NS_DST" \
+ iptables -A INPUT -p udp --dport 9000 -j DROP
+ fi
+
+ if [ "$USE_THREADED_NAPI" -eq 1 ]; then
+ # Pin veth_dst NAPI kthread to CPU 0 (sender is on CPU 1)
+ local napi_pid
+ napi_pid=$(pgrep -f "napi/veth_dst" 2>/dev/null | head -1)
+ if [ -n "$napi_pid" ]; then
+ taskset -p 0x1 "$napi_pid" >/dev/null 2>&1 || true
+ log "Pinned NAPI thread (pid $napi_pid) to CPU 0"
+ fi
+ fi
+
+ # Snapshot softnet_stat before sending (per-CPU: processed, time_squeeze)
+ local softnet_before
+ softnet_before=$(mktemp /tmp/softnet_before.XXXXXX)
+ cat /proc/net/softnet_stat > "$softnet_before"
+
+ # Send unconnected UDP from many source addresses.
+ # Each sendto() triggers ip_route_output -> __ip_dev_find -> rhltable_lookup.
+ local sender_log
+ sender_log=$(mktemp /tmp/udp_sender.XXXXXX)
+
+ log "Using C UDP sender (pre-created sockets, $ROUNDS rounds)"
+ local sndbuf_arg=""
+ [ "$USE_THREADED_NAPI" -eq 1 ] && sndbuf_arg="--sndbuf 4194304"
+
+ ip netns exec "$NS_SRC" \
+ taskset -c 1 "$sender_bin" "$NUM_ADDRS" "$ROUNDS" "$DURATION" $sndbuf_arg \
+ 2>&1 | tee "$sender_log"
+ [ "${PIPESTATUS[0]}" -ne 0 ] && rc=1
+
+ # Show per-CPU softnet activity (detect same-CPU vs multi-CPU NAPI)
+ local cpu=0 active_cpus=""
+ while read -r line; do
+ # shellcheck disable=SC2086
+ set -- $line
+ local cur_p=$((0x${1})) cur_sq=$((0x${3}))
+ local prev_p=0 prev_sq=0
+ if [ -n "$softnet_before" ]; then
+ local prev_line
+ prev_line=$(sed -n "$((cpu + 1))p" "$softnet_before")
+ if [ -n "$prev_line" ]; then
+ # shellcheck disable=SC2086
+ set -- $prev_line
+ prev_p=$((0x${1})); prev_sq=$((0x${3}))
+ fi
+ fi
+ local dp=$((cur_p - prev_p))
+ [ "$dp" -gt 0 ] && active_cpus="${active_cpus} cpu${cpu}(+${dp})"
+ cpu=$((cpu + 1))
+ done < /proc/net/softnet_stat
+ rm -f "$softnet_before"
+ local n_active
+ n_active=$(echo "$active_cpus" | wc -w)
+ local cpu_mode="single-CPU"
+ [ "$n_active" -gt 1 ] && cpu_mode="multi-CPU(${n_active})"
+ echo " softnet: ${cpu_mode}:${active_cpus}"
+
+ [ "$sender_bin" = "/tmp/udp_sender" ] && rm -f "$sender_bin"
+
+ if [ "$USE_SINK" -eq 1 ] && [ "$sink_pid" -ne 0 ]; then
+ # Let last packets reach socket buffer, then stop the sink
+ sleep 0.1
+ kill -TERM $sink_pid 2>/dev/null || true
+ wait $sink_pid 2>/dev/null || true
+
+ # Verify no packet drops: sent (includes warmup) should equal received
+ local total_sent sink_received
+ total_sent=$(sed -n 's/.*sent=\([0-9]*\).*/\1/p' "$sender_log" | head -1)
+ sink_received=$(sed -n 's/.*received=\([0-9]*\).*/\1/p' "$sink_log" | head -1)
+ rm -f "$sink_log"
+
+ if [ -n "$total_sent" ] && [ -n "$sink_received" ]; then
+ if [ "$total_sent" -eq "$sink_received" ]; then
+ echo " Sink received: $sink_received (matches sent)"
+ else
+ local diff=$((total_sent - sink_received))
+ echo " WARN: sent=$total_sent but sink received=$sink_received (diff=$diff)"
+ fi
+ fi
+ else
+ ip netns exec "$NS_DST" \
+ iptables -D INPUT -p udp --dport 9000 -j DROP 2>/dev/null
+ fi
+ rm -f "$sender_log"
+
+ check_result "unconnected UDP sendmsg stress" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 4: Duplicate keys (same IP on two different veth devices)
+# ---------------------------------------------------------------------------
+
+test_duplicate_addrs() {
+ local rc=0
+
+ echo "Test: Duplicate address keys (same IP, different devices)..."
+
+ # Create a second veth pair in NS_SRC
+ ip link add veth_src2 type veth peer name veth_dup
+ ip link set veth_src2 netns "$NS_SRC" up
+ ip link set veth_dup netns "$NS_DST" up
+ ip -n "$NS_DST" link set veth_dup up
+
+ # Add the same address that's already on veth_src
+ local dup_addr
+ dup_addr=$(idx_to_addr 1)
+ ip -n "$NS_SRC" addr add "${dup_addr}/32" dev veth_src2 2>/dev/null || true
+
+ # Verify both devices have the address
+ local count
+ count=$(ip -n "$NS_SRC" -4 addr show | grep -c "$dup_addr" || true)
+ log "Address $dup_addr appears on $count devices"
+
+ [ "$count" -ge 2 ] || rc=1
+
+ # Lookup should still work
+ if ! ip netns exec "$NS_SRC" ping -c 1 -W 1 -I "$dup_addr" 192.168.1.2 \
+ >/dev/null 2>&1; then
+ log "ping from duplicate addr failed (expected -- routing may prefer one)"
+ fi
+
+ # Remove duplicate and verify no crash
+ ip -n "$NS_SRC" addr del "${dup_addr}/32" dev veth_src2 2>/dev/null || true
+ ip -n "$NS_SRC" link del veth_src2 2>/dev/null || true
+
+ check_result "duplicate address keys" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 5: Remove all addresses (rhltable shrink)
+# ---------------------------------------------------------------------------
+
+test_remove_all_addrs() {
+ local i addr
+ local rc=0
+
+ echo "Test: Removing $NUM_ADDRS addresses..."
+ local batch
+ batch=$(mktemp /tmp/ip_batch_del.XXXXXX)
+ for ((i = 1; i <= NUM_ADDRS; i++)); do
+ echo "addr del 10.$(( (i >> 8) & 0xff )).$(( i & 0xff )).1/32 dev veth_src"
+ done > "$batch"
+ ip -n "$NS_SRC" -batch "$batch" 2>/dev/null || true
+ rm -f "$batch"
+
+ # Verify only the base address remains
+ local count
+ count=$(ip -n "$NS_SRC" -4 addr show dev veth_src | grep -c "inet " || true)
+ log "Addresses remaining: $count (expected 1)"
+
+ [ "$count" -eq 1 ] || rc=1
+ check_result "remove all addresses (rhltable shrink)" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Test 6: Re-add and check address lifetime (exercises check_lifetime)
+# ---------------------------------------------------------------------------
+
+test_addr_lifetime() {
+ local rc=0
+
+ echo "Test: Address lifetime expiry..."
+
+ # Add an address with short valid/preferred lifetime
+ ip -n "$NS_SRC" addr add 10.99.99.1/32 dev veth_src \
+ valid_lft 3 preferred_lft 2
+
+ # Verify it exists
+ local exists
+ exists=$(ip -n "$NS_SRC" -4 addr show dev veth_src | grep -c "10.99.99.1" || true)
+ [ "$exists" -ge 1 ] || { rc=1; check_result "address lifetime" $rc; return; }
+
+ log "Address 10.99.99.1 added with valid_lft=3s"
+
+ # Wait for it to expire (check_lifetime runs periodically)
+ sleep 5
+
+ exists=$(ip -n "$NS_SRC" -4 addr show dev veth_src | grep -c "10.99.99.1" || true)
+ log "After 5s: addr present=$exists (expected 0)"
+
+ [ "$exists" -eq 0 ] || rc=1
+ check_result "address lifetime expiry" $rc
+}
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+echo "============================================"
+echo "inet_addr_lst rhltable stress test"
+echo " addresses: $NUM_ADDRS"
+echo " rounds: $ROUNDS x ${DURATION}s"
+[ "$BENCH_ONLY" -eq 1 ] && echo " mode: bench-only"
+echo "============================================"
+
+setup
+bpftrace_start
+
+if [ "$BENCH_ONLY" -eq 1 ]; then
+ test_add_many_addrs
+ test_udp_sendmsg_stress
+else
+ test_add_many_addrs
+ test_lookup_ping
+ test_udp_sendmsg_stress
+ test_duplicate_addrs
+ test_remove_all_addrs
+ test_addr_lifetime
+fi
+
+bpftrace_stop
+
+echo ""
+echo "============================================"
+echo "Results: $PASS passed, $FAIL failed"
+echo "============================================"
+
+exit $RET
diff --git a/tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh b/tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh
new file mode 100755
index 000000000000..4d308b3e5346
--- /dev/null
+++ b/tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh
@@ -0,0 +1,282 @@
+#!/bin/bash
+# SPDX-License-Identifier: GPL-2.0
+# Launch ipv4_addr_lookup stress test inside virtme-ng
+#
+# Must be run from the kernel build tree root.
+#
+# Options:
+# --verbose Show kernel console (vng boot messages) in real time.
+# --taskset CPUS Pin the VM to specific CPUs via taskset.
+# Example: --taskset 12-19 (pin to E-cores on i7-12800H)
+# --isolated Run VM in bench.slice cgroup (proper CPU isolation).
+# --no-turbo Disable turbo boost for stable CPU frequency.
+# --freq MHZ Pin CPU frequency on bench CPUs (e.g. --freq 1200).
+# Sets scaling_min_freq=scaling_max_freq for thermal stability.
+# All other options are forwarded to ipv4_addr_lookup_test.sh (see --help).
+#
+# bench.slice setup (required for --isolated):
+# The --isolated option uses a dedicated cgroup slice to pin the VM to
+# specific CPUs while keeping other system processes off those CPUs.
+# The script also sets cpuset.cpus.partition=isolated at runtime to
+# remove bench CPUs from the scheduler's load balancing domain
+# (similar to isolcpus= but reversible). Restored on exit.
+#
+# One-time setup (as root, adjust CPU range for your system):
+#
+# # Create the slice (example: reserve CPUs 12-19 for benchmarks)
+# systemctl set-property --runtime bench.slice AllowedCPUs=12-19
+#
+# # Confine everything else to the remaining CPUs
+# systemctl set-property --runtime user.slice AllowedCPUs=0-11
+# systemctl set-property --runtime system.slice AllowedCPUs=0-11
+# systemctl set-property --runtime init.scope AllowedCPUs=0-11
+#
+# To make persistent, drop the --runtime flag (writes to /etc/systemd).
+#
+# Examples (run from kernel tree root):
+# ./tools/testing/selftests/net/ipv4_addr_lookup_test_virtme.sh
+# --num-addrs 1000 --duration 10
+# --verbose --num-addrs 2000
+# --taskset 12-19 --num-addrs 10000 # pinned to E-cores
+# --isolated --num-addrs 10000 # proper cgroup isolation
+
+set -eu
+
+# Parse options consumed here (not forwarded to the inner test).
+VERBOSE=""
+TASKSET_CPUS=""
+BENCH_SLICE=0
+NO_TURBO=0
+PIN_FREQ_KHZ=0
+INNER_ARGS=()
+while [ $# -gt 0 ]; do
+ case "$1" in
+ --verbose) VERBOSE="--verbose"; INNER_ARGS+=("--verbose"); shift ;;
+ --taskset) TASKSET_CPUS="$2"; shift 2 ;;
+ --isolated) BENCH_SLICE=1; shift ;;
+ --no-turbo) NO_TURBO=1; shift ;;
+ --freq) PIN_FREQ_KHZ=$(( $2 * 1000 )); shift 2 ;;
+ *) INNER_ARGS+=("$1"); shift ;;
+ esac
+done
+TEST_ARGS=""
+[ ${#INNER_ARGS[@]} -gt 0 ] && TEST_ARGS=$(printf '%q ' "${INNER_ARGS[@]}")
+
+if [ ! -f "vmlinux" ]; then
+ echo "ERROR: virtme-ng needs vmlinux; run from a compiled kernel tree:" >&2
+ echo " cd /path/to/kernel && $0" >&2
+ exit 1
+fi
+
+# Verify .config has the options needed for virtme-ng and this test.
+KCONFIG=".config"
+if [ ! -f "$KCONFIG" ]; then
+ echo "ERROR: No .config found -- build the kernel first" >&2
+ exit 1
+fi
+
+MISSING=""
+for opt in CONFIG_VIRTIO CONFIG_VIRTIO_PCI CONFIG_VIRTIO_NET \
+ CONFIG_VIRTIO_CONSOLE CONFIG_NET_9P CONFIG_NET_9P_VIRTIO \
+ CONFIG_9P_FS CONFIG_VETH CONFIG_IP_MULTIPLE_TABLES; do
+ if ! grep -q "^${opt}=[ym]" "$KCONFIG"; then
+ MISSING+=" $opt\n"
+ fi
+done
+if [ -n "$MISSING" ]; then
+ echo "ERROR: .config is missing options required by virtme-ng:" >&2
+ echo -e "$MISSING" >&2
+ echo "Consider: vng --kconfig (or make defconfig + enable above)" >&2
+ exit 1
+fi
+
+TESTDIR="tools/testing/selftests/net"
+TESTNAME="ipv4_addr_lookup_test.sh"
+LOGFILE="ipv4_addr_lookup_test.log"
+LOGPATH="$TESTDIR/$LOGFILE"
+CONSOLELOG="ipv4_addr_lookup_console.log"
+rm -f "$LOGPATH" "$CONSOLELOG"
+
+log_config() {
+ echo " Config: $*"
+}
+
+echo "Starting VM... test output in $LOGPATH, kernel console in $CONSOLELOG"
+
+# earlycon on COM2 for reliable kernel console capture.
+SERIAL_CONSOLE="earlycon=uart8250,io,0x2f8,115200"
+SERIAL_CONSOLE+=" console=uart8250,io,0x2f8,115200"
+CPU_PIN_CMD=""
+if [ "$BENCH_SLICE" -eq 1 ]; then
+ # bench.slice + systemd overrides confine all other processes to CPUs 0-11.
+ # Move ourselves into bench.slice cgroup (user.slice blocks affinity to
+ # CPUs 12-19), then use taskset. vng needs a PTY so systemd-run --scope
+ # is not an option.
+ BENCH_CPUS=$(systemctl show bench.slice -p AllowedCPUs --value 2>/dev/null)
+ if [ -z "$BENCH_CPUS" ]; then
+ echo "ERROR: bench.slice cgroup not configured." >&2
+ echo "" >&2
+ echo "One-time setup (adjust CPU range for your system):" >&2
+ echo " sudo systemctl set-property --runtime bench.slice AllowedCPUs=12-19" >&2
+ echo " sudo systemctl set-property --runtime user.slice AllowedCPUs=0-11" >&2
+ echo " sudo systemctl set-property --runtime system.slice AllowedCPUs=0-11" >&2
+ echo " sudo systemctl set-property --runtime init.scope AllowedCPUs=0-11" >&2
+ echo "" >&2
+ echo "Or use --taskset CPUS for simple pinning without isolation." >&2
+ exit 1
+ fi
+ # Set partition to isolated: removes bench CPUs from scheduler load
+ # balancing (like isolcpus= but reversible). Restore in EXIT trap.
+ PARTITION_PATH="/sys/fs/cgroup/bench.slice/cpuset.cpus.partition"
+ ORIG_PARTITION=""
+ if [ -f "$PARTITION_PATH" ]; then
+ ORIG_PARTITION=$(cat "$PARTITION_PATH")
+ if [ "$ORIG_PARTITION" != "isolated" ]; then
+ echo isolated | sudo tee "$PARTITION_PATH" >/dev/null 2>&1 || true
+ fi
+ fi
+ log_config "bench.slice CPUs: $BENCH_CPUS (partition=isolated)"
+ echo $$ | sudo tee /sys/fs/cgroup/bench.slice/cgroup.procs >/dev/null
+ CPU_PIN_CMD="taskset -c $BENCH_CPUS"
+elif [ -n "$TASKSET_CPUS" ]; then
+ # Try taskset directly first. If it fails (e.g. user.slice excludes
+ # the requested CPUs), move into bench.slice and retry.
+ if ! taskset -cp "$TASKSET_CPUS" $$ >/dev/null 2>&1; then
+ if [ -d /sys/fs/cgroup/bench.slice ]; then
+ echo $$ | sudo tee /sys/fs/cgroup/bench.slice/cgroup.procs >/dev/null
+ log_config "moved into bench.slice to reach CPUs $TASKSET_CPUS"
+ else
+ echo "ERROR: taskset to CPUs $TASKSET_CPUS failed and no bench.slice available" >&2
+ exit 1
+ fi
+ fi
+ log_config "taskset CPUs: $TASKSET_CPUS"
+ CPU_PIN_CMD="taskset -c $TASKSET_CPUS"
+fi
+
+# Disable turbo boost for stable frequencies during benchmarks
+TURBO_RESTORED=0
+NO_TURBO_PATH="/sys/devices/system/cpu/intel_pstate/no_turbo"
+ORIG_FREQS=()
+cleanup() {
+ # Restore CPU frequencies
+ for entry in "${ORIG_FREQS[@]}"; do
+ local cpu="${entry%%:*}" freq="${entry#*:}"
+ echo "$freq" | sudo tee /sys/devices/system/cpu/cpu"$cpu"/cpufreq/scaling_max_freq >/dev/null 2>&1 || true
+ echo "$freq" | sudo tee /sys/devices/system/cpu/cpu"$cpu"/cpufreq/scaling_min_freq >/dev/null 2>&1 || true
+ done
+ # Restore turbo boost
+ if [ "$NO_TURBO" -eq 1 ] && [ -f "$NO_TURBO_PATH" ]; then
+ echo 0 | sudo tee "$NO_TURBO_PATH" >/dev/null 2>&1 || true
+ fi
+ # Restore cpuset partition
+ if [ -n "${ORIG_PARTITION:-}" ] && [ -f "${PARTITION_PATH:-}" ]; then
+ echo "$ORIG_PARTITION" | sudo tee "$PARTITION_PATH" >/dev/null 2>&1 || true
+ fi
+}
+trap cleanup EXIT
+
+if [ "$NO_TURBO" -eq 1 ]; then
+ if [ -f "$NO_TURBO_PATH" ]; then
+ echo 1 | sudo tee "$NO_TURBO_PATH" >/dev/null
+ log_config "turbo boost disabled (will restore on exit)"
+ else
+ echo "WARN: $NO_TURBO_PATH not found, cannot disable turbo" >&2
+ fi
+fi
+
+# Pin CPU frequency for thermal stability
+if [ "$PIN_FREQ_KHZ" -gt 0 ]; then
+ # Determine which CPUs to pin: bench.slice CPUs, --taskset CPUs, or all
+ if [ -n "${BENCH_CPUS:-}" ]; then
+ FREQ_CPUS="$BENCH_CPUS"
+ elif [ -n "$TASKSET_CPUS" ]; then
+ FREQ_CPUS="$TASKSET_CPUS"
+ else
+ echo "WARN: --freq without --isolated or --taskset, skipping" >&2
+ PIN_FREQ_KHZ=0
+ fi
+ if [ "$PIN_FREQ_KHZ" -gt 0 ]; then
+ # Expand CPU list (e.g. "12-15,18" -> "12 13 14 15 18")
+ FREQ_CPU_LIST=""
+ IFS=',' read -ra parts <<< "$FREQ_CPUS"
+ for part in "${parts[@]}"; do
+ if [[ "$part" == *-* ]]; then
+ IFS='-' read -r a b <<< "$part"
+ FREQ_CPU_LIST+=" $(seq "$a" "$b")"
+ else
+ FREQ_CPU_LIST+=" $part"
+ fi
+ done
+ PIN_FREQ_MHZ=$((PIN_FREQ_KHZ / 1000))
+ for cpu in $FREQ_CPU_LIST; do
+ freq_dir="/sys/devices/system/cpu/cpu${cpu}/cpufreq"
+ [ -d "$freq_dir" ] || continue
+ orig=$(cat "$freq_dir/scaling_max_freq" 2>/dev/null) || continue
+ ORIG_FREQS+=("${cpu}:${orig}")
+ echo "$PIN_FREQ_KHZ" | sudo tee "$freq_dir/scaling_max_freq" >/dev/null 2>&1 || true
+ echo "$PIN_FREQ_KHZ" | sudo tee "$freq_dir/scaling_min_freq" >/dev/null 2>&1 || true
+ done
+ log_config "CPU frequency pinned to ${PIN_FREQ_MHZ} MHz on CPUs: $FREQ_CPUS (will restore on exit)"
+ fi
+fi
+
+echo "(VM is booting, please wait ~30s)"
+set +e
+$CPU_PIN_CMD vng $VERBOSE --cpus 4 --memory 2G \
+ --rwdir "$TESTDIR" \
+ --append "panic=5 loglevel=4 $SERIAL_CONSOLE" \
+ --qemu-opts="-serial file:$CONSOLELOG" \
+ --exec "cd $TESTDIR && \
+ ./$TESTNAME $TEST_ARGS 2>&1 | \
+ tee $LOGFILE; echo EXIT_CODE=\$? >> $LOGFILE"
+VNG_RC=$?
+set -e
+
+echo ""
+if [ "$VNG_RC" -ne 0 ]; then
+ echo "***********************************************************"
+ echo "* VM CRASHED -- kernel panic or BUG_ON (vng rc=$VNG_RC)"
+ echo "***********************************************************"
+ if [ -s "$CONSOLELOG" ] && \
+ grep -qiE 'kernel BUG|BUG:|Oops:|panic|WARN' "$CONSOLELOG"; then
+ echo ""
+ echo "--- kernel backtrace ($CONSOLELOG) ---"
+ grep -iE -A30 'kernel BUG|BUG:|Oops:|panic|WARN' \
+ "$CONSOLELOG" | head -50
+ else
+ echo ""
+ echo "Re-run with --verbose to see the kernel backtrace:"
+ echo " $0 --verbose ${INNER_ARGS[*]:-}"
+ fi
+ exit 1
+elif [ ! -f "$LOGPATH" ]; then
+ echo "No log file found -- VM may have crashed before writing output"
+ exit 2
+else
+ echo "=== VM finished ==="
+fi
+
+# Show test results from the log
+echo ""
+if grep -q "^Results:" "$LOGPATH"; then
+ grep "^Results:" "$LOGPATH"
+fi
+grep -E "^(PASS|FAIL):" "$LOGPATH" || true
+
+# Scan console log for unexpected kernel warnings (even on clean exit)
+if [ -s "$CONSOLELOG" ]; then
+ WARN_PATTERN='kernel BUG|BUG:|Oops:|WARNING:|WARN_ON|rhashtable'
+ WARN_LINES=$(grep -cE "$WARN_PATTERN" "$CONSOLELOG" 2>/dev/null) || WARN_LINES=0
+ if [ "$WARN_LINES" -gt 0 ]; then
+ echo ""
+ echo "*** kernel warnings in $CONSOLELOG ($WARN_LINES lines) ***"
+ grep -E "$WARN_PATTERN" "$CONSOLELOG" | head -20
+ fi
+fi
+
+# Extract exit code from log
+if grep -q "^EXIT_CODE=" "$LOGPATH"; then
+ INNER_RC=$(grep "^EXIT_CODE=" "$LOGPATH" | tail -1 | cut -d= -f2)
+ exit "$INNER_RC"
+fi
diff --git a/tools/testing/selftests/net/ipv4_addr_lookup_trace.bt b/tools/testing/selftests/net/ipv4_addr_lookup_trace.bt
new file mode 100644
index 000000000000..c63105faac03
--- /dev/null
+++ b/tools/testing/selftests/net/ipv4_addr_lookup_trace.bt
@@ -0,0 +1,178 @@
+#!/usr/bin/env bpftrace
+/*
+ * ipv4_addr_lookup_trace.bt - Trace inet_addr_lst rhltable code paths
+ * SPDX-License-Identifier: GPL-2.0
+ *
+ * Run alongside ipv4_addr_lookup_test.sh to verify that the correct
+ * kernel functions are exercised and to capture per-call overhead.
+ *
+ * Traces:
+ * - inet_lookup_ifaddr_rcu : hot lookup (latency histogram)
+ * - __ip_dev_find : full lookup incl. FIB fallback
+ * - inet_hash_remove : hash remove path
+ * - rhashtable_insert_slow : slow-path insert (fast path is inline)
+ * - rht_deferred_worker : resize worker (expand / shrink)
+ * - bucket_table_alloc : new table allocation (reveals new size)
+ * - rhashtable_rehash_table : actual data migration between tables
+ *
+ * Usage:
+ * bpftrace ipv4_addr_lookup_trace.bt # in one terminal
+ * ./ipv4_addr_lookup_test.sh --num-addrs 500 # in another
+ * # Ctrl-C the bpftrace when test finishes
+ */
+
+BEGIN
+{
+ printf("Tracing inet_addr_lst rhltable paths... Ctrl-C to stop.\n\n");
+ @phase = "idle";
+}
+
+/* ------------------------------------------------------------------ */
+/* Hot lookup path: inet_lookup_ifaddr_rcu (called from __ip_dev_find) */
+/* ------------------------------------------------------------------ */
+
+kprobe:inet_lookup_ifaddr_rcu
+{
+ @lookup_entry[tid] = nsecs;
+}
+
+kretprobe:inet_lookup_ifaddr_rcu
+/@lookup_entry[tid]/
+{
+ $dt = nsecs - @lookup_entry[tid];
+ @lookup_ns = hist($dt);
+ @lookup_count++;
+ delete(@lookup_entry[tid]);
+}
+
+/* __ip_dev_find: full overhead including FIB fallback path */
+
+kprobe:__ip_dev_find
+{
+ @ipdev_entry[tid] = nsecs;
+}
+
+kretprobe:__ip_dev_find
+/@ipdev_entry[tid]/
+{
+ $dt = nsecs - @ipdev_entry[tid];
+ @ipdev_ns = hist($dt);
+ @ipdev_count++;
+ delete(@ipdev_entry[tid]);
+}
+
+/* ------------------------------------------------------------------ */
+/* Insert / Remove */
+/* ------------------------------------------------------------------ */
+
+/* rhashtable_insert_slow is the non-inline slow path called on insert */
+kprobe:rhashtable_insert_slow
+{
+ @insert_slow++;
+}
+
+/* inet_hash_remove is static but not inlined in this build */
+kprobe:inet_hash_remove
+{
+ @remove_count++;
+}
+
+/* ------------------------------------------------------------------ */
+/* Resize events */
+/* ------------------------------------------------------------------ */
+
+/* rht_deferred_worker: the workqueue callback that drives resize */
+kprobe:rht_deferred_worker
+{
+ @resize_wq_entry[tid] = nsecs;
+ @resize_events++;
+ printf(">>> RESIZE #%lld: deferred_worker started\n",
+ @resize_events);
+}
+
+kretprobe:rht_deferred_worker
+/@resize_wq_entry[tid]/
+{
+ $dt = nsecs - @resize_wq_entry[tid];
+ @resize_wq_ns = hist($dt);
+ printf(" RESIZE: deferred_worker done in %lld us\n", $dt / 1000);
+ delete(@resize_wq_entry[tid]);
+}
+
+/* bucket_table_alloc: reveals the NEW table size being allocated.
+ * Signature: bucket_table_alloc(struct rhashtable *ht, size_t nbuckets, gfp_t)
+ * arg1 = nbuckets = new table size.
+ */
+kprobe:bucket_table_alloc*
+{
+ @new_tbl_size = arg1;
+ @bucket_allocs++;
+ printf(" RESIZE: bucket_table_alloc nbuckets=%lld\n", arg1);
+ print(kstack(6));
+}
+
+/* rhashtable_rehash_table: actual entry migration between old/new table */
+kprobe:rhashtable_rehash_table
+{
+ @rehash_entry[tid] = nsecs;
+}
+
+kretprobe:rhashtable_rehash_table
+/@rehash_entry[tid]/
+{
+ $dt = nsecs - @rehash_entry[tid];
+ @rehash_ns = hist($dt);
+ @rehash_count++;
+ printf(" RESIZE: rehash_table done in %lld us\n", $dt / 1000);
+ delete(@rehash_entry[tid]);
+}
+
+/* ------------------------------------------------------------------ */
+/* Summary on Ctrl-C */
+/* ------------------------------------------------------------------ */
+
+END
+{
+ printf("\n");
+ printf("========================================================\n");
+ printf(" inet_addr_lst rhltable trace summary\n");
+ printf("========================================================\n");
+
+ printf("\n--- Call counts ---\n");
+ printf(" inet_lookup_ifaddr_rcu : %8lld (hot lookup)\n",
+ @lookup_count);
+ printf(" __ip_dev_find : %8lld (full lookup)\n",
+ @ipdev_count);
+ printf(" rhashtable_insert_slow : %8lld (insert slow path)\n",
+ @insert_slow);
+ printf(" inet_hash_remove : %8lld (remove)\n",
+ @remove_count);
+
+ printf("\n--- Resize activity ---\n");
+ printf(" rht_deferred_worker : %8lld (resize worker runs)\n",
+ @resize_events);
+ printf(" bucket_table_alloc : %8lld (table allocations)\n",
+ @bucket_allocs);
+ printf(" rhashtable_rehash : %8lld (rehash completions)\n",
+ @rehash_count);
+
+ printf("\n--- inet_lookup_ifaddr_rcu latency (ns) ---\n");
+ print(@lookup_ns);
+
+ printf("\n--- __ip_dev_find latency (ns) ---\n");
+ print(@ipdev_ns);
+
+ printf("\n--- rht_deferred_worker duration (ns) ---\n");
+ print(@resize_wq_ns);
+
+ printf("\n--- rhashtable_rehash_table duration (ns) ---\n");
+ print(@rehash_ns);
+
+ /* clean up maps */
+ clear(@lookup_entry);
+ clear(@ipdev_entry);
+ clear(@resize_wq_entry);
+ clear(@rehash_entry);
+ clear(@new_tbl_size);
+ clear(@phase);
+}
diff --git a/tools/testing/selftests/net/ipv4_addr_lookup_udp_sender.c b/tools/testing/selftests/net/ipv4_addr_lookup_udp_sender.c
new file mode 100644
index 000000000000..ad1913ebba15
--- /dev/null
+++ b/tools/testing/selftests/net/ipv4_addr_lookup_udp_sender.c
@@ -0,0 +1,401 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Fast UDP sender/sink for ipv4_addr_lookup benchmarking.
+ *
+ * Sender mode: sends unconnected UDP packets from many source addresses
+ * to stress __ip_dev_find -> inet_lookup_ifaddr_rcu (rhltable_lookup).
+ * Each sendto() triggers: ip_route_output_key -> __ip_dev_find -> hash lookup.
+ *
+ * Sink mode (--sink): minimal C UDP receiver that counts packets received.
+ * Not used by default -- the test script uses an iptables DROP rule instead
+ * to avoid polluting perf profiles with recv() overhead. Enable with
+ * --sink on the test script command line for packet drop verification.
+ *
+ * Sender design for low-noise measurement:
+ * - Pre-create all sockets during setup (not timed)
+ * - Tight sendto() loop during measurement (no socket lifecycle overhead)
+ * - Clock check only every 1024 packets (avoid paravirt clock overhead)
+ * - 1 second warm-up to stabilize caches and hash table
+ * - Multiple rounds with per-round statistics (median, min, max, stdev)
+ *
+ * Usage:
+ * ipv4_addr_lookup_udp_sender <num_addrs> <rounds> <duration_sec>
+ * ipv4_addr_lookup_udp_sender --sink [port]
+ *
+ * Example: ipv4_addr_lookup_udp_sender 1000 10 3
+ * -> 10 rounds of 3s each (+ 1s warm-up) = ~31s total
+ */
+
+#include <arpa/inet.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <sys/socket.h>
+#include <time.h>
+#include <unistd.h>
+
+#define DST_ADDR "192.168.1.2"
+#define DST_PORT 9000
+#define SINK_PORT DST_PORT
+#define SINK_BUF 4096
+#define WARMUP_SEC 1
+#define CLOCK_INTERVAL 1024 /* check clock every N packets */
+#define MAX_ROUNDS 100
+#define PAYLOAD_LEN 64
+
+static double ts_diff(struct timespec *a, struct timespec *b)
+{
+ return (b->tv_sec - a->tv_sec) +
+ (b->tv_nsec - a->tv_nsec) * 1e-9;
+}
+
+static int cmp_double(const void *a, const void *b)
+{
+ double da = *(const double *)a;
+ double db = *(const double *)b;
+
+ return (da > db) - (da < db);
+}
+
+static void run_round(int *fds, int num_addrs, int duration,
+ struct sockaddr_in *dst, char *payload, int payload_len,
+ long long *out_sent, long long *out_errors,
+ double *out_rate)
+{
+ struct timespec ts_start, ts_now;
+ long long sent = 0, errors = 0;
+ double elapsed;
+ int i = 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts_start);
+ for (;;) {
+ if (fds[i] >= 0) {
+ if (sendto(fds[i], payload, payload_len, 0,
+ (struct sockaddr *)dst,
+ sizeof(*dst)) < 0)
+ errors++;
+ else
+ sent++;
+ }
+ i = (i + 1) % num_addrs;
+ if ((sent & (CLOCK_INTERVAL - 1)) == 0) {
+ clock_gettime(CLOCK_MONOTONIC, &ts_now);
+ if (ts_diff(&ts_start, &ts_now) >= duration)
+ break;
+ }
+ }
+
+ clock_gettime(CLOCK_MONOTONIC, &ts_now);
+ elapsed = ts_diff(&ts_start, &ts_now);
+
+ *out_sent = sent;
+ *out_errors = errors;
+ *out_rate = elapsed > 0 ? sent / elapsed : 0;
+}
+
+static volatile int sink_running = 1;
+
+static void sink_stop(int sig)
+{
+ sink_running = 0;
+}
+
+/* Not used by default -- the test script uses iptables DROP instead to keep
+ * perf profiles clean. Enable with: test_script --sink
+ */
+static int run_sink(int port)
+{
+ struct timeval tv = { .tv_sec = 0, .tv_usec = 100000 }; /* 100ms */
+ int rcvbuf = 4 * 1024 * 1024; /* 4 MB - prevent drops during bursts */
+ struct sigaction sa = { };
+ struct sockaddr_in addr;
+ long long received = 0;
+ char buf[SINK_BUF];
+ int fd;
+
+ fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd < 0) {
+ perror("socket");
+ return 1;
+ }
+
+ /* SO_RCVBUFFORCE bypasses net.core.rmem_max (requires root) */
+ if (setsockopt(fd, SOL_SOCKET, SO_RCVBUFFORCE, &rcvbuf, sizeof(rcvbuf)))
+ setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
+ setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv));
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_port = htons(port);
+ addr.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ perror("bind");
+ close(fd);
+ return 1;
+ }
+
+ /* Use sigaction without SA_RESTART so recv() returns -EINTR
+ * immediately on signal, rather than being silently restarted.
+ */
+ sa.sa_handler = sink_stop;
+ sigaction(SIGINT, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ fprintf(stderr, "sink: listening on port %d\n", port);
+
+ while (sink_running) {
+ if (recv(fd, buf, sizeof(buf), 0) > 0)
+ received++;
+ }
+
+ /* Drain in-flight packets (e.g. still traversing veth pipe).
+ * SO_RCVTIMEO (100ms) ensures we exit once the queue is idle.
+ */
+ while (recv(fd, buf, sizeof(buf), 0) > 0)
+ received++;
+
+ close(fd);
+
+ fprintf(stderr, "sink: received %lld packets\n", received);
+ /* Parseable output for test script */
+ printf("received=%lld\n", received);
+ fflush(stdout);
+ return 0;
+}
+
+/* Create and bind one UDP socket per source address: 10.B2.B3.1
+ * Returns the number of successfully bound sockets.
+ */
+static int setup_sockets(int *fds, int num_addrs, int sndbuf)
+{
+ struct sockaddr_in src;
+ int i, n_ok = 0;
+
+ for (i = 0; i < num_addrs; i++) {
+ int idx = i + 1;
+
+ fds[i] = -1;
+ memset(&src, 0, sizeof(src));
+ src.sin_family = AF_INET;
+ /* 10.<high byte>.<low byte>.1 */
+ src.sin_addr.s_addr = htonl(0x0a000001 |
+ ((idx & 0xff) << 8) |
+ (((idx >> 8) & 0xff) << 16));
+
+ fds[i] = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fds[i] < 0)
+ continue;
+ if (sndbuf > 0) {
+ if (setsockopt(fds[i], SOL_SOCKET, SO_SNDBUFFORCE,
+ &sndbuf, sizeof(sndbuf)))
+ setsockopt(fds[i], SOL_SOCKET, SO_SNDBUF,
+ &sndbuf, sizeof(sndbuf));
+ }
+ if (bind(fds[i], (struct sockaddr *)&src, sizeof(src)) < 0) {
+ close(fds[i]);
+ fds[i] = -1;
+ continue;
+ }
+ n_ok++;
+ }
+ return n_ok;
+}
+
+/* Warm-up: send for WARMUP_SEC to stabilize caches, hash table, softirq */
+static long long run_warmup(int *fds, int num_addrs, struct sockaddr_in *dst,
+ char *payload)
+{
+ struct timespec ts_start, ts_now;
+ long long sent = 0;
+ int i = 0;
+
+ clock_gettime(CLOCK_MONOTONIC, &ts_start);
+ for (;;) {
+ if (fds[i] >= 0) {
+ if (sendto(fds[i], payload, PAYLOAD_LEN, 0,
+ (struct sockaddr *)dst, sizeof(*dst)) >= 0)
+ sent++;
+ }
+ i = (i + 1) % num_addrs;
+ if ((sent & (CLOCK_INTERVAL - 1)) == 0) {
+ clock_gettime(CLOCK_MONOTONIC, &ts_now);
+ if (ts_diff(&ts_start, &ts_now) >= WARMUP_SEC)
+ break;
+ }
+ }
+ return sent;
+}
+
+/* Compute and print summary statistics (parseable by test script).
+ * sent= includes warmup so it matches the sink's received count.
+ */
+static void print_summary(double *rates, int rounds,
+ long long total_sent, long long warmup_sent,
+ long long total_errors)
+{
+ double median, mean, stdev, sum, sumsq;
+ int i;
+
+ qsort(rates, rounds, sizeof(double), cmp_double);
+
+ if (rounds % 2 == 0)
+ median = (rates[rounds / 2 - 1] + rates[rounds / 2]) / 2.0;
+ else
+ median = rates[rounds / 2];
+
+ sum = 0;
+ sumsq = 0;
+ for (i = 0; i < rounds; i++) {
+ sum += rates[i];
+ sumsq += rates[i] * rates[i];
+ }
+ mean = sum / rounds;
+
+ if (rounds > 1) {
+ double variance = (sumsq - sum * sum / rounds) /
+ (rounds - 1);
+
+ /* Sqrt via Newton's method (avoids -lm) */
+ stdev = variance;
+ if (stdev > 0) {
+ double s = stdev / 2;
+
+ for (i = 0; i < 20; i++)
+ s = (s + variance / s) / 2;
+ stdev = s;
+ }
+ } else {
+ stdev = 0;
+ }
+
+ printf("sent=%lld warmup=%lld errors=%lld rounds=%d "
+ "rate=%.0f pkt/s median=%.0f min=%.0f max=%.0f stdev=%.0f\n",
+ total_sent + warmup_sent, warmup_sent, total_errors, rounds,
+ mean, median, rates[0], rates[rounds - 1], stdev);
+}
+
+/* Prevent CPU C-state transitions for stable benchmark results.
+ * Holds /dev/cpu_dma_latency open with value 0 (lowest latency).
+ * Returns fd (caller must close), or -1 on failure (non-fatal).
+ */
+static int set_cpu_dma_latency(void)
+{
+ int32_t lat = 0;
+ int fd;
+
+ fd = open("/dev/cpu_dma_latency", O_WRONLY);
+ if (fd < 0)
+ return -1;
+ if (write(fd, &lat, sizeof(lat)) != sizeof(lat)) {
+ close(fd);
+ return -1;
+ }
+ return fd;
+}
+
+static int run_sender(int num_addrs, int rounds, int duration, int sndbuf)
+{
+ long long total_sent = 0, total_errors = 0, warmup_sent;
+ long long round_sent, round_errors;
+ int *fds, n_ok, i, dma_fd;
+ double rates[MAX_ROUNDS];
+ char payload[PAYLOAD_LEN];
+ struct sockaddr_in dst;
+ double round_rate;
+ struct rlimit rl;
+
+ if (rounds < 1)
+ rounds = 1;
+ if (rounds > MAX_ROUNDS)
+ rounds = MAX_ROUNDS;
+
+ /* Raise fd limit for high address counts */
+ if (num_addrs + 64 > 1024) {
+ rl.rlim_cur = num_addrs + 256;
+ rl.rlim_max = num_addrs + 256;
+ setrlimit(RLIMIT_NOFILE, &rl);
+ }
+
+ memset(payload, 'X', sizeof(payload));
+ memset(&dst, 0, sizeof(dst));
+ dst.sin_family = AF_INET;
+ dst.sin_port = htons(DST_PORT);
+ inet_pton(AF_INET, DST_ADDR, &dst.sin_addr);
+
+ /* Phase 1: Pre-create and bind all sockets (not timed) */
+ fds = calloc(num_addrs, sizeof(int));
+ if (!fds) {
+ perror("calloc");
+ return 1;
+ }
+
+ n_ok = setup_sockets(fds, num_addrs, sndbuf);
+ fprintf(stderr, "setup: %d/%d sockets bound\n", n_ok, num_addrs);
+
+ dma_fd = set_cpu_dma_latency();
+ if (dma_fd >= 0)
+ fprintf(stderr, "setup: cpu_dma_latency=0 (C-states disabled)\n");
+ if (n_ok == 0) {
+ fprintf(stderr, "no sockets created\n");
+ free(fds);
+ return 1;
+ }
+
+ /* Phase 2: Warm-up */
+ warmup_sent = run_warmup(fds, num_addrs, &dst, payload);
+
+ /* Phase 3: Measurement rounds */
+ for (i = 0; i < rounds; i++) {
+ run_round(fds, num_addrs, duration, &dst, payload,
+ PAYLOAD_LEN, &round_sent, &round_errors, &round_rate);
+ rates[i] = round_rate;
+ total_sent += round_sent;
+ total_errors += round_errors;
+ fprintf(stderr, " round %2d: %8.0f pkt/s\n",
+ i + 1, round_rate);
+ }
+
+ print_summary(rates, rounds, total_sent, warmup_sent, total_errors);
+
+ /* Cleanup */
+ if (dma_fd >= 0)
+ close(dma_fd);
+ for (i = 0; i < num_addrs; i++) {
+ if (fds[i] >= 0)
+ close(fds[i]);
+ }
+ free(fds);
+
+ return (total_errors > num_addrs / 10) ? 1 : 0;
+}
+
+int main(int argc, char **argv)
+{
+ int sndbuf = 0;
+ int port;
+
+ if (argc >= 2 && strcmp(argv[1], "--sink") == 0) {
+ port = (argc >= 3) ? atoi(argv[2]) : SINK_PORT;
+
+ return run_sink(port);
+ }
+
+ if (argc < 4) {
+ fprintf(stderr,
+ "Usage: %s <num_addrs> <rounds> <duration_sec> [--sndbuf bytes]\n"
+ " %s --sink [port]\n",
+ argv[0], argv[0]);
+ return 1;
+ }
+
+ if (argc >= 6 && strcmp(argv[4], "--sndbuf") == 0)
+ sndbuf = atoi(argv[5]);
+
+ return run_sender(atoi(argv[1]), atoi(argv[2]), atoi(argv[3]), sndbuf);
+}
--
2.43.0
next prev parent reply other threads:[~2026-03-31 21:08 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-03-31 21:07 [RFC PATCH net-next 0/4] ipv4/ipv6: local address lookup scaling hawk
2026-03-31 21:07 ` [RFC PATCH net-next 1/4] ipv4: make inet_addr_lst hash table size configurable hawk
2026-03-31 21:07 ` [RFC PATCH net-next 2/4] ipv6: make inet6_addr_lst " hawk
2026-03-31 21:07 ` [RFC PATCH net-next 3/4] ipv4: convert inet_addr_lst to rhltable for dynamic resizing hawk
2026-03-31 21:07 ` hawk [this message]
2026-04-03 22:35 ` [RFC PATCH net-next 0/4] ipv4/ipv6: local address lookup scaling David Ahern
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260331210739.3998753-5-hawk@kernel.org \
--to=hawk@kernel.org \
--cc=davem@davemloft.net \
--cc=dsahern@kernel.org \
--cc=edumazet@google.com \
--cc=horms@kernel.org \
--cc=ivan@cloudflare.com \
--cc=kernel-team@cloudflare.com \
--cc=kuba@kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=shuah@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox