天天看點

TCP的MTU探測功能

Linux核心預設情況下未開啟TCP的MTU探測功能。

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

當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