tcp的输入段的处理

42 篇文章 1 订阅
21 篇文章 0 订阅
tcp是全双工的协议,因此每一端都会有流控。一个tcp段有可能是一个数据段,也有可能只是一个ack,异或者即包含数据,也包含ack。如果是数据段,那么有可能是in-sequence的段,也有可能是out-of-order的段。如果是in-sequence的段,则马上加入到socket的receive队列中,如果是out-of-order的段,则会加入到socket的ofo队列。一旦当我们接收到数据,要么立即发送ack到对端,要么延迟等待和后面的数据一起将ack发送出去。 

当发送ack之前,我们需要检测一些我们已经从对端得到的信息。也就是说我们需要通过对端的信息来执行ack的生成。详细的去看tcp 协议的相关部分。一般来说就是tcp option和tcp flag的一些东西。 

这里就不介绍协议相关的东西了。随便一本tcp协议的书上都将的很详细。 

这次我们主要就来看内核协议栈的核心的数据交互是如何进行的。 

由于发送段的执行比较简单,因此我们主要来看接收端的处理。 

我们知道tcp的处理输入段的函数是tcp_rcv_established。在linux内核中有两种方法来执行输入段,分别是slow 和fast path。在fast path中,我们要做的事情非常少,只是处理输入数据(一般都是放到socket的receive队列),发送ack,存储时间戳等。而在slow path中,我们需要处理out-of-order段,PAWS,urgent数据等等。而在内核中通过实现一个伪的flag来区分是slow 还是fast path,这个伪flag是tcp头中的第12个字节组成的。分别是头长度,flag以及advertised windows。 


然后来看这个flag的相关结构以及tcp头的结构。.其中pred-flag都是保存在tcp_sock的pred_flag域中的: 

Java代码   收藏代码
  1. struct tcphdr {  
  2.     __be16  source;  
  3.     __be16  dest;  
  4.     __be32  seq;  
  5.     __be32  ack_seq;  
  6. ///下面就是flag以及头的长度。  
  7. #if defined(__LITTLE_ENDIAN_BITFIELD)  
  8.     __u16   res1:4,  
  9.         doff:4,  
  10.         fin:1,  
  11.         syn:1,  
  12.         rst:1,  
  13.         psh:1,  
  14.         ack:1,  
  15.         urg:1,  
  16.         ece:1,  
  17.         cwr:1;  
  18. #elif defined(__BIG_ENDIAN_BITFIELD)  
  19.     __u16   doff:4,  
  20.         res1:4,  
  21.         cwr:1,  
  22.         ece:1,  
  23.         urg:1,  
  24.         ack:1,  
  25.         psh:1,  
  26.         rst:1,  
  27.         syn:1,  
  28.         fin:1;  
  29. #else  
  30. #error  "Adjust your <asm/byteorder.h> defines"  
  31. #endif    
  32.     __be16  window;  
  33.     __sum16 check;  
  34.     __be16  urg_ptr;  
  35. };  
  36.   
  37.   
  38.   
  39. struct tcp_sock {  
  40. .........................  
  41.   
  42. /* 
  43.  *  Header prediction flags 
  44.  *  0x5?10 << 16 + snd_wnd in net byte order 
  45.  */  
  46.     __be32  pred_flags;  
  47. .......................  
  48. }  
  49.   
  50. union tcp_word_hdr {   
  51.     struct tcphdr hdr;  
  52.     __be32        words[5];  
  53. };   
  54.   
  55. #define tcp_flag_word(tp) ( ((union tcp_word_hdr *)(tp))->words [3])   


可以看到我们如果要取pre-flag的话,直接取得就是tcphdr的第12个字节,也就是从长度开始。然后flag是32位。 

然后就是对应的tcp flag在pre flag中的值,这个是为了方便我们存取对应的tcp flag。tcp 控制位刚好是高2个字节。 

这里有一个要注意的就是,psh不影响我们判断slow还是fast path,因此这里我们忽略调psh,所以这里又构造了一个TCP_HP_BITS. 
Java代码   收藏代码
  1. enum {   
  2.     TCP_FLAG_CWR = __cpu_to_be32(0x00800000),  
  3.     TCP_FLAG_ECE = __cpu_to_be32(0x00400000),  
  4.     TCP_FLAG_URG = __cpu_to_be32(0x00200000),  
  5.     TCP_FLAG_ACK = __cpu_to_be32(0x00100000),  
  6.     TCP_FLAG_PSH = __cpu_to_be32(0x00080000),  
  7.     TCP_FLAG_RST = __cpu_to_be32(0x00040000),  
  8.     TCP_FLAG_SYN = __cpu_to_be32(0x00020000),  
  9.     TCP_FLAG_FIN = __cpu_to_be32(0x00010000),  
  10.     TCP_RESERVED_BITS = __cpu_to_be32(0x0F000000),  
  11.     TCP_DATA_OFFSET = __cpu_to_be32(0xF0000000)  
  12. };   
  13. #define TCP_HP_BITS (~(TCP_RESERVED_BITS|TCP_FLAG_PSH))  


