zoukankan      html  css  js  c++  java
  • tcp那个孤独的小包到底怎么回事?

    内核3.10,接《tcp的发送端一个小包就能打破对端的delay_ack么?》

    我们继续来分析这个没满mss的小包,

    可以看到,由于受到syn ack这端是发包端,所以该发送链路协商的mss为 1460,而接收的时候,mss只能为1412.

    我们来找下规律,看下小包发送有没有其他时间规律:

    可以看到,小包就是下面那排小点,其中总长度为490,很明显的一排。

    图形还是比较好找规律,可以大概看出,小包的发送间隔几乎是固定的,间隔为400ms。凭着对tcp的了解,协议栈肯定不会有固定400ms发送一个固定大小的小包的

    行为,所以这个应该是用户态行为。

    用户态如何制造这种情况,我们再来看两个时间间隔内的报文总长度:

     对应的seq分别为:1052063,1576351,2100639,2624927,3149215,3673503,4197791,4722079,5246367。

    可以算出,两个小包之间的seq相差刚好为524288字节,也就是512k,而从下图的报文时间分布来看,发包的时候,是一种burst现象,也就是有包赶紧发,有的时间段是没有包的。

    所以得出这样的结论:每400ms,用户态进程发送512k的报文,其中最后一个报文,是不满mss的,这个不满mss的报文,因为某种原因,

    等待了一会,发现后面没有报文继续发送,所以还是发送出去了,前面等待是因为不满mss,等待超时之后,只能发送出去,发送出去之后,因为没有新的数据要下发,所以形成了一个明显的前后间隔。

    那什么样的情况,不满mss的报文需要等待呢?熟悉tcp的兄弟肯定会想起来,这个就像nagle啊。

    我们来看这样一个流程,在一个establish的链路中,在收到ack之后,会先调用tcp_ack处理这个ack,尽可能地清理到之前因为未收到ack而保存在在发送队列中的skb,

    void tcp_rcv_established(struct sock *sk, struct sk_buff *skb,
                 const struct tcphdr *th, unsigned int len)
    {
        。。。
    
            if (len <= tcp_header_len) {
                /* Bulk data transfer: sender */
                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);//入向ack处理
                    __kfree_skb(skb);
                    tcp_data_snd_check(sk);---------------检查是不是有数据需要发送,收到ack是触发数据发送的三大原因之一,其余两个为用户调用发送以及定时器超时后发送
                    return;
                } 

    处理完ack之后,也就是清理了那些ack的skb之后,需要再检查是否有数据发送,因为一个有效的ack除了清理我们保存的skb外,也就是调用tcp_clean_rtx_queue,还可能为我们提供了新的窗口信息:

    static inline void tcp_data_snd_check(struct sock *sk)
    {
        tcp_push_pending_frames(sk);
    。。。
    }
    
    static inline void tcp_push_pending_frames(struct sock *sk)
    {
        if (tcp_send_head(sk)) {
            struct tcp_sock *tp = tcp_sk(sk);
    
            __tcp_push_pending_frames(sk, tcp_current_mss(sk), tp->nonagle);
        }
    }
    
    void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
                       int nonagle)
    {
    .......
    
        if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
                   sk_gfp_atomic(sk, GFP_ATOMIC)))
            tcp_check_probe_timer(sk);--------------------------tcp_write_xmit返回1,则设置timer
    }

    最后进入到熟悉的tcp_write_xmit 函数,

    static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
                   int push_one, gfp_t gfp)
    {
    。。。。。
    
            if (tso_segs == 1) {------走的这个流程
                if (unlikely(!tcp_nagle_test(tp, skb, mss_now,
                                 (tcp_skb_is_last(sk, skb) ?-----------------确实是最后一个报文,因为其他报文因为nginx限速没有下来
                                  nonagle : TCP_NAGLE_PUSH))))
                    break;
            } else {
                if (!push_one &&
                    tcp_tso_should_defer(sk, skb, &is_cwnd_limited,
                             max_segs))
                    break;
            }
    。。。。
          return (push_one == 2) || (!tp->packets_out && tcp_send_head(sk));-------push_one为0,且tp->packet_out为0,tcp_send_head不为NULL

    }

    break出去之后,并没有发包,而是等待,因为

    return (push_one == 2) || (!tp->packets_out && tcp_send_head(sk));返回1,主要是tp->packets_out 为0,而 tcp_send_head(sk)非NULL。
    这样就进入了:
    static inline void tcp_check_probe_timer(struct sock *sk)
    {
        const struct tcp_sock *tp = tcp_sk(sk);
        const struct inet_connection_sock *icsk = inet_csk(sk);
    
        if (!tp->packets_out && !icsk->icsk_pending)
            inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                          icsk->icsk_rto, TCP_RTO_MAX);
    }

    主要就是设置定时器,ICSK_TIME_PROBE0,对应的超时时间为rto,

    static inline u32 __tcp_set_rto(const struct tcp_sock *tp)
    {
        return usecs_to_jiffies((tp->srtt_us >> 3) + tp->rttvar_us);
    }

    tp->rttvar_us 其实不能小于 200ms,由于tp->srtt_us存储的是平滑rtt的8倍(为了不进行float计算,因为平滑的时候,是取的7/8的老rtt+1/8新rtt),所以需要>>3,按照当前流来说,
    在设置rto之前,前面都是有多个40ms的delay_ack作为rtt,所以最终的定时器大概就是在240ms左右。和ttl图形是吻合的。当然,由于第一个小包之前的rtt都不大,平滑之后大概20ms左右,
    所以第一个小包离收到ack之后的rto大概在220ms,也是符合图形的。

  • 相关阅读:
    MySQL技术内幕 InnoDB存储引擎 之 InnoDB体系架构
    ORACLE同义词使用
    五大好用的开源MySQL管理工具推荐
    MySQL Online DDL工具
    10046事件及其用法介绍
    MySQL表碎片清理
    MyRocks安装部署
    TiDB单机安装测试
    TiDB官方文档
    GoldenGate—AUTORESTART配置
  • 原文地址:https://www.cnblogs.com/10087622blog/p/10431785.html
Copyright © 2011-2022 走看看