linux关于tcp协议ack以及乱序报文暂存的实现--立即ack/延迟ack/捎带ack

tcp需要ack,可是为了效率,并不是每发送一个数据都要等待ack,而是尽可能利用窗口机制,积累发送ack的,当然在某些特殊情况下还是需要马上发送ack的,比如接收到乱序的数据,这种情况下,虽然接收端可以将乱序的数据包暂存,但是接收方必须发送一个ack号为按序的期望的序列号的ack给发送端,另外就是接收窗口需要调整,此时就要立刻发送ack,否则则可以延迟发送ack,看一下linux的这方面的代码:
static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
    struct tcp_sock *tp = tcp_sk(sk);
    //rcv_mss是估算的对端的mss,它对本端接收窗口的计算也有很大意义
    if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss //如果接收到了大于一个的报文,那么就发送ack,一下子确认两个报文
         && __tcp_select_window(sk) >= tp->rcv_wnd) || //需要调整窗口,最大化吞吐量
        tcp_in_quickack_mode(sk) ||
        (ofo_possible && //收到乱序的包
         skb_peek(&tp->out_of_order_queue))) {
        tcp_send_ack(sk);
    } else {
        tcp_send_delayed_ack(sk);
    }
}
大体上,上述的函数实现了RFC1122和RFC2581的关于ack的建议。
     不是说ack可以在send数据的时候捎带发送吗?确实是这样,每当发送数据的时候,ack都会被发送,但是发送数据是应用层的事,如果应用层不发送呢?那岂不是永远都无法ack了吗?所以还必须有传输层本身的一套机制来支持ack的发送,捎带ack仅仅是一个补充,传输层的ack发送就是立即模式和延迟模式,正如上面的__tcp_ack_snd_check所展示的那样。
     发送ack其实很简单,就是填写一个tcp数据,ack字段设置为接收窗口最左边的那个数据的序列号加1,延迟发送不怕和捎带发送重复,RFC2581规定每个到来的报文只能生成一个ack,除非需要发送端重传才会发送冗余ack,如果tcp进入了等待延迟发送ack的状态,当接收端有数据要发送的时候就会将ack捎带到发送端,同时清除延迟ack定时器的pendding,如此延迟ack定时器到期后就不会再发送ack了。
     稳定的理想情况下,接收端的窗口也是稳定的,不需要调整的,如果接收端不发送数据只是接收数据,ack几乎全部以延迟的方式发送给发送端,如果接收端同时也发送数据,那么ack就会以捎带的方式发给发送端,立即ack只会在几种特殊的异常情况才发送。
     一种是收到了一个以上的完整的tcp段,并且可能要放大窗口,为了使得吞吐量最大化,放大的窗口决不能浪费掉,于是需要立即发送ack给发送端,发送端接收到这个ack以后就会继续发送其发送窗口中后面的数据了。发送端的mss和接收端的窗口大小相关联,接收端的窗口设定为发送端mss的整数倍比较好,这样内存的利用率最高,确定好接收端可以承受的窗口大小之后,如果其比当前窗口大,那么立即发送ack使得发送端可以尽快发送数据。
     另外一种是在所谓的quick模式,quick模式并不是经常的,只有在非交互的tcp连接才可能进入quick模式,因为交互的连接表明ack已经足够快了,没有必要立即发送ack了,一般都是捎带ack或者延迟发送ack的,那么如何判断是否是交互连接呢?内核中tcp_opt结构体中有一个ack子结构体,内部有一个quick和pingpong两个字段,其中pingpong就是判断交互连接的,内核会在很多地方进行抉择,根据很多参数,比如收发间隔或者用户配置等判断是否一个连接是交互的,如果不是交互的,那么就存在一系列问题:1.由于用户进程长期不取出收到的数据导致一系列的问题,于是需要协议栈瞬间回复ack,2.积压的ack没有回复,影响了发送端的发送速率。此时就会给quick赋予一定的数值,每发送一个ack就会消耗掉一些quick值,直到用尽了quick而进入延迟模式,quick的值和窗口相关,因为接收端最多只能确认接收窗口这么大的数据。
     立即ack的最后一种可能就是收到乱序包,表明数据已经可能丢失了,那么应该尽快地进入补救阶段,就是说要尽快进入快速重传,此时ack也要立即发送(内核发现越界包[和乱序包有区别]后会调用tcp_send_dupack发送一个ack后丢次报文而返回),内核收到乱序报文后会在out_of_order_queue队列缓存该乱序报本,最后会调用tcp_ack_snd_check再次发送一个ack,这个ack确认的是按序报文的最后一个,以前应该发送过该ack,这样接收端收到乱序报文后就会发送一个冗余的ack,如果下次接收的数据仍然是乱序的,那么就再发送一个前两个相同的ack,这样发送端可能就会连续接收到三个一模一样的ack,在接收端,第三次接收到的仍然是乱序报文时,再次发送冗余ack,只有这第4个ack被发送端收到后才会进行快速重传。这里的一个细节就是发送端收到了4个相同的ack(3个冗余ack),从而作为进入快速重传的标志,linux是这么实现的,符合了rfc的建议,但是这种实现所依赖的是其背后的一个思想。
     一个报文谈不上顺序,最少两个报文才有顺序的概念,正如字节序一样,utf8以一个字节为编码单位,因此就没有字节序的问题,同样的,仅仅来了一个报文也不能说它对于当前按序的报文来讲是乱序的,只有当第二个报文到来的时候,如果当前按序报文,第一个报文,第二个报文拼不成顺序才能说明后来的这两个报文是乱序的,当然这也是一种权衡的结果,正如三次握手为何是三次一样,即使接收端收到了第三个乱序报文,仍有可能被第四个填充后成为按序报文,没完没了等下去是不行的,必须在发送端接收到确定的,不是很大的数目冗余ack的时候进入快速重传,同时也不能频繁的快速重传,因此就选择了3个冗余ack,当然这个数字是可以配置的。
     最后看一下乱序报文的重新调整。linux的协议栈实现中将乱序的报文按照序列号大小顺序插入到一个队列当中,此队列是基于连接的,如果该乱序队列有报文暂存的话,每接收到一个报文都会尝试调用tcp_ofo_queue函数,它的意义在于努力将乱序的报文顺序化,正如上述冗余ack相关的背后的思想,每个新的报文都有可能填补按序报文和乱序报文之间的缺口,换句话说,每一个新到的报文都可能直接拼接到按序报文队列最后一个的后面,同时也有可能完成这种拼接后,和后面的乱序队列的最前面一个或者几个或者全部的报文拼接,最终成为一系列按序的报文:
static void tcp_ofo_queue(struct sock *sk)
{
    struct tcp_opt *tp = tcp_sk(sk);
    __u32 dsack_high = tp->rcv_nxt;
    struct sk_buff *skb;
    while ((skb = skb_peek(&tp->out_of_order_queue)) != NULL) {
        if (after(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) //最前面的skb都拼不上
            break;
        ...
        if (!after(TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt)) {
            __skb_unlink(skb, skb->list); //曾经接收的报文段,继续
            __kfree_skb(skb);
            continue;
        }
        __skb_unlink(skb, skb->list); //可以拼接,更新tp的rcv_next字段
        __skb_queue_tail(&sk->sk_receive_queue, skb);
        tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
        ...
    }
}

你可能感兴趣的:(linux关于tcp协议ack以及乱序报文暂存的实现--立即ack/延迟ack/捎带ack)