
🔥小叶-duck:个人主页
❄️个人专栏:《Data-Structure-Learning》《C++入门到进阶&自我学习过程记录》
《Linux系统从入门到实践》《Linux网络从入门到实践》
《Qt 方寸极境》 《MySQL》
✨未择之路,不须回头
已择之路,纵是荆棘遍野,亦作花海遨游
目录
1.1 公网的层级结构:全球骨干网络 + 国内骨干网络拓扑规划
1.1.1 国际骨干网划分(/8,掩码 255.0.0.0)
1.1.2 国内骨干网(省级)划分(/16,掩码 255.255.0.0)
1.1.3 市级骨干网划分(/20,掩码 255.255.240.0)
1.2.1 问题一:国外俄罗斯要访问国内目标地址 5.1.16.X
1.2.3 问题二:国内河北的报文,发往国内目标地址 5.1.16.X
3.5.4 13 位片偏移(Fragment Offset)
前言
在上一篇文章中,我们从 IP 地址的本质出发,梳理了网络号与主机号、分类编址与 CIDR 无类域间路由、以及私有 IP / 公网 IP 的边界与 NAT 技术。
本篇我们继续深入网络层。首先从宏观视角看公网的真实层级结构:全球骨干网如何按国家划分、国内骨干网又如何按省、市逐级下沉。接着通过两个典型案例 —— 国外主机访问国内地址、国内跨省转发 —— 完整走一遍报文跨网传输流程,并讲清楚默认路由、默认网关的本质。在此基础上,系统讲解路由表的结构、最长前缀匹配、缺省路由与路由表生成算法。
文章后半部分是重点加餐:IP 报文的分片与组装。我们从 MTU 与分片的由来讲起,解释为什么分片不该是主流、TCP 如何通过 MSS 从源头规避分片、UDP 又为何只能把大数据交给 IP 层分片;随后完整拆解分片与组装的机制 —— 标识、标志位、片偏移三个字段如何协作,接收端如何判断报文是否分片、如何判断分片收齐、如何用连续性公式发现丢包;最后结合 Linux 内核
ip_fragment()与ip_defrag()源码,把分片发送与分片重组的实现细节落到代码层面。
一、尝试理解公网

1.1 公网的层级结构:全球骨干网络 + 国内骨干网络拓扑规划
真实互联网并不是扁平化的网络,而是一套多层级的树形拓扑结构。真实网络受 ICANN、五大区域 IP 分配机构(APNIC/RIPE 等)以及全球各国 ISP 多方管控。由于全球公网的拓结构十分复杂我们无法在真实情况下讲解,所以接下来我们讲解的网段划分属于最简练的划分了,仅用于我们对公网的理解和学习,现实环境中的 IP 地址并不会按照国家、省级行政区、地级市这样固化分配。我们可以简化为以下四层:
- 国际骨干网:由各国的国际路由器组成,通过海底光缆连接。每个国家向 ICANN 申请 IP 地址段,例如中国拥有
5.0.0.0/8这样的大段地址。 - 国内骨干网:国家将 IP 地址进一步划分给各个省份,例如陕西分配到
5.1.0.0/16,河北分配到5.5.0.0/16。 - 市级骨干网:省份再将地址划分给各个城市,例如西安分配到
5.1.16.0/20。 - 局域网:城市运营商不再使用公网 IP,而是为用户分配私有 IP,构建家庭或企业局域网。
1.1.1 国际骨干网划分(/8,掩码 255.0.0.0)
我们假定:IP 地址前 8bit 作为国家网络号。2^8=256个网段,数量足够覆盖全球 200 余个主权国家。
示例:俄罗斯 1.0.0.0/8、美国 2.0.0.0/8、英国 3.0.0.0/8、法国 4.0.0.0/8、中国 5.0.0.0/8、意大利 6.0.0.0/8。
每个国家部署国家级边界路由器,本网段的网关为本网段第一个 IP,例如俄罗斯网关1.0.0.1、中国网关5.0.0.1。各国运营商将本国国际路由器物理互联,依托海底光纤搭建国际公网骨干链路。
跨国路由使用 BGP 协议,向全网通告自身网段、掩码、互联 IP;其他网络设备接收路由信息,写入本地路由表,后续跨国报文依靠路由表完成查表选路。

