侧边栏壁纸
  • 累计撰写 53 篇文章
  • 累计收到 3 条评论

P2-06 · libmtcp-v3 实现(下):状态机、RTO 与 cwnd 简化版

PacketForge
2026-08-18 / 0 评论 / 1 阅读 /
对应路线图:Phase 2 (Day 50-91) | DPDK 24.11.6 LTS | 内核对照 v6.6.143
代码出处:source/mtcp/mtcp/src/timer.c、tcp_in.c(真实实现)+ source/linux-stable/net/ipv4/tcp_timer.c、tcp_cong.c(对照)+ code/libmtcp-v3/(计划中)
上一篇:P2-05 实现(上)RX/TX | 下一篇:P2-07 性能调优实录

一句话定位

状态机决定"包往哪走",RTO 决定"丢了怎么办",cwnd 决定"发多快"。libmtcp-v3 的取舍是:状态机抄 mTCP 的 switch,定时器抄 mTCP 的时间轮,拥塞控制只做 Reno 主干——先跑通,再谈 RACK/BBR。

mTCP 的 RTO:一个公式 + 指数退避

mTCP 没有每流定时器,所有待重传流挂时间轮,主循环每轮扫到期槽(timer.c:384-441,P2-02 的 ④)。到期后进 HandleRTO:

HandleRTO(mtcp_manager_t mtcp, uint32_t cur_ts, tcp_stream *cur_stream)
{                                                       // timer.c:183
    uint8_t backoff;
    // ...
    /* count number of retransmissions */
    if (cur_stream->sndvar->nrtx < TCP_MAX_RTX) {
        cur_stream->sndvar->nrtx++;                 // timer.c:201-202
    } else {
        /* if it exceeds the threshold, destroy and notify to application */
        // ... state = TCP_ST_CLOSED; RaiseErrorEvent / DestroyTCPStream
        return ERROR;                               // timer.c:203-220
    }

    /* update rto timestamp */
    if (cur_stream->state >= TCP_ST_ESTABLISHED) {
        uint32_t rto_prev;
        backoff = MIN(cur_stream->sndvar->nrtx, TCP_MAX_BACKOFF);

        rto_prev = cur_stream->sndvar->rto;
        cur_stream->sndvar->rto = ((cur_stream->rcvvar->srtt >> 3) +
                cur_stream->rcvvar->rttvar) << backoff;   // timer.c:229-233
        // ...
    } else if (cur_stream->state >= TCP_ST_SYN_SENT) {
        /* if there is no rtt measured, update rto based on the previous one */
        if (cur_stream->sndvar->nrtx < TCP_MAX_BACKOFF) {
            cur_stream->sndvar->rto <<= 1;          // timer.c:240-244
        }
    }

    /* reduce congestion window and ssthresh */
    cur_stream->sndvar->ssthresh = MIN(cur_stream->sndvar->cwnd,
                cur_stream->sndvar->peer_wnd) / 2;   // timer.c:249
    if (cur_stream->sndvar->ssthresh < (2 * cur_stream->sndvar->mss)) {
        cur_stream->sndvar->ssthresh = cur_stream->sndvar->mss * 2;  // :250-252
    }
    cur_stream->sndvar->cwnd = cur_stream->sndvar->mss;          // :253

三个要点:① rto = (srtt>>3 + rttvar) << backoff 是 RFC 6298 的一元简化(srtt 存 8 倍定点数,>>3 还原);② 重传次数到 TCP_MAX_RTX 直接销毁连接——不做内核的"路由失效探测";③ 超时减窗一句话:ssthresh 砍半(下限 2MSS),cwnd 回到 1MSS。

mTCP 的快重传:三重复 ACK(tcp_in.c:417-473)

ProcessACK(tcp_in.c:310)里数 dup_acks,到 3 就动手:

    /* Fast retransmission */
    if (dup && cur_stream->rcvvar->dup_acks == 3) {   // tcp_in.c:417
        // ...
        if (TCP_SEQ_LT(ack_seq, cur_stream->snd_nxt)) {
            // ...
            cur_stream->snd_nxt = ack_seq;      // 回退发送指针 :441
        }

        /* update congestion control variables */
        /* ssthresh to half of min of cwnd and peer wnd */
        sndvar->ssthresh = MIN(sndvar->cwnd, sndvar->peer_wnd) / 2;  // :447
        if (sndvar->ssthresh < 2 * sndvar->mss) {
            sndvar->ssthresh = 2 * sndvar->mss;
        }
        sndvar->cwnd = sndvar->ssthresh + 3 * sndvar->mss;    // :451
        // ...
        AddtoSendList(mtcp, cur_stream);            // 立即重传 :464
    } else if (cur_stream->rcvvar->dup_acks > 3) {
        /* Inflate congestion window until before overflow */
        if ((uint32_t)(sndvar->cwnd + sndvar->mss) > sndvar->cwnd) {
            sndvar->cwnd += sndvar->mss;            // :466-473
        }
    }

🐧 内核对照:同一件事的完整版

定时器侧:内核没有"时间轮统一扫",而是每 sock 一个 write timer,tcp_write_timer_handler 按 icsk_pending 分派四类事件(tcp_timer.c:666-698):REO_TIMEOUT(RACK 乱序超时)、LOSS_PROBE(尾丢探测)、RETRANS(重传,进 tcp_retransmit_timer tcp_timer.c:511)、PROBE0(零窗探测)。

tcp_retransmit_timer 的真实行为(tcp_timer.c:511 起):!tp->packets_out 直接返回(:531)→ 零窗时转 PROBE0(:538-573)→ tcp_write_timeout 判超时死亡(:576)→ tcp_enter_loss(sk)(:600)→ tcp_retransmit_skb 重传队头(:603)→ 失败则按 TCP_RESOURCE_PROBE_INTERVAL 节流重试(:607-609)→ 最后 inet_csk_reset_xmit_timer(..., TCP_RTO_MAX) 夹上限(:656-657)。mTCP 的 30 行,内核用了 150 行——多出来的都是状态机:零窗、loss recovery、资源节流。

拥塞控制侧:内核是插件化(struct tcp_congestion_ops),Reno 只是 fallback(tcp_cong.c:532 struct tcp_congestion_ops tcp_reno)。三个真实函数:

__bpf_kfunc u32 tcp_slow_start(struct tcp_sock *tp, u32 acked)
{
    u32 cwnd = min(tcp_snd_cwnd(tp) + acked, tp->snd_ssthresh);
    acked -= cwnd - tcp_snd_cwnd(tp);
    tcp_snd_cwnd_set(tp, min(cwnd, tp->snd_cwnd_clamp));
    return acked;                                   // tcp_cong.c:457-465
}

__bpf_kfunc void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w, u32 acked)
{
    if (tp->snd_cwnd_cnt >= w) {
        tp->snd_cwnd_cnt = 0;
        tcp_snd_cwnd_set(tp, tcp_snd_cwnd(tp) + 1);     // tcp_cong.c:474-477
    }
    tp->snd_cwnd_cnt += acked;                       // 每 ACK 记学分 :479
    // ...
}

