前面的文章分析了接收端如何发送ack给发送端,总结一下就是立即ack,捎带ack和延迟ack,现在看一下tcp的发送端是如何处理ack的,本质上tcp所谓的有连接就是双方对于seq和ack的处理,对于seq,发送方是主动的,而接收端是被动的,但是对于ack则相反,因此参照tcp的流控以及拥塞控制加之性能因素的需要,首先要设计接收端如何发送ack,其次再来设计发送端如何处理,linux采纳了rfc的建议(好像没有不采纳的OS,除非它猛到自己定义标准)。对于发送ack,前面已经说过了,对于如何处理ack,完全就是应付一下,要简单的多,简单的说,接收端只会发送按序报文的最后一个未被确认的报文的seq作为其ack,但是对于发送端收到这个ack后如何处理,虽然比发送ack的逻辑和策略要简单,但是也要彻底理解协议才能看懂代码:
static int tcp_ack(struct sock *sk, struct sk_buff *skb, int flag)
{
...
if (after(ack, tp->snd_nxt))
goto uninteresting_ack;
if (before(ack, prior_snd_una))
goto old_ack;
if (!(flag&FLAG_SLOWPATH) && after(ack, prior_snd_una)) { //非冗余的ack,正常,不进入快速重传
tcp_update_wl(tp, ack, ack_seq);
tp->snd_una = ack;
tcp_westwood_fast_bw(sk, skb);
flag |= FLAG_WIN_UPDATE;
} else {
if (ack_seq != TCP_SKB_CB(skb)->end_seq) //说明是捎带ack
flag |= FLAG_DATA;
flag |= tcp_ack_update_window(sk, tp, skb, ack, ack_seq); //flag可能置update位
if (TCP_SKB_CB(skb)->sacked) //如果是sack,则说明可能丢包了,有可能置上sack位,选择确认接收端已经收到的报文,sack完全是为了提高性能的,实际上在分析代码的时候可以忽略这种情况,只有确认丢失报文的时候才会选择确认(sack)
flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una);
if (TCP_ECN_rcv_ecn_echo(tp, skb->h.th)) //路由器通知丢包,置上ece位
flag |= FLAG_ECE;
...
}
...
flag |= tcp_clean_rtx_queue(sk, &seq_rtt); //尽量清除所有已经被确认的报文,如果有被确认的报文,则置上acked位
...
if (tcp_ack_is_dubious(tp, flag)) { //如果没有设置acked位,也没有设置data位也没有设置update位,或者存在sack或者ece位,则说明可能已经丢失报文,这里判断比较复杂,注意||运算符,只要第一个比较对象返回真就返回,依次类推,flag如果有data位或者acked位,我们也不能确定就一定没有丢失报文,因为ack由对端发送,可能的方式有好几种,不仅仅是裸ack,还可能是捎带ack,对于设置了acked位的flag也不能说就一定没有丢失报文,因为虽然该ack确认一部分报文,但是后面的报文可能丢失,复杂的情况下还可能出现选择确认-sack,因此还需要进一步判断ece和sack标志,但是反过来可以说的是,如果既没有data位,也没有acked位,也没有update位,则一定要进入重传。
...//进入拥塞控制,快速重传
tcp_fastretrans_alert(sk, prior_snd_una, prior_packets, flag);
}
...//否则正常返回
return 1;
...
}
其中最麻烦的就是sack的相关逻辑了,前面说了,sack完全是为性能考虑的一个可选的机制,它可以使得发送端只重传丢失的报文,而不必重传已经发来的有选择的乱序的确认报文,这是通过tcp头的选项进行配置的。使用sack机制前要允许sack选项,在冗余ack发来的时候,它携带一些信息,这些信息包含哪些乱序的报文已经安全正确接收且被暂存在接收端了,如此一来发送端重传报文的时候就不必再传输这些已经确认的乱序报文了,具体可以看一下tcp_sacktag_write_queue的代码。
linux关于tcp协议ack的实现--发送端对ack的处理
最新推荐文章于 2024-08-15 10:49:31 发布