1.1.2 国内骨干网(省级)划分(/16,掩码 255.255.0.0)
我国一共 34 个省级行政区:23 省、5 自治区、4 直辖市、2 特别行政区。
我们假定:在在中国内5.0.0.0/8大网段中,使用 IP 第二个 8bit 划分各个省份。
示例:陕西 5.1.0.0/16、河南 5.2.0.0/16、浙江 5.3.0.0/16、吉林 5.4.0.0/16、河北 5.5.0.0/16。
各省部署省级间路由器,陕西网关5.1.0.1。省级各个出入口路由器采用双 WAN 设计:WAN1 对接国际骨干网络,WAN2 对接省内下级地市网络。

使用 BGP 完成省内路由同步;而真实国内运营商骨干网,更多使用 OSPF/IS-IS。各省接入国内骨干网之后,发布本地路由条目,条目同步至全国路由表,同时上传至国际骨干网。

1.1.3 市级骨干网划分(/20,掩码 255.255.240.0)
我们假定:在省级网段内部,占用第三个字节的高 4bit 划分市级。2^4=16,单个省级网段最多承载 16 个地市。
以陕西省举例:西安5.1.16.0/20、延安5.1.32.0/20、咸阳5.1.64.0/20,西安出入口路由 IP 为5.1.16.1。

但是此时,IP 经过国家、省、市三级公网网段划分之后,剩余地址已经不足以对于一个市级中所有主机设备再分配公网 IP 了。
所以地市运营商会在网关 LAN 侧部署私有网段,例如10.1.1.1/24、192.168.1.0/24;家庭路由器再次通过 NAT 技术,拆分出家庭内网。

简易公网全图总览:

1.2 报文的跨网传输流程
1.2.1 问题一:国外俄罗斯要访问国内目标地址 5.1.16.X
1. 俄罗斯国家级边界路由器处理流程
俄罗斯主机发出报文,目的 IP:5.1.16.X,报文送到俄罗斯国际边界路由器。 路由器取出目的 IP,遍历本地路由表中每一条网段条目,将目的 IP 和条目子网掩码做按位与运算,比对运算结果是否匹配路由表内的网络地址。
- 路由表中存在条目
5.0.0.0/8,匹配成功,查到下一跳为中国的国际边界路由器。 - 跨国骨干路由器之间通过 BGP 互相交换网段路由信息,各国国际路由器都保存有全球网段的路由条目。
- 路由器将报文转发给中国出口路由器。
2. 中国国家级边界路由器处理流程
中国国际边界路由器收到报文,提取目的 IP5.1.16.X,查表匹配:
- 拿目的 IP 与路由条目掩码做与运算,首先会匹配到
5.0.0.0/8,但该网段下一跳是自身,并且发现目的 IP 和 自身的 IP 并不一样,说明这个报文只是经过我,并不是传到我手上,所以会继续往下查找。 - 继续在路由表中查找更长掩码(更精细)的路由条目,找到
5.1.0.0/16(陕西省级网段),下一跳指向陕西省出口路由器。
核心规则:最长前缀匹配。掩码越长、网段越精细,优先级越高。只有所有精确网段全部匹配失败,才会使用默认路由。
设计目的:当路由器收到属于本网大段网段,但不是发给自己的报文时,路由器继续向下查找更精细的子路由,避免路由环路,选择国内骨干网更具体的转发路径。
报文转发至陕西省省级路由器。
3. 陕西省省级路由器处理流程
陕西路由器收到报文,查找路由表,匹配5.1.16.0/20西安市级网段,下一跳指向西安的出入口路由器。 报文转发到西安市级路由器。
4. 西安市级路由器 + 内网转发
西安路由器收到报文,匹配本地网段,后续将报文转发到内网对应的主机,完成交付。

