当前位置: 首页 > 工具软件 > MSS > 使用案例 >

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

谢雅珺
2023-12-01

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

 类似资料: