8.3 时间戳(Time Stamp)选项

  在时间戳选项诞生之前,TCP有三个问题难以解决:

(1)通信延迟RTT(Round Trip Time)测量

   RTT对于拥塞控制是十分重要的(比如计算多长时间重传数据)。通常,测量RTT的方法是发送一个报文,记录发送时间t1;当收到这个报文的确认时记录时间t2,t2 - t1就可以得到RTT。但TCP使用延迟确认机制,而且ACK可能会丢失,使得收到ACK时也无法确定是对哪个报文的回应。

(2)序列号快速回绕

  TCP判断数据是新是旧的方法是检查数据的序列号是否位于sun.una到sun.una + 2**31的范围内,而序列号空间的总大小为2*32,即约4.29G。在万兆局域网中,4.29G字节数据回绕只需几秒钟,这时TCP就无法准确判断数据的新旧。

(3)SYN Cookie的选项信息

  TCP开启SYN Cookie功能时由于Server在收到SYN请求后不保存连接,故SYN包中携带的选项(WScale、SACK)无法保存,当SYN Cookie验证通过、新连接建立之后,这些选项都无法开启。

  使用时间戳选项就可以解决上述问题。

  问题(1)解决方法:发送一个报文时将发送时间写入时间戳选项,在收到的ACK报文时通过其时间戳选项的回显值就能知道它确认的是什么时候发送的报文,用当前时间减去回显时间就可以得到一个RTT。

  问题(2)解决方法:收到一个报文时记录选项中的时间戳值,收到下一个报文时将其中的时间戳与上次的进行对比即可。时间戳回绕的速度只与对端主机时钟频率有关。Linux以本地时钟计数(jiffies)作为时间戳的值,假设时钟计数加1需要1ms,则需要约24.8天才能回绕一半,只要报文的生存时间小于这个值的话判断新旧数据就不会出错,这个功能被称为PAWS(Protect Against Wrapped Sequence numbers)。这样虽然可以解决问题(2),但随着硬件时钟频率的提升,时间戳回绕的速度也会加快,用时间戳解决序列号回绕问题的方法早晚会遇到困境。

  问题(3)解决方法:将WScale和SACK选项信息编码进32 bit的时间戳值中,建立连接时会收到ACK报文,将报文的时间戳选项的回显信息解码就可以还原WScale和SACK信息(这部分内容见《3.6 SYN Cookie》)。

  时间戳选项格式:

  其中,TSval是本端填写的时间戳,TSecr是回显给对端的时间戳。两端必须都分别在SYN包和SYN|ACK包中开启时间戳选项,时间戳功能才能生效。

  SYN包的时间戳选项在tcp_transmit_skb调用tcp_syn_options函数时设置:

 498 static unsigned int tcp_syn_options(struct sock *sk, struct sk_buff *skb,
 499                 struct tcp_out_options *opts,
 500                 struct tcp_md5sig_key **md5)
 501 {                      
 502     struct tcp_sock *tp = tcp_sk(sk);
 503     unsigned int remaining = MAX_TCP_OPTION_SPACE;
 504     struct tcp_fastopen_request *fastopen = tp->fastopen_req;
 505 
 506 #ifdef CONFIG_TCP_MD5SIG
 507     *md5 = tp->af_specific->md5_lookup(sk, sk);
 508     if (*md5) {
 509         opts->options |= OPTION_MD5;
 510         remaining -= TCPOLEN_MD5SIG_ALIGNED;
 511     }
 512 #else
 513     *md5 = NULL;
 514 #endif
...
 528     if (likely(sysctl_tcp_timestamps && *md5 == NULL)) {   //开启MD5选项就不能使用时间戳,why?
 529         opts->options |= OPTION_TS;
 530         opts->tsval = TCP_SKB_CB(skb)->when + tp->tsoffset; //设置时间戳
 531         opts->tsecr = tp->rx_opt.ts_recent; //设置回显值
 532         remaining -= TCPOLEN_TSTAMP_ALIGNED;
 533     }
...
  SYN|ACK报文的时间戳是在tcp_synack_options选项中设置:

 560 static unsigned int tcp_synack_options(struct sock *sk,
 561                    struct request_sock *req,
 562                    unsigned int mss, struct sk_buff *skb,
 563                    struct tcp_out_options *opts,
 564                    struct tcp_md5sig_key **md5,
 565                    struct tcp_fastopen_cookie *foc)
 566 {
 567     struct inet_request_sock *ireq = inet_rsk(req);
 568     unsigned int remaining = MAX_TCP_OPTION_SPACE;
 569 
 570 #ifdef CONFIG_TCP_MD5SIG
 571     *md5 = tcp_rsk(req)->af_specific->md5_lookup(sk, req);
 572     if (*md5) {
 573         opts->options |= OPTION_MD5;
 574         remaining -= TCPOLEN_MD5SIG_ALIGNED;
 575 
 576         /* We can't fit any SACK blocks in a packet with MD5 + TS
 577          * options. There was discussion about disabling SACK
 578          * rather than TS in order to fit in better with old,
 579          * buggy kernels, but that was deemed to be unnecessary.
 580          */
 581         ireq->tstamp_ok &= !ireq->sack_ok;
 582     }
 583 #else
 584     *md5 = NULL;
 585 #endif
...
 596     if (likely(ireq->tstamp_ok)) { //对端开启时间戳
 597         opts->options |= OPTION_TS;
 598         opts->tsval = TCP_SKB_CB(skb)->when;
 599         opts->tsecr = req->ts_recent;
 600         remaining -= TCPOLEN_TSTAMP_ALIGNED;
 601     }
...
   非SYN包中的时间戳选项由tcp_transmit_skb调用tcp_established_options函数填写:
 623 static unsigned int tcp_established_options(struct sock *sk, struct sk_buff *skb,
 624                     struct tcp_out_options *opts,  
 625                     struct tcp_md5sig_key **md5)
 626 {
 627     struct tcp_skb_cb *tcb = skb ? TCP_SKB_CB(skb) : NULL;
 628     struct tcp_sock *tp = tcp_sk(sk);
 629     unsigned int size = 0;
 630     unsigned int eff_sacks;
...
 642     if (likely(tp->rx_opt.tstamp_ok)) { //对端开启时间戳选项
 643         opts->options |= OPTION_TS;    
 644         opts->tsval = tcb ? tcb->when + tp->tsoffset : 0;
 645         opts->tsecr = tp->rx_opt.ts_recent;
 646         size += TCPOLEN_TSTAMP_ALIGNED;
 647     }
...
  时间戳选项的值opts->tsval 由TCP_SKB_CB(skb)->when和tp->tsoffset构成(SYN|ACK的只有前者,why?),其中tp->tsoffset为用户使用TCP_TIMESTAMP socket选项设置的偏移值,TCP_SKB_CB(skb)->when为skb发送的时间:

1811 static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
1812                int push_one, gfp_t gfp)
1813 {
...
1882         TCP_SKB_CB(skb)->when = tcp_time_stamp;
...
  tcp_time_stamp就是Linux的jiffies:
 674 #define tcp_time_stamp      ((__u32)(jiffies))
  tcp_options_write函数将选项写入报文:
 409 static void tcp_options_write(__be32 *ptr, struct tcp_sock *tp,
 410                   struct tcp_out_options *opts)
 411 {
 412     u16 options = opts->options;    /* mungable copy */
...
 428     if (likely(OPTION_TS & options)) {
 429         if (unlikely(OPTION_SACK_ADVERTISE & options)) { //如果有SACK_PERMIT选项则将其与时间戳选项写在一起以便节省空间
 430             *ptr++ = htonl((TCPOPT_SACK_PERM << 24) |
 431                        (TCPOLEN_SACK_PERM << 16) |
 432                        (TCPOPT_TIMESTAMP << 8) |
 433                        TCPOLEN_TIMESTAMP);
 434             options &= ~OPTION_SACK_ADVERTISE;
 435         } else {
 436             *ptr++ = htonl((TCPOPT_NOP << 24) |
 437                        (TCPOPT_NOP << 16) |
 438                        (TCPOPT_TIMESTAMP << 8) |
 439                        TCPOLEN_TIMESTAMP);
 440         }
 441         *ptr++ = htonl(opts->tsval); //写入时间戳值
 442         *ptr++ = htonl(opts->tsecr); //写入回显值
 443     }
...
  收到报文时 TCP使用tcp_parse_options函数解析时间戳选项:
 3481 void tcp_parse_options(const struct sk_buff *skb,
3482                struct tcp_options_received *opt_rx, int estab,
3483                struct tcp_fastopen_cookie *foc)
3484 {
3485     const unsigned char *ptr;
3486     const struct tcphdr *th = tcp_hdr(skb);
3487     int length = (th->doff * 4) - sizeof(struct tcphdr);
...
3534             case TCPOPT_TIMESTAMP:
3535                 if ((opsize == TCPOLEN_TIMESTAMP) &&
3536                     ((estab && opt_rx->tstamp_ok) || //建立状态且时间戳已经开启
3537                      (!estab && sysctl_tcp_timestamps))) { //非建立状态但时间戳允许开启
3538                     opt_rx->saw_tstamp = 1; //发现时间戳选项
3539                     opt_rx->rcv_tsval = get_unaligned_be32(ptr);
3540                     opt_rx->rcv_tsecr = get_unaligned_be32(ptr + 4);
3541                 }
3542                 break;
...
  另外, tcp_parse_aligned_timestamp函数用于快速解析报文中只有时间戳选项时的时间戳值:

3591 static bool tcp_parse_aligned_timestamp(struct tcp_sock *tp, const struct tcphdr *th)
3592 {
3593     const __be32 *ptr = (const __be32 *)(th + 1);
3594 
3595     if (*ptr == htonl((TCPOPT_NOP << 24) | (TCPOPT_NOP << 16)
3596               | (TCPOPT_TIMESTAMP << 8) | TCPOLEN_TIMESTAMP)) {
3597         tp->rx_opt.saw_tstamp = 1;
3598         ++ptr;
3599         tp->rx_opt.rcv_tsval = ntohl(*ptr);
3600         ++ptr;
3601         if (*ptr)
3602             tp->rx_opt.rcv_tsecr = ntohl(*ptr) - tp->tsoffset;
3603         else
3604             tp->rx_opt.rcv_tsecr = 0;
3605         return true;
3606     }
3607     return false;
3608 }

  收到SYN或SYN|ACK时如果发现有时间戳选项则会设置tp->opt_rx.tstamp_ok为1。

  收到SYN时:

1465 int tcp_v4_conn_request(struct sock *sk, struct sk_buff *skb)
1466 {
1467     struct tcp_options_received tmp_opt;
...
1517     tcp_parse_options(skb, &tmp_opt, 0, want_cookie ? NULL : &foc);
1518 
1519     if (want_cookie && !tmp_opt.saw_tstamp) //需要使用SYN Cookie但时间戳选项未开启
1520         tcp_clear_options(&tmp_opt); //清除选项信息,因为没法保存
1521 
1522     tmp_opt.tstamp_ok = tmp_opt.saw_tstamp;
1523     tcp_openreq_init(req, &tmp_opt, skb); //将tmp_opt中的信息转存入request_sock中
...
  tcp_openreq_init:

1075 static inline void tcp_openreq_init(struct request_sock *req,
1076                     struct tcp_options_received *rx_opt,
1077                     struct sk_buff *skb)
1078 {
...
1087     req->ts_recent = rx_opt->saw_tstamp ? rx_opt->rcv_tsval : 0;
1088     ireq->tstamp_ok = rx_opt->tstamp_ok;
...
  收到SYN|ACK时:

5373 static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb,
5374                      const struct tcphdr *th, unsigned int len)
5375 {
...
5456         if (tp->rx_opt.saw_tstamp) {
5457             tp->rx_opt.tstamp_ok       = 1;
5458             tp->tcp_header_len =
5459                 sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED; //TCP头长度加上时间戳选项长度
5460             tp->advmss      -= TCPOLEN_TSTAMP_ALIGNED; //MSS减去时间戳选项长度
5461             tcp_store_ts_recent(tp); //记录时间戳的值用于回显
...
  tcp_store_ts_recent:

3271 static void tcp_store_ts_recent(struct tcp_sock *tp)
3272 {
3273     tp->rx_opt.ts_recent = tp->rx_opt.rcv_tsval; //用于回显,同时也用于PAWS
3274     tp->rx_opt.ts_recent_stamp = get_seconds(); //保存记录的时间
3275 }
  先来看看Linux TCP是如果利用时间戳解决问题(1)的。TCP接收数据时使用tcp_rcv_rtt_measure_ts函数用来计算RTT:

 459 static void tcp_rcv_rtt_update(struct tcp_sock *tp, u32 sample, int win_dep)
 460 {
 461     u32 new_sample = tp->rcv_rtt_est.rtt;
 462     long m = sample;
 463 
 464     if (m == 0)
 465         m = 1;
 466 
 467     if (new_sample != 0) {
 468         /* If we sample in larger samples in the non-timestamp
 469          * case, we could grossly overestimate the RTT especially
 470          * with chatty applications or bulk transfer apps which
 471          * are stalled on filesystem I/O.
 472          *
 473          * Also, since we are only going for a minimum in the
 474          * non-timestamp case, we do not smooth things out
 475          * else with timestamps disabled convergence takes too
 476          * long.
 477          */
 478         if (!win_dep) {
 479             m -= (new_sample >> 3);
 480             new_sample += m;
 481         } else {
 482             m <<= 3;
 483             if (m < new_sample)
 484                 new_sample = m;
 485         }
 486     } else {
 487         /* No previous measure. */
 488         new_sample = m << 3;
 489     }
 490 
 491     if (tp->rcv_rtt_est.rtt != new_sample)
 492         tp->rcv_rtt_est.rtt = new_sample;
 493 }
...                               
508 static inline void tcp_rcv_rtt_measure_ts(struct sock *sk,
 509                       const struct sk_buff *skb)
 510 {
 511     struct tcp_sock *tp = tcp_sk(sk);
 512     if (tp->rx_opt.rcv_tsecr && //对端回显的时间戳,可以看做数据发送时间
 513         (TCP_SKB_CB(skb)->end_seq -
 514          TCP_SKB_CB(skb)->seq >= inet_csk(sk)->icsk_ack.rcv_mss)) //skb中的数据大于一个MSS,意味着数据稳定传输
 515         tcp_rcv_rtt_update(tp, tcp_time_stamp - tp->rx_opt.rcv_tsecr, 0); //当前时间可以看做数据接收时间,用当前时间减去数据发送的时间去更新RTT
 516 }
  收到ACK时在tcp_clean_rtx_queue函数中使用tcp_ack_update_rtt来更新RTT:

3001 static int tcp_clean_rtx_queue(struct sock *sk, int prior_fackets,
3002                    u32 prior_snd_una)
3003 {
...
3095     if (flag & FLAG_ACKED) {
...
3104         tcp_ack_update_rtt(sk, flag, seq_rtt); //用当前时间减去数据发送的时间去更新RTT
...
  tcp_ack_update_rtt用有时间戳和没有时间戳两种方式更新RTT:
2866 static void tcp_ack_saw_tstamp(struct sock *sk, int flag)
...
2883     struct tcp_sock *tp = tcp_sk(sk);
2884 
2885     tcp_valid_rtt_meas(sk, tcp_time_stamp - tp->rx_opt.rcv_tsecr);
2886 }
...
2905 static inline void tcp_ack_update_rtt(struct sock *sk, const int flag,
2906                       const s32 seq_rtt)
2907 {
2908     const struct tcp_sock *tp = tcp_sk(sk);
2909     /* Note that peer MAY send zero echo. In this case it is ignored. (rfc1323) */
2910     if (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr) //有时间戳选项且回显值非0
2911         tcp_ack_saw_tstamp(sk, flag);
2912     else if (seq_rtt >= 0) 
2913         tcp_ack_no_tstamp(sk, seq_rtt, flag);
2914 }
  再来看问题(2)。收到包时 TCP在tcp_rcv_established函数中会检查时间戳:
 5076 int tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
5077             const struct tcphdr *th, unsigned int len)
5078 {
5079     struct tcp_sock *tp = tcp_sk(sk);
...
5119         /* Check timestamp */
5120         if (tcp_header_len == sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED) { //TCP报头中只有时间戳选项
5121             /* No? Slow path! */
5122             if (!tcp_parse_aligned_timestamp(tp, th)) //快速解析时间戳选项失败?
5123                 goto slow_path;
5124
5125             /* If PAWS failed, check it more carefully in slow path */
5126             if ((s32)(tp->rx_opt.rcv_tsval - tp->rx_opt.ts_recent) < 0) //当前包的时间戳小于上次收到的包的时间戳
5127                 goto slow_path; //并不意味着一定是旧包,需仔细检查
5128
5129             /* DO NOT update ts_recent here, if checksum fails
5130              * and timestamp was corrupted part, it will result
5131              * in a hung connection since we will drop all
5132              * future packets due to the PAWS test.
5133              */
5134         }
...
5251 slow_path:
...
5262     if (!tcp_validate_incoming(sk, skb, th, 1))
5263         return 0;
...
   tcp_validate_incoming函数中会仔细检查时间戳:
 3696 static int tcp_disordered_ack(const struct sock *sk, const struct sk_buff *skb)
3697 {
3698     const struct tcp_sock *tp = tcp_sk(sk);
3699     const struct tcphdr *th = tcp_hdr(skb);
3700     u32 seq = TCP_SKB_CB(skb)->seq;
3701     u32 ack = TCP_SKB_CB(skb)->ack_seq;
3702
3703     return (/* 1. Pure ACK with correct sequence number. */
3704         (th->ack && seq == TCP_SKB_CB(skb)->end_seq && seq == tp->rcv_nxt) &&
3705
3706         /* 2. ... and duplicate ACK. */
3707         ack == tp->snd_una &&
3708
3709         /* 3. ... and does not update window. */
3710         !tcp_may_update_window(tp, ack, seq, ntohs(th->window) << tp->rx_opt.snd_wscale) &&
3711
3712         /* 4. ... and sits in replay window. */
3713         (s32)(tp->rx_opt.ts_recent - tp->rx_opt.rcv_tsval) <= (inet_csk(sk)->icsk_rto * 1024) / HZ);
3714 }   
3715     
3716 static inline bool tcp_paws_discard(const struct sock *sk,
3717                    const struct sk_buff *skb)
3718 {   
3719     const struct tcp_sock *tp = tcp_sk(sk);
3720         
3721     return !tcp_paws_check(&tp->rx_opt, TCP_PAWS_WINDOW) && //TCP_PAWS_WINDOW的值为1
3722            !tcp_disordered_ack(sk, skb);
3723 }       
...
4985 static bool tcp_validate_incoming(struct sock *sk, struct sk_buff *skb,
4986                   const struct tcphdr *th, int syn_inerr)
4987 {
4988     struct tcp_sock *tp = tcp_sk(sk);
4989
4990     /* RFC1323: H1. Apply PAWS check first. */
4991     if (tcp_fast_parse_options(skb, th, tp) && tp->rx_opt.saw_tstamp &&
4992         tcp_paws_discard(sk, skb)) { //tcp_paws_discard为真意味着PAWS检查失败
4993         if (!th->rst) {
4994             NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_PAWSESTABREJECTED);
4995             tcp_send_dupack(sk, skb);
4996             goto discard;
4997         }
4998         /* Reset is accepted even if it did not pass PAWS. */
4999     }
...
   tcp_paws_check函数如果返回true则PAWS通过:
1143 static inline bool tcp_paws_check(const struct tcp_options_received *rx_opt,
1144                   int paws_win)
1145 {   
1146     if ((s32)(rx_opt->ts_recent - rx_opt->rcv_tsval) <= paws_win) 
1147         return true; //当前包的时间戳只能比上次收到包的时间戳早少于2个jiffies,再多了就会被认为是旧包
1148     if (unlikely(get_seconds() >= rx_opt->ts_recent_stamp + TCP_PAWS_24DAYS)) //从上次收到包到现在经历的时间多于24天
1149         return true;
1150     /*
1151      * Some OSes send SYN and SYNACK messages with tsval=0 tsecr=0,
1152      * then following tcp messages have valid values. Ignore 0 value,
1153      * or else 'negative' tsval might forbid us to accept their packets.
1154      */
1155     if (!rx_opt->ts_recent)  //没有上次收到包的时间戳的记录
1156         return true; //有些OS会在SYN和SYN|ACK中发送0值的时间戳和回显,但在随后的报文中会携带正常的值。对于这种情况还是先让它通过吧
1157     return false;
1158 }
  看来,如果一个TCP连接连续24天不收发数据则在接收第一个包时基于时间戳的PAWS会失效,否则当前包就会被当做旧包丢弃。

  现在讨论一下基于时间戳的PAWS的潜在问题。如果jiffies 1ms加1,则时间戳回绕需要24.8天;如果jiffies提高到1us加1,则回绕需要约71.58分钟才能回绕,这时问题也不大,因为网络中旧报文几乎不可能生存超过70分钟,只是如果70分钟没有报文收发则会有一个包越过PAWS(这种情况会比较多见,相比之下24天没有数据传输的TCP连接少之又少),但除非这个包碰巧是序列号回绕的旧数据包而被放入接收队列(太巧了吧),否则也不会有问题;但如果主机的时钟频率提高到jiffies每0.1us加1呢?回绕需要7分钟多一点,这时就可能会有问题了:

(1)连接如果7分钟没有数据收发就会有一个报文越过PAWS,对于TCP连接而言这么短的时间内没有数据交互太常见了吧!这样的话会频繁有包越过PAWS检查,从而使得旧包混入数据中的概率大大增加

(2)高时钟频率条件下数据的发送和接收应该都是十分迅速的,TCP序列号的回绕速度应该快于时间戳的回绕速度,而旧报文的网络中的生存时间是否会大于7分钟呢?如果不是,则不会有问题;否则就很危险了。可以考虑以下解决方案:

1)增加时间戳的大小,由32 bit扩大到64bit

这样虽然可以在能够预见的未来解决时间戳回绕的问题,但会导致新旧协议兼容性问题,像现在的IPv4与IPv6一样

2)将一个与时钟频率无关的值作为时间戳,时钟频率可以增加但时间戳的增速不变

随着时钟频率的提高,TCP在相同时间内能够收发的包也会越来越多。如果时间戳的增速不变,则会有越来越多的报文使用相同的时间戳。这种趋势到达一定程度则时间戳就会失去意义,除非在可预见的未来这种情况不会发生。

3)暂时没想到

你可能感兴趣的:(TCP协议,TCP协议详解)