接下来我们来看如何构造pred-flag. 
一旦进入fast path,prediction flag将会马上被赋值到tcp_sock的pred_flags上,而在内核中是通过__tcp_fast_path_on来做得。 

Java代码   收藏代码
  1. static inline void __tcp_fast_path_on(struct tcp_sock *tp, u32 snd_wnd)  
  2. {  
  3. ///计算pred flags。  
  4.     tp->pred_flags = htonl((tp->tcp_header_len << 26) |  
  5.                    ntohl(TCP_FLAG_ACK) |  
  6.                    snd_wnd);  
  7. }  


这个计算很简单,就是直接按照pred-flag的定义进行计算。最高位是tcp_header_len,所以它需要左移26位.然后由于我们进入了fast path,因此我们这里flag就是ack。最后是对端传递过来的窗口大小。 


当fast path打开了tcp_socket的pred_flag肯定是非0,否则就是0,而每次当我们需要打开fast path,之前,我们需要先进行检测是否能够进入fast path,在内核中,是通过tcp_fast_path_check实现的。 

它的检测分为4个条件: 

1 是否ofo队列为空。 

2 当前的接收窗口是否大于0. 

3 当前的已经提交的数据包大小是否小于接收缓冲区的大小。 

4 是否含有urgent 数据 

如果上面4个条件都为真则打开fast path. 
Java代码   收藏代码
  1. static inline void tcp_fast_path_check(struct sock *sk)  
  2. {  
  3.     struct tcp_sock *tp = tcp_sk(sk);  
  4.   
  5.     if (skb_queue_empty(&tp->out_of_order_queue) &&  
  6.         tp->rcv_wnd &&  
  7.         atomic_read(&sk->sk_rmem_alloc) < sk->sk_rcvbuf &&  
  8.         !tp->urg_data)  
  9.         tcp_fast_path_on(tp);  
  10. }  


ok,接下来来看什么时候才会打开slow path或者fast path。 

先来看slow path。 

1 我们在tcp_data_queue中接收到了一个ofo的段。 

2 当我们调用tcp_prune_queue中协议栈的内存不够用了并且开始丢包。 

3 我们通过调用tcp_urg_check发现是一个urgent段。而处理erg段是在tcp_urg函数中。 

4 我们的发送窗口已经为0了。然后在tcp_select_window判断,然后打开slow path。 

5 每一个新的连接默认都是slow path的。 

然后是fast path。 

fast path的打开是调用tcp_fast_path_check来实现的,因此我们来看什么时候这个函数会被调用: 

1 在tcp_recvmsg中我们已经读取了urgent数据。urgent数据是在tcp_rcv_established中被handle的,而tcp_recvmsg是拷贝数据到用户空间的,这里我们得到urgent数据(tcp_rcv_established),然后就会在slow path中,直到我们接收到了urgent 数据(tcp_recvmsg),然后我们就进入fast path。 

2 当在tcp_data_queue填充一些gap的时候。 

3 当调用tcp_ack_update_window来修改窗口的时候. 

这里只是先文字简要的介绍下,后面我们分析代码的时候会更好的理解这些。 

其实简而言之,fast path就是tcp协议的最理想的状态下才会进入这个,比如:数据段都是按顺序到达,窗口都是固定大小,没有urgent数据,缓存够用等等。 

而slow path则是比较恶劣的情况。情况正好和上面相反。 

接下来我们就来详细分析slow 和fast path。 

先来看fast path的详细实现。 

我们就从函数tcp_rcv_established开始: 

先来看第一个判断,下面这个判断如果为true,则我们进入fast path处理。 

1 tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags  这里TCP_HP_BITS是pred_flags的掩码,如果tp为slow path,则pred_flags为 0,自然就不会相等了。 

2 TCP_SKB_CB(skb)->seq == tp->rcv_nxt 这里seq为对端发送过来的序列起始号,而rcv-nxt则是我们期望接受的序列号,如果不等,说明是ofo数据。 

3 !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt) ack_seq是当前的发送缓冲区中,已经ack了的最后一个字节号,snd_nxt是我们将要发送的下一个段的起始序列号。一般来说ack_seq都是比snd_nxt小,也就是这个值为true。 

而当ack_seq比snd_nxt大的情况我不太明白,不知道谁能解释下,什么情况下ack_seq比snd_nxt大。  

Java代码   收藏代码
  1. if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&  
  2.         TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&  
  3.         !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt))   


只要上面的三个表达式都是true,则我们进入fast path处理。 

