当前位置: 首页 > article >正文

Linux 网络:PMTUD 简介

文章目录

  • 1. 前言
  • 2. Path MTU Discovery(PMTUD) 协议
    • 2.1 PMTUD 发现最小 MTU 的过程
  • 3. Linux 的 PMTUD 简析
    • 3.1 创建 socket 时初始化 PMTUD 模式
    • 3.2 数据发送时 PMTUD 相关处理
      • 3.2.1 源头主机发送过程中 PMTU 处理
      • 3.2.2 转发过程中 PMTUD 处理
  • 4. PMTUD 观察
  • 5. 参考链接

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. Path MTU Discovery(PMTUD) 协议

在说明 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,而中间的路由设备的 MTU576 ,也就是说,数据经过的各种传输媒介,它们各自可能拥有不同的 MTU 值,这就意味着数据帧经过不同 MTU 的设备时,要进行分片(从 更大 MTU 设备 到 更小 MTU 的设备)、组包(从更小 MTU 设备 到 更大 MTU 设备)。这样的不停分片、组包,需要开辟额外的缓存进行数据排队,通常来说对于网络传输效率是不利的(尤其是交换机这类设备),更不要说丢包等情形的处理。为了适应这种不同设备具有 MTU 的情形,引入了 Path MTU Discovery(PMTUD) 协议,协议 RFC 编号为 RFC1191 ,该协议用来发现网络数据传输整个路径中的最小 MTU,然后数据传输路径中所有设备使用这个最小 MTU 来传输数据,因此所有的 IP 数据 都可以不用进行分片,以期达到更大的传输效率。这个 最小 MTU 有个名目,叫做 PMTU(Path MTU)

2.1 PMTUD 发现最小 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) 协议只适用于 TCPUDP 协议。

3. Linux 的 PMTUD 简析

首先,本文分析以 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) 开启的情形,对其它情形感兴趣的读者可自行阅读源码进行分析。

3.1 创建 socket 时初始化 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));

3.2 数据发送时 PMTUD 相关处理

要发送的数据,当前可能有两种情形:

情形1:当前正从源头主机往外发送
情形2:当前数据正经过某中间设备(譬如路由器)往外转发

下面分别对这两种情形下,和 PMTUD 协议相关的处理部分。

3.2.1 源头主机发送过程中 PMTU 处理

// 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); /* 数据重传 */
	}
}

3.2.2 转发过程中 PMTUD 处理

// 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 处理一样。

4. PMTUD 观察

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

再来用 WireShark 的观察一下抓到的数据包:
在这里插入图片描述

5. 参考链接

[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


http://www.kler.cn/a/133631.html

相关文章:

  • 云运维基础
  • 如何判定linux系统CPU的核心架构
  • 软件工程概论项目(二),node.js的配置,npm的使用与vue的安装
  • 【Vue】Vue3.0(十九)Vue 3.0 中一种组件间通信方式-自定义事件
  • C++,STL 054(24.11.13)
  • 【前端】Vue中如何避免出现内存泄漏
  • 麦克风阵列入门
  • Linux - 物理内存管理 - memmap
  • java游戏制作-拼图游戏
  • Qt控件样式表
  • shell之file命令
  • 指南:关于帮助中心需要注意的一些细节
  • java大作业中的文件操作
  • pnpm的安装及其使用
  • Nginx(七) root和alias的区别及详细测试
  • C# 实现腾讯云多路直播流的云端混合录制
  • Python学习笔记--Python关键字yield
  • 潇洒郎: 小白一次性成功——小米红米手机解BL锁+ ROOT-刷面具
  • CentOS挂载:解锁文件系统的力量
  • websocket学习笔记【springboot+websocket聊天室demo】
  • PostgreSQL 数据类型
  • 基于JavaWeb+SSM+社区居家养老服务平台—颐养者端微信小程序系统的设计和实现
  • 软件质量保护与测试(第2版)学习总结第十三章 集成测试
  • 切换阿里云ES方式及故障应急处理方案
  • vue之Error: Unknown option: .devServer.
  • Panda3d 动画序列