TCP的MTU探测功能

Linux内核默认情况下未开启TCP的MTU探测功能。

$ cat /proc/sys/net/ipv4/tcp_mtu_probing
0


当TCP客户端发起连接建立请求时,在函数tcp_connect_init中调用TCP的MTU探测初始化函数tcp_mtup_init。如上所述默认情况下enabled为零,使用MSS最大限制值mss_clamp加上TCP头部长度和网络层头部长度作为MTU探测的上限值,下限值由函数tcp_mss_to_mtu通过基础MSS值计算得到。

void tcp_mtup_init(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct inet_connection_sock *icsk = inet_csk(sk);

    icsk->icsk_mtup.enabled = net->ipv4.sysctl_tcp_mtu_probing > 1;
    icsk->icsk_mtup.search_high = tp->rx_opt.mss_clamp + sizeof(struct tcphdr) + icsk->icsk_af_ops->net_header_len;
    icsk->icsk_mtup.search_low = tcp_mss_to_mtu(sk, net->ipv4.sysctl_tcp_base_mss);
    icsk->icsk_mtup.probe_size = 0;
    if (icsk->icsk_mtup.enabled)
        icsk->icsk_mtup.probe_timestamp = tcp_jiffies32;
}

TCP的MTU探测的基础MSS默认初始化为1024,见宏定义TCP_BASE_MSS,可通过PROC文件tcp_base_mss修改其值。

$ cat /proc/sys/net/ipv4/tcp_base_mss
1024
$ cat /proc/sys/net/ipv4/tcp_probe_threshold
8
$ cat /proc/sys/net/ipv4/tcp_probe_interval
600

内核定义值如下:

#define TCP_BASE_MSS        1024
#define TCP_PROBE_INTERVAL  600
#define TCP_PROBE_THRESHOLD 8

static int __net_init tcp_sk_init(struct net *net)
{
    net->ipv4.sysctl_tcp_base_mss = TCP_BASE_MSS;
    net->ipv4.sysctl_tcp_probe_threshold = TCP_PROBE_THRESHOLD;
    net->ipv4.sysctl_tcp_probe_interval = TCP_PROBE_INTERVAL;
}

MTU到MSS推算


基础函数__tcp_mtu_to_mss如下。首先,路径MTU减去网路层头部和TCP标准头部的长度得到一个MSS的长度。其次,对于IPv6而言,需要在减去一个分片头部的长度;再次,MSS值不能够超过协商的限定值mss_clamp(其不包括TCP选项长度);之后减去扩展头部长度,例如IP选项的长度;最终得到的MSS值不能小于48,否则使用48,即全部TCP选项的长度40加上8字节的数据。需要注意的是__tcp_mtu_to_mss函数在计算过程中并没有考虑TCP选项的长度。

static inline int __tcp_mtu_to_mss(struct sock *sk, int pmtu)
{   
    /* Calculate base mss without TCP options: It is MMS_S - sizeof(tcphdr) of rfc1122 */
    mss_now = pmtu - icsk->icsk_af_ops->net_header_len - sizeof(struct tcphdr);
    
    /* IPv6 adds a frag_hdr in case RTAX_FEATURE_ALLFRAG is set */
    if (icsk->icsk_af_ops->net_frag_header_len) {
        const struct dst_entry *dst = __sk_dst_get(sk);
        if (dst && dst_allfrag(dst))
            mss_now -= icsk->icsk_af_ops->net_frag_header_len;
    }

    if (mss_now > tp->rx_opt.mss_clamp)
        mss_now = tp->rx_opt.mss_clamp;
    mss_now -= icsk->icsk_ext_hdr_len;
    if (mss_now < 48)
        mss_now = 48;
    return mss_now;
}

函数tcp_mtu_to_mss为对以上函数的封装,将函数__tcp_mtu_to_mss的返回结果值减去了选项的长度,即其考虑了TCP大部分选项的长度,但是并没有将SACK的选项考虑在内。

int tcp_mtu_to_mss(struct sock *sk, int pmtu)
{                     
    /* Subtract TCP options size, not including SACKs */
    return __tcp_mtu_to_mss(sk, pmtu) - (tcp_sk(sk)->tcp_header_len - sizeof(struct tcphdr));
} 