接下来来的代码片断就是处理当tcp timestamp option打开时的情况。 

这里有个概念是paws,简而言之就是一种依靠时间戳防止重复报文的机制。详细的东西可以看下这里: 

http://www.linuxforum.net/forum/printthread.php?Cat=&Board=linuxK&main=139290&type=thread 


Java代码   收藏代码
  1. int tcp_header_len = tp->tcp_header_len;  
  2.   
  3. ///相等说明tcp timestamp option被打开。  
  4. if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) {  
  5.   
  6. ///这里主要是parse timestamp选项,如果返回0则表明pase出错,此时我们进入slow_path  
  7.         if (!tcp_parse_aligned_timestamp(tp, th))  
  8.                 goto slow_path;  
  9.   
  10. ///如果上面pase成功,则tp对应的rx_opt域已经被正确赋值,此时如果rcv_tsval(新的接收的数据段的时间戳)比ts_recent(对端发送过来的数据(也就是上一次)的最新的一个时间戳)小,则我们要进入slow path 处理paws。  
  11. if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0)  
  12.                 goto slow_path;  
  13.   
  14.         }  


fast path的最后我们看一下ack的发送处理部分。这里可以看到最终会调用__tcp_ack_snd_check来进行ack的处理,下面我们会看这个函数。这个函数前面的blog已经分析过了,不过这里再来看下。 

这里主要是通过几个条件判断来决定到底是立即发送ack还是说,等会等有数据了和数据一起将ack发送。这里delay ack的话会有一个定时器,我们前面分析定时器的时候已经分析过了,这里就不分析了。我们着重来看这几个条件: 

1 (tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss 

rcv_nxt我们知道是接收方期待接收的序列号,而rcv_wup则是窗口update之前的最后一次的rcv_nxt。rcv_mss表示delay 使用的mss。 

这里如果为真说明我们已经至少接收了一个完整的段(mss). 

2  __tcp_select_window(sk) >= tp->rcv_wnd 

第一个是计算当前的接收窗口,而rcv_wnd则是当前的接收窗口。 

如果大于等于,则说明我们可能需要改变窗口,此时就必须把ack立即发送。 

3 tcp_in_quickack_mode(sk) 

这个主要是看有没有设置立即发送的标记。 

4 (ofo_possible && skb_peek(&tp->out_of_order_queue) 

这个是测试有没有ofo数据,也就是乱序的段。 

Java代码   收藏代码
  1. static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)  
  2. {  
  3.     struct tcp_sock *tp = tcp_sk(sk);  
  4.   
  5.     if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss  
  6.          && __tcp_select_window(sk) >= tp->rcv_wnd) ||  
  7.         tcp_in_quickack_mode(sk) ||  
  8.         (ofo_possible && skb_peek(&tp->out_of_order_queue))) {  
  9.   
  10. ///立即发送ack  
  11.         tcp_send_ack(sk);  
  12.     } else {  
  13. ///否则进入delay ack的处理。  
  14.         tcp_send_delayed_ack(sk);  
  15.     }  
  16. }  

剩下的代码就不介绍了,都是一些拷贝数据到用户进程,更新相关的sock域的工作,我们前面已经基本分析过了(详见我前面的blog). 

然后我们来看slow path。 

下面的代码就是slow path开始的地方。 
这里首先是校验,然后调用tcp_validate_incoming处理paws以及段的序列号的完整性。 
Java代码   收藏代码
  1. slow_path:  
  2.   
  3. /*len是当前的段的长度,而doff<<2则是当前段的头的长度。数据段肯定要比头段要大。而第二个是数据的校验。如果有一个是true,我们则丢掉这个段。*/  
  4.   
  5. if (len < (th->doff << 2) || tcp_checksum_complete_user(sk, skb))  
  6.         goto csum_error;  
  7. ///接下来开始进入paws以及序列号的处理。  
  8.     res = tcp_validate_incoming(sk, skb, th, 1);  
  9.     if (res <= 0)  
  10.         return -res;  



接下来我们就来看tcp_validate_incoming的实现,它的代码很简单,就是一些校验。 


1 首先是处理paws。 

Java代码   收藏代码
  1. ///处理paws。  
  2. if (tcp_fast_parse_options(skb, th, tp) && tp->rx_opt.saw_tstamp &&  
  3.         tcp_paws_discard(sk, skb)) {  
  4.         if (!th->rst) {  
  5.             tcp_send_dupack(sk, skb);  
  6.             goto discard;  
  7.         }  
  8.         /* Reset is accepted even if it did not pass PAWS. */  
  9.     }  

2 然后是判断序列号的合法性。 

首先end_seq(也就是当前的段的结束序列号)不能小于rcv_wup(这个的序列号表示最后一次窗口改变时我们的rcv_nxt,也就是说这个序列号之前的段已经被确认过了). 