1.2.2 衍生问题:报文如何进行跨网传输?
假设当一个报文从国内主机发出,到达陕西省级出入口路由器时,路由器取出目的 IP1.1.1.1,检索本地路由表。 陕西路由器的路由表内仅维护国内各省、各地市的网段路由条目:5.1.0.0/16、5.2.0.0/16、5.3.0.0/16…… 所有条目全部无法匹配目的地址1.1.1.1。
此时就产生一个问题:路由表里找不到任何匹配的网段,路由器该如何向上一级递交报文?
查表处理逻辑
- 检索路由表所有具体网段条目,匹配失败:
1.1.1.1不属于本省维护的任何国内网段。 - 两种分支结果:
- 配置了默认路由(0.0.0.0/0):将报文转发给默认路由指定的下一跳。在本拓扑中,陕西路由器的默认路由下一跳,指向它的直接上级 —— 中国国际出入口路由器
5.0.0.1。 - 未配置默认路由:直接丢弃报文,并且向源主机发送
ICMP目标不可达(Destination Unreachable)报错报文。
- 配置了默认路由(0.0.0.0/0):将报文转发给默认路由指定的下一跳。在本拓扑中,陕西路由器的默认路由下一跳,指向它的直接上级 —— 中国国际出入口路由器
1、默认路由长什么样
在当前拓扑的路由表中,默认路由条目信息:
- 网络地址:
0.0.0.0 - 子网掩码:
0.0.0.0(简写/0) - 下一跳地址:
5.0.0.1(中国国际出入口路由器) - 概念:默认路由也叫缺省路由,是路由表里面兜底的路由条目。
2、默认路由为什么最后才匹配
掩码为0,代表不需要任何位匹配,任意 IP 地址和0.0.0.0掩码做按位与运算,结果都会匹配上这条路由。
如果路由器优先匹配默认路由,那么去往河南、河北、浙江等国内网段的所有报文,都会直接全部转发到国际出口路由器,造成路由错乱。
这就引出路由查找的核心规则:最长前缀匹配原则。
掩码越长,网段描述越精细,路由优先级越高。例如/16这类具体网段路由,优先级远高于掩码最短的/0默认路由。
只有路由表中所有/16、/8这类具体网段全部匹配失败之后,路由器才会使用这条0.0.0.0/0默认路由。
简单理解:默认路由就是当路由器不知道该往哪里转发报文时,报文交给默认路由对应的下一跳设备,这个下一跳设备,就是我们常说的默认网关。
3、默认网关概念补充
默认网关本质就是默认路由的下一跳设备。 当主机或路由器需要访问一个不在自身网段、且路由表没有精确路由条目的目标地址时,就会把报文交给默认网关,由网关继续代为转发。
在本案例中,陕西路由器的默认网关就是上级的国家级边界路由器5.0.0.1。报文交付给默认网关之后,由国家级路由器继续查表,走国际骨干网完成跨国转发。
报文到中国国际出入口路由器之后,后续跨国转发流程,就和前面【俄罗斯主机访问 5.1.16.X】案例中,报文到达中国边界路由器之后的处理逻辑一致。

1.2.3 问题二:国内河北的报文,发往国内目标地址 5.1.16.X
河北网段内主机发出报文,目的 IP 为5.1.16.X,报文送达河北省级出入口路由器。
路由器取出报文的目的 IP:5.1.16.X,遍历本地路由表,将目的 IP 依次和路由表内每条条目的子网掩码做按位与运算,进行网段匹配。
计算匹配时,目的 IP5.1.16.X与5.1.0.0/16掩码按位与,匹配成功,对应的下一跳地址是5.1.0.1,也就是陕西省级出入口路由器。
河北路由器直接将报文转发给下一跳5.1.0.1。 报文到达陕西省级路由器之后,后续查表、转发流程和前面俄罗斯主机访问5.1.16.X案例中,报文抵达陕西路由器之后的处理逻辑完全一致:陕西路由器继续匹配更长掩码5.1.16.0/20,转发到西安市级路由器,最终交付内网主机。
要点小结:
同属国内骨干网的跨省份转发,不需要先送到国家级国际出口路由器。省级路由表中已经预先学习到其他省份的网段路由,能够直接找到对端省份的下一跳,实现省内、省间骨干网直接转发。
底层依旧遵循最长前缀匹配原则,路由表中多条条目会优先匹配掩码更长的网段。