另外一个与MTU到MSS转换相关的函数为tcp_bound_to_half_wnd。如果当前最大的接收窗口大于TCP_MSS_DEFAULT(536),将发送MSS限制在最大接收窗口的一半内;否则,对于小于536的小窗口,发送MSS的值不应超出整个窗口的值。

static inline int tcp_bound_to_half_wnd(struct tcp_sock *tp, int pktsize)
{  
    /* When peer uses tiny windows, there is no use in packetizing are enough packets in the pipe for fast recovery.
     * On the other hand, for extremely large MSS devices, handling smaller than MSS windows in this way does make sense. */
    if (tp->max_window > TCP_MSS_DEFAULT)
        cutoff = (tp->max_window >> 1);
    else
        cutoff = tp->max_window;

    if (cutoff && pktsize > cutoff)
        return max_t(int, cutoff, 68U - tp->tcp_header_len);
    else
        return pktsize;
}

最终由函数tcp_sync_mss负责更新当前TCP发送使用的MSS值mss_cache,其包括除SACK选项之外的所有其它选项的长度,参见函数tcp_mtu_to_mss。并且使用tcp_bound_to_half_wnd控制发送MSS与对端接收窗口的比例关系,得到mss_cache的值,同时也更新当前连接的路径PMTU值icsk_pmtu_cookie。需要注意的是,如果启用了TCP的MTU探测功能,最后的发送mss_cache的值取当前值和以search_low计算得到的mss值两者之间的较小值。

unsigned int tcp_sync_mss(struct sock *sk, u32 pmtu)
{
    if (icsk->icsk_mtup.search_high > pmtu)
        icsk->icsk_mtup.search_high = pmtu;

    mss_now = tcp_mtu_to_mss(sk, pmtu);
    mss_now = tcp_bound_to_half_wnd(tp, mss_now);

    icsk->icsk_pmtu_cookie = pmtu;
    if (icsk->icsk_mtup.enabled)
        mss_now = min(mss_now, tcp_mtu_to_mss(sk, icsk->icsk_mtup.search_low));
    tp->mss_cache = mss_now;

    return mss_now;
}

当前发送MSS的计算有函数tcp_current_mss实现,其更进一步的考虑了TCP的SACK选项数据长度,最后得到TCP发送路径使用的MSS值。

unsigned int tcp_current_mss(struct sock *sk)
{
    const struct dst_entry *dst = __sk_dst_get(sk);
    mss_now = tp->mss_cache;
    if (dst) {
        u32 mtu = dst_mtu(dst);
        if (mtu != inet_csk(sk)->icsk_pmtu_cookie)
            mss_now = tcp_sync_mss(sk, mtu);
    }
    header_len = tcp_established_options(sk, NULL, &opts, &md5) + sizeof(struct tcphdr);
    if (header_len != tp->tcp_header_len) {
        int delta = (int) header_len - tp->tcp_header_len;
        mss_now -= delta;
    }
    return mss_now;
}

MTU探测


在TCP发送路径中,如果由于TCP_CORK选项累计了数据包,或者合并了小微数据包,在数据发送函数tcp_write_xmit中,内核调用tcp_mtu_probe发送MTU探测报文。首要条件是,没有正在运行的探测、拥塞状态在初始态、拥塞窗口大于11,并且没有SACK,以上条件只要有一个不满足,就不能进行MTU探测。