__bpf_kfunc u32 tcp_reno_ssthresh(struct sock *sk)
{
    // ...
    return max(tcp_snd_cwnd(tp) >> 1U, 2U);          // tcp_cong.c:520
}

慢启动按 acked 字节加窗(还处理 stretch ACK,tcp_cong.c:448-456 注释);拥塞避免用 snd_cwnd_cnt 学分制实现"每 RTT +1";丢包减半在 tcp_reno_ssthresh。mTCP 直接 cwnd += MSS(每包),是"每 ACK 一个 MSS"的粒度放大版。

libmtcp-v3(mtcp_fsm.c 计划中)的取舍清单

RTO 与 cwnd 对照

  1. 状态机:照抄 mTCP 的 11 态 switch 结构(tcp_in.c:1298 布局),handler 全部 static inline;
  2. 定时器:时间轮 + 每轮批查(抄 timer.c:384 的 CheckRtmTimeout 模式),不做内核的每流 timer;
  3. RTO:(srtt>>3 + rttvar) << backoff(timer.c:232 原式),SYN 阶段 rto <<= 1(timer.c:243);
  4. 快重传:三重复 ACK 触发(tcp_in.c:417),cwnd = ssthresh + 3MSS(tcp_in.c:451);
  5. 明确不做:RACK/Probe0/SACK 主动恢复/PRR——第一版目标是"和 mTCP 行为等价",内核的完整版是 P3+ 的课题。

面试 3 问

  1. mTCP 的 RTO 公式 (srtt>>3 + rttvar) << backoff 里 >>3 是什么? → srtt 存的是 8 倍定点数(3 位小数位),>>3 还原成真实值;backoff 是重传次数的指数退避(timer.c:229-233)。
  2. 内核为什么需要 LOSS_PROBE 事件而 mTCP 不需要? → 尾丢场景(发了一批丢最后几个)RTO 触发太慢,内核用 TLP 提前探测(tcp_timer.c:687-688);mTCP 的简化模型里这类优化不存在,代价是尾丢恢复慢一个 RTO。
  3. 三重复 ACK 后为什么 cwnd = ssthresh + 3MSS? → 3 个 dupack 证明有 3 个包已离开网络,加 3MSS 补偿这 3 个"已确认离网"的段(tcp_in.c:451),对应 RFC 5681 的 fast recovery 简化。

🔧 动手实验

用 tc netem 给测试对端注入 5%/10% 丢包,跑 mTCP 的 epserver/epwget 大文件传输,tcpdump 抓重传:① 验证快重传(3 dupack 触发,重传包 seq 连续);② 验证 RTO 指数退避(重传间隔 1x/2x/4x);③ 对照内核 iperf3 同丢包率下的恢复曲线。抓包与时间线落点 perf-data/P2/06-rto.md。


下一篇:P2-07 · 性能调优实录:从 perf top 到单核 14.88 Mpps

0

评论 (0)

取消