• TCP协议之《对端MSS值估算》


    由于没有直接的信息可以获得对端的MSS值,内核中的代码实际上是估算以得到对端MSS值。


    一、RCV_MSS初始化

    初始化对端的MSS值,首先起始值取自本地通告advmss值与当前发送MSS缓存值两者之中的较小值,在TCP的三次握手建立连接过程中,双方协商了MSS的钳制值即最大值,其值介于通告advmss与MSS缓存值mss_cache之间。其次,如果此接收MSS值大于对端发送窗口的二分之一,取后者为新的发送MSS值。如果新增大于默认的MSS值TCP_MSS_DEFAULT(536),取后者为新的发送MSS值。最后,保证rcv_mss的值不小于最小的MSS值TCP_MIN_MSS(88)。最小值是由最大的IP头部和最大的TCP头部长度加上8个字节的数据长度,减去标准IP和TCP头部长度而得到的值。

    高估此值将导致ACK确认发送不及时,低估此值没有关系,内核将在函数tcp_measure_rcv_mss中进行修正。

    void tcp_initialize_rcv_mss(struct sock *sk)
    {
        const struct tcp_sock *tp = tcp_sk(sk);
        unsigned int hint = min_t(unsigned int, tp->advmss, tp->mss_cache);
     
        hint = min(hint, tp->rcv_wnd / 2);
        hint = min(hint, TCP_MSS_DEFAULT);
        hint = max(hint, TCP_MIN_MSS);
     
        inet_csk(sk)->icsk_ack.rcv_mss = hint;
    }
    #define TCP_MSS_DEFAULT 536U   /* IPv4 (RFC1122, RFC2581) */
    #define TCP_MIN_MSS     88U    /* Minimal accepted MSS. It is (60+60+8) - (20+20). */
    TCP客户端在发起连接请求,初始化SYN报文时调用tcp_initialize_rcv_mss初始化rcv_mss。接收到服务端的SYN+ACK报文,或者,接收到SYN报文,意味着TCP两端同时发送SYN报文,同时打开连接时,再次初始化话接收MSS值。注意在第二次调用rcv_mss初始化函数之前,内核函数tcp_sync_mss将先更新本地MSS缓存值,所以两次初始化rcv_mss可能得到不一样的值。

    static void tcp_connect_init(struct sock *sk)
    {
        tcp_initialize_rcv_mss(sk);
    }
    static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
    {
        if (th->ack) {
            if (!th->syn)
                goto discard_and_undo;
            tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
            tcp_initialize_rcv_mss(sk);
        }
    }
    TCP服务端接收到SYN报文请求和接收到三次握手的第三个ACK确认报文后都将调用接收MSS初始化函数tcp_initialize_rcv_mss。但是,在第一次调用前服务端将先更新缓存MSS值(见函数tcp_sync_mss)以及MSS通告值advmss。在第二次调用前根据TCP的timestamp选项,首先调整通告MSS的值advmss,之后在初始化接收MSS值。

    struct sock *tcp_v4_syn_recv_sock(const struct sock *sk, struct sk_buff *skb, struct request_sock *req, struct dst_entry *dst, struct request_sock *req_unhash, bool *own_req)
    {
        tcp_sync_mss(newsk, dst_mtu(dst));
        newtp->advmss = tcp_mss_clamp(tcp_sk(sk), dst_metric_advmss(dst));
        tcp_initialize_rcv_mss(newsk);
    }
    int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
    {
        switch (sk->sk_state) {
        case TCP_SYN_RECV:
            if (tp->rx_opt.tstamp_ok)
                tp->advmss -= TCPOLEN_TSTAMP_ALIGNED;
     
            tcp_initialize_rcv_mss(sk);
        }
    }

    二、RCV_MSS估算

    入口函数为tcp_event_data_recv,其在接收到对端的TCP数据时被调用,在其中使用tcp_measure_rcv_mss函数估算RCV_MSS的值。

    static void tcp_event_data_recv(struct sock *sk, struct sk_buff *skb)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        struct inet_connection_sock *icsk = inet_csk(sk);
     
        tcp_measure_rcv_mss(sk, skb);
    }
    第一种比较简单的情况,接收到的数据长度大于或者等于当前的接收MSS值,如果其小于本地通告的advmss值,将其设置为新的接收MSS值rcv_mss,假设对端在以MSS值的长度发送数据包。反之,如果接收数据长度大于本地的通告MSS值,而且还超出MAX_TCP_OPTION_SPACE(40字节)的长度,通常这种情况不会发生,如果发生并且接收数据长度大于入口设备的MTU值,意味着在网卡接收后内核可能进行了数据包合并(GRO)操作,此举将可能导致TCP性能降低。

    #define MAX_TCP_OPTION_SPACE 40
     
    static void tcp_measure_rcv_mss(struct sock *sk, const struct sk_buff *skb)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        const unsigned int lss = icsk->icsk_ack.last_seg_size;
     
        icsk->icsk_ack.last_seg_size = 0;
     
        len = skb_shinfo(skb)->gso_size ? : skb->len;
        if (len >= icsk->icsk_ack.rcv_mss) {
            icsk->icsk_ack.rcv_mss = min_t(unsigned int, len, tcp_sk(sk)->advmss);
            
            if (unlikely(len > icsk->icsk_ack.rcv_mss + MAX_TCP_OPTION_SPACE))
                tcp_gro_dev_warn(sk, skb, len);
        } else {

    第二种情况是接收数据长度小于当前估算的接收MSS值rcv_mss。数据长度加上传输层头部长度(TCP及选项)之和,条件一:如果大于等于MSS默认值TCP_MSS_DEFAULT与标准TCP头部长度之和;条件二:或者大于等于最小MSS值TCP_MIN_MSS与标准TCP头部长度之和,并且未设置TCP头部的PUSH标志位;以上两个条件符合其一,记录本次接收数据长度值last_seg_size,而且如果此值等于上次接收到的数据长度值,意味着已经连续两次接收到此长度数据的报文,更新接收MSS值rcv_mss为此长度值。

    对于条件二,如果设置PSH标志的话很有可能并非MSS长度报文,未设置PSH标志,通常情况下接收到的为一个具有MSS长度数据的报文,然而此数据长度大于等于最小MSS值加上标准TCP头部长度表明为合法的长度值,小于默认MSS加上TCP标准头部长度,意味着此TCP连接的路径MTU值较小,应当对接收MSS进行尽快的更新。

            len += skb->data - skb_transport_header(skb);
            if (len >= TCP_MSS_DEFAULT + sizeof(struct tcphdr) ||
                (len >= TCP_MIN_MSS + sizeof(struct tcphdr) && !(tcp_flag_word(tcp_hdr(skb)) & TCP_REMNANT))) {
                
                /* Subtract also invariant (if peer is RFC compliant), tcp header plus fixed timestamp option length.
                 * Resulting "len" is MSS free of SACK jitter. */
                len -= tcp_sk(sk)->tcp_header_len;
                icsk->icsk_ack.last_seg_size = len;
                if (len == lss) {
                    icsk->icsk_ack.rcv_mss = len;
                    return;
                }
            }
        }
    }

    在介绍最近一次的接收数据长度值last_seg_size,先来看一下TCP头部的长度tcp_header_len的值。对于发起TCP连接的客户端而言,如下函数tcp_connect_init所示,内核默认开启了timestamps选项,tcp_header_len的长度为标准TCP头部长度与timestamps选项长度之和。在接收到服务端回复的SYN+ACK报文后,如果服务端带有timestamp选项,tcp_header_len还是之前两者的和,否则,tcp_header_len仅为TCP标准头部的长度。

    $ cat /proc/sys/net/ipv4/tcp_timestamps
    1
    static void tcp_connect_init(struct sock *sk)
    {
        tp->tcp_header_len = sizeof(struct tcphdr);
        if (sock_net(sk)->ipv4.sysctl_tcp_timestamps)
            tp->tcp_header_len += TCPOLEN_TSTAMP_ALIGNED;
    }
    static int tcp_rcv_synsent_state_process(struct sock *sk, struct sk_buff *skb, const struct tcphdr *th)
    {
        if (th->ack) {
            if (tp->rx_opt.saw_tstamp) {
                tp->rx_opt.tstamp_ok       = 1;
                tp->tcp_header_len = sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
                tp->advmss      -= TCPOLEN_TSTAMP_ALIGNED;
            } else {
                tp->tcp_header_len = sizeof(struct tcphdr);
            }
            tcp_initialize_rcv_mss(sk);
        }
    }

    对于TCP服务端而言,如果接收到的客户端SYN报文包含有timestamp选项,tcp_header_len为标准TCP头部长度与timestamp选项的长度之和,否则,其仅为TCP标准头部的长度。另外,对于如果SYN报文带有数据(TCP的fastopen),并且其报文长度大于等于默认MSS长度与tcp_header_len之和,服务端将初始化last_seg_size为报文长度减去TCP头部长度的所的值。

    struct sock *tcp_create_openreq_child(const struct sock *sk, struct request_sock *req, struct sk_buff *skb)
    {
        struct sock *newsk = inet_csk_clone_lock(sk, req, GFP_ATOMIC);
        if (newsk) {
            if (newtp->rx_opt.tstamp_ok) {
                newtp->tcp_header_len = sizeof(struct tcphdr) + TCPOLEN_TSTAMP_ALIGNED;
            } else {
                newtp->tcp_header_len = sizeof(struct tcphdr);
            }
            if (skb->len >= TCP_MSS_DEFAULT + newtp->tcp_header_len)
                newicsk->icsk_ack.last_seg_size = skb->len - newtp->tcp_header_len;
    }
    在接收MSS估算函数tcp_measure_rcv_mss中,使用TCP数据的长度加上TCP头部总长度(包括所有选项长度),之后减去tcp_header_len的长度,默认情况下tcp_header_len包括TCP标准头部长度和timestamp选项长度,得到的值为TCP数据长度与SACK选项的长度之和(假设有SACK选项)。此值记录未近次接收数据段长度last_seg_size。


    三、对端MSS与本端通告窗口

    关于通告接收窗口的内容详见:https://blog.csdn.net/sinat_20184565/article/details/89037265。通告窗口值的选择函数__tcp_select_window,起初内核使用MSS钳制值mss_clamp为基础进行窗口值的推倒,当前改为了使用估算的对端MSS值,参考内核代码中的注释,可能由于rcv_mss的估算抖动导致TCP性能的下降。

    u32 __tcp_select_window(struct sock *sk)
    {
        int mss = icsk->icsk_ack.rcv_mss;
        int free_space = tcp_space(sk);
        int allowed_space = tcp_full_space(sk);
        int full_space = min_t(int, tp->window_clamp, allowed_space);
     
        if (unlikely(mss > full_space)) {
            mss = full_space;
            if (mss <= 0)
                return 0;
        }
    }
    窗口增长函数tcp_grow_window如下,如果数据报文的长度大于等于数据报文所占用空间truesize所换算的窗口空间,表明本端缓存空间充裕,内核将窗口增加本地通告MSS值advmss的两倍。反之如果数据包长度小于truesize换算的空间大小,本地缓存可能将要不足,但是窗口也有可能按照对端MSS的2倍增长。

    static void tcp_grow_window(struct sock *sk, const struct sk_buff *skb)
    {
        if (tp->rcv_ssthresh < tp->window_clamp && (int)tp->rcv_ssthresh < tcp_space(sk) && !tcp_under_memory_pressure(sk)) {
     
            /* Check #2. Increase window, if skb with such overhead will fit to rcvbuf in future. */
            if (tcp_win_from_space(sk, skb->truesize) <= skb->len)
                incr = 2 * tp->advmss;
            else
                incr = __tcp_grow_window(sk, skb);
        }
    }
    static int __tcp_grow_window(const struct sock *sk, const struct sk_buff *skb)
    {
        int truesize = tcp_win_from_space(sk, skb->truesize) >> 1;
        int window = tcp_win_from_space(sk, sock_net(sk)->ipv4.sysctl_tcp_rmem[2]) >> 1;
     
        while (tp->rcv_ssthresh <= window) {
            if (truesize <= skb->len)
                return 2 * inet_csk(sk)->icsk_ack.rcv_mss;
            truesize >>= 1;
            window >>= 1;
        }
        return 0;
    }

  • 相关阅读:
    前端文件、图片直传OOS、分片上传、el-upload上传(vue+elementUI)
    独立站源码建站和模板建站如何选择?
    Git分支
    原生input使用+v-model在input中使用
    JVM执行流程
    java面试(JVM)
    redis-shake 2.x说明
    清华、IDEA、港中文联合发表的 DQ-DETR: Dual Query Detection Transformer for Phrase Extraction and Grounding论文阅读笔记
    0913 理论知识,项目
    ffmpeg转码生成的m3u8格式详解
  • 原文地址:https://blog.csdn.net/wuyongmao/article/details/126247214