聊一聊tcp 拥塞控制 二
拥塞窗口的调整撤销
很多网络不支持ECN,所以追踪丢失包时需要推测。重新排序(reordering)对于发送方来说通常是一个问题,因为它不能分清缺失的ACK是由于丢失还是被延迟了,所以TCP可能会做出错误的判断,不必要的调整了拥塞窗口。这时就需要一种对错误的拥塞调整做出修正的机制——拥塞窗口调整撤销。
从Recovery状态撤销
在TCP_CA_Recovery拥塞状态接收到ACK报文,其ack_seq序号确认了high_seq之前的所有报文(SND.UNA >= high_seq),如上节所述,high_seq记录了进入拥塞时的最大发送序号SND.NXT,故表明对端接收到了SND.NXT之前的所有报文,未发生丢包,需要撤销拥塞状态,由函数tcp_try_undo_recovery实现。
static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
int num_dupack, int *ack_flag, int *rexmit)
{
int fast_rexmit = 0, flag = *ack_flag;
bool do_lost = num_dupack || ((flag & FLAG_DATA_SACKED) &&
tcp_force_fast_retransmit(sk));
...
if (icsk->icsk_ca_state == TCP_CA_Open) {
...
} else if (!before(tp->snd_una, tp->high_seq)) {
switch (icsk->icsk_ca_state) {
...
case TCP_CA_Recovery:
if (tcp_is_reno(tp))
tcp_reset_reno_sack(tp);
if (tcp_try_undo_recovery(sk))
return;
tcp_end_cwnd_reduction(sk);
break;
}
}
怎么来探测是否需要undo_recovery呢? 看tcp_may_undo
- D-SACK 在最近一次恢复期间重传的段都被D-SACK确认。这就说明了调整是不必要的。最近一次恢复期间重传的数据包个数记为undo_retrans,如果收到一个D-SACK,则undo_retrans--,直到undo_retrans为0,说明全部的重传都是没必要的,则需要撤销窗口调整。
- Timestamp 使用该选项时,通过比较收到ACK的时间戳和重发数据包的时间戳,可以判断窗口调整是否没必要。 tcp_may_undo()中的!tp->undo_retrans和tcp_packet_delayed(tp)分别对应以上两种方法。
- tcp_packet_delayed()中其实也包含两种方法:Timestamp和F-RTO。!tp->retrans_stamp表示已经使用F-RTO进行处理。只有检查出至少有一种成立时,才能进行拥塞窗口调整撤销。
/* Nothing was retransmitted or returned timestamp is less * than timestamp of the first retransmission. ------------传了之后还没有接收到对方发送的确认
*/ static inline bool tcp_packet_delayed(const struct tcp_sock *tp) {
return !tp->retrans_stamp || tcp_tsopt_ecr_before(tp, tp->retrans_stamp);
}
对于Reno算法,如果当前窗口中还有重传报文存在于网络中,保留retrans_stamp的值,避免这些重传报文触发dupack,再次引起错误的快速重传,此时需要保持拥塞状态不撤销,当再次接收到新的ACK报文(tcp_try_undo_recovery再次运行,但不会再执行以上的撤销拥塞窗口部分),确认SND.UNA大于high_seq后,进入TCP_CA_Open状态。
/* 1、undo_marker表明套接口进入了拥塞状态(TCP_CA_Recovery/TCP_CA_Loss),调整了拥塞窗口;否则就没有必要调整---记录了重传 2、undo_retrans等于0. 报文重传之后被D-SACK确认,表明这些重传为不必要的,原始报文未丢失。----->没有重传的段数
3、retrans_stamp等于0(重传报文时间戳retrans_stamp等于零). 在进入拥塞状态后还没有进行过任何重传,或者重传报文都已送达
4、接收到的ACK确认报文中的回复时间戳(rcv_tsecr)在重传报文的时间戳之前,表明是对于原始报文的确认,而不是对重传报文。-----------》重传了之后还没有接收到对方发送的确认
*/ static inline bool tcp_may_undo(const struct tcp_sock *tp) { return tp->undo_marker && (!tp->undo_retrans || tcp_packet_delayed(tp)); } /* People celebrate: "We love our President!" */ static bool tcp_try_undo_recovery(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); if (tcp_may_undo(tp)) { /*是否可以进行拥塞撤销*/ int mib_idx; /* Happy end! We did not retransmit anything * or our original transmission succeeded. */ DBGUNDO(sk, inet_csk(sk)->icsk_ca_state == TCP_CA_Loss ? "loss" : "retrans"); tcp_undo_cwnd_reduction(sk, false); if (inet_csk(sk)->icsk_ca_state == TCP_CA_Loss) mib_idx = LINUX_MIB_TCPLOSSUNDO; else mib_idx = LINUX_MIB_TCPFULLUNDO; NET_INC_STATS_BH(sock_net(sk), mib_idx); } if (tp->snd_una == tp->high_seq && tcp_is_reno(tp)) { /* Hold old state until something *above* high_seq * is ACKed. For Reno it is MUST to prevent false * fast retransmits (RFC2582). SACK TCP is safe 这个是什么鬼 虚假快速重传??.
,如果当前窗口中还有重传报文存在于网络中,保留retrans_stamp的值,避免这些重传报文触发dupack,再次引起错误的快速重传,
此时需要保持拥塞状态不撤销,当再次接收到新的ACK报文(tcp_try_undo_recovery再次运行,但不会再执行以上的撤销拥塞窗口部分)*/ tcp_moderate_cwnd(tp); if (!tcp_any_retrans_done(sk)) tp->retrans_stamp = 0; return true; }
//如果SND.UNA大于high_seq,套接口直接恢复到TCP_CA_Open状态
tcp_set_ca_state(sk, TCP_CA_Open);
return false; } static void tcp_undo_cwnd_reduction(struct sock *sk, bool unmark_loss) { struct tcp_sock *tp = tcp_sk(sk); if (unmark_loss) {// 是否取消lost 报文丢失 标志 struct sk_buff *skb; tcp_for_write_queue(skb, sk) { if (skb == tcp_send_head(sk)) break; TCP_SKB_CB(skb)->sacked &= ~TCPCB_LOST; } tp->lost_out = 0; tcp_clear_all_retrans_hints(tp); } if (tp->prior_ssthresh) {// 根据前一个 慢启动阈值的旧值是否存在来判断是否撤销操作 const struct inet_connection_sock *icsk = inet_csk(sk); if (icsk->icsk_ca_ops->undo_cwnd)// 如果当前拥塞算法有 undo_cwnd 接口 则使用他 tp->snd_cwnd = icsk->icsk_ca_ops->undo_cwnd(sk); else// 否则就是当前窗口 和 2倍满启动阈值的 最大值 tp->snd_cwnd = max(tp->snd_cwnd, tp->snd_ssthresh << 1); if (tp->prior_ssthresh > tp->snd_ssthresh) { tp->snd_ssthresh = tp->prior_ssthresh; //恢复到出现 recovery 状态时 的ssh_thresh tcp_ecn_withdraw_cwr(tp);// 取消 TCP_ECN_DEMAND_CWR } } else {// 不存在满启动阈值旧值 则在当前拥塞窗口和启动阈值间 选一个较大值作为当前拥塞窗口 tp->snd_cwnd = max(tp->snd_cwnd, tp->snd_ssthresh); } tp->snd_cwnd_stamp = tcp_time_stamp; // 记录最近一次检测cwnd 的时间 tp->undo_marker = 0;/*复位撤销标志*/ }
上述是在 !before(tp->snd_una, tp->high_seq) 条件成立的前提下,也就是 ack 的序号已经大于 拥塞开始时的 high_seq
undo_marker表明套接口进入了拥塞状态(TCP_CA_Recovery/TCP_CA_Loss),调整了拥塞窗口
撤销TCP_CA_Recovery状态二(undo_partial)
对于TCP_CA_Recovery拥塞状态,如果ACK报文没有确认全部的进入拥塞时SND.NXT(high_seq)之前的数据,仅确认了一部分(FLAG_SND_UNA_ADVANCED),执行撤销函数tcp_try_undo_partial
static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
int num_dupack, int *ack_flag, int *rexmit)
{
/* E. Process state. */
switch (icsk->icsk_ca_state) {
case TCP_CA_Recovery:
if (!(flag & FLAG_SND_UNA_ADVANCED)) {
...
} else {
if (tcp_try_undo_partial(sk, prior_snd_una))
return;
/* Partial ACK arrived. Force fast retransmit. */
do_lost = tcp_is_reno(tp) || tcp_force_fast_retransmit(sk);
}
//对于处在TCP_CA_Recovery拥塞状态的套接口,ACK报文并没有推进SND.UNA序号,或者,
//在partial-undo未执行的情况下,尝试进行DSACK相关的撤销操作
if (tcp_try_undo_dsack(sk)) {
tcp_try_keep_open(sk);
return;
}
tcp_identify_packet_loss(sk, ack_flag);
break;
函数tcp_try_undo_partial所示,与tcp_try_undo_recovery函数不同,这里没有使用tcp_may_undo进行撤销拥塞窗口的判断,而是使用了其中的一部分,即tcp_packet_delayed判断报文是否仅是被延迟了,忽略undo_retrans值的判断
/* Undo during fast recovery after partial ACK. 如果ack 确认了部分重传的段,则会调用此函数 进行拥塞窗口撤销。 其中参数acked 为此次确认的段 数目 重传结束 退出recovery 状态*/ static bool tcp_try_undo_partial(struct sock *sk, const int acked, const int prior_unsacked, int flag) { struct tcp_sock *tp = tcp_sk(sk); /*没有使用tcp_may_undo进行撤销拥塞窗口的判断,而是使用了其中的一部分,即tcp_packet_delayed判断报文是否仅是被延迟了, 忽略undo_retrans值的判断。其逻辑是在接收到部分确认ACK的情况下,只要tcp_packet_delayed成立,原始报文就没有丢失而是被延时了, 就应检查当前的乱序级别设置是否需要更新(tcp_update_reordering),防止快速重传被误触发。*/ if (tp->undo_marker && tcp_packet_delayed(tp)) { /* Plain luck! Hole if filled with delayed * packet, rather than with a retransmit. */ tcp_update_reordering(sk, tcp_fackets_out(tp) + acked, 1); /* We are getting evidence that the reordering degree is higher * than we realized. If there are no retransmits out then we * can undo. Otherwise we clock out new packets but do not * mark more packets lost or retransmit more. *///不进行undo_retrans值的判断,但是这里判断变量retrans_out(重传报文数量)是否有值 //如果网络中还有发出的重传报文,不进行拥塞窗口的撤销操作,而是进行拥塞窗口调整 1 ----- 函数结束处理,等待重传报文被确认 if (tp->retrans_out) { tcp_cwnd_reduction(sk, prior_unsacked, 0, flag); return true; } if (!tcp_any_retrans_done(sk)) tp->retrans_stamp = 0;//变量retrans_stamp记录了第一个重传报文的时间戳,如果已经没有了重传报文,清零此时间戳 DBGUNDO(sk, "partial recovery"); // 网络中没有重传的报文or发送skb缓存中没有报文是被重传过 ------ 则 取消所哟报文lost 状态以及调整拥塞窗口阈值等 tcp_undo_cwnd_reduction(sk, true); NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPARTIALUNDO); //尝试进入TCP_CA_Open状态,但是,如果套接口还有乱序报文或者丢失报文,将进入TCP_CA_Disorder拥塞状态。 tcp_try_keep_open(sk); return true; } return false; }
上述情况存在尝试进入TCP_CA_Open状态,但是,如果套接口还有乱序报文或者丢失报文,将进入TCP_CA_Disorder拥塞状态。
撤销TCP_CA_Recovery状态三(dsack)
对于处在TCP_CA_Recovery拥塞状态的套接口,ACK报文并没有推进SND.UNA序号,或者在partial-undo未执行的情况下,尝试进行DSACK相关的撤销操作,由函数tcp_try_undo_dsack完成。
如果undo_marker有值,并且undo_retrans为零,表明所有的重传报文都被D-SACK所确认,即重传是不必要的,执行拥塞窗口恢复操作。
/* Try to undo cwnd reduction, because D-SACKs acked all retransmitted data 发送方在探测到一个D-SACK块时,可使undo_retrans减一。如果D-SACK块最终确认了在最近窗口 中的每个不必要的重传,重传计数器因为D-SACK降为0,发送方增大拥塞窗口,恢复最新一次对ssthresh的修改。 */ static bool tcp_try_undo_dsack(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); //如果undo_marker有值,并且undo_retrans为零,表明所有的重传报文都被D-SACK所确认,即重传是不必要的,执行拥塞窗口恢复操作 if (tp->undo_marker && !tp->undo_retrans) { DBGUNDO(sk, "D-SACK"); tcp_undo_cwnd_reduction(sk, false); NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDSACKUNDO); return true; } return false; }
在DSACK判断函数tcp_check_dsack中,如果SACK序号块被认定为DSACK,并且undo_retrans大于零(进行过重传操作),并且,DSACK序号块的终止序号满足如下条件:
prior_SND.UNA >= end_seq_0 > undo_marker 表明对端接收到了原始报文和拥塞之后发送的重传报文,将undo_retrans递减一 dup-seg
参考之前文章Selective Acknowledgment 选项 浅析
tcp_check_dsack用于判断收到的第一个SACK块是否是DSACK,参数sp指向输入段携带的SACK选项信息,num_sacks表示输入段携带了几个SACK块。
参考RFC 2883.,就是接收端只有在收到重复段的情况下才会发送DSACK,而重复段有两种情况:
- 该段已经被确认过了;
- 该段是个乱序段,但是之前也已经接收过该乱序段了;
所以,对应的DSACK块有两种情况:
- DSACK块的起始序号小于ACK序号;
- DSACK块的序号范围一定在后一个SACK块的序号范围之内。
static bool tcp_check_dsack(struct sock *sk, const struct sk_buff *ack_skb,
struct tcp_sack_block_wire *sp, int num_sacks,
u32 prior_snd_una, struct tcp_sacktag_state *state)
{
struct tcp_sock *tp = tcp_sk(sk);
//DSACK只能出现在sp[0],这里提取sp[0]的起始序号
u32 start_seq_0 = get_unaligned_be32(&sp[0].start_seq);
u32 end_seq_0 = get_unaligned_be32(&sp[0].end_seq);
u32 dup_segs;
//如果SACK块的起始序号小于输入段携带的确认号,那么说明对端一定是收到了
//一个已经确认过的重复段,才会触发这样的SACK,所以认为这是一个DSACK
if (before(start_seq_0, TCP_SKB_CB(ack_skb)->ack_seq)) {
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPDSACKRECV);
} else if (num_sacks > 1) {
//如果有多个SACK块,那么提取第二个SACK块的起始序号
u32 end_seq_1 = get_unaligned_be32(&sp[1].end_seq);
u32 start_seq_1 = get_unaligned_be32(&sp[1].start_seq);
//如果第二个SACK块将第一个SACK块完全包含,那么说明对端一定是收到了
//一个乱序的重复段,所以也认为这是一个DSACK
if (after(end_seq_0, end_seq_1) || before(start_seq_0, start_seq_1))
return false;
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPDSACKOFORECV);
} else {
return false;
}
dup_segs = tcp_dsack_seen(tp, start_seq_0, end_seq_0, state);
if (!dup_segs) { /* Skip dubious DSACK */
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPDSACKIGNOREDDUBIOUS);
return false;
}
NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPDSACKRECVSEGS, dup_segs);
/* D-SACK for already forgotten data... Do dumb counting. */
if (tp->undo_marker && tp->undo_retrans > 0 &&
!after(end_seq_0, prior_snd_una) &&
after(end_seq_0, tp->undo_marker))
// 表明对端接收到了原始报文和拥塞之后发送的重传报文,将undo_retrans---dupseg
tp->undo_retrans = max_t(int, 0, tp->undo_retrans - dup_segs);
return true;
}
/* Take a notice that peer is sending D-SACKs. Skip update of data delivery
* and spurious retransmission information if this DSACK is unlikely caused by
* sender's action:
* - DSACKed sequence range is larger than maximum receiver's window.
* - Total no. of DSACKed segments exceed the total no. of retransmitted segs.
*/
static u32 tcp_dsack_seen(struct tcp_sock *tp, u32 start_seq,
u32 end_seq, struct tcp_sacktag_state *state)
{
u32 seq_len, dup_segs = 1;
if (!before(start_seq, end_seq))
return 0;
seq_len = end_seq - start_seq;
/* Dubious DSACK: DSACKed range greater than maximum advertised rwnd */
if (seq_len > tp->max_window)
return 0;
if (seq_len > tp->mss_cache)
dup_segs = DIV_ROUND_UP(seq_len, tp->mss_cache);
tp->dsack_dups += dup_segs;
/* Skip the DSACK if dup segs weren't retransmitted by sender */
if (tp->dsack_dups > tp->total_retrans)
return 0;
//设置sack_ok的bit3,表示检测到了DSACK
tp->rx_opt.sack_ok |= TCP_DSACK_SEEN;
tp->rack.dsack_seen = 1;
state->flag |= FLAG_DSACKING_ACK;
/* A spurious retransmission is delivered */
state->sack_delivered += dup_segs;
return dup_segs;
}
撤销TCP_CA_Loss状态
在TCP_CA_Loss状态的套接口,如果ACK报文推进了SND.UNA序号,尝试进行TCP_CA_Loss状态撤销,由函数tcp_try_undo_loss完成。对于FRTO,如果S/ACK确认了并没有重传的报文(原始报文),同样尝试进入撤销流程,因为此ACK报文表明RTO值设置的不够长(并非拥塞导致报文丢失),过早进入了TCP_CA_Loss状态。
static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
int num_dupack, int *ack_flag, int *rexmit)
{
case TCP_CA_Loss:
tcp_process_loss(sk, flag, num_dupack, rexmit);
tcp_identify_packet_loss(sk, ack_flag);
if (!(icsk->icsk_ca_state == TCP_CA_Open ||
(*ack_flag & FLAG_LOST_RETRANS)))
return;
/* Change state if cwnd is undone or retransmits are lost */
/* fall through */
同时SND.UNA不在high_seq之前,表明恢复流程已经结束,进入TCP_CA_Loss状态时的发送报文(SND.NXT(high_seq))都已经被确认,执行tcp_try_undo_recovery。
/* Process an ACK in CA_Loss state. Move to CA_Open if lost data are * recovered or spurious. Otherwise retransmits more on partial ACKs. 如果ACK报文推进了SND.UNA序号,尝试进行TCP_CA_Loss状态撤销,由函数tcp_try_undo_loss完成。 对于FRTO,如果S/ACK确认了并没有重传的报文(原始报文),同样尝试进入撤销流程, 因为此ACK报文表明RTO值设置的不够长(并非拥塞导致报文丢失),过早进入了TCP_CA_Loss状态。 */ static void tcp_process_loss(struct sock *sk, int flag, bool is_dupack) { struct tcp_sock *tp = tcp_sk(sk); bool recovered = !before(tp->snd_una, tp->high_seq); //SND.UNA不在high_seq之前,表明恢复流程已经结束 if ((flag & FLAG_SND_UNA_ADVANCED) && tcp_try_undo_loss(sk, false))//如果ACK报文推进了SND.UNA序号,尝试使用tcp_try_undo_loss进行TCP_CA_Loss状态撤销 return; if (tp->frto) { /* F-RTO RFC5682 sec 3.1 (sack enhanced version). */ /* Step 3.b. A timeout is spurious if not all data are * lost, i.e., never-retransmitted data are (s)acked. 如果S/ACK确认了并没有重传的报文(原始报文),同样尝试进入撤销流程,因为此ACK报文表明RTO值设置的不够长(并非拥塞导致报文丢失), 过早进入了TCP_CA_Loss状态。 */ if ((flag & FLAG_ORIG_SACK_ACKED) && tcp_try_undo_loss(sk, true)) return; if (after(tp->snd_nxt, tp->high_seq)) { if (flag & FLAG_DATA_SACKED || is_dupack) tp->frto = 0; /* Step 3.a. loss was real loss 状态是真的 不是误判*/ } else if (flag & FLAG_SND_UNA_ADVANCED && !recovered) { tp->high_seq = tp->snd_nxt; __tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF); if (after(tp->snd_nxt, tp->high_seq)) return; /* Step 2.b */ tp->frto = 0; } } if (recovered) {//为此ACK报文表明RTO值设置的不够长(并非拥塞导致报文丢失),过早进入了TCP_CA_Loss状态。 /* F-RTO RFC5682 sec 3.1 step 2.a and 1st part of step 3.a */ tcp_try_undo_recovery(sk); return; } if (tcp_is_reno(tp)) { /* A Reno DUPACK means new data in F-RTO step 2.b above are * delivered. Lower inflight to clock out (re)tranmissions. */ if (after(tp->snd_nxt, tp->high_seq) && is_dupack) tcp_add_reno_sack(sk); else if (flag & FLAG_SND_UNA_ADVANCED) tcp_reset_reno_sack(tp); } tcp_xmit_retransmit_queue(sk); }
/* Undo during loss recovery after partial ACK or using F-RTO. */
//如果FRTO执行撤销操作,或者tcp_may_undo检测到需要执行撤销,调用tcp_undo_cwnd_reduction函数恢复拥塞窗口。
static bool tcp_try_undo_loss(struct sock *sk, bool frto_undo)
{
struct tcp_sock *tp = tcp_sk(sk);
if (frto_undo || tcp_may_undo(tp)) {
tcp_undo_cwnd_reduction(sk, true);
DBGUNDO(sk, "partial loss");
NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPLOSSUNDO);
if (frto_undo)
NET_INC_STATS(sock_net(sk),
LINUX_MIB_TCPSPURIOUSRTOS);
inet_csk(sk)->icsk_retransmits = 0;
if (frto_undo || tcp_is_sack(tp)) {
tcp_set_ca_state(sk, TCP_CA_Open);
tp->is_sack_reneg = 0;
}
return true;
}
return false;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 25岁的心里话
· 闲置电脑爆改个人服务器(超详细) #公网映射 #Vmware虚拟网络编辑器
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 零经验选手,Compose 一天开发一款小游戏!
· 一起来玩mcp_server_sqlite,让AI帮你做增删改查!!