Skip to content
2 changes: 1 addition & 1 deletion data/loongarch_musl/etc/init.d/rcS
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ stage_musl_group_to_tmpfs() {
items="lua lua_testcode.sh test.sh date.lua file_io.lua max_min.lua random.lua remove.lua round_num.lua sin30.lua sort.lua strings.lua"
;;
iperf_testcode.sh)
items="iperf3 iperf_testcode.sh"
items="busybox iperf3 iperf_testcode.sh"
;;
*)
return 1
Expand Down
58 changes: 11 additions & 47 deletions data/risc-v_musl/etc/init.d/rcS
Original file line number Diff line number Diff line change
Expand Up @@ -95,70 +95,34 @@ mount_official_test_image_if_present() {
return 1
}

stage_musl_group_to_tmpfs() {
stage_all_musl_to_tmpfs() {
src="/tests/musl"
dst="/tmp/musl"
script="$1"

[ -d "$src" ] || return 1
[ -n "$script" ] || return 1

[ -d "$dst" ] || mkdir "$dst" || return 1

echo "[Tests] staging $script into tmpfs at $dst"

case "$script" in
basic_testcode.sh)
items="basic basic_testcode.sh busybox"
;;
busybox_testcode.sh)
items="busybox busybox_cmd.txt busybox_testcode.sh"
;;
lua_testcode.sh)
items="lua lua_testcode.sh test.sh date.lua file_io.lua max_min.lua random.lua remove.lua round_num.lua sin30.lua sort.lua strings.lua"
;;
iperf_testcode.sh)
items="iperf3 iperf_testcode.sh"
;;
*)
return 1
;;
esac

for item in $items; do
[ -e "$src/$item" ] || continue
[ -e "$dst/$item" ] && continue
cp -R "$src/$item" "$dst/" || return 1
done

[ -f "$dst/$script" ] || return 1
echo "[Tests] staged $script into tmpfs"
echo "[Tests] staging full musl test tree into tmpfs at $dst"
cp -R "$src"/. "$dst/" || return 1
echo "[Tests] staged full musl test tree into tmpfs"
return 0
}