二、路由:网络层的核心功能
2.1 路由的基本概念
- 路由:在复杂的网络结构中,找出一条通往终点的路线。
- 一跳(Hop) :数据链路层中的一个传输区间,在以太网中就是从源 MAC 地址到目的 MAC 地址的帧传输。
- 点对点通信:IP 协议实现了从源主机到目的主机的端到端通信,而数据链路层只负责一跳内的通信。

2.2 路由表的结构与转发逻辑
每个主机和路由器都维护一张路由表,我们可以在 Linux 系统中使用route命令查看:
whb@iv-ye4ege8iyo5i3z3clix9:~$ route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.0.1 0.0.0.0 UG 100 0 0 eth0
192.168.0.0 0.0.0.0 255.255.0.0 U 100 0 0 eth0
192.168.0.1 0.0.0.0 255.255.255.255 UH 100 0 0 eth0
路由表关键字段解释:
| 字段 | 含义 |
|---|---|
| Destination | 目的网络地址 |
| Gateway | 下一跳地址(0.0.0.0 表示直连网络) |
| Genmask | 子网掩码 |
| Flags | 标志位:U = 有效,G = 下一跳是路由器,H = 目的是主机 |
| Iface | 发送接口 |
转发逻辑(最长前缀匹配):
- 将目的 IP 与路由表中每一条的子网掩码进行按位与运算
- 得到网络号后,与对应的 Destination 字段比较
- 选择前缀最长(子网掩码中 1 最多)的匹配条目
- 按照该条目的下一跳地址和发送接口转发报文
转发示例:
- 目的 IP 为
192.168.56.3:与255.255.255.0按位与得到192.168.56.0,匹配对应条目,从 eth1 接口直接发送到目的主机 - 目的 IP 为
202.10.1.2:与所有条目都不匹配,使用缺省路由(0.0.0.0),转发给网关192.168.10.1

