tcp 的mtu 探测
为什么这个东西???
TCP连接只是一个“虚拟”的连接;一个TCP连接,其报文可能从不同的IP路径传输到对端。不同的传输路径,自然会经过不同的网络设备,其MTU值自然不同。这样的话,即使对端按照MSS的值发送TCP报文,也可能会超过其中间路径的MTU值,导致数据包发送失败。
所以就有了:TCP如何感知这种PMTU(Path MTU)发生变化
mtu 探测成功后: 更新拥塞窗口相关参数 pmtu、 mss ; 拥塞窗口大小都是等于几个mss(n x mss)
MTU探测原理:通过在IP报头中设置不分片DF(Don't Fragment)标志来探测路径中的MTU值, 如果路径中设备的MTU值小于此报文长度,并且发现DF标志,就会发回一个Internet控制消息协议(ICMP)(类型3、代码4需要分片的消息ICMP_FRAG_NEEDED),消息中包含它可接受的MTU值
-M pmtudisc_opt
Select Path MTU Discovery strategy. pmtudisc_option may be
either do (prohibit fragmentation, even local one), want (do PMTU
discovery, fragment locally when packet size is large), or dont
(do not set DF flag).
ping -s 1700 -M do www.qq.com
在数据发送函数tcp_write_xmit中,内核在某些条件下会调用tcp_mtu_probe发送MTU探测报文:
分析如下:
那些情况不需要探测?
1、未启用路径MTU
2、当前路径MTU探测段的长度不为0,表示路径MTU发现段已经发出尚未得到确认
3、拥塞控制状态不处于OPEN
4.下一个发送的段存在SACK 中
5.当前已写出的字节数不大于对端通告的最大窗口的一半且发送队列中只有一个skb
只有tcp_write_xmit函数的参数push_one为0时TCP才会开启PMTU探测。直接调用tcp_write_xmit且push_one为0的函数有两个:tcp_tsq_handler和__tcp_push_pending_frames。前者是使用TSQ tasklet发送数据时调用的函数,而直接或间接调用后者的函数有:tcp_push_pending_frames、tcp_push、tcp_data_snd_check
SO啥时候需要探测MTU?
1、发现数据丢失并且使用了Forward RTO-Recovery (F-RTO)算法时
/* Process an ACK in CA_Loss state. Move to CA_Open if lost data are * recovered or spurious. Otherwise retransmits more on partial ACKs. 如果ACK报文推进了SND.UNA序号,尝试进行TCP_CA_Loss状态撤销,由函数tcp_try_undo_loss完成。 对于FRTO,如果S/ACK确认了并没有重传的报文(原始报文),同样尝试进入撤销流程, 因为此ACK报文表明RTO值设置的不够长(并非拥塞导致报文丢失),过早进入了TCP_CA_Loss状态。 */ static void tcp_process_loss(struct sock *sk, int flag, bool is_dupack) { struct tcp_sock *tp = tcp_sk(sk); bool recovered = !before(tp->snd_una, tp->high_seq); //SND.UNA不在high_seq之前,表明恢复流程已经结束 if ((flag & FLAG_SND_UNA_ADVANCED) && tcp_try_undo_loss(sk, false))//如果ACK报文推进了SND.UNA序号,尝试使用tcp_try_undo_loss进行TCP_CA_Loss状态撤销 return; if (tp->frto) { /* F-RTO RFC5682 sec 3.1 (sack enhanced version). */ /* Step 3.b. A timeout is spurious if not all data are * lost, i.e., never-retransmitted data are (s)acked. 如果S/ACK确认了并没有重传的报文(原始报文),同样尝试进入撤销流程,因为此ACK报文表明RTO值设置的不够长(并非拥塞导致报文丢失), 过早进入了TCP_CA_Loss状态。 */ if ((flag & FLAG_ORIG_SACK_ACKED) &&//high_seq之前的非重传的数据被ack/sack tcp_try_undo_loss(sk, true)) //认为上次超时是spurious的,undo return; //走到这里表明 ----重传数据被ack/sack或者新发送的数据sack, 或者reno收到dupack if (after(tp->snd_nxt, tp->high_seq)) {//有新数据发送,说明是f-rto算法的第2个ack // 如果是刚进入LOSS状态,会先尝试重传,这时候snd_nxt总是等于high_seq的, // 这个分支主要对应3.2.b之后发送了两个新分片。 // 虽然发送了新分片,没有发重传,但是这时候收到的ack并没有更新una // 说明这个rtt中,之前una的包仍旧没有达到,因此这里认为他是真的超时// 关闭frto。对应3.3.a if (flag & FLAG_DATA_SACKED || is_dupack)//收到sack或者dupack,说明对方收到乱序包 tp->frto = 0; /* Step 3.a. loss was real loss 说明loss判断是对的,关闭frto算法*/ } else if (flag & FLAG_SND_UNA_ADVANCED && !recovered) {//窗口移动, 但high_seq没有全部确认 // 这里进入的条件为// 1. snd_nxt == high_seq,还没发送过新分片// 2. una更新过,且没有完全恢复 // 执行3.2.b,发送新分片。// 对应论文图中收到了一个更新过snd_una的ack。 tp->high_seq = tp->snd_nxt; __tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF);//F-FTO发送新数据 /*/Else, if the acknowledgment advances the window AND the Acknowledgment field does not cover "recover", transmit up to two new (previously unsent) segments and enter step 3*/ if (after(tp->snd_nxt, tp->high_seq))//有新数据发送则返回,不重传 return; /* Step 2.b */ tp->frto = 0; } } //snd_una > high_seq // 已经完全恢复,则撤销对应的恢复操作,并进入TCP_CA_OPEN状态。后续将进入恢复状态。 // 这里主要处理了其他几个不在FRTO可处理的场景,如3.2.a和3.3.a // 唯一进入这里但frto还可能生效的场景为: // 发送新分片后,但是收到了一个不是新的sack,且不是一个dup sack。 // 在这种情况下的处理应该和上一个旧的sack相同。 if (recovered) {//为此ACK报文表明RTO值设置的不够长(并非拥塞导致报文丢失),过早进入了TCP_CA_Loss状态。 /* F-RTO RFC5682 sec 3.1 step 2.a and 1st part of step 3.a */ tcp_try_undo_recovery(sk); return; } //snd_una < high_seq, 进入loss前发送的包没有全部确认 if (tcp_is_reno(tp)) { /* A Reno DUPACK means new data in F-RTO step 2.b above are * delivered. Lower inflight to clock out (re)tranmissions. */ if (after(tp->snd_nxt, tp->high_seq) && is_dupack) //snd_nxt > high_seq 很可能是frto中发送的新数据被dupack tcp_add_reno_sack(sk);//就dupack,减少inflight包数量,从而减少重传包 else if (flag & FLAG_SND_UNA_ADVANCED)//本次窗口更新 una 在增长 tcp_reset_reno_sack(tp); //因为reno只会重传第一个包,窗口滑动reno就可以重置了 } tcp_xmit_retransmit_queue(sk); }
2、发送FIN关闭连接时:tcp_send_fin
3、使用setsockopt设置TCP_NODELAY功能时:do_tcp_setsockopt
TCP_NODELAY
4、使用setsockopt取消TCP_CORK功能时:do_tcp_setsockopt
TCP_CORK
5、收到对端发过来的ACK或数据包时
6、非TCP_LISTEN和TCP_CLOSE状态下收到合法的包时
/* Create a new MTU probe if we are ready. * MTU probe is regularly attempting to increase the path MTU by * deliberately sending larger packets. This discovers routing * changes resulting in larger path MTUs. * * Returns 0 if we should wait to probe (no cwnd available), 没有窗口可以使用 延迟探测 * 1 if a probe was sent, 已经发送mtu发现段 * -1 otherwise 其他情况 */ static int tcp_mtu_probe(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct inet_connection_sock *icsk = inet_csk(sk); struct sk_buff *skb, *nskb, *next; struct net *net = sock_net(sk); int len; int probe_size; int size_needed; int copy; int mss_now; int interval; /* Not currently probing/verifying, * not in recovery, * have enough cwnd, and * not SACKing (the variable headers throw things off) */ if (!icsk->icsk_mtup.enabled ||// MTU 没有开启 icsk->icsk_mtup.probe_size ||// 当前MTU探测长度,如果probe_size不为0 表示探测进行中 inet_csk(sk)->icsk_ca_state != TCP_CA_Open ||// 拥塞控制不处于open 状态 tp->snd_cwnd < 11 ||//拥塞窗口大小不足使用 tp->rx_opt.num_sacks || tp->rx_opt.dsack)// 下一个发送段中存在SACK return -1; /* Use binary search for probe_size between tcp_mss_base, * and current mss_clamp. if (search_high - search_low) * smaller than a threshold, backoff from probing. */ mss_now = tcp_current_mss(sk); probe_size = tcp_mtu_to_mss(sk, (icsk->icsk_mtup.search_high + icsk->icsk_mtup.search_low) >> 1); // probe_size 大约== 2* mss size_needed = probe_size + (tp->reordering + 1) * tp->mss_cache; interval = icsk->icsk_mtup.search_high - icsk->icsk_mtup.search_low; /* When misfortune happens, we are reprobing actively, * and then reprobe timer has expired. We stick with current * probing process by not resetting search range to its orignal. */// 超过上限 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; } /* Have enough data in the send queue to probe? 没有足够的数据用于发送MTU探测 */ if (tp->write_seq - tp->snd_nxt < size_needed) return -1; if (tp->snd_wnd < size_needed) return -1; if (after(tp->snd_nxt + size_needed, tcp_wnd_end(tp))) return 0; /* Do we need to wait to drain cwnd? With none in flight, don't stall 检测对方是有足够空间来接收MTU*/ if (tcp_packets_in_flight(tp) + 2 > tp->snd_cwnd) { if (!tcp_packets_in_flight(tp)) return -1; else return 0; } /* We're allowed to probe. Build it now. */ nskb = sk_stream_alloc_skb(sk, probe_size, GFP_ATOMIC, false); if (!nskb) return -1; sk->sk_wmem_queued += nskb->truesize; sk_mem_charge(sk, nskb->truesize); 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_SKB_CB(nskb)->sacked = 0; nskb->csum = 0; nskb->ip_summed = skb->ip_summed; tcp_insert_write_queue_before(nskb, skb, sk);// 插入到发送队列的对首 tcp_highest_sack_replace(sk, skb, nskb); len = 0; tcp_for_write_queue_from_safe(skb, next, sk) { copy = min_t(int, skb->len, probe_size - len); if (nskb->ip_summed) { skb_copy_bits(skb, 0, skb_put(nskb, copy), copy); } else { __wsum csum = skb_copy_and_csum_bits(skb, 0, skb_put(nskb, copy),BUG_ON copy, 0); nskb->csum = csum_block_add(nskb->csum, csum, len); } if (skb->len <= copy) { /* We've eaten all the data from this skb. * Throw it away. */ TCP_SKB_CB(nskb)->tcp_flags |= TCP_SKB_CB(skb)->tcp_flags; tcp_unlink_write_queue(skb, sk); sk_wmem_free_skb(sk, skb); } else { TCP_SKB_CB(nskb)->tcp_flags |= TCP_SKB_CB(skb)->tcp_flags & ~(TCPHDR_FIN|TCPHDR_PSH); if (!skb_shinfo(skb)->nr_frags) { skb_pull(skb, copy); if (skb->ip_summed != CHECKSUM_PARTIAL) skb->csum = csum_partial(skb->data, skb->len, 0); } else { __pskb_trim_head(skb, copy); tcp_set_skb_tso_segs(skb, mss_now); } TCP_SKB_CB(skb)->seq += copy; } len += copy; if (len >= probe_size) break; }// 将发送队列中 路径MTU探测段后的skb 中的数据赋值到路径MTU探测段中,并释放被copy的skb /* 也就是根据MTU 以及剩下未发送的data 构造一个 MTU 探测包*/ tcp_init_tso_segs(nskb, nskb->len); /* 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);// 记录MTU 探测长度 tp->mtu_probe.probe_seq_start = TCP_SKB_CB(nskb)->seq;// 记录MTU探测序号 用于确认路径MTU探测成功与否 tp->mtu_probe.probe_seq_end = TCP_SKB_CB(nskb)->end_seq; return 1; } return -1; }
如果探测后结果是:在tcp_mtu_probe函数发送大的探测包后需要等待三种结果:
(1)收到ACK确认了探测包;这意味着PMTU大于或等于当前探测包的MTU;
----->收到ACK确认了探测包。TCP在收到ACK后会调用tcp_clean_rtx_queue函数来清理发送缓存中的skb:
3001 static int tcp_clean_rtx_queue(struct sock *sk, int prior_fackets, 3002 u32 prior_snd_una) 3003 { ... 3099 if (unlikely(icsk->icsk_mtup.probe_size && //正在PMTU探测中 3100 !after(tp->mtu_probe.probe_seq_end, tp->snd_una))) { //探测报文全部到达对端 3101 tcp_mtup_probe_success(sk); //探测成功 3102 } ...
static void tcp_mtup_probe_success(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); struct inet_connection_sock *icsk = inet_csk(sk); /* FIXME: breaks with very large cwnd */ 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_time_stamp; tp->snd_ssthresh = tcp_current_ssthresh(sk); icsk->icsk_mtup.search_low = icsk->icsk_mtup.probe_size;//记录最小PMTU的值 icsk->icsk_mtup.probe_size = 0;//本次探测结束 tcp_sync_mss(sk, icsk->icsk_pmtu_cookie);//保存探测结果 NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPMTUPSUCCESS); }
(2)收到ICMP“需要分片”的报文;这时需要根据报文中通告的MTU来调整PMTU值;
---->处理icmp出错报文对应的tcp层是TCPv4中处理ICMP报文的函数是 tcp_v4_err
void tcp_v4_err(struct sk_buff *icmp_skb, u32 info) { ... switch (type) { ...... case ICMP_DEST_UNREACH: //路由器丢弃探测包后发送的ICMP报文会由这个分支处理 if (code == ICMP_FRAG_NEEDED) { /* PMTU discovery (RFC1191) */ /* We are not interested in TCP_LISTEN and open_requests * (SYN-ACKs send out by Linux are always <576bytes so * they should go through unfragmented). */ if (sk->sk_state == TCP_LISTEN) goto out; tp->mtu_info = info; //记录ICMP报文返回的MTU值 if (!sock_owned_by_user(sk)) { //进程没有锁定socket tcp_v4_mtu_reduced(sk); //修改MSS的值 } else { if (!test_and_set_bit(TCP_MTU_REDUCED_DEFERRED, &tp->tsq_flags)) //推迟到进程解除锁定socket时调用tcp_v4_mtu_reduced sock_hold(sk); } goto out; ... }
(3)数据包丢失导致重传
在路径MTU过大被路由器丢弃并收到ICMP报文的情况下,TCP会把ICMP中通告的PMTU作为结果保存下来。但出于安全等考虑,并不是所有的路由器在丢弃分片过大的报文时都会发送ICMP消息。如果探测包被这样的路由器丢弃,TCP不会收到任何响应,就好像探测包进入了“黑洞”一样,这就是TCP PMTU发现中的Black Hole Detection问题。即结果(3)。探测包丢失后TCP有两张方式处理:快速重传和超时重传。先来看快速重传,指向这个功能的是tcp_fastretrans_alert函数:
--->快速重传和超时重传
超时重传:
156 static int tcp_write_timeout(struct sock *sk) 157 { 158 struct inet_connection_sock *icsk = inet_csk(sk); 159 int retry_until; 160 bool do_reset, syn_set = false; 161 162 if ((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV)) { 163 if (icsk->icsk_retransmits) 164 dst_negative_advice(sk); 165 retry_until = icsk->icsk_syn_retries ? : sysctl_tcp_syn_retries; 166 syn_set = true; 167 } else { 168 if (retransmits_timed_out(sk, sysctl_tcp_retries1, 0, 0)) { 169 /* Black hole detection */ 170 tcp_mtu_probing(icsk, sk); ...
确定是超时时,tcp_write_timeout函数会调用tcp_mtu_probing函数处理PMTU:
102 static void tcp_mtu_probing(struct inet_connection_sock *icsk, struct sock *sk) 103 { 104 /* Black hole detection */ 105 if (sysctl_tcp_mtu_probing) { 106 if (!icsk->icsk_mtup.enabled) { //如果未开启PMTU发现机制 107 icsk->icsk_mtup.enabled = 1; //开启之 108 tcp_sync_mss(sk, icsk->icsk_pmtu_cookie); //初始化PMTU和MSS 109 } else { 110 struct tcp_sock *tp = tcp_sk(sk); 111 int mss; 112 113 mss = tcp_mtu_to_mss(sk, icsk->icsk_mtup.search_low) >> 1; //缩小MSS 减小一半 114 mss = min(sysctl_tcp_base_mss, mss); 115 mss = max(mss, 68 - tp->tcp_header_len); 116 icsk->icsk_mtup.search_low = tcp_mss_to_mtu(sk, mss); 117 tcp_sync_mss(sk, icsk->icsk_pmtu_cookie); //保存缩小后的MSS 118 } 119 } 120 }
TCP在发送数据时会将小段数据合并到大的探测包再发送,TCP发送的包的IP头设置会不分片的DF位。如果包顺利抵达目的地,则用这个包的MTU作为PMTU;如果包过大被丢弃,若路由器会发送ICMP“需要分片,但设置了DF位”的ICMP报文,则使用ICMP报文中的MTU值作为PMTU;若路由器不发送IMCP,则在超时重传时TCP会减小PMTU。在得到PMTU后,TCP会将其保存在socket中,并更新MSS信息,接下来用新的MSS继续发送探测包和普通数据包。
路径MTU发现是用来确定到达目的地的路径中最大传输单元(MTU)的大小。通过在IP报头中设置不分片DF(Don't Fragment)标志来探测路径中的MTU值, 如果路径中设备的MTU值小于此报文长度,并且发现DF标志,就会发回一个Internet控制消息协议(ICMP)(类型3、代码4需要分片的消息ICMP_FRAG_NEEDED),消息中包含它可接受的MTU值。
PMTU发现控制模式
#define IP_PMTUDISC_DONT 0 /* Never send DF frames */
#define IP_PMTUDISC_WANT 1 /* Use per route hints */
#define IP_PMTUDISC_DO 2 /* Always DF */
#define IP_PMTUDISC_PROBE 3 /* Ignore dst pmtu */
#define IP_PMTUDISC_INTERFACE 4 /* 使用出接口的设备MTU值 */
#define IP_PMTUDISC_OMIT 5 /* 忽略DF位 */
IP_PMTUDISC_DONT策略表示从不设置DF位,即不进行PMTU发现(参见函数ip_dont_fragment)。
IP_PMTUDISC_WANT策略根据路由中表项是否锁定了MTU,来决定是否设置DF位,如锁定,不设置DF位。
IP_PMTUDISC_DO策略总是设置DF位,除非内核设置了忽略df(ignore_df),参见以下内容。
IP_PMTUDISC_INTERFACE策略不设置DF位,不发送设置了DF位并且长度超过出接口设备MTU的数据包。
IP_PMTUDISC_OMIT策略与IP_PMTUDISC_INTERFACE策略含义相同,唯一区别在于,即使数据包设置了DF位,内核也会对长度超过出接口设备MTU的数据包进行分片处理并发送。