第二个检测就比较容易理解了,就是当前的序列号不能超过当前的窗口大小。 

不过这里有一个要注意的就是RFC793,这里我们虽然不接受这个段,可是还是会通过tcp_send_dupack来发送一个ack。这个的详细描述救在rfc793中。 

引用
RFC793, page 37: "In all states except SYN-SENT, all reset 
(RST) segments are validated by checking their SEQ-fields." 
And page 69: "If an incoming segment is not acceptable, 
an acknowledgment should be sent in reply (unless the RST 
bit is set, if so drop the segment and return)".


Java代码   收藏代码
  1.       
  2. ///这个函数其实包装了两个检测。  
  3.   
  4. if (!tcp_sequence(tp, TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq)) {  
  5.         if (!th->rst)  
  6. ///发送ack  
  7.             tcp_send_dupack(sk, skb);  
  8.         goto discard;  
  9.     }  


3 检测是否是rst段。 

4 最后一个是检测syn段。也就是握手时的序列号交换校验。 

Java代码   收藏代码
  1. ///当前的数据段的序列号不能小于期待接收的序列号。  
  2. if (th->syn && !before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt)) {  
  3.         if (syn_inerr)  
  4.             TCP_INC_STATS_BH(sock_net(sk), TCP_MIB_INERRS);  
  5.         NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONSYN);  
  6.         tcp_reset(sk);  
  7.         return -1;  
  8.     }  


接下来继续看slow path的处理。 

Java代码   收藏代码
  1. step5:  
  2. ///首先处理ack,如果是ack段,则需要更新相关域,并且进行sack,等等的处理.  
  3.     if (th->ack && tcp_ack(sk, skb, FLAG_SLOWPATH) < 0)  
  4.         goto discard;  
  5. ///更新rtt  
  6.     tcp_rcv_rtt_measure_ts(sk, skb);  
  7.   
  8.     /* Process urgent data. */  
  9.     tcp_urg(sk, skb, th);  
  10.   
  11. ///数据段的处理都在这里。  
  12.     tcp_data_queue(sk, skb);  
  13.   
  14. ///如果有数据需要发送,则会发送数据到对端。  
  15.     tcp_data_snd_check(sk);  
  16. ///检测是否需要发送ack到对端。如果要则发送ack。  
  17.     tcp_ack_snd_check(sk);  
  18.     return 0;  


接下来一个个的看,先来看tcp_ack,这里我就不详细分析这个函数了,这个函数主要是用来处理接受到ack后,我们的接收缓冲区中所需要做得一些工作。 

校验ack序列号,update 滑动窗口,清除重传队列中的已经ack了的段,执行sack信息。管理拥塞窗口,以及处理 0窗口定时器。 


tcp_data_queue这个我前面的blog已经分析过一些了。这里也不详细分析了,就简要介绍下它的功能。 

处理ofo段,处理内存超过限制,重传,重复的段的处理,如果我们在sack打开的情况下收到重复的段我们也会设置dsack。(这个函数很复杂) 


然后来看tcp_data_snd_check这个函数,这个函数主要用来将一些暂时pending住的数据(比如打开了nagle)发送出去。并且调用tcp_check_space来唤醒等待内存的写队列。这是因为我们有可能已经ack了一些数据,从而一些skb会被释放。 


而这里为什么要将pending的数据发送出去呢,主要是因为我们有可能已经akced了一些段,从而增加了拥塞窗口的大小,也就是cwnd,此时我们就需要将一些pending的数据迅速发送出去。 

Java代码   收藏代码
  1. static inline void tcp_data_snd_check(struct sock *sk)  
  2. {  
  3. ///发送pending的数据  
  4.     tcp_push_pending_frames(sk);  
  5. ///如果内存有释放则唤醒等待内存的队列  
  6.     tcp_check_space(sk);  
  7. }  
  8.   
  9. static inline void tcp_push_pending_frames(struct sock *sk)  
  10. {  
  11.     struct tcp_sock *tp = tcp_sk(sk);  
  12.   
  13.     __tcp_push_pending_frames(sk, tcp_current_mss(sk), tp->nonagle);  
  14. }  


接下来是tcp_ack_snd_check,它主要用来判断是否需要发送ack,还是说delay ack。 

Java代码   收藏代码
  1. static inline void tcp_ack_snd_check(struct sock *sk)  
  2. {  
  3.     if (!inet_csk_ack_scheduled(sk)) {  
  4.         /* We sent a data segment already. */  
  5.         return;  
  6.     }  
  7. ///这个函数前面已经分析过了,就是判断是要立即ack还是delay ack。  
  8.     __tcp_ack_snd_check(sk, 1);  
  9. }  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值