2.3 缺省路由
当目的 IP 与路由表中所有条目都不匹配时,报文会被转发到缺省路由指定的下一跳地址。缺省路由就像我们问路时遇到的 “扫地大妈”,她不知道具体地址,但会告诉你去问更专业的人。
在路由表中,缺省路由的 Destination 为0.0.0.0,Genmask 为0.0.0.0,Flags 为UG。
2.4 路由表生成算法
路由表的生成有两种方式:
- 静态路由:由网络管理员手动配置,适用于小型网络
- 动态路由:通过路由协议自动生成,适用于大型网络
- 距离向量算法(RIP)
- 链路状态算法(OSPF)
- 边界网关协议(BGP):用于 互联网骨干网的路由协议
三、加餐:IP 报文的分片与组装(重点)
3.1 MTU 与 IP 分片的由来
数据链路层有一项硬性约束:上层交付的完整报文不能超过 MTU(最大传输单元),以太网默认 MTU 大小为 1500 字节。 当 IP 报文总长度超过 MTU 限制时,IP 层就需要对报文进行分片处理。
这里引出两个核心问题:
问题 1:由哪一层完成分片?
分片操作由 IP 网络层执行。数据链路层仅定义 MTU 的限制规则,链路层本身不会做分片,分片逻辑交由 IP 层自行处理。
问题 2:分片后的多个数据包到达接收方,谁负责重组?
由接收端 IP 网络层完成分片重组,谁分片谁组装。
3.2 为什么 IP 分片不应该是主流?
重点结论:IP 分片存在一个严重缺陷,只要任意一个分片丢失,整个 IP 报文就视为报文整体丢失。
举个例子,一份报文被拆分为 10 片,单分片丢包概率 1%。
- 不分片,一次性发送完整报文,传输成功率 99%;
- 拆成 10 片,必须 10 个分片全部到达才算传输成功,整体成功率下降至 99%^10,远低于 99%。
分片越多,报文整体丢失概率越高。分片与重组并不是网络通信的主流设计,只是为了满足链路层 MTU 约束,不得已采用的兜底方案。
所以降低丢包率的核心思路:尽可能减少 IP 分片。
3.3 MSS:传输层从源头规避分片
减少 IP 分片的控制逻辑,放在传输层 TCP实现。
TCP 引入 MSS(最大分段大小),用来控制交付给 IP 层的数据长度,从源头避免 IP 分片。
IP 头部默认 20 字节,TCP 头部默认 20 字节;以太网 MTU=1500 字节,因此 MSS = 1500 - 20 - 20 = 1460 字节。
TCP 会切割应用层的数据,保证交给 IP 层的数据不超过 1460 字节,加上 TCP 头部、IP 头部之后,整个 IP 报文不会超过 1500 字节,IP 层就无需分片。
MSS 不是固定常量,由操作系统自行协商、管控。
3.4 TCP 滑动窗口分段的深层目的
思考:滑动窗口为什么不一次性把窗口内全部数据一次性发送,而是按固定大小分段发送?
- TCP 滑动窗口本身的核心作用:实现流量控制、超时重传,保障字节流的可靠传输。
- 窗口内部每一段数据的大小,额外承担了适配下层网络的作用,用来减少 IP 分片。
滑动窗口分段的尺寸,不由滑动窗口本身的可靠传输逻辑决定,而是选取合适大小,控制向下交付 IP 层的数据长度,在源头减少 IP 分片操作。
小结:TCP 滑动窗口负责可靠传输与流量控制;MSS 用来限制每个 TCP 分段的最大长度,目的就是规避 IP 分片,降低丢包风险。滑动窗口内的数据分段,会参考 MSS 进行数据切割。
补充:UDP 场景下的 IP 分片
上面 MSS 规避分片的整套机制,仅适用于 TCP。
UDP 没有发送缓冲区,不存在滑动窗口,也没有 MSS 协商机制。当应用程序调用 sendto 发送大数据时,传输层直接给数据加上 UDP 头部,然后交付给 IP 层。
如果这份整体报文超过链路 MTU,IP 层只能直接执行分片。
- UDP 本身不会在传输层预先切割数据,不会做分段;
- 分片工作完全交给 IP 网络层;
- 同样遵守 IP 分片的规则:任意一个分片丢失,整个 UDP 报文就无法重组,报文直接丢失。
实践要点: 使用 UDP 传输时,应用层需要自己控制单次发送的数据长度,保证整体报文不超过 MTU,否则就会触发 IP 分片,一旦某个分片丢包,整个报文作废。
所以 UDP 开发中,一般建议应用层将单次数据大小控制在 1472 字节以内(1500 - IP 头 20 - UDP 头 8),从应用层避免 IP 分片。

3.5 IP 分片与组装机制详解
3.5.1 IP 报头中的三个分片相关字段
回顾 IP 报文头部,之前讲到第二行字段时跳过了,分片组装正好需要借助这三个字段:
| 字段 | 位数 | 作用 |
|---|---|---|
| 标识(ID) | 16 位 | 唯一标识主机发送的原始报文 |
| 标志(Flags) | 3 位 | 控制分片行为 |
| 片偏移(Fragment Offset) | 13 位 | 标识分片在原始报文中的位置 |
3.5.2 16 位标识(Identification)
- 不同原始报文,标识不同;
- 同一个原始报文被分片成多个分片后,所有分片的标识值完全相同。 接收端正是靠这个字段,把属于同一个原始报文的分片聚合到一起。
3.5.3 3 位标志(Flags)
- 第 1 位:保留,目前未使用。
- 第 2 位(DF 位,禁止分片):
1:禁止分片。如果报文长度超过 MTU,IP 层直接丢弃该报文,不分片;0:允许分片。
- 第 3 位(MF 位,更多分片):
1:当前分片后面还有后续分片;0:当前分片是最后一个分片。
最后一个分片的 MF=0,类似一个结束标记。
3.5.4 13 位片偏移(Fragment Offset)
片偏移表示当前分片的有效载荷,相对于原始报文有效载荷起始位置的偏移量。
- 实际偏移字节数 = 片偏移值 × 8;
- 为什么要乘 8?因为片偏移只有 13 位,乘以 8 相当于把 8 字节作为一个基本单位;
- 除了最后一个分片外,其他分片的长度必须是 8 的整数倍,否则报文不连续。
关键修正:片偏移只针对有效载荷部分计算,不包含 IP 报头。

