如果两台主机之间的通信要通过多个网络,那么每个网络的链路层就可能有不同的MTU。两台通信主机路径中的最小MTU。它被称作路径MTU(PMTU)。两台主机之间的路径MTU不一定是个常数,它取决于当时所选择的路由。而选路不一定是对称的(从A到B的路由可能与从B到A的路由不同),因此路径MTU在两个方向上不一定是一致的。
本文研究路径MTU发现主要是要弄明白以下几个问题:
1、路径MTU发现有什么用处?
2、TCP什么时候执行路径MTU发现?
3、TCP路径MTU发现的原理是什么?
4、TCP路径MTU探测的结果是如何维护的?
5、TCP如何使用路径MTU探测的结果?
下面回答问题1:TCP报文需要封装成IP报文才会发送,报文在网络中按照一定路径传输后会抵达目的地。最理想的情况是IP报文的大小正好是这条路径所能容纳的最大尺寸,因为报文小了则数据传输效率不高,大了则会引起分片。分片会使得路由器的负担加重,增加延迟,而且会增加报文丢失的概率。而IP报文的传输路径是事先不知道的,而且在传输过程中也可能发送变化,所以TCP需要动态测路径MTU的大小,这就是TCP的路径MTU发现。
接下来我们来寻找问题2的答案:PMTU探测包的发送是在tcp_write_xmit函数中进行:
1811 static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
1812 int push_one, gfp_t gfp)
1813 {
1814 struct tcp_sock *tp = tcp_sk(sk);
1815 struct sk_buff *skb;
1816 unsigned int tso_segs, sent_pkts;
1817 int cwnd_quota;
1818 int result;
1819
1820 sent_pkts = 0;
1821
1822 if (!push_one) {
1823 /* Do MTU probing. */
1824 result = tcp_mtu_probe(sk);
1825 if (!result) {
1826 return false;
...
可见只有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。下面一一列举开启PMTU探测的条件:
(1)TSQ tasklet发送数据时:
684 static void tcp_tsq_handler(struct sock *sk)
685 {
686 if ((1 << sk->sk_state) &
687 (TCPF_ESTABLISHED | TCPF_FIN_WAIT1 | TCPF_CLOSING |
688 TCPF_CLOSE_WAIT | TCPF_LAST_ACK))
689 tcp_write_xmit(sk, tcp_current_mss(sk), 0, 0, GFP_ATOMIC);
690 }
(2)通过发包系统调用或使用TCP Splice功能调用do_tcp_sendpages发送数据时:
1016 int tcp_sendmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
1017 size_t size)
1018 {
...
1204 if (forced_push(tp)) {
1205 tcp_mark_push(tp, skb);
1206 __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
1207 } else if (skb == tcp_send_head(sk))
1208 tcp_push_one(sk, mss_now);
1209 continue;
1210
1211 wait_for_sndbuf:
1212 set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
1213 wait_for_memory:
1214 if (copied)
1215 tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
1216
1217 if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
1218 goto do_error;
1219
1220 mss_now = tcp_send_mss(sk, &size_goal, flags);
1221 }
1222 }
1223
1224 out:
1225 if (copied)
1226 tcp_push(sk, flags, mss_now, tp->nonagle);
1227 release_sock(sk);
1228 return copied + copied_syn;
TCP Splice:
827 static ssize_t do_tcp_sendpages(struct sock *sk, struct page *page, int offset,
828 size_t size, int flags)
829 {
...
914 if (forced_push(tp)) {
915 tcp_mark_push(tp, skb);
916 __tcp_push_pending_frames(sk, mss_now, TCP_NAGLE_PUSH);
917 } else if (skb == tcp_send_head(sk))
918 tcp_push_one(sk, mss_now);
919 continue;
920
921 wait_for_sndbuf:
922 set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
923 wait_for_memory:
924 tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
925
926 if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
927 goto do_error;
928
929 mss_now = tcp_send_mss(sk, &size_goal, flags);
930 }
931
932 out:
933 if (copied && !(flags & MSG_SENDPAGE_NOTLAST))
934 tcp_push(sk, flags, mss_now, tp->nonagle);
935 return copied;
...
这里只有一种情况是不开启PMTU探测的:当前已写出的字节数不大于对端通告的最大窗口的一半且发送队列中只有一个skb。其它情况下发送skb都会开启PMTU探测功能。
(3)发现数据丢失并且使用了Forward RTO-Recovery (F-RTO)算法时:
2685 static void tcp_process_loss(struct sock *sk, int flag, bool is_dupack)
2686 {
2687 struct inet_connection_sock *icsk = inet_csk(sk);
2688 struct tcp_sock *tp = tcp_sk(sk);
2689 bool recovered = !before(tp->snd_una, tp->high_seq);
2690
2691 if (tp->frto) { /* F-RTO RFC5682 sec 3.1 (sack enhanced version). */
2692 if (flag & FLAG_ORIG_SACK_ACKED) {
2693 /* Step 3.b. A timeout is spurious if not all data are
2694 * lost, i.e., never-retransmitted data are (s)acked.
2695 */
2696 tcp_try_undo_loss(sk, true);
2697 return;
2698 }
2699 if (after(tp->snd_nxt, tp->high_seq) &&
2700 (flag & FLAG_DATA_SACKED || is_dupack)) {
2701 tp->frto = 0; /* Loss was real: 2nd part of step 3.a */
2702 } else if (flag & FLAG_SND