小叶-duck头像
《Linux 网络编程》深入理解 IP 协议(三):公网路由转发原理与 IP 分片组装机制剖析封面图

《Linux 网络编程》深入理解 IP 协议(三):公网路由转发原理与 IP 分片组装机制剖析

🔥小叶-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.2.1 问题一:国外俄罗斯要访问国内目标地址 5.1.16.X

    1.2.2 衍生问题:报文如何进行跨网传输?

查表处理逻辑

1、默认路由长什么样

2、默认路由为什么最后才匹配

3、默认网关概念补充

    1.2.3 问题二:国内河北的报文,发往国内目标地址 5.1.16.X

二、路由:网络层的核心功能

  2.1 路由的基本概念

  2.2 路由表的结构与转发逻辑

  2.3 缺省路由

  2.4 路由表生成算法

三、加餐:IP 报文的分片与组装(重点)

  3.1 MTU 与 IP 分片的由来

  3.2 为什么 IP 分片不应该是主流?

  3.3 MSS:传输层从源头规避分片

  3.4 TCP 滑动窗口分段的深层目的

补充:UDP 场景下的 IP 分片

  3.5 IP 分片与组装机制详解

    3.5.1  IP 报头中的三个分片相关字段

    3.5.2 16 位标识(Identification)

    3.5.3 3 位标志(Flags)

    3.5.4 13 位片偏移(Fragment Offset)

  3.6 IP 组装的具体过程

     3.6.1  接收端如何判断一个报文是否分片了?

常见误区:

正确判断方法:

    3.6.2 接收端如何判断分片是否收齐?

聚合分片

分片丢失的三种情况

    3.6.3 连续性检查公式

    3.6.4 组装过程小结

  3.7 IP 分片的具体过程

补充:8 字节对齐,用 13 位表达 16 位偏移

四、Linux 内核 IP 分片与组装源码解读

4.1 IP 分片函数:ip_fragment ()

4.2 IP 组装函数:ip_defrag ()

核心考点总结

结束语


前言

      在上一篇文章中,我们从 IP 地址的本质出发,梳理了网络号与主机号、分类编址与 CIDR 无类域间路由、以及私有 IP / 公网 IP 的边界与 NAT 技术。

      本篇我们继续深入网络层。首先从宏观视角看公网的真实层级结构:全球骨干网如何按国家划分、国内骨干网又如何按省、市逐级下沉。接着通过两个典型案例 —— 国外主机访问国内地址、国内跨省转发 —— 完整走一遍报文跨网传输流程,并讲清楚默认路由、默认网关的本质。在此基础上,系统讲解路由表的结构、最长前缀匹配、缺省路由与路由表生成算法。

      文章后半部分是重点加餐:IP 报文的分片与组装。我们从 MTU 与分片的由来讲起,解释为什么分片不该是主流、TCP 如何通过 MSS 从源头规避分片、UDP 又为何只能把大数据交给 IP 层分片;随后完整拆解分片与组装的机制 —— 标识、标志位、片偏移三个字段如何协作,接收端如何判断报文是否分片、如何判断分片收齐、如何用连续性公式发现丢包;最后结合 Linux 内核ip_fragment()与ip_defrag()源码,把分片发送与分片重组的实现细节落到代码层面。