static int tcp_mtu_probe(struct sock *sk)
{
    if (likely(!icsk->icsk_mtup.enabled || icsk->icsk_mtup.probe_size || inet_csk(sk)->icsk_ca_state != TCP_CA_Open ||
           tp->snd_cwnd < 11 || tp->rx_opt.num_sacks || tp->rx_opt.dsack))
        return -1;

选取的MTU探测值probe_size等于下限值search_low加上其与上限值search_high之差的1/2,即search_low+1/2*(search_high-search_low)转换得到的MSS值,作为新的MTU探测值。但是如果新选取的值大于search_high对应的MSS值,或者上限值与下限值小于设定的探测阈值tcp_probe_threshold(8),返回失败。

    mss_now = tcp_current_mss(sk);
    probe_size = tcp_mtu_to_mss(sk, (icsk->icsk_mtup.search_high + icsk->icsk_mtup.search_low) >> 1);
    size_needed = probe_size + (tp->reordering + 1) * tp->mss_cache;
    interval = icsk->icsk_mtup.search_high - icsk->icsk_mtup.search_low;

    if (probe_size > tcp_mtu_to_mss(sk, icsk->icsk_mtup.search_high) || interval < net->ipv4.sysctl_tcp_probe_threshold) {
        /* Check whether enough time has elaplased for another round of probing. */
        tcp_mtu_check_reprobe(sk);
        return -1;
    }

在判断可发送之后,内核将开始组建数据长度为probe_size值的探测报文,新分配一个nskb,将发送队列sk_write_queue前端的数据包拷贝probe_size的数据到新的nskb中,释放拷贝过的数据包。

    nskb = sk_stream_alloc_skb(sk, probe_size, GFP_ATOMIC, false);
    skb = tcp_send_head(sk);

    TCP_SKB_CB(nskb)->seq = TCP_SKB_CB(skb)->seq;
    TCP_SKB_CB(nskb)->end_seq = TCP_SKB_CB(skb)->seq + probe_size;
    TCP_SKB_CB(nskb)->tcp_flags = TCPHDR_ACK;

    tcp_insert_write_queue_before(nskb, skb, sk);
    tcp_highest_sack_replace(sk, skb, nskb);
    tcp_for_write_queue_from_safe(skb, next, sk) {

    }
    tcp_init_tso_segs(nskb, nskb->len);

最后,调用TCP传输函数发送此数据包。

    /* We're ready to send.  If this fails, the probe will be resegmented into mss-sized pieces by tcp_write_xmit(). */
    if (!tcp_transmit_skb(sk, nskb, 1, GFP_ATOMIC)) {
        /* Decrement cwnd here because we are sending effectively two packets. */
        tp->snd_cwnd--;
        tcp_event_new_data_sent(sk, nskb);

        icsk->icsk_mtup.probe_size = tcp_mss_to_mtu(sk, nskb->len);
        tp->mtu_probe.probe_seq_start = TCP_SKB_CB(nskb)->seq;
        tp->mtu_probe.probe_seq_end = TCP_SKB_CB(nskb)->end_seq;
        return 1;
    }
    return -1;
}

以上tcp_mtu_probe函数中,如果遇到新的探测值probe_size大于search_high对应的MSS值,或者上限值与下限值小于设定的探测阈值tcp_probe_threshold(8),在返回错误之前,内核调用tcp_mtu_check_reprobe重新安排一次探测。前提是本次探测与上一次探测的时间间隔不小于设定的间隔值tcp_probe_interval(600),即10分钟。

static inline void tcp_mtu_check_reprobe(struct sock *sk)
{
    interval = net->ipv4.sysctl_tcp_probe_interval;
    delta = tcp_jiffies32 - icsk->icsk_mtup.probe_timestamp;
    if (unlikely(delta >= interval * HZ)) {
        int mss = tcp_current_mss(sk);

        /* Update current search range */
        icsk->icsk_mtup.probe_size = 0;
        icsk->icsk_mtup.search_high = tp->rx_opt.mss_clamp + sizeof(struct tcphdr) + icsk->icsk_af_ops->net_header_len;
        icsk->icsk_mtup.search_low = tcp_mss_to_mtu(sk, mss);

        /* Update probe time stamp */
        icsk->icsk_mtup.probe_timestamp = tcp_jiffies32;
    }
}

在tcp_ack函数中,如果接收到的时旧的ACK或者重复的SACK报文等非正常ACK报文,将调用tcp_fastretrans_alert处理。其中拥塞状态为非TCP_CA_Recovery,即处于TCP_CA_Loss或者其它,并且未确认的序号等于探测报文的开始序号,内核判断探测失败。

static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una, bool is_dupack, int *ack_flag, int *rexmit)
{
    switch (icsk->icsk_ca_state) {
    case TCP_CA_Recovery:
        break;
    case TCP_CA_Loss:
    default:
        /* MTU probe failure: don't reduce cwnd */
        if (icsk->icsk_ca_state < TCP_CA_CWR && icsk->icsk_mtup.probe_size && tp->snd_una == tp->mtu_probe.probe_seq_start) {
            tcp_mtup_probe_failed(sk);
            /* Restores the reduction we did in tcp_mtup_probe() */
            tp->snd_cwnd++;
            tcp_simple_retransmit(sk);
            return;
        }
    }
}