3.6 IP 组装的具体过程
3.6.1 接收端如何判断一个报文是否分片了?
常见误区:
有人认为:只要看 16 位标识是否相同,相同就是分片报文,不同就是独立报文。
这个判断不完整。因为如果分片报文前面的分片全部丢失,只剩下最后一个分片,那么就没有另一个报文的标识和它相同,仅凭标识无法判断。
正确判断方法:
收到一个报文后,看两个条件:
报文被分片了(满足任一条件):
- a. MF 标志位为 1(后面还有分片);
- b. MF 标志位为 0,但片偏移值大于 0(自己就是最后一个分片)。
报文没有分片(两个条件同时满足):
- a. MF 标志位为 0;
- b. 片偏移值等于 0。
反过来理解:如果片偏移 = 0,说明分片从原始报文起点开始;如果 MF 也 = 0,说明后面没有分片,那它就是一个完整的独立报文,根本没分片。
3.6.2 接收端如何判断分片是否收齐?
聚合分片
相同标识的所有分片聚合到一起,再根据片偏移排序。
分片丢失的三种情况
- 第一个分片丢失
表现:没有收到 MF=1 且片偏移 = 0 的报文。
第一个分片是整个报文的起点,起点都没了,组装无法开始。 - 中间若干分片丢失
表现:需要用连续性公式检查(见下文)。 - 最后一个分片丢失
表现:没有收到 MF=0 且片偏移 > 0 的报文。
没有结束标记,接收端一直等不到 "组装完成" 的信号。
3.6.3 连续性检查公式
判断中间分片是否丢失,靠一个关键公式:
当前分片片偏移 = 上一个分片片偏移 + 上一个分片有效载荷长度
注意:这里用的是上一个分片有效载荷长度,不是整个报文总长度(之前简单理解时说的 "报文总长度" 需要修正,应扣除 IP 报头部分)。
举个例子:一个原始报文分成 4 片,片偏移依次为 0、1000、2000、3000。 假设中间 2000~3000 这片丢了,其他分片都到了:
- 接收端按片偏移排序后,0~1000 这片到了;
- 用公式计算下一片应该的片偏移:1000 + 1000(上一片有效载荷长度)= 2000;
- 但实际收到的下一片片偏移是 3000,对不上 2000;
- 于是接收端立刻判断:2000~3000 这个分片丢失了。
3.6.4 组装过程小结
- 相同标识的分片聚合;
- 按片偏移从小到大排序;
- 用连续性公式逐片检查;
- 如果发现间隙(某片丢失),组装失败,该报文不上交;
- 一直检查到 MF=0 且片偏移 > 0 的最后一个分片,且全程无间隙,组装成功,整份报文上交传输层。

