zoukankan      html  css  js  c++  java
  • Linux TCP滑动窗口代码简述

    前言:TCP的可靠性大致通过3类方法来保障:1.确认和重传。2.流量控制。3.拥塞避免。其中的流量控制中使用的滑动窗口,使得TCP的发送方和接收方速度得以匹配,从而为传输提供了可靠性支撑。本篇就介绍一下滑动窗口在Linux的大致代码,对于滑动窗口的基本知识已经有无数优秀的文章,更有TCP/IP卷一可参考,本篇不再赘述。代码基于 Linux 2.6.32。

    1. 背景问题介绍

    我们知道TCP是有确认机制的,就是对于发送方发送的每个字节,接收方都会显式的进行确认(连续确认实际也是确认了每个字节)。那么仔细想象一下,该如何处理这个确认过程呢?比如,sender发送完一定数据后,停下来等待receiver的确认,然后再继续发送,再继续等待... 因此这里存在一个传输效率的问题。

    接着再想另外一个问题,如果发送端的发送速度快,而接收端的接收速度慢,此时,如果不能协调收发速度,将会导致接收不及时丢包,进而加重重传,丢更多的包,引发网络雪崩。

    上面两个问题基本就是滑动窗口诞生要解决的问题,即提高传输效率和流量控制的功能。

    2. 流量控制简单过程

    TCP的流量控制主要是协调收发的速度,在发送端维护着发送和接收窗口,同样的,在接收端,也同样如此。在说具体的操作之前,先说几个相关的概念:发送队列,接收队列,重传队列,滑动窗口,发送窗口,拥塞窗口,通告窗口。

    2.1 概念解释
    • 发送队列——我们知道协议栈在发送报文的时候,是要先放入到发送队列的,每个打开的socket都维护着一个接收队列和发送队列。
    • 重传队列——当报文从发送队列被发送后,会拷贝一份放到重传队列,重传队列用于超时定时器到期后,进行重传。
    • 滑动窗口——滑动窗口是一个可以滑动的区间。在发送时,通常就是指发送窗口;在接收时,就是指接收窗口。滑动窗口是一种提高传输效率的机制,因此在发送和接收过程中都有。
    • 拥塞窗口——因为TCP有拥塞避免机制,因此引出了一个拥塞窗口,他也是一个限制发送速度的东西,当出现拥塞的时候,调节发送窗口大小。通常发送窗口是取拥塞窗口和通告窗口的较小值。
    • 通告窗口——通告窗口是接收端传递给发送端的一个窗口值,表示接收端还有多大空余空间接收数据。所以,发送端的发送窗口就是根据通告窗口进行调节,使发送和接收速度匹配。
    2.2 发送和接收
    • 当有数据需要发送时,会把数据挂在这个socket的发送队列中,在linux中这个队列使用的是双向链表实现的
    struct sk_buff_head {
    	/* These two members must be first. */
    	struct sk_buff	*next;
    	struct sk_buff	*prev;
    
    	__u32		qlen;
    	spinlock_t	lock;
    };
    

    TCP接收报文的函数为tcp_v4_do_rcv(),当建立连接后,接收处理的函数为tcp_rcv_established(),进来后,会进行fast path和slow path的区分处理,快通道和慢通道是由首部预测来确认的,首部预测用于提高TCP的处理速度。通常网络上大多数报文都会走fast path,看处理细节

    if (len == tcp_header_len) 
    {
    	/* Predicted packet is in window by definition.
    	 * seq == rcv_nxt and rcv_wup <= rcv_nxt.
    	 * Hence, check seq<=rcv_wup reduces to:
    	 */
    	if (tcp_header_len ==
    	    (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
    	    tp->rcv_nxt == tp->rcv_wup)
    		tcp_store_ts_recent(tp);
    
    	/* We know that such packets are checksummed
    	 * on entry.
    	 */
    	tcp_ack(sk, skb, 0);
    	__kfree_skb(skb);
    	tcp_data_snd_check(sk);
    	return 0;
    }
    

    那么就进行ack报文的处理tcp_ack(),在这个函数中,会更新发送窗口,使窗口右移,这样就会有新的报文可以被发送,tcp_data_snd_check(sk);就把这些报文发送出去。
    最后调用tcp_write_xmit(),其函数说明如下:

    This routine writes packets to the network. It advances the
    send_head. This happens as incoming acks open up the remote
    window for us.

    可以看出,这个确实是用于发送窗口扩大后的报文。从这里可以看出TCP报文的发送在窗口机制下是由接收的ack来

    • 当有数据需要接收时,依然看tcp_rcv_established()函数,先检查是否满足快速路径,如果没有乱序的话,就是这样。
    if (tp->copied_seq == tp->rcv_nxt &&
    			    len - tcp_header_len <= tp->ucopy.len) {
    #ifdef CONFIG_NET_DMA
    				if (tcp_dma_try_early_copy(sk, skb, tcp_header_len)) {
    					copied_early = 1;
    					eaten = 1;
    				}
    #endif
    				if (tp->ucopy.task == current &&
    				    sock_owned_by_user(sk) && !copied_early) {
    					__set_current_state(TASK_RUNNING);
    
    					if (!tcp_copy_to_iovec(sk, skb, tcp_header_len))
    						eaten = 1;
    				}
    

    然后就把报文从内核态拷贝到用户态,如果拷贝成功,标志成eaten = 1;接下来就是计算RTT以及更新接收的序列号。

    if (eaten) 
    {
    	/* Predicted packet is in window by definition.
    	 * seq == rcv_nxt and rcv_wup <= rcv_nxt.
    	 * Hence, check seq<=rcv_wup reduces to:
    	 */
    	if (tcp_header_len ==
    	    (sizeof(struct tcphdr) +
    	     TCPOLEN_TSTAMP_ALIGNED) &&
    	    tp->rcv_nxt == tp->rcv_wup)
    		tcp_store_ts_recent(tp);
    
    	tcp_rcv_rtt_measure_ts(sk, skb);
    
    	__skb_pull(skb, tcp_header_len);
    	tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
    	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITSTOUSER);
    }
    

    如果没有拷贝成功,那么就把报文放在接收队列中,同时更新RTT,失败的原因比如用户报文数据长度比用户空间缓存的剩余量大等。

    if (!eaten) 
    {
    	if (tcp_checksum_complete_user(sk, skb))
    		goto csum_error;
    
    	/* Predicted packet is in window by definition.
    	 * seq == rcv_nxt and rcv_wup <= rcv_nxt.
    	 * Hence, check seq<=rcv_wup reduces to:
    	 */
    	if (tcp_header_len ==
    	    (sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) &&
    	    tp->rcv_nxt == tp->rcv_wup)
    		tcp_store_ts_recent(tp);
    
    	tcp_rcv_rtt_measure_ts(sk, skb);
    
    	if ((int)skb->truesize > sk->sk_forward_alloc)
    		goto step5;
    
    	NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPHPHITS);
    
    	/* Bulk data transfer: receiver */
    	__skb_pull(skb, tcp_header_len);
    	__skb_queue_tail(&sk->sk_receive_queue, skb);
    	skb_set_owner_r(skb, sk);
    	tp->rcv_nxt = TCP_SKB_CB(skb)->end_seq;
    }
    

    最后检查一下是否需要发送ack报文或者是sack报文。

    if (!copied_early || tp->rcv_nxt != tp->rcv_wup)
    				__tcp_ack_snd_check(sk, 0);
    

    另外一个是如果出现乱序等,快速路径没有满足条件,则走慢速路径,在慢速路径中会有把报文放入乱序队列等操作,具体不表了。

    tcp_data_queue(sk, skb);
    

    之后,用户进程通过recv读操作,把报文从接收队列中读取报文,在tcp_recvmsg可以看其过程:

    skb_queue_walk(&sk->sk_receive_queue, skb) 
    {
    	/* Now that we have two receive queues this
    	 * shouldn't happen.
    	 */
    	if (WARN(before(*seq, TCP_SKB_CB(skb)->seq),
    	     KERN_INFO "recvmsg bug: copied %X "
    		       "seq %X rcvnxt %X fl %X
    ", *seq,
    		       TCP_SKB_CB(skb)->seq, tp->rcv_nxt,
    		       flags))
    		break;
    
    	offset = *seq - TCP_SKB_CB(skb)->seq;
    	if (tcp_hdr(skb)->syn)
    		offset--;
    	if (offset < skb->len)
    		goto found_ok_skb;
    	if (tcp_hdr(skb)->fin)
    		goto found_fin_ok;
    	WARN(!(flags & MSG_PEEK), KERN_INFO "recvmsg bug 2: "
    			"copied %X seq %X rcvnxt %X fl %X
    ",
    			*seq, TCP_SKB_CB(skb)->seq,
    			tp->rcv_nxt, flags);
    }
    

    3. 总结

    TCP的滑动窗口的流量控制是通过协调发送方和接收方的速度来实现的,具体来说,就是发送方窗口是由接收方回的ack驱动的,也就是说发送方要能持续发送包需要持续接收ack。另一个方面,接收方在读取报文后,发送ack进行响应,循环进行接收。这个过程通过驱动窗口的可持续滑动,进而实现了流量控制和提高传输效率。

  • 相关阅读:
    数据共享之死锁
    响应式菜单制作
    工作日志2014-07-01
    Thinkphp 无法使用-&gt;order() 排序的两种解决的方法!
    C#
    HDU1232 畅通project 并查集
    Cocos2d-x优化中图片优化
    1.3.4 设计并发友好的应用程序
    UIView的层介绍
    《鸟哥的Linux私房菜-基础学习篇(第三版)》(三)
  • 原文地址:https://www.cnblogs.com/yhp-smarthome/p/8338072.html
Copyright © 2011-2022 走看看