From b5c0f6f1e958a4aa0caaca8931d3e30c1154558b Mon Sep 17 00:00:00 2001 From: Nikolay Shopik Date: Sun, 24 May 2026 14:17:18 +0300 Subject: [PATCH] iperf_sctp: apply default socket buffer to fix slow single-CPU throughput Linux SCTP, unlike TCP, does not auto-tune SO_RCVBUF/SO_SNDBUF (there is no equivalent of tcp_moderate_rcvbuf). Without an explicit setting the per-association receive window collapses to ~40 KB, which on single-CPU systems gates single-stream loopback throughput to roughly 1 Gbps even after the delayed-SACK fix from #1815: sender and softirq compete for the same core, so the small rwnd is never reopened in time. Apply DEFAULT_SCTP_SOCKBUF (512 KB) to SO_RCVBUF and SO_SNDBUF on the client connect socket, the server listen socket, and the per- association accept socket whenever --window is not given. The kernel silently caps at net.core.{r,w}mem_max where smaller, and the explicit --window path is unchanged. Failures on the default path are non-fatal (best-effort, not user-requested). Measured on Debian 13 (kernel 6.12.48), loopback, single stream, 30 s runs, median of 3: 1-vCPU box TCP default : 25.5 Gbps (unchanged) SCTP default before : 0.94 Gbps SCTP default after : 25.5 Gbps (~27x) 2-vCPU box SCTP default before/after: ~22 Gbps (within run-to-run variance) The gap only manifests when sender and softirq share a CPU; on multi- core systems the kernel already works around the small rwnd and the patch is a no-op there. --- src/iperf_api.h | 1 + src/iperf_sctp.c | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/src/iperf_api.h b/src/iperf_api.h index 25ac951e0..2246faa98 100644 --- a/src/iperf_api.h +++ b/src/iperf_api.h @@ -66,6 +66,7 @@ typedef atomic_uint_fast64_t atomic_iperf_size_t; #define DEFAULT_UDP_BLKSIZE 1460 /* default is dynamically set, else this */ #define DEFAULT_TCP_BLKSIZE (128 * 1024) /* default read/write block size */ #define DEFAULT_SCTP_BLKSIZE (64 * 1024) +#define DEFAULT_SCTP_SOCKBUF (512 * 1024) /* Linux SCTP has no rcvbuf auto-tune */ #define DEFAULT_PACING_TIMER 1000 #define DEFAULT_NO_MSG_RCVD_TIMEOUT 120000 #define MIN_NO_MSG_RCVD_TIMEOUT 100 diff --git a/src/iperf_sctp.c b/src/iperf_sctp.c index 1e02a8d50..09ef05428 100644 --- a/src/iperf_sctp.c +++ b/src/iperf_sctp.c @@ -125,6 +125,23 @@ iperf_sctp_accept(struct iperf_test * test) return -1; } + { + /* + * Apply SO_RCVBUF/SO_SNDBUF on the accepted association. The + * listener-side setting does not reliably open the per-association + * receive window on Linux SCTP (see iperf_sctp_listen()), so repeat + * here. Honour --window if the user supplied it, otherwise apply + * DEFAULT_SCTP_SOCKBUF. Best-effort; failures are silently ignored + * to match the iperf_sctp_listen() / iperf_sctp_connect() default + * branches. + */ + int opt = test->settings->socket_bufsize; + if (opt == 0) + opt = DEFAULT_SCTP_SOCKBUF; + (void) setsockopt(s, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(opt)); + (void) setsockopt(s, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(opt)); + } + if (Nread(s, cookie, COOKIE_SIZE, Psctp) < 0) { i_errno = IERECVCOOKIE; close(s); @@ -206,6 +223,18 @@ iperf_sctp_listen(struct iperf_test *test) i_errno = IESETBUF; return -1; } + } else { + /* + * Linux SCTP does not auto-tune SO_RCVBUF/SO_SNDBUF the way TCP does + * (no equivalent of tcp_moderate_rcvbuf), so without an explicit + * setting the per-association rwnd collapses to a few tens of KB and + * gates single-stream throughput. Apply a sensible default; the + * kernel will silently cap at net.core.{r,w}mem_max where smaller. + * Failures are non-fatal -- this is best-effort, not user-requested. + */ + opt = DEFAULT_SCTP_SOCKBUF; + (void) setsockopt(s, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(opt)); + (void) setsockopt(s, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(opt)); } if (test->bind_dev) { @@ -341,6 +370,11 @@ iperf_sctp_connect(struct iperf_test *test) i_errno = IESETBUF; return -1; } + } else { + /* see iperf_sctp_listen() for rationale; best-effort, non-fatal. */ + opt = DEFAULT_SCTP_SOCKBUF; + (void) setsockopt(s, SOL_SOCKET, SO_RCVBUF, &opt, sizeof(opt)); + (void) setsockopt(s, SOL_SOCKET, SO_SNDBUF, &opt, sizeof(opt)); } if (test->bind_dev) {