探测失败处理函数tcp_mtup_probe_failed如下,如果探测失败的话,表明探测的MTU值过大,将探测值减去1赋值给探测上限值search_high。

static void tcp_mtup_probe_failed(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);

    icsk->icsk_mtup.search_high = icsk->icsk_mtup.probe_size - 1;
    icsk->icsk_mtup.probe_size = 0;
    NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPMTUPFAIL);
}

TCP接收到的ACK报文处理tcp_ack函数,检查重传队列tcp_clean_rtx_queue。如果发送了MTU探测报文(probe_size有值),并且探测报文的结束序号已被对端确认,意味值探测成功,由函数tcp_mtup_probe_success进行处理。

static int tcp_clean_rtx_queue(struct sock *sk, u32 prior_fack, u32 prior_snd_una, struct tcp_sacktag_state *sack)
{
    if (flag & FLAG_ACKED) {
        flag |= FLAG_SET_XMIT_TIMER;  /* set TLP or RTO timer */
        if (unlikely(icsk->icsk_mtup.probe_size && !after(tp->mtu_probe.probe_seq_end, tp->snd_una))) {
            tcp_mtup_probe_success(sk);
        }
    }
}

函数tcp_mtup_probe_success的调用表明探测成功,意味着连接的MTU值已增加,随即将探测值probe_size赋予MTU探测的下限值,复位probe_size。由函数tcp_sync_mss同步TCP的MSS值。

static void tcp_mtup_probe_success(struct sock *sk)
{
    tp->prior_ssthresh = tcp_current_ssthresh(sk);
    tp->snd_cwnd = tp->snd_cwnd * tcp_mss_to_mtu(sk, tp->mss_cache) / icsk->icsk_mtup.probe_size;
    tp->snd_cwnd_cnt = 0;
    tp->snd_cwnd_stamp = tcp_jiffies32;
    tp->snd_ssthresh = tcp_current_ssthresh(sk);

    icsk->icsk_mtup.search_low = icsk->icsk_mtup.probe_size;
    icsk->icsk_mtup.probe_size = 0;
    tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
}

路径黑洞探测

如果TCP的重传次数超过了tcp_retries1限定的值(默认为3),表明网络可能存在一定的问题,但是此时内核并不会结束此连接(直到超过tcp_retries2的值),此时TCP重传处理函数,将发起MTU探测。

static int tcp_write_timeout(struct sock *sk)
{
    if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) {
    } else {
        if (retransmits_timed_out(sk, net->ipv4.sysctl_tcp_retries1, 0)) {
            /* Black hole detection */
            tcp_mtu_probing(icsk, sk);
            dst_negative_advice(sk);
        }
    }
}

如下函数tcp_mtu_probing,如果tcp_mtu_probing等于零表示为开启,直接返回。如果未使能,此处使能并且更新探测开始时间戳。否则,内核在此时将降低探测所使用的MTU值,首先将探测的下限值search_low减低一半,但是不能低于规定的最小值tcp_base_mss,还要至少大于TCP头部最大长度加上8个TCP数据长度的结果减去TCP实际头部长度的值。

static void tcp_mtu_probing(struct inet_connection_sock *icsk, struct sock *sk)
{
    /* Black hole detection */
    if (!net->ipv4.sysctl_tcp_mtu_probing)
        return;

    if (!icsk->icsk_mtup.enabled) {
        icsk->icsk_mtup.enabled = 1;
        icsk->icsk_mtup.probe_timestamp = tcp_jiffies32;
    } else {
        mss = tcp_mtu_to_mss(sk, icsk->icsk_mtup.search_low) >> 1;
        mss = min(net->ipv4.sysctl_tcp_base_mss, mss);
        mss = max(mss, 68 - tcp_sk(sk)->tcp_header_len);
        icsk->icsk_mtup.search_low = tcp_mss_to_mtu(sk, mss);
    }
    tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);
}

 

内核版本 4.15

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页