TCP
- 每流一个线程 + 紧循环发送
- 每个 TCP 流独立一个线程,在 iperf_send_mt() 中做 一次性批量发送 10 次(multisend=10)才去检查绿绿灯,减少 clock_gettime 系统调用开销。
- 默认块大小 128 KB,远大于 MTU,一次 write() 就能塞满 TCP 发送窗口。
- 零拷贝传输
- --zerocopy:用 sendfile() 将 mmap 映射的文件直接推进 socket 内核缓冲区,完全绕过用户态拷贝。
- --skip-rx-copy:接收端用 MSG_TRUNC 丢弃数据但不 memcpy,只统计字节数,测纯网络吞吐。
- 多点小细节
- -N:关 Nagle 算法,防止小包延迟聚合
- -w:调整 SO_SNDBUF/SO_RCVBUF 对齐 BDP,验证 getsockopt() 是否生效
- -C bbr:切换拥塞控制算法
- -P N:多流并行打满瓶颈链路
- --fq-rate:用内核 SO_MAX_PACING_RATE 做精确定速
- mmap 临时文件填随机数据后立刻 unlink(),为 sendfile() 提供载体
本质上就是:单线程阻塞式大块写 + 批量发送 + N 流并行
UDP
- 用户指定目标带宽(-b 参数)
没有 -b 的情况下默认只有 1 Mbps------所以 UDP 测速必须显式指定 bitrate:
iperf3 -c -u -b 10G # 10 Gbps
程序内部用 iperf_check_throttle() (iperf_api.c:2077) 做闭环控制:
elapsed = now - start_time
current_rate = (bytes_sent * 8) / elapsed
if current_rate > target_rate:
green_light = 0 → 停止发送
delta_bits = bytes_sent*8 - elapsed * target_rate
sleep_ns = delta_bits / target_rate → clock_nanosleep() 精确休眠
else:
green_light = 1 → 继续发送
用的是平均速率控制而非即时速率,所以短时间可以全速塞,超了就sleep。
- GSO:一次 syscall 发 128 个包
默认 -l 1460 的 UDP 包要打满 10Gbps,需要 每秒 85 万次 write()。GSO (iperf_udp.c:461) 用一个 sendmsg() + UDP_SEGMENT cmsg 让内核拆包,一次 syscall 送多达 128 个 datagram------syscall 次数降 128 倍。
// iperf_udp.c:280-321
for (每个 datagram 槽位) {
写入 sec、usec、pcount 头部
}
Nwrite_gso() → sendmsg() with UDP_SEGMENT // 内核拆成独立 UDP 包
- GRO:一次 syscall 收多个包
接收端用 UDP_GRO 让内核将多个 UDP 包聚合到 recvmsg() 的缓冲区 (iperf_udp.c:495),然后遍历解析每个 datagram 的头部做丢包/抖动统计------同样大幅降低 syscall 开销。
- 压榨带宽的 UDPSocket 选项
- SO_RCVBUF / SO_SNDBUF(-w):拉到 BDP 级别,防止内核缓冲区溢出丢包
- SO_MAX_PACING_RATE(--fq-rate):用内核 fq qdisc 把步调卸载到网卡驱动,比用户态 nanosleep 更平滑
- --burst N:一次绿灯内连续发 N 个包,减少 iperf_check_throttle() 调用频率
- 每个包的自定义头部
sec:4B\]\[usec:4B\]\[pcount:4/8B\]\[payload:...
接收端通过序列号间隔检测丢包(cnt_error),用 sender 时间戳 + 到达时间计算 jitter(RFC 1889 指数平滑):
transit = arrival_time - sent_time
jitter += (|transit - prev_transit| - jitter) / 16
事件循环
事件循环是一个经典的 单线程 select() + 定时器驱动 模型,主控线程只负责调度,数据收发全在 worker 线程里。
txt
主线程 (客户端: iperf_run_client / 服务端: iperf_run_server)
│
├── while (test->state != IPERF_DONE)
│ │
│ ├── ① memcpy(&read_set, &test->read_set, ...) // 每次重新复制 fd_set
│ ├── ② iperf_time_now(&now) // 获取当前时间
│ ├── ③ timeout = tmr_timeout(&now) // 计算最近定时器剩余时间
│ ├── ④ select(max_fd+1, &read_set, &write_set, NULL, timeout)
│ │
│ ├── [select 返回 > 0]
│ │ ├── FD_ISSET(ctrl_sck) → iperf_handle_message_client() // 控制通道消息
│ │ ├── FD_ISSET(listener) → iperf_accept() // 服务端 accept 新连接
│ │ └── FD_ISSET(prot_listener) → test->protocol->accept() // 数据流 accept
│ │
│ ├── [首次进入 TEST_RUNNING]
│ │ └── 创建 N 个 worker 线程 → iperf_client_worker_run()
│ │
│ ├── ⑤ tmr_run(&now) // 执行到期定时器回调
│ │
│ └── ⑥ 检查终止条件 (时间到/字节到/块数到) → cancel timer + join 线程
│
└── join 所有接收线程 → 清理退出
- select() 的两个 fd_set
- read_set:控制通道 socket(test->ctrl_sck),监听服务端发来的交换结果、错误等消息
- write_set:客户端非反向模式时也在 write_set 上监听到控制通道可写
服务端额外还监听了 test->listener(监听端口)和 test->prot_listener(数据流监听端口),处理 accept 事件。
- 定时器系统 (timer.h/c)
基于双向链表的时间轮,支持两类:
定时器 类型
test_timer 一次性
omit_timer 一次性
stats_timer 周期性
reporter_timer 周期性
每次循环调用 tmr_timeout(&now) 获取最近到期的定时器剩余时间,这就是 select() 的 timeout 参数。select 返回后调用 tmr_run(&now) 触发到期的回调。
- 超时守卫
select 的 timeout 被强制封顶在 1 秒,防止长时间阻塞导致无法处理以下场景:
- 接收端超时检测:如果长时间没收到新数据块,判定对端卡死 → IENOMSG
- 服务端空闲超时:IPERF_START 状态下超时无连接 → 自动重启(return 2)
- 控制通道协议
iperf_handle_message_client() / iperf_handle_message_server() 通过 ctrl_sck 收发固定格式的 JSON 消息,推进状态机流转:
PARAM_EXCHANGE → CREATE_STREAMS → TEST_START → TEST_RUNNING → EXCHANGE_RESULTS → TEST_END → IPERF_DONE
- worker 线程的独立循环
// iperf_client_worker_run() line 56
while (!test->done && !sp->done) {
if (sp->sender)
iperf_send_mt(sp); // 批量发送, multisend=10
else
iperf_recv_mt(sp); // 批量接收
}
Worker 线程完全独立于主事件循环。它们被 pthread_cancel + pthread_join 终止,没有回调通知到事件循环。
这个事件循环设计非常传统------select + 双向链表定时器 ,没有任何 epoll/kqueue/libevent。
之所以够用,是因为它只需要管理极少量 fd(控制socket + 监听socket)
真正的数据平面在 worker 线程中用 blocking I/O 暴力收发