run_musl_tests_if_present() {
# 官方测试镜像挂载到 /tests 后,当前自动入口只跑 musl 分组
# 官方测试镜像挂载到 /tests 后,自动扫描并运行 musl 下的全部测试脚本
# glibc 分组保留在测试镜像中,后续需要时直接调整本脚本。
mount_official_test_image_if_present || true
[ -d /tests ] || return 1
[ -d /tests/musl ] || return 1

ran=0
echo "[Tests] detected whitelisted musl test scripts; running in rcS"
export PATH="/bin:/sbin:/usr/bin:/usr/sbin:/tmp/musl:/tests/musl"
echo "[Tests] detected musl test scripts; running all in rcS"
stage_all_musl_to_tmpfs || return 1

export PATH="/bin:/sbin:/usr/bin:/usr/sbin:/tmp/musl"
export HOME="/"

for name in \
basic_testcode.sh \
busybox_testcode.sh \
lua_testcode.sh \
iperf_testcode.sh
do
if stage_musl_group_to_tmpfs "$name"; then
f="/tmp/musl/$name"
else
echo "[Tests] tmpfs staging failed for $name; running directly from /tests"
f="/tests/musl/$name"
fi
for f in /tmp/musl/*_testcode.sh; do
[ -f "$f" ] || continue

dir="${f%/*}"
Expand Down
97 changes: 97 additions & 0 deletions docs/os-myself/big-bug-record.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,100 @@ BusyBox applet 的链接森林只是在运行时需要,不应该作为 Git 仓
2. 减少 `mount`/`umount` 测试里的 VFAT 初始化成本。
3. 对目录项查找和路径解析加缓存,减少 exec 高频路径的重复 ext4 访问。
4. 如果评测机总时间更长,可以保留当前按组懒 staging;如果只追 basic 分数,可以进一步只运行 basic 组,避免后续组影响关机和输出。

## 2026-06-23 iperf TCP 吞吐从几 KB/s 提升到数百 Mbit/s

### 现象

最初 iperf 组能启动,但 TCP 分数几乎没有贡献:

- `BASIC_TCP` 只发送 4KiB,sender 约 16Kbit/s,receiver 为 0。
- `REVERSE_TCP` sender 也只有 4KiB,receiver 为 0。
- `PARALLEL_TCP` 第二条以后连接报 `Connection refused`。
- UDP 能跑通,说明基本 socket、loopback 地址和 iperf 控制连接不是完全坏掉;问题集中在 TCP 数据面和监听队列语义。

### 关键发现

第一层瓶颈是监听语义。iperf3 `-P 5` 会为同一个 server port 建多条 TCP data stream。smoltcp 的 TCP socket 没有 Linux 那种单 socket backlog 队列模型,监听端需要准备多个 TCP socket 才能同时接住多个 SYN。原实现只有一个 listener handle,所以第一条连接建立后,后续 stream 容易被拒绝。

第二层瓶颈是 loopback MTU 和 adapter RX buffer 不一致。loopback 设备暴露 `mtu=65535`,smoltcp 因此会发接近 64KiB 的 TCP/IP 包;但 `NetDeviceAdapter` 原来只有 2048 字节接收缓冲。结果大 TCP frame 在 adapter 层直接被丢掉,表现为客户端以为发出了少量数据,服务端几乎收不到。

第三层是 syscall 和轮询节奏。iperf3 会传大 buffer;如果每次 syscall 都按用户长度一次性分配/copy,会增加内核堆压力,也让 loopback 队列 drain 不及时。写入后不主动 poll,会把推进 TCP 状态机的工作推迟到后续调度点,吞吐很差。

### 处理步骤

这轮 TCP 相关修复按小步提交推进:

- `da0d56c tests: stage busybox for iperf script`:iperf 脚本里会调用 BusyBox 工具,staging 只复制 `iperf3` 不够,先保证测试脚本自身能稳定运行。
- `56c01e8 net: enlarge socket buffers for iperf`:TCP/UDP buffer 扩到 256KiB,避免 iperf 数据面频繁因为小 buffer 进入 WouldBlock。
- `de092e8 net: chunk socket syscall buffers`:`send/recv` 单次内核复制限制到 64KiB,控制临时分配和 copy 成本。
- `e63c8a5 net: drain loopback after socket writes`:socket write/sendto 成功后 bounded poll,尽快把 loopback Tx frame 回灌到 Rx 并推进 smoltcp 状态机。
- `eb9d8ce net: support parallel tcp listeners`:为监听 socket 维护一组 spare TCP listener,accept 时把已建立连接交给新 fd,并补充新的 listener,解决 `PARALLEL_TCP` 连接拒绝。
- `491634c net: reduce iperf socket log noise`:把热路径 socket/TCP 连接日志从 info 降到 debug,避免串口日志本身拖慢性能。
- `aae6897 net: size adapter rx buffer for mtu`:adapter RX buffer 改为按 `device.mtu() + Ethernet header` 分配,修复 64KiB loopback TCP frame 被 2048B buffer 丢弃的问题。
- `474b6ea net: cap tcp buffers below unstable window`:验证 UDP 改动时复现 smoltcp `SeqNumber` subtraction underflow,最终把 TCP buffer 从 256KiB 收到 128KiB-1,避开不稳定的大窗口组合。

### 决策和取舍

没有直接 patch smoltcp。这里的问题主要是我们给 smoltcp 的设备能力、buffer 和 listen 模型不一致,先修本内核适配层更稳,也更容易解释。

TCP buffer 起初选 256KiB,是为了减少 WouldBlock 并提高窗口;这一版峰值很好,`BASIC_TCP`/`REVERSE_TCP` 能到约 490Mbit/s,`PARALLEL_TCP` 能到约 709Mbit/s。但后续在完整 iperf-only 复测中,`PARALLEL_TCP` 五条连接建立后触发 smoltcp sequence number underflow panic。64KiB 以内能稳定,但单流 TCP 只剩约 70Mbit/s。最终选 `128KiB-1`:仍然明显高于原始 4KiB/0 receiver,且完整 iperf 组稳定通过。

listener pool 做了上限,不直接相信 iperf 传入的巨大 backlog。当前逻辑把用户 backlog clamp 到 128,实际 spare listener pool clamp 到 16,足够覆盖 iperf `-P 5`,同时避免一次 listen 分配过多 TCP buffer。

保留 loopback 的大 MTU,而不是降回 1500。降 MTU 能减少单帧内存,但 TCP throughput 会被更多包处理开销限制。真正的问题是 adapter 宣告大 MTU 却没有同等大小的 RX buffer,因此修 buffer 更符合设备模型。

### 验证

临时把 RISC-V `rcS` 改成只跑 `iperf_testcode.sh`,构建 release 内核,用临时分区盘 `/tmp/ccyos-disk-udpq-tcp128k.img` 跑官方 RISC-V musl iperf 组。

最终采用 128KiB-1 TCP buffer 后,最新一次验证中 TCP 三项都通过:

- `BASIC_TCP`:sender 71.7MiB / 300Mbit/s,receiver 71.4MiB / 299Mbit/s。
- `PARALLEL_TCP`:5 stream 全部连接成功,SUM sender 102MiB / 424Mbit/s,receiver 102MiB / 418Mbit/s。
- `REVERSE_TCP`:sender 72.2MiB / 302Mbit/s,receiver 72.0MiB / 302Mbit/s。

这说明最初 “4KiB / receiver 0” 和 “parallel connection refused” 两个 TCP 主问题已经解决。TCP 的峰值为了稳定性放弃了 256KiB buffer 下的最高数字,但所有 TCP 子项稳定通过,且吞吐仍是原始结果的数量级提升。

## 2026-06-23 iperf UDP 单流和反向模式高丢包

### 现象

TCP 主问题解决后,UDP 仍然拖分:

- `BASIC_UDP` receiver 约 46Mbit/s,丢包约 58%-61%。
- `REVERSE_UDP` receiver 约 48Mbit/s,同样有大量丢包。
- `PARALLEL_UDP` 反而能到约 137Mbit/s 且 0% loss。

这个现象说明链路、loopback MTU 和基本 UDP socket 不是完全坏掉;单 fd 收包路径比多 stream 分摊路径更容易溢出。

### 关键发现

本内核对 UDP 做了 per-port smoltcp socket 加 per-fd 队列的分发。smoltcp 共享 socket 收到包以后,`udp_dispatch_drain_locked()` 会复制到目标 `SocketFile` 的 `udp_rx_queue`,用户态再从这个队列 `recvfrom()`。

原来的 `UDP_RXQ_CAP = 64` 太小。iperf UDP 使用极高目标带宽发包,发送端在短时间内产生的 datagram 比接收线程 drain 得更快;队列满后 `udp_push()` 直接返回 false,后续 datagram 被丢掉。`PARALLEL_UDP` 因为 5 条 stream 分散到多个 fd 队列,每个队列压力小,所以表现比单流好。

试过把 UDP 队列固定预分配到 512。这个版本把 `BASIC_UDP` 提到 77.8Mbit/s 且 0% loss,但 `PARALLEL_UDP` 随后触发 `memory allocation of 1064960 bytes failed`。原因是每个 UDP 队列项包含最大 2048 字节 payload 缓冲,512 项接近 1MiB;所有 UDP socket 一创建就预分配,会把 parallel 场景的内存压力放大。

### 处理步骤

- `f4905d9 net: grow udp receive queues lazily`:UDP per-fd 队列初始仍为 64,只有队列满时才尝试翻倍扩容,最大 512。
- 如果扩容失败或已达上限,队列丢弃最旧 datagram 再放入新 datagram。这样在内存紧张时仍然有 UDP 丢包,但不会因为固定大队列把内核堆打爆,也更偏向保留较新的 iperf 数据。

### 决策和取舍

没有把所有 UDP socket 都预分配 512 项。固定大队列能改善单流 loss,但并行时内存占用不可控;lazy growth 只让真正承压的 socket 付出内存成本。

没有把 UDP 队列做成无限增长。iperf `-u -b 1000G` 本质上会制造远超内核处理能力的 burst;无限增长只是把丢包变成内存耗尽。512 是这次本地验证中能消掉单流丢包、又不会让 parallel UDP 分配失败的上限。

丢弃策略从“队列满就丢新包”变成“扩不动时丢旧包”。这对 UDP 是可接受取舍:UDP 本来不保证可靠性,保留较新的 datagram 对 iperf 的实时统计和反向模式更有价值。

### 验证

临时把 RISC-V `rcS` 改成只跑 `iperf_testcode.sh`,构建 release 内核并用临时分区盘 `/tmp/ccyos-disk-udpq-tcp128k.img` 跑官方 RISC-V musl iperf 组。最新一次完整 iperf-only 验证全部通过:

- `BASIC_UDP`:sender 25.9MiB / 109Mbit/s,receiver 25.9MiB / 108Mbit/s,0% loss。
- `PARALLEL_UDP`:SUM sender 36.6MiB / 154Mbit/s,receiver 36.5MiB / 153Mbit/s,0% loss。
- `REVERSE_UDP`:sender 19.9MiB / 82.7Mbit/s,receiver 19.8MiB / 82.8Mbit/s,0% loss。

这说明 UDP 的主要问题是 per-fd 接收队列容量和内存策略,而不是 UDP 校验、地址绑定或 loopback 设备本身。
20 changes: 12 additions & 8 deletions os/src/kernel/syscall/network/connection_ops.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use super::*;

const SOCKET_IO_CHUNK_SIZE: usize = 64 * 1024;

/// 连接到远程地址
pub fn connect(sockfd: i32, addr: *const u8, addrlen: u32) -> isize {
let task = current_task();
Expand Down Expand Up @@ -140,7 +142,7 @@ pub fn connect(sockfd: i32, addr: *const u8, addrlen: u32) -> isize {
}

pr_debug!("connect: tcp_connect success, nonblock={}", is_nonblock);
crate::pr_info!(
crate::pr_debug!(
"[TCP] Connection established: {} -> {}",
local_endpoint,
endpoint
Expand Down Expand Up @@ -233,13 +235,14 @@ pub fn send(sockfd: i32, buf: *const u8, len: usize, _flags: i32) -> isize {
(tid, file)
};

let chunk_len = core::cmp::min(len, SOCKET_IO_CHUNK_SIZE);
let result = {
let mut kernel_buf = alloc::vec![0u8; len];
let mut kernel_buf = alloc::vec![0u8; chunk_len];
unsafe {
crate::arch::ArchImpl::copy_from_user(
crate::arch::address::UA::from_usize(buf as usize),
kernel_buf.as_mut_ptr(),
len,
chunk_len,
)
.ok();
}
Expand All @@ -248,11 +251,11 @@ pub fn send(sockfd: i32, buf: *const u8, len: usize, _flags: i32) -> isize {

match result {
Ok(n) => {
pr_debug!("send: sockfd={}, len={} -> sent={}", sockfd, len, n);
pr_debug!("send: sockfd={}, len={} -> sent={}", sockfd, chunk_len, n);
return n as isize;
}
Err(e) => {
pr_debug!("send: sockfd={}, len={} -> error={:?}", sockfd, len, e);
pr_debug!("send: sockfd={}, len={} -> error={:?}", sockfd, chunk_len, e);
if e == crate::vfs::FsError::WouldBlock {
if let Some(socket_file) = file.as_any().downcast_ref::<SocketFile>()
&& !socket_file.flags().contains(OpenFlags::O_NONBLOCK)
Expand Down Expand Up @@ -301,8 +304,9 @@ pub fn recv(sockfd: i32, buf: *mut u8, len: usize, _flags: i32) -> isize {
(tid, file)
};

let chunk_len = core::cmp::min(len, SOCKET_IO_CHUNK_SIZE);
let result = {
let mut kernel_buf = alloc::vec![0u8; len];
let mut kernel_buf = alloc::vec![0u8; chunk_len];
match file.read(&mut kernel_buf) {
Ok(n) => {
unsafe {
Expand All @@ -321,11 +325,11 @@ pub fn recv(sockfd: i32, buf: *mut u8, len: usize, _flags: i32) -> isize {

match result {
Ok(n) => {
pr_debug!("recv: sockfd={}, len={} -> received={}", sockfd, len, n);
pr_debug!("recv: sockfd={}, len={} -> received={}", sockfd, chunk_len, n);
return n as isize;
}
Err(e) => {
pr_debug!("recv: sockfd={}, len={} -> error={:?}", sockfd, len, e);
pr_debug!("recv: sockfd={}, len={} -> error={:?}", sockfd, chunk_len, e);
if e == crate::vfs::FsError::WouldBlock {
if let Some(socket_file) = file.as_any().downcast_ref::<SocketFile>()
&& !socket_file.flags().contains(OpenFlags::O_NONBLOCK)
Expand Down
2 changes: 1 addition & 1 deletion os/src/kernel/syscall/network/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ use crate::{
wait_unix_would_block, write_sockaddr_un, write_socketpair_fds,
},
},
pr_debug, pr_info, println,
pr_debug, println,
uapi::{
fcntl::{FdFlags, OpenFlags},
socket::{AF_UNIX, SOCK_CLOEXEC, SOCK_DGRAM, SOCK_NONBLOCK, SOCK_STREAM, SOCK_TYPE_MASK},
Expand Down
Loading
Loading