欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

TCP对端MSS值估算

程序员文章站 2022-07-01 20:29:45
...

由于没有直接的信息可以获得对端的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;
}

 

内核版本 4.15