一、尝试理解公网

  1.1 公网的层级结构:全球骨干网络 + 国内骨干网络拓扑规划

      真实互联网并不是扁平化的网络,而是一套多层级的树形拓扑结构。真实网络受 ICANN、五大区域 IP 分配机构(APNIC/RIPE 等)以及全球各国 ISP 多方管控。由于全球公网的拓结构十分复杂我们无法在真实情况下讲解,所以接下来我们讲解的网段划分属于最简练的划分了,仅用于我们对公网的理解和学习,现实环境中的 IP 地址并不会按照国家、省级行政区、地级市这样固化分配。我们可以简化为以下四层:

  1. 国际骨干网:由各国的国际路由器组成,通过海底光缆连接。每个国家向 ICANN 申请 IP 地址段,例如中国拥有5.0.0.0/8这样的大段地址。
  2. 国内骨干网:国家将 IP 地址进一步划分给各个省份,例如陕西分配到5.1.0.0/16,河北分配到5.5.0.0/16。
  3. 市级骨干网:省份再将地址划分给各个城市,例如西安分配到5.1.16.0/20。
  4. 局域网:城市运营商不再使用公网 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,查表匹配:

  1. 拿目的 IP 与路由条目掩码做与运算,首先会匹配到5.0.0.0/8,但该网段下一跳是自身,并且发现目的 IP 和 自身的 IP 并不一样,说明这个报文只是经过我,并不是传到我手上,所以会继续往下查找。
  2. 继续在路由表中查找更长掩码(更精细)的路由条目,找到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.1不属于本省维护的任何国内网段。
  2. 两种分支结果:
    • 配置了默认路由(0.0.0.0/0):将报文转发给默认路由指定的下一跳。在本拓扑中,陕西路由器的默认路由下一跳,指向它的直接上级 —— 中国国际出入口路由器5.0.0.1。
    • 未配置默认路由:直接丢弃报文,并且向源主机发送ICMP目标不可达(Destination Unreachable)报错报文。
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发送接口

转发逻辑(最长前缀匹配):

  1. 将目的 IP 与路由表中每一条的子网掩码进行按位与运算
  2. 得到网络号后,与对应的 Destination 字段比较
  3. 选择前缀最长(子网掩码中 1 最多)的匹配条目
  4. 按照该条目的下一跳地址和发送接口转发报文

转发示例:

  • 目的 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 接收端如何判断分片是否收齐?

聚合分片

      相同标识的所有分片聚合到一起,再根据片偏移排序。

分片丢失的三种情况
  1. 第一个分片丢失
          表现:没有收到 MF=1 且片偏移 = 0 的报文。
          第一个分片是整个报文的起点,起点都没了,组装无法开始。
  2. 中间若干分片丢失
          表现:需要用连续性公式检查(见下文)。
  3. 最后一个分片丢失
          表现:没有收到 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 组装过程小结

  1. 相同标识的分片聚合;
  2. 按片偏移从小到大排序;
  3. 用连续性公式逐片检查;
  4. 如果发现间隙(某片丢失),组装失败,该报文不上交;
  5. 一直检查到 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;
}

核心逻辑解读

  1. 读取 DF 标志位:如果 DF(不分片)标记置 1,无法分片,发送 ICMP「需要分片但 DF 置位」报文,向上层返回-EMSGSIZE。
  2. 计算分片大小:取MTU-IP首部长度,非最后分片的数据载荷强制对齐到 8 字节整数倍,和 IP 协议规范保持一致。
  3. 循环分配新skb缓冲区,复制原始 IP 头部,再拷贝对应范围的报文数据。
  4. 填充frag_off字段:同时写入片偏移与MF 标志位,并且重新计算 IP 首部校验和。
  5. 调用输出回调函数,逐个发送分片;全部分片发送完成后释放原始 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);
}

核心逻辑解读

  1. 以IP 标识号、源 IP、目的 IP、上层协议四元组,查找对应的分片队列;不存在则新建队列。
  2. 将当前收到的分片加入队列,内核内部会依据片偏移对分片自动排序。
  3. 调用ip_frag_complete判断整套报文的分片是否全部接收完毕。
  4. 分片全部收齐后,调用ip_frag_reasm,把多个分片 skb 重组为一个完整的 IP 报文,向上层返回。
  5. 分片还没收齐时返回-EINPROGRESS,继续等待后续分片。

核心考点总结

  1. DF 位的作用:DF=1 代表禁止分片,超过 MTU 直接返回 ICMP 不可达,TCP 的路径 MTU 探测(PMTUd)就是依赖这个机制。
  2. 分片发生在发送侧路由器;分片重组只会在目的主机执行,中间转发设备不会重组报文。
  3. 分片的识别依靠:源 IP、目的 IP、协议号、IP 标识 id;片偏移以 8 字节为单位。
  4. 丢包特性:只要任意一个分片丢失,整套报文无法重组,全部分片超时后被内核丢弃。
  5. 分片对齐:除最后分片外,其余分片的数据长度必须是 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

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--