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