iperf代码走读

https://github.com/esnet/iperf

TCP

  1. 每流一个线程 + 紧循环发送
  • 每个 TCP 流独立一个线程,在 iperf_send_mt() 中做 一次性批量发送 10 次(multisend=10)才去检查绿绿灯,减少 clock_gettime 系统调用开销。
  • 默认块大小 128 KB,远大于 MTU,一次 write() 就能塞满 TCP 发送窗口。
  1. 零拷贝传输
  • --zerocopy:用 sendfile() 将 mmap 映射的文件直接推进 socket 内核缓冲区,完全绕过用户态拷贝。
  • --skip-rx-copy:接收端用 MSG_TRUNC 丢弃数据但不 memcpy,只统计字节数,测纯网络吞吐。
  1. 多点小细节
  • -N:关 Nagle 算法,防止小包延迟聚合
  • -w:调整 SO_SNDBUF/SO_RCVBUF 对齐 BDP,验证 getsockopt() 是否生效
  • -C bbr:切换拥塞控制算法
  • -P N:多流并行打满瓶颈链路
  • --fq-rate:用内核 SO_MAX_PACING_RATE 做精确定速
  • mmap 临时文件填随机数据后立刻 unlink(),为 sendfile() 提供载体

本质上就是:单线程阻塞式大块写 + 批量发送 + N 流并行

UDP

  1. 用户指定目标带宽(-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。

  1. 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 包
  1. GRO:一次 syscall 收多个包

接收端用 UDP_GRO 让内核将多个 UDP 包聚合到 recvmsg() 的缓冲区 (iperf_udp.c:495),然后遍历解析每个 datagram 的头部做丢包/抖动统计------同样大幅降低 syscall 开销。

  1. 压榨带宽的 UDPSocket 选项
  • SO_RCVBUF / SO_SNDBUF(-w):拉到 BDP 级别,防止内核缓冲区溢出丢包
  • SO_MAX_PACING_RATE(--fq-rate):用内核 fq qdisc 把步调卸载到网卡驱动,比用户态 nanosleep 更平滑
  • --burst N:一次绿灯内连续发 N 个包,减少 iperf_check_throttle() 调用频率
  1. 每个包的自定义头部

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 所有接收线程 → 清理退出
  1. select() 的两个 fd_set
  • read_set:控制通道 socket(test->ctrl_sck),监听服务端发来的交换结果、错误等消息
  • write_set:客户端非反向模式时也在 write_set 上监听到控制通道可写

服务端额外还监听了 test->listener(监听端口)和 test->prot_listener(数据流监听端口),处理 accept 事件。

  1. 定时器系统 (timer.h/c)

基于双向链表的时间轮,支持两类:

定时器 类型

test_timer 一次性

omit_timer 一次性

stats_timer 周期性

reporter_timer 周期性

每次循环调用 tmr_timeout(&now) 获取最近到期的定时器剩余时间,这就是 select() 的 timeout 参数。select 返回后调用 tmr_run(&now) 触发到期的回调。

  1. 超时守卫

select 的 timeout 被强制封顶在 1 秒,防止长时间阻塞导致无法处理以下场景:

  • 接收端超时检测:如果长时间没收到新数据块,判定对端卡死 → IENOMSG
  • 服务端空闲超时:IPERF_START 状态下超时无连接 → 自动重启(return 2)
  1. 控制通道协议

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

  1. 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 暴力收发