3.7 IP 分片的具体过程
IP 分片发生在 IP 层,当 IP 报文长度超过链路 MTU 时,路由器会把报文切分成多个分片,接收端再依靠标识、标志位、片偏移字段重组。
示例:IP 层收到一份总大小 3000 字节报文,包含 20 字节固定 IP 首部,有效载荷 2980 字节;链路 MTU=1500 字节。
1. 计算分片数量
单个分片可承载的最大有效载荷:MTU - IP首部长度 = 1500 - 20 = 1480字节
注意:IP 分片的数据部分必须是 8 字节的整数倍,所以每次切割的数据载荷只能取 1480。
原始有效载荷 2980 字节,拆分:1480 + 1480 + 20 = 2980,一共分为 3 片。
2. 每个分片头部字段设置
所有分片使用相同的标识号,接收端依靠这个标识判断哪些分片属于同一个原始报文。片偏移单位为 8 字节,代表当前分片数据距离原始报文数据起始位置的偏移量。
分片 1
- 总长度:20 + 1480 = 1500 字节
- 标识:111(和原始报文保持一致)
- 标志位:MF=1,代表后面还有更多分片
- 片偏移:0,
0 × 8 = 0字节,代表这是数据最开头部分
分片 2
- 总长度:20 + 1480 = 1500 字节
- 标识:111
- 标志位:MF=1,仍存在后续分片
- 片偏移:185,
185 × 8 = 1480字节,代表当前分片数据从原始报文载荷的 1480 字节位置开始
分片 3(最后一片)
- 总长度:20 + 20 = 40 字节
- 标识:111
- 标志位:MF=0,代表这是该报文最后一个分片
- 片偏移:370,
370 × 8 = 2960字节,代表当前分片数据从原始报文载荷的 2960 字节位置开始

补充:8 字节对齐,用 13 位表达 16 位偏移
IP 头部的 “片偏移” 字段只有 13 位,但它并不是直接表示字节数,而是以 8 字节为单位。也就是说:
实际偏移字节数 = 片偏移值 × 8
这里有两个问题:
问题 1:为什么片偏移不直接按字节编号?
因为片偏移字段只有 13 位。
如果直接按字节编号:
2^13 = 8192 字节
这意味着最大只能表示 8KB 的偏移,对于一个较大的 IP 报文来说很容易不够用。
所以片偏移采用 “单位更小、可扩展更大” 的做法: 不直接存字节,而是存 “有多少个 8 字节块”。
这样,13 位片偏移能表示的最大偏移就是:
2^13 × 8 = 8192 × 8 = 65536 字节
也就是说,最大可以表示 64KB 的偏移,这正好和 IPv4 头部 “16 位总长度” 的最大范围相匹配。
问题 2:为什么偏偏是 8 字节?
因为 IPv4 头部的 “16 位总长度” 最大可以表示:
2^16 - 1 = 65535 字节
而 13 位片偏移 × 8 字节:
2^13 × 8 = 65536 字节
这个数值刚好能覆盖一个 IPv4 报文的最大可能长度。
所以:
片偏移设计为 “8 字节单位”,是为了用 13 位字段覆盖 IPv4 报文最大 64KB 左右的长度范围。

