Skip to content

Fix :iperf Throughput And Musl Test Discovery - #264

Merged
a6d9a6m merged 12 commits into
mainfrom
fix/iperf-throughput
Jun 23, 2026
Merged

Fix :iperf Throughput And Musl Test Discovery#264
a6d9a6m merged 12 commits into
mainfrom
fix/iperf-throughput

Conversation

@a6d9a6m

@a6d9a6m a6d9a6m commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Fix iperf Throughput And Musl Test Discovery

描述

本次拉取请求基于 fix/iperf-throughput,目标是修复 RISC-V musl 下 iperf 分数极低的问题,并把 musl 测试入口从少量白名单调整为全量测试脚本扫描。

主要改动如下:

  • 修复 iperf 脚本依赖:iperf_testcode.sh 运行时需要 BusyBox 工具,RISC-V 和 LoongArch musl 的 iperf staging 都补入 busybox
  • 扩大 socket 数据面缓冲:TCP/UDP smoltcp buffer 从 4KiB 级别提升到更适合 iperf 的容量,减少热路径 WouldBlock
  • 限制 socket syscall 单次拷贝大小:send/recv 每轮最多处理 64KiB,避免 iperf 大 buffer 造成一次性内核分配和拷贝压力。
  • 改善 loopback 推进节奏:socket 写入成功后做 bounded poll,尽快把 loopback Tx frame 回灌到 Rx 并推进 smoltcp 状态机。
  • 支持 parallel TCP:为 TCP listener 维护 spare listener 池,解决 iperf -P 5 多 stream 下后续连接 Connection refused 的问题。
  • 降低网络热路径日志等级:socket 创建和 TCP 连接建立日志从 info 降到 debug,避免串口输出拖慢 iperf。
  • 修复 loopback 大 MTU 接收缓冲:adapter RX buffer 按 device.mtu() + Ethernet header 分配,避免 64KiB 级 TCP frame 被 2048B 缓冲丢弃。
  • 调整 TCP buffer 稳定性取舍:256KiB 曾获得更高峰值,但 parallel TCP 复测会触发 smoltcp sequence underflow;最终使用 128KiB - 1,保留较高吞吐并稳定通过完整 iperf 组。
  • 优化 UDP per-fd 接收队列:UDP 接收队列从固定 64 改为按需增长到 512,扩容失败或达到上限时丢弃旧 datagram,解决单流和 reverse UDP 高丢包,同时避免固定 512 预分配导致内存耗尽。
  • 调整 RISC-V musl 测试入口:启动后把 /tests/musl 全量 staging 到 /tmp/musl,再扫描 /tmp/musl/*_testcode.sh 执行所有 musl 测试,避免直接在官方测试盘上运行写入型测试。

关键原因

原始 iperf 分数低不是单点问题:

  • BASIC_TCPREVERSE_TCP 只有 4KiB 传输,receiver 甚至为 0,主要受小 TCP buffer、loopback poll 不及时和 adapter RX buffer 过小影响。
  • PARALLEL_TCPConnection refused,原因是 smoltcp 没有 Linux 风格的单 socket backlog 队列,需要在本内核层补 listener 池。
  • UDP 单流和 reverse UDP 丢包高,是因为 smoltcp 收到包后还要分发到 SocketFile 的 per-fd 队列,原队列 64 项无法吸收 iperf 的 burst。
  • 直接从 /tests/musl 跑全量测试会遇到官方测试盘写入/权限/性能问题,因此全量 musl 测试需要先 staging 到 tmpfs。

取舍

  • 没有 patch smoltcp。当前问题主要来自本内核提供给 smoltcp 的设备能力、监听语义、buffer 和轮询节奏不匹配,先修适配层更可控。
  • TCP buffer 没有保留 256KiB。它能把 TCP 峰值推到约 490/709Mbit/s,但 parallel TCP 复测会触发 smoltcp sequence underflow;128KiB - 1 是稳定性优先的折中。
  • UDP 队列没有固定预分配 512 项。固定大队列能改善单流 loss,但 parallel 场景会放大内存占用;lazy growth 只让承压 socket 付出内存成本。
  • RISC-V musl 改为全量 staging 会增加启动测试前的复制成本,但换来所有测试都在可写 tmpfs 下运行,符合全量 musl 测试需求。

验证

构建验证:

  • ARCH=riscv cargo build --release --target riscv64gc-unknown-none-elf 通过。
  • 构建仍有既有 warnings,本 PR 未处理这些历史告警。

iperf-only QEMU 验证:

  • BASIC_UDP: receiver 25.9MiB / 108Mbit/s,0% loss。
  • PARALLEL_UDP: receiver 36.5MiB / 153Mbit/s,0% loss。
  • REVERSE_UDP: receiver 19.8MiB / 82.8Mbit/s,0% loss。
  • BASIC_TCP: receiver 71.4MiB / 299Mbit/s。
  • PARALLEL_TCP: 5 stream 全部连接成功,SUM receiver 102MiB / 418Mbit/s。
  • REVERSE_TCP: receiver 72.0MiB / 302Mbit/s。

脚本语法验证:

  • sh -n data/risc-v_musl/etc/init.d/rcS 通过。

风险

  • 全量 staging /tests/musl 会增加 tmpfs 内存占用。如果测试镜像继续变大,需要评估 tmpfs 容量和复制时间。
  • TCP listener 池是对 smoltcp listen 模型的兼容层,不是完整 Linux backlog 语义;目前覆盖 iperf parallel stream,但复杂 TCP server 场景仍可能暴露差异。
  • UDP lazy queue 上限为 512,极端 burst 下仍会丢包;这是 UDP 语义和内存上限之间的有意取舍。
  • 目前只把 RISC-V musl 入口改成全量 staging 和全量扫描,LoongArch musl 仍保持原白名单式 staging 逻辑。

本记录基于以下提交

  • da0d56c tests: stage busybox for iperf script
  • 56c01e8 net: enlarge socket buffers for iperf
  • de092e8 net: chunk socket syscall buffers
  • e63c8a5 net: drain loopback after socket writes
  • eb9d8ce net: support parallel tcp listeners
  • 491634c net: reduce iperf socket log noise
  • aae6897 net: size adapter rx buffer for mtu
  • 692b19c docs: record iperf tcp tuning
  • 474b6ea net: cap tcp buffers below unstable window
  • f4905d9 net: grow udp receive queues lazily
  • e75f172 docs: record iperf udp tuning
  • 671f1c4 调成测试脚本探测范围

关联 Issue

暂无关联 Issue。

@a6d9a6m
a6d9a6m merged commit 34a43e8 into main Jun 23, 2026
1 check failed
@a6d9a6m
a6d9a6m deleted the fix/iperf-throughput branch June 23, 2026 16:16

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request significantly improves network performance and stability, particularly for iperf TCP and UDP tests. Key changes include refactoring the test staging script to stage the entire musl test tree, chunking socket I/O buffers to 64 KiB, implementing a parallel TCP listener pool, dynamically sizing the network adapter RX buffer based on MTU, and introducing lazy growth with a fallback drop-oldest strategy for UDP receive queues. The feedback highlights two critical robustness issues: first, the UDP queue-full check using VecDeque capacity is unreliable and could lead to excessive memory usage or panics on allocation failure; second, the TCP listener replenishment logic should not fail the entire accept call if a new listener cannot be allocated but existing spare listeners are still available.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment thread os/src/net/socket.rs
Comment on lines 172 to 183
if q.len() == q.capacity() {
return false;
let old_capacity = q.capacity();
if old_capacity < UDP_RXQ_MAX_CAP {
let new_capacity = old_capacity
.saturating_mul(2)
.clamp(UDP_RXQ_INITIAL_CAP, UDP_RXQ_MAX_CAP);
let _ = q.try_reserve(new_capacity.saturating_sub(old_capacity));
}
if q.len() == q.capacity() {
let _ = q.pop_front();
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

udp_push 中,使用 q.len() == q.capacity() 来判断队列是否已满并限制最大容量存在严重缺陷。因为 VecDeque 的实际 capacity() 是由底层分配器决定的,通常会大于请求的容量(例如向上取整到 2 的幂)。如果 q.capacity() 超过了 UDP_RXQ_MAX_CAP(512),队列将无法在达到 512 时触发丢包逻辑,从而导致内存占用超出预期。
此外,如果 try_reserve 失败(内存不足),由于没有成功扩容, q.push_back 可能会触发 panic。
建议显式使用 UDP_RXQ_MAX_CAP 限制长度,并在扩容失败时安全地丢弃旧数据包。

        if q.len() >= UDP_RXQ_MAX_CAP {
            let _ = q.pop_front();
        } else if q.len() == q.capacity() {
            let old_capacity = q.capacity();
            let new_capacity = old_capacity
                .saturating_mul(2)
                .clamp(UDP_RXQ_INITIAL_CAP, UDP_RXQ_MAX_CAP);
            let reserved = if new_capacity > old_capacity {
                q.try_reserve(new_capacity - old_capacity).is_ok()
            } else {
                false
            };
            if !reserved && q.len() == q.capacity() {
                let _ = q.pop_front();
            }
        }

Comment on lines +493 to +514
fn replenish_tcp_listeners(
socket_file: &SocketFile,
listen_endpoint: smoltcp::wire::IpListenEndpoint,
backlog: usize,
) -> Result<(), isize> {
let target = backlog.clamp(1, TCP_LISTENER_POOL_LIMIT);
while network_stack().tcp_spare_listener_count(socket_file, listen_endpoint) < target {
let new_listen_handle = match create_tcp_socket() {
Ok(SocketHandle::Tcp(h)) => h,
Err(e) => return Err(e.to_errno()),
Ok(SocketHandle::Udp(_)) => return Err(-(crate::uapi::errno::EINVAL as isize)),
};

if let Err(e) = network_stack().tcp_listen(new_listen_handle, listen_endpoint) {
network_stack().remove_tcp_socket(new_listen_handle);
return Err(e.to_errno());
}

socket_file.add_listen_socket(SocketHandle::Tcp(new_listen_handle));
}
Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

replenish_tcp_listeners 中,如果创建或监听新的 TCP socket 失败(例如由于内存不足 NoMemory),函数会立即返回错误。这会导致 accept 系统调用直接失败。然而,如果此时 tcp_spare_listener_count 已经大于 0,说明内核中仍有可用的 spare listener,我们应该允许 accept 继续处理已有的连接,而不是因为无法补充新的 listener 而让整个调用失败。建议在已有可用 listener 时仅中断补充循环,而不返回错误。

fn replenish_tcp_listeners(
    socket_file: &SocketFile,
    listen_endpoint: smoltcp::wire::IpListenEndpoint,
    backlog: usize,
) -> Result<(), isize> {
    let target = backlog.clamp(1, TCP_LISTENER_POOL_LIMIT);
    while network_stack().tcp_spare_listener_count(socket_file, listen_endpoint) < target {
        let new_listen_handle = match create_tcp_socket() {
            Ok(SocketHandle::Tcp(h)) => h,
            Err(e) => {
                if network_stack().tcp_spare_listener_count(socket_file, listen_endpoint) > 0 {
                    break;
                }
                return Err(e.to_errno());
            }
            Ok(SocketHandle::Udp(_)) => return Err(-(crate::uapi::errno::EINVAL as isize)),
        };

        if let Err(e) = network_stack().tcp_listen(new_listen_handle, listen_endpoint) {
            network_stack().remove_tcp_socket(new_listen_handle);
            if network_stack().tcp_spare_listener_count(socket_file, listen_endpoint) > 0 {
                break;
            }
            return Err(e.to_errno());
        }

        socket_file.add_listen_socket(SocketHandle::Tcp(new_listen_handle));
    }
    Ok(())
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant