对应路线图: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 计划中)的取舍清单
- 状态机:照抄 mTCP 的 11 态 switch 结构(tcp_in.c:1298 布局),handler 全部
static inline; - 定时器:时间轮 + 每轮批查(抄 timer.c:384 的 CheckRtmTimeout 模式),不做内核的每流 timer;
- RTO:
(srtt>>3 + rttvar) << backoff(timer.c:232 原式),SYN 阶段rto <<= 1(timer.c:243); - 快重传:三重复 ACK 触发(tcp_in.c:417),cwnd = ssthresh + 3MSS(tcp_in.c:451);
- 明确不做:RACK/Probe0/SACK 主动恢复/PRR——第一版目标是"和 mTCP 行为等价",内核的完整版是 P3+ 的课题。
面试 3 问
- mTCP 的 RTO 公式
(srtt>>3 + rttvar) << backoff里>>3是什么? → srtt 存的是 8 倍定点数(3 位小数位),>>3还原成真实值;backoff 是重传次数的指数退避(timer.c:229-233)。 - 内核为什么需要 LOSS_PROBE 事件而 mTCP 不需要? → 尾丢场景(发了一批丢最后几个)RTO 触发太慢,内核用 TLP 提前探测(tcp_timer.c:687-688);mTCP 的简化模型里这类优化不存在,代价是尾丢恢复慢一个 RTO。
- 三重复 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。
评论 (0)