四、Linux 内核 IP 分片与组装源码解读
Linux 内核中 IP 分片、分片重组的核心代码,存放于net/ipv4/ip_output.c(分片发送)与net/ipv4/ip_input.c(接收组装)。
4.1 IP 分片函数:ip_fragment ()
int ip_fragment(struct sock *sk, struct sk_buff *skb,
int (*output)(struct sock *, struct sk_buff *))
{
struct iphdr *iph = ip_hdr(skb);
int mtu = dst_mtu(skb_dst(skb));
int hlen = iph->ihl * 4;
int len = skb->len - hlen; // 有效载荷长度
int ptr = 0;
struct sk_buff *skb2;
// 检查是否禁止分片
if (iph->frag_off & htons(IP_DF)) {
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(mtu));
kfree_skb(skb);
return -EMSGSIZE;
}
// 循环分片
while (ptr < len) {
int frag_size = min(mtu - hlen, len - ptr);
// 除了最后一个分片,其他分片的数据长度必须是8的整数倍
if (frag_size < len - ptr)
frag_size &= ~7;
// 分配新的skb
skb2 = alloc_skb(frag_size + hlen + LL_MAX_HEADER, GFP_ATOMIC);
if (!skb2) {
kfree_skb(skb);
return -ENOMEM;
}
// 复制IP首部
skb_reserve(skb2, LL_MAX_HEADER);
skb_put(skb2, hlen + frag_size);
skb_copy_to_linear_data(skb2, iph, hlen);
// 复制数据部分
skb_copy_bits(skb, hlen + ptr, skb2->data + hlen, frag_size);
// 设置分片字段
iph2 = ip_hdr(skb2);
iph2->frag_off = htons((ptr / 8) | (ptr + frag_size < len ? IP_MF : 0));
iph2->tot_len = htons(skb2->len);
ip_send_check(iph2);
// 发送分片
output(sk, skb2);
ptr += frag_size;
}
kfree_skb(skb);
return 0;
}
核心逻辑解读
- 读取 DF 标志位:如果 DF(不分片)标记置 1,无法分片,发送 ICMP「需要分片但 DF 置位」报文,向上层返回
-EMSGSIZE。 - 计算分片大小:取
MTU-IP首部长度,非最后分片的数据载荷强制对齐到 8 字节整数倍,和 IP 协议规范保持一致。 - 循环分配新
skb缓冲区,复制原始 IP 头部,再拷贝对应范围的报文数据。 - 填充
frag_off字段:同时写入片偏移与MF 标志位,并且重新计算 IP 首部校验和。 - 调用输出回调函数,逐个发送分片;全部分片发送完成后释放原始 skb。
4.2 IP 组装函数:ip_defrag ()
struct sk_buff *ip_defrag(struct net *net, struct sk_buff *skb, u32 user)
{
struct iphdr *iph = ip_hdr(skb);
struct ipq *qp;
int err;
// 查找或创建对应的IP分片队列
qp = ip_find(net, iph->id, iph->saddr, iph->daddr, iph->protocol, user);
if (!qp) {
kfree_skb(skb);
return ERR_PTR(-ENOMEM);
}
// 将分片加入队列
err = ip_frag_queue(qp, skb);
if (err) {
ipq_put(qp);
return ERR_PTR(err);
}
// 检查是否所有分片都已到达
if (ip_frag_complete(qp)) {
struct sk_buff *ret = ip_frag_reasm(qp);
ipq_put(qp);
return ret;
}
ipq_put(qp);
return ERR_PTR(-EINPROGRESS);
}
核心逻辑解读
- 以IP 标识号、源 IP、目的 IP、上层协议四元组,查找对应的分片队列;不存在则新建队列。
- 将当前收到的分片加入队列,内核内部会依据片偏移对分片自动排序。
- 调用
ip_frag_complete判断整套报文的分片是否全部接收完毕。 - 分片全部收齐后,调用
ip_frag_reasm,把多个分片 skb 重组为一个完整的 IP 报文,向上层返回。 - 分片还没收齐时返回
-EINPROGRESS,继续等待后续分片。
核心考点总结
- DF 位的作用:DF=1 代表禁止分片,超过 MTU 直接返回 ICMP 不可达,TCP 的路径 MTU 探测(PMTUd)就是依赖这个机制。
- 分片发生在发送侧路由器;分片重组只会在目的主机执行,中间转发设备不会重组报文。
- 分片的识别依靠:源 IP、目的 IP、协议号、IP 标识 id;片偏移以 8 字节为单位。
- 丢包特性:只要任意一个分片丢失,整套报文无法重组,全部分片超时后被内核丢弃。
- 分片对齐:除最后分片外,其余分片的数据长度必须是 8 字节整数倍,源码中
frag_size &= ~7就是做对齐。
结束语
本篇我们从公网骨干网络的层级结构入手,通过真实的跨网报文转发案例,完整理解了路由查表、最长前缀匹配、默认网关与缺省路由的工作逻辑。随后进入本文的核心重点,深入剖析 IP 报文分片与组装机制,理清 MTU、MSS 之间的关联差异,对比 TCP 与 UDP 在分片处理上的不同行为,拆解 IP 头部分片相关字段的作用,以及接收端分片重组、丢包判断的整套流程。最后我们阅读 Linux 内核源码,从
ip_fragment()和ip_defrag()函数,看懂内核是如何实现分片发送与分片重组。路由转发和 IP 分片是网络层非常核心的底层能力,也是后续学习传输层协议、网络排障的重要基础。掌握了这些内容,我们就能更好理解数据包在互联网上跨越无数路由器,从源主机抵达目标主机的完整过程。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2501_91275995/article/details/165719838




