🔥 星光编译者 · 个人主页
📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》
🌄 向云端飞扬,编译属于自己的代码星河。
☕ 写在开篇
你好,这里是 星光编译者。
这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。
比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。
如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。
🔥 本文定位:在认识 IPv4 首部之后,沿着一份数据报走完"超过 MTU → 分片 → 独立传输 → 乱序到达 → 重组"的全过程。
💡 学习目标:算对每片长度、MF 与片偏移;判断哪些分片属于同一数据报;解释接收端如何确认收齐;用离线实验和 Linux 抓包读法验证理解。
📌 阅读范围:以普通 IPv4 单播为主。计算示例统一采用无选项的 20 字节 IPv4 首部;分片示例默认 DF=0,且没有 NAT、隧道和过滤干预。IPv6 的差异单独说明。
假设 IP 层准备发送一份总长 3000 字节的数据报,出口接口的 MTU 却只有 1500 字节。
"分成两份,每份 1500 字节"看起来很合理,但只要继续问一句------拆开以后,每一份自己的 IP 首部放在哪里?------这个答案就站不住了。
接收端面临的问题更多:先收到最后一片怎么办?两台主机恰好用了相同的 ID,能不能混在一起?已经收到三片,是否就说明收全?如果路径上还有一个更小的 MTU,已经分过的片还能再拆吗?
这些问题并不需要靠死记首部图来解决。把"长度""位置"和"完整性"分开,再用同一组数字算一遍,IP 分片就会变得很具体。
文章目录
- [为什么需要分片:先把 MTU 与层次说清楚](#为什么需要分片:先把 MTU 与层次说清楚)
- [ID、DF、MF 和片偏移,各自回答什么](#ID、DF、MF 和片偏移,各自回答什么)
- [完整算一次:3000 字节如何分成三片](#完整算一次:3000 字节如何分成三片)
- 每片独立传输,哪些首部会重复
- 接收端怎样组装:归组、定位与判断收齐
- 已经分片,还能再次分片吗
- 丢一片为什么影响整份数据,以及怎样减少分片
- 离线实验:模拟乱序、缺片与二次分片
- [Linux 抓包:把线上字段读回计算过程](#Linux 抓包:把线上字段读回计算过程)
- 常见误区与三道自测题
一、为什么需要分片:先把 MTU 与层次说清楚
1.1 MTU 限制的是当前链路承载的 IP 报文大小
MTU 是 Maximum Transmission Unit,最大传输单元。在本文的 IPv4 over Ethernet 场景中,接口 MTU=1500 表示这条链路通常允许承载的 IP 报文总长度最大为 1500 字节,其中已经包含 IP 首部。
不要把 1500 当成应用正文长度,也不要再从中扣掉以太网首部和 FCS。以太网的这些字段位于外层,不包含在这个 IP MTU 数字里。
以无选项的 IPv4 + UDP 为例:
text
1500 字节 IP 报文
= 20 字节 IPv4 首部
+ 8 字节 UDP 首部
+ 1472 字节应用数据
这个等式有两个前提:本例采用 MTU 1500,且 IPv4 首部没有选项。换成不同的 MTU、额外封装或不同首部长度,就必须重新计算。

1.2 链路给出限制,IPv4 层执行分片
分片的直接原因是当前出口无法一次容纳这份 IP 报文,但执行这里所说的 IPv4 分片的是 IP 层。它切分 IP 载荷,为每一片设置自己的 IP 首部,再交给下层封装发送。
因此,"因为链路 MTU 不够而分片"与"由数据链路层执行 IP 分片"不是同一回事。链路协议也可能有自己的分段机制,不能与 IPv4 的 MF、Fragment Offset 混为一谈。
分片既可能发生在源主机,也可能发生在转发途中。比如源主机的出口可以承载 3000 字节,而某台路由器的下一段链路只允许 1500 字节,那么在允许分片的条件下,这台路由器就可能拆分数据报。
1.3 大于 MTU,并不保证一定能分片发出去
判断要同时看大小和 DF:
| 当前条件 | 基础处理思路 |
|---|---|
| IP 总长不超过出口 MTU | 无需因为这一跳的 MTU 而分片 |
| IP 总长超过出口 MTU,DF=0 | 允许 IPv4 分片;实际发送还受系统与网络策略影响 |
| IP 总长超过出口 MTU,DF=1 | 禁止分片,不能直接把超长报文塞进该链路 |
源主机可能在发送时就返回"消息过长"一类错误;转发路由器遇到不能分片的超长报文,则会丢弃,并在符合 ICMP 差错发送条件时返回需要分片的通知。不要把本地错误和途中 ICMP 混成一个固定现象。
二、ID、DF、MF 和片偏移,各自回答什么
2.1 Identification:这是哪一份原始数据报的片段
IPv4 的 Identification 字段长 16 位,通常简称 ID。同一份原始数据报拆出来的各个分片,携带相同的 ID。
但接收端不能只按照 ID 归组。基础协议层面的重组标识是:
text
源 IP + 目的 IP + 上层协议号 Protocol + Identification
例如,来自不同源 IP 的两个 UDP 数据报,即使都使用 ID=4242,也不能拼成同一份数据。Protocol 则区分 TCP、UDP 等上层协议;它不是端口号。
ID 只有 16 位,也不能理解成全互联网永不重复的编号。有关唯一性的要求有相应范围和生存期条件;对不会被分片的原子数据报,更新后的规范也不要求把 ID 当作通用递增序列。排查时不要用 ID 的连续性判断应用是否丢消息。RFC 6864:IPv4 ID 的用途与约束
2.2 DF 和 MF:一个管"能不能拆",一个管"后面还有没有"
Flags 共 3 位,依次是保留位、DF、MF。保留位应为 0。
| 字段 | 值为 0 | 值为 1 |
|---|---|---|
| DF,Don't Fragment | 允许分片 | 禁止分片 |
| MF,More Fragments | 本片后没有更多原始载荷 | 本片后还有更多原始载荷 |
DF=0 只说明允许拆,报文很小时完全可以不拆。MF=0 也不等于"这个报文从未分片",因为一组分片的最后一片同样是 MF=0。
把 MF 与片偏移放在一起,才好判断:
| MF | Fragment Offset | 对有效报文的常见解释 |
|---|---|---|
| 0 | 0 | 未分片报文,或重组后恢复的形式 |
| 1 | 0 | 首片 |
| 1 | 大于 0 | 中间片 |
| 0 | 大于 0 | 末片 |
所以,检查"这是不是分片"的基础条件是:MF=1,或者 Fragment Offset 不为 0。

2.3 Fragment Offset:从原始 IP 载荷的哪里开始
片偏移字段长 13 位,单位是 8 字节 ,基准是原始 IP 数据报的数据区起点,不包含原始 IP 首部。
两个方向的公式要分清:
text
字节起点 = Fragment Offset 字段值 × 8
Fragment Offset 字段值 = 字节起点 ÷ 8
字段值 185,表示从原始 IP 载荷的第 1480 字节处开始,不是从第 185 字节开始,更不是再除以 8。
这里按从 0 开始的字节下标计数。为了避免"最后一个字节算不算在内"的歧义,下文使用左闭右开区间:[0, 1480) 表示包含下标 0~1479,共 1480 字节。
2.4 为什么非末片载荷长度要是 8 的倍数
下一片的起点必须能够用 8 字节单位表达。如果前一片携带 1479 字节,下一片应该从字节 1479 开始,但 1479 ÷ 8 不是整数,偏移字段就无法精确表示。
因此,合法分片中 MF=1 的片,其载荷长度必须按 8 字节对齐。末片没有后继起点要表达,载荷长度可以不是 8 的倍数。
这里有两个容易写反的细节:
- 要对齐的是分片载荷,不是"IP 首部 + 载荷"的总长度。
- 末片的起点仍然按 8 字节对齐,只是末片的长度不一定对齐。
三、完整算一次:3000 字节如何分成三片
3.1 先消除"3000 字节"本身的歧义
本节明确约定:
text
原始 IP 总长:3000 字节,包含 IP 首部
IPv4 首部:20 字节,无选项
原始 IP 载荷:3000 - 20 = 2980 字节
出口 MTU:1500 字节
DF:0
ID:4242
如果题目说的是"应用数据 3000 字节"或"IP 载荷 3000 字节",答案都会不同。因此算分片之前,第一句应该是:这个长度包含哪些首部?
3.2 算出每个非末片最多放多少载荷
扣掉本片自己的 IP 首部,最多还剩:
text
1500 - 20 = 1480 字节
1480 正好是 8 的倍数,所以可以直接作为非末片载荷长度。
一般地,在本文无选项、固定首部长度 H 的模型中:
text
非末片最大载荷 = floor((MTU - H) / 8) × 8
例如 MTU=1499 时,虽然扣掉首部剩 1479 字节,但非末片只能取 floor(1479 / 8) × 8 = 1472 字节。剩余空间不能靠破坏对齐来填满。
注意:若整份报文本来就能放进 MTU,不需要为了套这个公式强行切开。只有需要拆分时,才讨论非末片的对齐上限。
3.3 按原始载荷顺序切三段
2980 字节载荷切分为:
text
第一片 A:[0, 1480) → 1480 字节
第二片 B:[1480, 2960) → 1480 字节
第三片 C:[2960, 2980) → 20 字节
再分别补上 20 字节 IP 首部:
| 分片 | 原始载荷区间 | 本片载荷 | 本片 IP 总长 | Offset 字段值 | MF | ID |
|---|---|---|---|---|---|---|
| A | [0, 1480) | 1480 | 1500 | 0 | 1 | 4242 |
| B | [1480, 2960) | 1480 | 1500 | 185 | 1 | 4242 |
| C | [2960, 2980) | 20 | 40 | 370 | 0 | 4242 |

B 的偏移为 1480 / 8 = 185;C 的偏移为 (1480 + 1480) / 8 = 370。片偏移始终从最初那份 IP 载荷的起点算,不是相对前一片计算。
3.4 用三个检查证明结果自洽
第一,载荷守恒:
text
1480 + 1480 + 20 = 2980
第二,每片都能通过当前链路:1500、1500、40 均不超过 MTU 1500。
第三,区间连续:A 的结束位置等于 B 的开始位置,B 的结束位置等于 C 的开始位置,没有空洞,也没有重复覆盖。
所有分片的 IP 总长之和则是:
text
1500 + 1500 + 40 = 3040 字节
比原始 3000 字节多出的 40 字节,来自新增的两份 IP 首部。这不是多发了应用数据。这里也没有计入每个以太网帧的额外开销。
3.5 如果"3000"指 IP 载荷呢
此时原始 IP 总长变为 3020 字节,载荷切分为 1480 + 1480 + 40,三个分片的 IP 总长为 1500、1500、60,Offset 仍是 0、185、370。
如果"3000"指 UDP 应用数据,那么还要加上 8 字节 UDP 首部:IP 载荷为 3008 字节,最后一片载荷变为 48 字节,总长为 68 字节。
先确定层次,再扣首部,再做对齐,最后填字段。 这个顺序比记住某组数字更可靠。
四、每片独立传输,哪些首部会重复
4.1 每片都有 IP 首部,不代表每片都有 UDP 首部
把原始数据报写成:
text
IPv4 首部 | UDP 首部 | 应用数据
从 IP 的角度看,UDP 首部也是载荷的一部分。IP 分片切的是 UDP 首部 + 应用数据 这一整段字节,不会给每一片重新构造一个完整 UDP 数据报。
沿用刚才的 2980 字节 IP 载荷,假设上层协议为 UDP:
text
原始 IP 载荷 = 8 字节 UDP 首部 + 2972 字节应用数据
A:20 字节 IP 首部 + 8 字节 UDP 首部 + 1472 字节应用数据
B:20 字节 IP 首部 + 1480 字节后续应用数据
C:20 字节 IP 首部 + 20 字节后续应用数据

本例首片空间充足,完整包含 UDP 首部。对于一般传输层协议,不应把"任意首片都一定包含完整传输层首部"当作无条件规律;分片大小与首部长度也要结合来看。
4.2 相同与变化的字段,要分别看
在本文的普通路由、无选项示例中:
| 项目 | 分片后怎样理解 |
|---|---|
| 源 IP、目的 IP、Protocol、ID | 同组分片保持对应关系,用于归组与上层分用 |
| Total Length | 描述本片总长,不再填写原始的 3000 |
| MF、Fragment Offset | 随片的位置而变化 |
| Header Checksum | 按各片自己的首部重新计算;只校验 IP 首部 |
| TTL | 随转发处理递减;不同路径可能导致到达时不同 |
| 链路层封装 | 每片分别封装,跨链路转发时按下一跳重新构造 |
如果存在 IPv4 选项,哪些选项复制到后续片还有专门规则,首部长度也可能变化。因此本文固定 20 字节首部的算式不能不加修改地套到所有带选项报文上。RFC 791:分片时的字段与选项处理
4.3 独立传输,不等于一定走不同路线
每片作为独立 IP 报文被转发,所以接收端不能依赖它们按原顺序到达。但"独立"不等于每片必然选择不同路径:在稳定的路由条件下,它们也可能沿同一路径到达。
更关键的是普通转发过程的职责:路由器不需要先收齐原始数据报的所有片,再进行下一跳转发。最终 IP 接收端负责重组。连接跟踪、隧道端点或其他中间处理机制可能引入重组行为,那属于额外场景,不能反过来当成普通路由的必经步骤。
五、接收端怎样组装:归组、定位与判断收齐
5.1 第一步:找到同一份数据报的重组上下文
接收端首先检查报文的基本合法性,再用源地址、目的地址、Protocol、ID 等信息,找到对应的重组上下文。实际内核还可能把网络命名空间等本地上下文纳入管理。
可以把这个上下文理解成一张"收件登记表":它保存已经收到哪些字节区间、是否知道最终长度、还缺哪些部分,以及等待期限。
这不是应用层的 recv 缓冲区。应用一般不会收到 A、B、C 三份独立的 UDP 消息,再自己把它们拼起来;重组发生在交给普通传输层处理之前。
5.2 第二步:根据 Offset 放到原来的位置
对每一个有效片,先计算:
text
本片首部长度 H = IHL × 4
本片载荷长度 L = Total Length - H
本片开始位置 start = Fragment Offset × 8
本片结束位置 end = start + L
因此 C 虽然只有 20 字节载荷,却应该放在 [2960, 2980),而不是放在已经收到的数据后面。
"按 Offset 排序再拼接"有助于入门,但实现并不一定真的先维护一个排好序的数组。它可以使用缓冲区、区间结构或空洞记录;关键是最终字节的位置与完整性正确。
5.3 第三步:末片告诉你终点,连续覆盖告诉你收齐
若收到 MF=0 的末片,就能推导原始 IP 载荷的总长度:
text
原始载荷总长 = 末片 Offset × 8 + 末片载荷长度
= 370 × 8 + 20
= 2980 字节
但是,知道终点在哪里,并不代表起点到终点之间已经全部填满。
假设到达顺序是 C → A → B:
| 到达时刻 | 已收到的区间 | 已知的缺口 | 是否可以交付 |
|---|---|---|---|
| 只收到 C | [2960, 2980) | [0, 2960) | 不可以,虽然已经知道总长 |
| 再收到 A | [0, 1480)、[2960, 2980) | [1480, 2960) | 不可以,中间仍缺一整片 |
| 最后收到 B | [0, 2980) 连续覆盖 | 无 | 通过其余校验后可以重组交付 |

对本例的合法、无重叠分片,可以把完成条件概括为:已知最终载荷长度,且从 0 到这个长度的字节区间完整连续。
IP 首部没有一个字段直接写着"总共三片"。所以不能说"收到了三片就完成",也不能因为三个片的长度之和恰好相等就直接交付。重复片会让简单累加得到错误结论。
5.4 缺片、重复片和重叠片怎么处理
缺片会留下空洞。系统不会永远等下去:重组有超时和资源限制,最终可能丢弃无法完成的上下文。具体超时时间属于实现与配置,不能写成所有系统统一的固定秒数。
重复片描述已经覆盖过的范围,不会让有效数据凭空增加。重叠片更麻烦:不同分片可能声称自己覆盖同一段字节,甚至给出不同内容。对这些异常,具体系统的检查和丢弃策略可能不同,不能用"后到的数据覆盖先到的数据"概括所有实现。
学习时可以采用严格的无重叠模型,先把正常分片和空洞判断算明白。RFC 815 讨论的空洞思想,就提供了从"还缺哪里"理解重组的角度;它不是要求所有操作系统使用同一份数据结构。RFC 815:IP 重组中的空洞管理思路
六、已经分片,还能再次分片吗
6.1 下一跳 MTU 更小,原来的片也可能放不下
假设第二片 B 的总长为 1500,继续前进时遇到 MTU=620 的出口。DF=0,首部仍为 20 字节,则非末片的最大载荷为:
text
floor((620 - 20) / 8) × 8 = 600 字节
B 的 1480 字节载荷于是继续拆成 600 + 600 + 280。
6.2 二次分片的偏移不能重新从 0 开始
B 在原始载荷中的起点本来就是 1480。三个新片的绝对起点因此分别是:
text
B1:1480 → Offset = 185
B2:1480 + 600 → Offset = 260
B3:1480 + 1200 → Offset = 335
这里可以直接使用字段单位计算:
text
新 Offset = 旧 Offset + 在旧片载荷内的字节偏移 / 8

6.3 B3 是这次拆分的最后一片,为什么 MF 仍是 1
因为 MF 问的是:本片之后,原始数据报是否还有载荷?
B 原本就不是整份数据报的末片,后面还有 C 的 20 字节。于是 B1、B2、B3 都应该是 MF=1。不能因为 B3 是"B 拆出来的最后一个",就把它设为 MF=0。
| 新片 | 原始载荷区间 | 载荷 | IP 总长 | Offset | MF |
|---|---|---|---|---|---|
| B1 | [1480, 2080) | 600 | 620 | 185 | 1 |
| B2 | [2080, 2680) | 600 | 620 | 260 | 1 |
| B3 | [2680, 2960) | 280 | 300 | 335 | 1 |
概括二次分片的 MF 规则:前面的新片置 1,最后一个新片继承旧片的 MF。只有原始数据报的真正末尾,才可能得到 MF=0。
这里为了聚焦偏移,只展开了 B。若 A 同样经过这个 MTU=620 的出口,它也需要进一步拆分;C 总长 40,可以直接通过。目的端仍使用原始载荷的统一坐标重组,无需先恢复 B,再恢复整份数据。
七、丢一片为什么影响整份数据,以及怎样减少分片
7.1 IP 不负责替缺失的片安排重传
如果 B 丢失,接收端即使收到了 A 和 C,也不能把不完整的原始 IP 载荷作为正常数据交给上层。等待超时后,这次重组可能失败。
IP 本身没有"请把 ID=4242 的第二片再发一次"的可靠重传协议。若上层是 TCP,可靠性由 TCP 根据确认、丢失检测等机制恢复相应字节;UDP 本身不自动重传,应用是否重试由自己的协议决定。
因此,"IP 分片对传输层透明"主要描述接口与处理层次:普通传输层收到的是重组后的内容。它不意味着分片丢失对延迟、吞吐量和应用成功率没有影响。
7.2 分片越多,成功交付需要同时满足的条件越多
做一个只用于直觉的概率模型:假设各片独立丢失,且每片丢失概率同为 p,那么 n 片全部到达的概率为:
text
P(全部到达) = (1 - p)^n
若 p=1%,单片成功率为 99%,三片全部成功约为 97.03%,十片全部成功约为 90.44%。真实网络的丢包可能相关,且包长也会影响行为,不能拿这个简化模型直接预测生产网络。
除此以外,分片还增加首部开销与重组状态。更小的报文也可能带来更多包处理开销,所以实际优化应结合业务和路径测量;这里只说明为什么不宜把大量 IP 分片当作默认设计目标。
7.3 PMTU 与 TCP MSS 分别做什么
接口 MTU 描述某一条链路,Path MTU 则关注整条路径能承载的大小。经典 IPv4 PMTU 发现利用 DF 和 ICMP "需要分片"反馈,帮助发送端调整报文大小。如果必要的反馈收不到,可能出现小包可达、大包失败等现象,但这种现象本身不能直接证明根因。RFC 1191:Path MTU Discovery
TCP 则会在传输层切分字节流。MSS 描述 TCP 数据部分的大小上限,不包含 TCP 与 IP 首部;实际发送段还受路径、选项、窗口等约束影响。
在无 IP/TCP 选项的 MTU 1500 示例里,经常看到:
text
IP 载荷上限:1500 - 20 = 1480
TCP 数据上限:1500 - 20 - 20 = 1460
所以不能把 1480 直接称为"TCP 应用数据最大长度"。TCP 分段与 IP 分片也不能混为一谈:前者形成各自带 TCP 首部的段,后者拆分一份 IP 载荷。

7.4 IPv6 不照搬 IPv4 路由器分片
IPv6 中,转发路由器不执行这种途中分片;需要分片时由源节点处理,并使用 Fragment 扩展首部。遇到不能转发的超大包,路由器通过 ICMPv6 Packet Too Big 等机制反馈。
这也是为什么本文反复限定"IPv4":IPv4 的 16 位 ID、DF、MF 首部布局和路由器分片方式,不能直接套到 IPv6 上。RFC 8200:IPv6 分片与重组
八、离线实验:模拟乱序、缺片与二次分片
8.1 先在本地把数字算明白
文章包的 code/ipv4_fragment_lab.py 使用 Python 标准库,不发送网络报文,也不修改系统配置。从文章目录运行:
bash
python3 code/ipv4_fragment_lab.py calculate --total 3000 --mtu 1500
python3 code/ipv4_fragment_lab.py demo
python3 code/ipv4_fragment_lab.py selftest
Windows 环境可根据安装方式将 python3 换成 python。这是离线教学模型,限定无选项 IPv4 首部、单一数据报的重组上下文;它不模拟 Linux 的真实队列、超时、路由或 PMTU 策略。
calculate 的预期关键输出为:
text
part start end payload total offset MF
1 0 1480 1480 1500 0 1
2 1480 2960 1480 1500 185 1
3 2960 2980 20 40 370 0
8.2 真正值得观察的是"缺口"
demo 把载荷构造成可比对的字节序列,按 C → A → B 送入重组器,展示末片先到时的缺口变化。只有第三次输入后,返回的数据才应与原始 2980 字节完全一致。
再移除 B,只输入 A 与 C,结果应保持不完整。最后把所有初始分片继续经过 MTU=620 的拆分器,会得到以下 Offset:
text
0、75、150、185、260、335、370
前六片 MF=1,最后一片 MF=0;它们仍应可以乱序重组回相同载荷。这比只比较总字节数更有说服力,因为错误的片序也可能保留相同长度。
8.3 核心计算其实很短
下面是正常分片场景中的核心思路,完整参数校验与重组演示见配套脚本:
python
header_len = 20
capacity = ((mtu - header_len) // 8) * 8
start = 0
while start < len(payload):
# 末片可直接容纳剩余字节,不要求长度是 8 的倍数。
remaining = len(payload) - start
size = remaining if remaining <= mtu - header_len else capacity
end = start + size
offset_field = start // 8
mf = int(end < len(payload))
fragment_payload = payload[start:end]
start = end
这一小段没有构造真实 IP 首部,也没有发送数据。它只把"从哪里切、切多少、字段填什么"的关系展开。实际二次分片还必须加上旧片的起点,并让最后一个新片继承旧 MF。
配套重组器采用保守策略:拒绝重叠片及矛盾的末片信息,避免把教学代码误当作跨平台的真实内核行为。它也没有定时器,因此缺片示例只展示"不完整",不会假装演示系统超时。
九、Linux 抓包:把线上字段读回计算过程
9.1 先确定接口与目标,再制造少量测试流量
下面使用 Linux 的 iputils ping 和 tcpdump。将接口名及 192.0.2.2 替换成你自己的测试环境;这个地址只是文档示例,不是可直接探测的公共服务。
bash
# 查看接口概况,并查询去往测试目标的出口
ip -brief link
ip -4 route get 192.0.2.2
# 将 eth0 换成上面查询到的出口;完整输出中可看到 mtu 字段
ip link show dev eth0
# 终端 A:观察指定目标的 IPv4 流量
sudo tcpdump -i eth0 -nn -vv -s 0 -c 30 'ip and host 192.0.2.2'
# 终端 B:发送两次允许 IPv4 分片的 ICMP Echo 请求
ping -4 -c 2 -W 2 -M dont -s 2972 192.0.2.2
为什么是 -s 2972?ping 的这个参数是 ICMP 数据长度。在无 IP 选项的 Echo 请求中:
text
20 字节 IPv4 首部 + 8 字节 ICMP 首部 + 2972 字节数据 = 3000 字节
-M dont 表示不设置 DF。如果实际出口 MTU 为 1500,并且路径与策略符合本文条件,便可观察与计算例子对应的分片。不要使用环回接口期待相同结果:环回 MTU 通常更大,不一定会触发这个例子中的切分。ping(8):-M 和 -s 参数
抓包达到 30 包才会自动退出;测试结束仍不足这个数量时,按 Ctrl+C 停止。请求和响应可能都有分片,比较时按源/目的方向区分它们各自的分片组。
这里给出的是可复现步骤与预期字段,不是某台 Linux 主机的实测截图。实际 MTU、工具版本、过滤规则与抓包位置都可能影响观察。
9.2 不要让端口过滤漏掉后续片
对 UDP 分片使用 udp port 9000 过滤时,非首片没有位于片首的 UDP 首部,端口条件可能把它们排除。于是你以为"只发了第一片",实际上只是过滤条件看不到后续片。
观察一组分片时,可以先用 IP 地址缩小范围,再使用 IPv4 字段条件:
bash
# MF=1 或片偏移非零:覆盖首片、中间片与末片
sudo tcpdump -i eth0 -nn -vv -s 0 \
'ip and host 192.0.2.2 and (ip[6:2] & 0x3fff != 0)'
# 只看片偏移非零的后续片;这条有意不包含首片
sudo tcpdump -i eth0 -nn -vv -s 0 \
'ip and host 192.0.2.2 and (ip[6:2] & 0x1fff != 0)'
ip[6:2] 读取 IP 首部中 Flags 与 Fragment Offset 组成的 16 位字段。掩码 0x1fff 取偏移部分;0x3fff 额外保留 MF,排除 DF 和保留位。因此未分片但 DF=1 的普通报文不会被第一个表达式误选成分片。pcap-filter(7):字段访问与分片过滤

9.3 抓包工具显示的 offset,可能已经换成字节
本文表格里的 Offset=185、370,是报文首部字段的原始单位。tcpdump 的常见文本输出会显示换算后的字节偏移,例如 1480、2960。
因此看到下面这组信息时,要先确认显示单位:
text
A:id 4242,offset 0, MF=1,IP length 1500
B:id 4242,offset 1480,MF=1,IP length 1500
C:id 4242,offset 2960,MF=0,IP length 40
这是一份按字节解释的字段示意 ,不是逐字复制的终端输出。实际工具可能用 flags [+] 表达 MF,也可能在详细字段面板里同时给出原始值与换算值。不要把工具已经换算过的 1480 再乘以 8。
另一个常见混淆是 length:IP 总长、UDP 长度、应用载荷长度和捕获帧长度并非同一层次。看到数字时,先看它对应的协议和显示位置。
9.4 改成 DF=1,观察另一种处理路径
可以再做两次很小规模的对照:
bash
ping -4 -c 2 -W 2 -M do -s 2972 192.0.2.2
-M do 设置 DF,并受内核 PMTU 检查影响。如果本地已知道尺寸超限,可能直接报本地错误,根本不会发出这个大包;也可能在途中触发相应 ICMP 差错。没有看到分片,不等于 tcpdump 一定坏了。
观察结果还应结合抓包位置。发送端看到的"大包"可能受 GSO/TSO 等卸载影响;接收侧合并也可能改变可见形态。优先结合接收端或链路上的抓包,不要把本机捕获到的一条记录直接等同于线上一个以太网帧。
十、常见误区与三道自测题
10.1 这些结论,补上条件才准确
| 常见说法 | 更准确的理解 |
|---|---|
| IP 报文超过 MTU 就一定分片 | 还要看 DF、发送策略与实际处理条件 |
| 分片由数据链路层完成 | 本文讨论的 IPv4 分片由 IP 层执行,链路 MTU 提供约束 |
| ID 相同就属于同一个数据报 | 还需结合源 IP、目的 IP、Protocol 等上下文 |
| Offset=185 表示偏移 185 字节 | 原始字段单位为 8 字节,对应 1480 字节 |
| 偏移从原始 IP 首部开始算 | 从原始 IP 载荷的起点开始算 |
| 每个分片总长都必须是 8 的倍数 | MF=1 分片的载荷长度按 8 字节对齐 |
| MF=0 表示没有分片 | 也可能是偏移非零的末片 |
| 末片到了就可以交给 UDP | 还要确保整个载荷区间完整覆盖 |
| IP 首部中保存了分片总数量 | 没有这个总片数字段,收齐要根据区间判断 |
| 每一片都会复制 TCP/UDP 首部 | 传输层首部是原始 IP 载荷的一部分,不逐片重建 |
| 二次分片从 0 重新编号 | 继续使用原始载荷坐标,末个子片继承旧 MF |
| IP 会负责重传丢失的第二片 | IP 不提供这种可靠重传机制 |
| IPv6 路由器与 IPv4 一样拆包 | IPv6 转发路由器不执行这种途中分片 |
10.2 自测一:MTU 变成 1499,怎么算
仍然是总长 3000、IP 首部 20、DF=0。
非末片最大载荷为 1472,因而载荷切分为 1472、1472、36;分片总长为 1492、1492、56;Offset 为 0、184、368;MF 为 1、1、0。
前两片没有把 1499 字节全部用满,是因为要满足 8 字节对齐。末片 36 字节不是 8 的倍数,不影响它作为末片存在。
10.3 自测二:只有 MF=0、Offset=370,够不够判断收齐
不够。首先还要知道这片实际有多少载荷,才能推导总长度;其次即使已经知道总长度,也需要核对前面的区间是否连续覆盖。
如果该片 IP 总长为 40、IHL=5,那么载荷为 20,总长度可算为 2980。但它完全可能是最先到达的片,此时 [0, 2960) 仍然全都缺失。
10.4 自测三:B3 的 MF 错设成 0 会怎样
B3 的 Offset=335,载荷 280。如果把 MF 错写成 0,就声称原始载荷在 335 × 8 + 280 = 2960 处结束。
真正末片 C 表明终点是 2980。如果 C 已经进入同一个重组上下文,再收到这个错误的 B3,就会发现两个终点矛盾;配套教学模型会拒绝这种输入。
但到达顺序很重要:如果错误的 B3 先到,且 [0, 2960) 已经连续覆盖,IP 层仅凭当前分片信息无法预知后面还应有 C,可能把这个较短的结果视为重组完成。配套模型也会返回这 2960 字节。因此不能声称"MF 写错一定会被 IP 重组器提前发现"。
后续还需要上层检查。沿用本文的 UDP 示例,若 UDP 首部保持不变,其中的 Length 仍声明 2980 字节,与这个较短结果不符;UDP 的长度检查可以发现这种截断。UDP Length 包括 UDP 首部和数据,这与 IP 分片首部的 Total Length 是两个不同字段。RFC 768:UDP Length 字段
🌟 写在最后
IP 分片最值得掌握的,不是"一个大包拆成几个小包"这句话,而是三个可以动手验证的问题:每片携带哪些字节,怎样记录这些字节的位置,接收端如何证明所有位置都已经填满。
把 3000 字节例子再走一遍:原始载荷 2980,按 MTU 与对齐要求切成 1480、1480、20;每片补上自己的 IP 首部;用相同的归组信息和不同的 Offset、MF 传输;接收端按原始坐标放回,等到没有空洞后再向上交付。
下次在抓包中看到 MF、offset 和多个相同的 ID,可以先画出它们覆盖的字节区间。只要坐标清楚,乱序、缺片、末片先到甚至二次分片,就都有了可以核对的解释。
如果这篇文章帮你理清了某个细节,欢迎点赞、收藏,也欢迎交流你在 Linux 网络编程中遇到的实际现象。我们下一篇见。