限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。
在说明 Path MTU Discovery(PMTUD)
之前,先得说说 MTU(Maximum Transmission Unit)
。什么是 MTU(Maximum Transmission Unit)
?MTU
是网卡的最大传输单元,即网卡最多一次传输数据的字节数
,这是一个网卡硬件的参数。当数据从 IP 层
向下传递 数据链路层
时,如果发现 IP 数据包的长度
大于 网卡的 MTU
时,就需要将 IP 数据包 进程 分片
(在 IP 协议没有设置 DF 标志位时
),以适应网卡的 MTU
。需要知道的是,MTU
限定的仅指 IP 层 向下传递数据的最大长度
,这并不包含以太网帧头和帧尾长度
在内。
说完了 MTU
,接下来说说本篇的主角 Path MTU Discovery(PMTUD)
。数据在传输过程中,可能经过多个各种类型的网络数据的传输介质,如交换机、路由器等,下图给出一个简单的示例:
从上图中看到,数据的源头和目的设备的 MTU
均为 1500
,而中间的路由设备的 MTU
为 576
,也就是说,数据经过的各种传输媒介,它们各自可能拥有不同的 MTU
值,这就意味着数据帧经过不同 MTU
的设备时,要进行分片(从 更大 MTU
设备 到 更小 MTU
的设备)、组包(从更小 MTU
设备 到 更大 MTU
设备)。这样的不停分片、组包,需要开辟额外的缓存进行数据排队,通常来说对于网络传输效率是不利的(尤其是交换机这类设备),更不要说丢包等情形的处理。为了适应这种不同设备具有 MTU
的情形,引入了 Path MTU Discovery(PMTUD)
协议,协议 RFC 编号为 RFC1191 ,该协议用来发现网络数据传输整个路径中的最小 MTU
,然后数据传输路径中所有设备使用这个最小 MTU
来传输数据,因此所有的 IP 数据
都可以不用进行分片,以期达到更大的传输效率。这个 最小 MTU
有个名目,叫做 PMTU(Path MTU)
。
上面说了,Path MTU Discovery(PMTUD)
用来发现传输路径中的 最小 MTU
,那是如何发现的呢?过程也不复杂,就是在传输 IP 数据 的时候数据发送端
设置 DF(Don't Fragment)
标记,如下图:
然后数据接收端
如果发现接收的 IP 数据的长度超过自己的 MTU
,则回复发送端一个 Type=3,Code=4 的 ICMP 消息
,表示 Destination Unreachable Message, fragmentation needed and DF set
,告知发送端数据太长,需要进行分片,同时带上接收端的自己 MTU
;发送端接收到 ICMP 消息后,缓存接收端会送的 MTU
值,然后调整数据重新进行发送。更多关于 ICMP(Internet Control Message Protocol)
的细节可参考 RFC792 。
应该了解的是,Path MTU Discovery(PMTUD)
协议只适用于 TCP
和 UDP
协议。
首先,本文分析以 Linux 4.14
内核代码为背景进行分析。Linux 下默认开启 Path MTU Discovery(PMTUD)
功能。另外,可以通过文件节点 /proc/sys/net/ipv4/ip_no_pmtu_disc
来开启或关闭 Path MTU Discovery(PMTUD)
:向文件写 0 开启 PMTUD,写非零值(1-3)关闭 PMTUD
。
本文只讨论 IPv4
协议栈下 Path MTU Discovery(PMTUD)
开启的情形,对其它情形感兴趣的读者可自行阅读源码进行分析。
socket()
...
inet_create() // net/ipv4/af_inet.c
...
if (net->ipv4.sysctl_ip_no_pmtu_disc)
...
else /* 开启 PMTUD 的情形 */
inet->pmtudisc = IP_PMTUDISC_WANT;
...
当然,内核也提供了接口修改 socket 的 PMTUD 的配置。如:
on = IP_PMTUDISC_PROBE;
setsockopt(fd, IPPROTO_IP, IP_MTU_DISCOVER, &on, sizeof(on));
要发送的数据,当前可能有两种情形:
情形1:当前正从源头主机往外发送
情形2:当前数据正经过某中间设备(譬如路由器)往外转发
下面分别对这两种情形下,和 PMTUD
协议相关的处理部分。
// net/ipv4/ip_output.c
ip_queue_xmit()
...
packet_routed:
if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df) /* 不允许对 IP 数据分片 */
iph->frag_off = htons(IP_DF); /* 标记 DF */
else
...
...
res = ip_local_out(net, sk, skb); /* 将数据包传递给网络设备 */
...
接收端设备收到数据后,如果发现大于自己的 MTU
,且设置了 DF(Don't Fragment)
标记,则会送 Type=3,Code=4 的 ICMP 消息
:
// net/ipv4/ip_output.c
ip_finish_output()
...
/* 包长度大于本机 MTU, 进行分片处理 */
if (skb->len > mtu || (IPCB(skb)->flags & IPSKB_FRAG_PMTU))
return ip_fragment(net, sk, skb, mtu, ip_finish_output2);
struct iphdr *iph = ip_hdr(skb);
if ((iph->frag_off & htons(IP_DF)) == 0) /* 允许 IP 数据 分片 */
...
/* 不允许 IP 数据 分片(设置了 IP_DF 标记) */
if (unlikely(!skb->ignore_df ||
(IPCB(skb)->frag_max_size &&
IPCB(skb)->frag_max_size > mtu)/*IP 分片 的 长度大于 MTU*/)) {
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
/*
* IP 分片长度 超过 MTU && 禁止分片,
* 则给本地 socket 发送 ICMP 的 {ICMP_DEST_UNREACH,ICMP_FRAG_NEEDED} 包,
* 告知其包将不被发送 (IP 数据 由本地 socket 往外发送,发不出去就回送
* 给 socket 回 ICMP 的 {ICMP_DEST_UNREACH,ICMP_FRAG_NEEDED} 包 告知 socket).
*/
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
kfree_skb(skb);
return -EMSGSIZE;
}
...
发送端收到 Type=3,Code=4 的 ICMP 消息
后更新缓存 PMTU
:
// net/ipv4/icmp.c
static bool icmp_unreach(struct sk_buff *skb)
{
const struct iphdr *iph;
struct icmphdr *icmph;
...
icmph = icmp_hdr(skb);
iph = (const struct iphdr *)skb->data;
...
switch (icmph->type) {
case ICMP_DEST_UNREACH:
switch (icmph->code & 15) {
...
case ICMP_FRAG_NEEDED:
switch (net->ipv4.sysctl_ip_no_pmtu_disc) {
...
case 0:
info = ntohs(icmph->un.frag.mtu); /* 解析 接收端回传 的 MTU */
}
}
}
...
icmp_socket_deliver(skb, info);
...
ipprot = rcu_dereference(inet_protos[protocol]);
if (ipprot && ipprot->err_handler)
ipprot->err_handler(skb, info); /* tcp_v4_err() */
tcp_v4_err()
...
}
// net/ipv4/tcp_ipv4.c
void tcp_v4_err(struct sk_buff *icmp_skb, u32 info)
{
...
const int type = icmp_hdr(icmp_skb)->type;
const int code = icmp_hdr(icmp_skb)->code;
...
switch (type) {
...
case ICMP_DEST_UNREACH:
...
if (code == ICMP_FRAG_NEEDED) { /* PMTU discovery (RFC1191) */
...
tp->mtu_info = info;
if (!sock_owned_by_user(sk)) {
tcp_v4_mtu_reduced(sk);
} else {
...
}
goto out;
}
...
}
...
out:
...
}
void tcp_v4_mtu_reduced(struct sock *sk)
{
...
u32 mtu;
...
mtu = tcp_sk(sk)->mtu_info; /* 接收端 回送 的 MTU */
dst = inet_csk_update_pmtu(sk, mtu);
...
mtu = dst_mtu(dst);
if (inet->pmtudisc != IP_PMTUDISC_DONT &&
ip_sk_accept_pmtu(sk) &&
inet_csk(sk)->icsk_pmtu_cookie > mtu) {
tcp_sync_mss(sk, mtu); /* MSS 同步 */
/* Resend the TCP packet because it's
* clear that the old packet has been
* dropped. This is the new "fast" path mtu
* discovery.
*/
tcp_simple_retransmit(sk); /* 数据重传 */
}
}
// net/ipv4/ip_forward.c
int ip_forward(struct sk_buff *skb)
{
...
IPCB(skb)->flags |= IPSKB_FORWARDED;
mtu = ip_dst_mtu_maybe_forward(&rt->dst, true);
if (ip_exceeds_mtu(skb, mtu)) { /* 转发的 @skb 的 数据长度 超过 MTU */
IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
/*
* 当前 @skb 正经过 【交换机】 或 【路由器 上】 进行 转发, 当
* 【 @skb 的 数据长度 超过 MTU 】 && 【 数据源头设定不允许分片(DF=1) 】
* 时, 给数据发送源头回送 ICMP 包 {ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED}
* 数据将被丢弃.
*/
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
goto drop;
}
}
数据发送源收到 Type=3,Code=4 的 ICMP 消息
后的处理和 3.2.1
处理一样。
ifconfig
等工具可看到网卡配置的 MTU
:
$ ifconfig ens33
ens33 Link encap:Ethernet HWaddr 00:0c:29:4f:b1:e7
inet addr:192.168.0.9 Bcast:192.168.0.255 Mask:255.255.255.0
inet6 addr: fe80::bbc7:b835:be2a:a578/64 Scope:Link
UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1
RX packets:2077 errors:0 dropped:0 overruns:0 frame:0
TX packets:775 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:1000
RX bytes:1684142 (1.6 MB) TX bytes:74056 (74.0 KB)
用 ping
发送超过 MTU
的数据包,且禁止 IP 分片
:
$ ping www.baidu.com -s 2000 -M do
PING www.baidu.com (183.2.172.185) 2000(2028) bytes of data.
ping: local error: Message too long, mtu=1500
我们可以通过 tracepath
工具来跟踪数据发送超 MTU
时接收设备回送的 ICMP
包:
$ tracepath www.baidu.com
1?: [LOCALHOST] pmtu 1500
1: 192.168.0.1 43.888ms
1: 192.168.0.1 2.902ms
2: 192.168.1.1 37.109ms
3: 192.168.1.1 117.816ms pmtu 1492
3: 100.64.0.1 33.586ms
4: 61.146.242.189 33.665ms
5: 177.107.38.59.broad.fs.gd.dynamic.163data.com.cn 39.025ms
6: 113.96.5.38 54.439ms
7: no reply
8: 121.14.67.174 64.413ms
9: 182.61.216.71 39.233ms
用 tcpdump
工具抓回送的 ICMP
包:
$ sudo tcpdump icmp
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ens33, link-type EN10MB (Ethernet), capture size 262144 bytes
......
16:17:26.350958 IP 192.168.1.1 > 192.168.0.9: ICMP time exceeded in-transit, length 556
16:17:26.421870 IP 192.168.1.1 > 192.168.0.9: ICMP 183.2.172.185 unreachable - need to frag (mtu 1492), length 556
[1] https://packetlife.net/blog/2008/aug/18/path-mtu-discovery/#:~:text=RFC%201191%20defines%20path%20MTU%20discovery%2C%20a%20simple,of%20the%20ICMP%20Destination%20Unreachable%20message%2C%20Fragmentation%20Needed.
[2] https://www.rfc-editor.org/rfc/rfc1191
[3] https://datatracker.ietf.org/doc/html/rfc792