IPv4之报文分片

当本地产生的或转发的数据包长度超过出口设备MTU或PMTU时,IPv4通过ip_fragment()函数进行分片处理。对于本机发送的数据包,TCP在封装时会考虑MTU避免分片,而UDP在ip_append_data()中组织分片。转发数据包则在网络层处理分片。分片主要在ip_finish_output()中触发,由ip_fragment()执行实际分片操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


分片是网络层的一个重要任务,IPv4需要对两种IP数据包进行分片:

  1. 本地产生的数据包;
  2. 转发的数据包;

这两种数据包的长度如果超过了出口设备的MTU(或者PMTU),IPv4就会对数据包进行分片处理,使其适配出口设备的MTU。

重要说明

IPv4使用ip_fragment()函数执行分片处理,在设计时,要求该函数能够处理所有的情况。在实现过程中,可能高层协议已经为分片进行了一些准备工作,所以代码也充分考虑了实际可能的情况,对某些场景进行了优化,下面分情况介绍。

对于本机发送的数据包,TCP在封装skb时就会考虑MTU的限制,它会尽可能的保证每个skb不超过MTU,从而可以避免网络层再进行分段,因为分段对TCP性能的影响较大。考虑UDP,它并不会像TCP一样保证skb不超过MTU,但是其在封装skb时(通过ip_append_data()函数),会将属于同一个IP报文的所有分片都组织成skb列表(非第一个分片都放在第一个分片skb的frag_list链表中),这样网络层在执行分片时将会节省很多工作量。

对于转发的数据包,则无法向本地发送一样,提前做很多的工作,网络层必须依靠自己来兼容所有可能的情况。同样的,天有不测风云,对于一些特殊的异常场景,本机发送的数据包也有可能并没有按照预期情况组织,这时网络层也要能够兼容处理。

综上,网络层在实现分片时,设计了快速路径和慢速路径两个流程来分别对应上面的两种情况。

分片时机: ip_finish_output()

如笔记IPv4之数据包发送流程IPv4之数据包接收流程介绍,无论是本机发送的数据包,还是转发的数据包,最后在数据包通过Netfilter的POST_ROUTING点后,都会交由ip_finish_output()函数继续处理。

static int ip_finish_output(struct sk_buff *skb)
{
...
	// 如果报文长度超过了MTU并且不是GSO场景,那么需要分片,分片后再输出。否则直接输出
	if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
		return ip_fragment(skb, ip_finish_output2);
	else
		return ip_finish_output2(skb);
}

报文分片: ip_fragment()

如注释所述,入参skb代表了一个完整的IP报文,ip_fragement()将其分割成一个个mtu大小的分片。

/*
 *	This IP datagram is too large to be sent in one piece.  Break it up into
 *	smaller pieces (each of size equal to IP header plus
 *	a block of the data of the original IP data part) that will yet fit in a
 *	single device frame, and queue such a frame for sending.
 */
int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))
{
	struct iphdr *iph;
	int raw = 0;
	int ptr;
	struct net_device *dev;
	struct sk_buff *skb2;
	unsigned int mtu, hlen, left, len, ll_rs, pad;
	int offset;
	__be16 not_last_frag;
	struct rtable *rt = skb->rtable;
	int err = 0;

	dev = rt->u.dst.dev;

	/*
	 *	Point into the IP datagram header.
	 */
	iph = ip_hdr(skb);

	// 需要进行分片,但是报文本身又不允许分片,那么发送失败,向源端发送ICMP需要分片报文
	if (unlikely((iph->frag_off & htons(IP_DF)) && !skb->local_df)) {
		IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
		icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
			  htonl(ip_skb_dst_mtu(skb)));
		kfree_skb(skb);
		return -EMSGSIZE;
	}

	/*
	 *	Setup starting values.
	 */
    // hlen保存IP首部长度,包括选项,每个IP分片都会包含该首部(部分选项不需要在每个分片都存在)
	hlen = iph->ihl * 4;
	// 在MTU基础上去掉IP首部的开销,所以mtu代表每个IP分片能够容纳的L4载荷
	hlen = iph->ihl * 4;
	mtu = dst_mtu(&rt->u.dst) - hlen;	/* Size of data space */
	IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;

	/* When frag_list is given, use it. First, check its validity:
	 * some transformers could create wrong frag_list or break existing
	 * one, it is not prohibited. In this case fall back to copying.
	 *
	 * LATER: this step can be merged to real generation of fragments,
	 * we can switch to copy when see the first bad fragment.
	 */
	// 如注释,这部分代码利用L4协议准备好的frag_list链表,预期每个skb是一个分片
	if (skb_shinfo(skb)->frag_list) {
		struct sk_buff *frag;
		int first_len = skb_pagelen(skb); // skb线性缓冲区+frags[]数组部分长度
		int truesizes = 0;

        // 检查L4协议的准备工作是否正确,如果不正确那么走慢速路径
		if (first_len - hlen > mtu || // 第一个分片超过了mtu
		    ((first_len - hlen) & 7) || // 第一个分片的数据长度不是8字节对齐的(IP首部偏移字段限制)
		    (iph->frag_off & htons(IP_MF|IP_OFFSET)) || // 已经包含了分片信息,说明不是第一次进行分片了
		    skb_cloned(skb)) // skb被共享了
			goto slow_path;
        // 检查frag_list中每个分片的长度设置是否合理,一旦有一个不合理,那么走慢速路径
		for (frag = skb_shinfo(skb)->frag_list; frag; frag = frag->next) {
			/* Correct geometry. */
			if (frag->len > mtu || // 分片长度不能超过mtu,注意此时分片长度中不能包含IP首部了
			    ((frag->len & 7) && frag->next) || // 非最后一个分片的长度必须是8字节对齐的
			    skb_headroom(frag) < hlen) // 分片首部要有足够的空间容纳分片报文的IP首部
			    goto slow_path;

			/* Partially cloned skb? */
			if (skb_shared(frag)) // 分片的skb也不能是共享的
				goto slow_path;

			BUG_ON(frag->sk);
			if (skb->sk) { // 让每个分片skb都持有sk的引用
				sock_hold(skb->sk);
				frag->sk = skb->sk;
				frag->destructor = sock_wfree;
				truesizes += frag->truesize;
			}
		}
		// 所有分片都满足要求,可以按照快速路径进行分片

        // 为IP报文的第一个IP分片设置长度、偏移量、校验和信息
		err = 0;
		offset = 0;
		frag = skb_shinfo(skb)->frag_list;
		skb_shinfo(skb)->frag_list = NULL;
		skb->data_len = first_len - skb_headlen(skb);
		skb->truesize -= truesizes;
		skb->len = first_len;
		iph->tot_len = htons(first_len);
		iph->frag_off = htons(IP_MF);
		ip_send_check(iph);

        // 循环进行后续分片处理,skb指向当前要处理的分片,frag指向其下一个分片
		for (;;) {
			/* Prepare header of the next frame, before previous one went down. */
			if (frag) {
				frag->ip_summed = CHECKSUM_NONE;
				skb_reset_transport_header(frag);
				__skb_push(frag, hlen);
				skb_reset_network_header(frag);
				memcpy(skb_network_header(frag), iph, hlen);
				iph = ip_hdr(frag);
				iph->tot_len = htons(frag->len);
				ip_copy_metadata(frag, skb);
				if (offset == 0)
					ip_options_fragment(frag);
				offset += skb->len - hlen;
				iph->frag_off = htons(offset>>3);
				if (frag->next != NULL)
					iph->frag_off |= htons(IP_MF);
				/* Ready, complete checksum */
				ip_send_check(iph);
			}
            // 将处理完毕的分片发送出去,就是ip_finish_output2()
			err = output(skb);
            // 发送失败则结束后续分片过程
			if (!err)
				IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
			if (err || !frag)
				break;
            // 继续下一个分片
			skb = frag;
			frag = skb->next;
			skb->next = NULL;
		}
        // 所有分片都正确处理完毕,返回0
		if (err == 0) {
			IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
			return 0;
		}
        // 有分片处理失败,将剩余的分片释放掉,返回失败
		while (frag) {
			skb = frag->next;
			kfree_skb(frag);
			frag = skb;
		}
		IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
		return err;
	}

slow_path: // 慢速路径处理,可以处理任意的情况

	// left代表了整个IP报文中剩余需要分片的数据量,在下面分片过程中会逐渐减小,减为0说明分片结束
	left = skb->len - hlen;	/* Space per frame */
	// ptr指向L4载荷的偏移,初始值指向L4报文的开头
	ptr = raw + hlen; /* Where to start from */

	/* for bridged IP traffic encapsulated inside f.e. a vlan header,
	 * we need to make room for the encapsulating header
	 */
	// 网桥功能需要在首部预留空间,会占用mtu部分
	pad = nf_bridge_pad(skb);
	ll_rs = LL_RESERVED_SPACE_EXTRA(rt->u.dst.dev, pad);
	mtu -= pad;

	/*
	 *	Fragment the datagram.
	 */
    // 第一个分片的片偏移(字节单位)
	offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
	not_last_frag = iph->frag_off & htons(IP_MF); // 标记是否是最后一个分片

	/*
	 *	Keep copying data until we run out.
	 */
    // 逐个拷贝分片数据
	while (left > 0) {
	    // 计算本次分片要拷贝的字节数
		len = left;
		/* IF: it doesn't fit, use 'mtu' - the data space left */
		if (len > mtu)
			len = mtu;
		/* IF: we are not sending upto and including the packet end
		   then align the next start on an eight byte boundary */
		if (len < left)	{ // 非最后一个分片长度要8字节对齐
			len &= ~7;
		}
		// 分片一个新的skb保存分片数据
		if ((skb2 = alloc_skb(len+hlen+ll_rs, GFP_ATOMIC)) == NULL) {
			NETDEBUG(KERN_INFO "IP: frag: no memory for new fragment!\n");
			err = -ENOMEM;
			goto fail;
		}

        // 拷贝skb控制信息兵设置报文长度信息
		ip_copy_metadata(skb2, skb);
		skb_reserve(skb2, ll_rs);
		skb_put(skb2, len + hlen);
		skb_reset_network_header(skb2);
		skb2->transport_header = skb2->network_header + hlen;

		// 将skb属主设置为对应套接字
		if (skb->sk)
			skb_set_owner_w(skb2, skb->sk);
        // 从skb线性缓冲区拷贝报文首部到分片
		skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);

		// 拷贝L4报文内容到分片
		if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
			BUG();
		left -= len;

		// 填充分片报文的IP首部
		iph = ip_hdr(skb2);
		iph->frag_off = htons((offset >> 3));

		/* ANK: dirty, but effective trick. Upgrade options only if
		 * the segment to be fragmented was THE FIRST (otherwise,
		 * options are already fixed) and make it ONCE
		 * on the initial skb, so that all the following fragments
		 * will inherit fixed options.
		 */
		// 第一个报文需要处理选项,后续拷贝即可
		if (offset == 0)
			ip_options_fragment(skb);

		/*
		 *	Added AC : If we are fragmenting a fragment that's not the
		 *		   last fragment then keep MF on each bit
		 */
		if (left > 0 || not_last_frag) // 设置MF标记
			iph->frag_off |= htons(IP_MF);
		ptr += len; // 调整指针为下一个分片拷贝做准备
		offset += len;

		// 设置IP分片报文长度
		iph->tot_len = htons(len + hlen);
        // 设置校验和
		ip_send_check(iph);
        // 输出该分片报文
		err = output(skb2);
		if (err)
			goto fail;

		IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
	}
	kfree_skb(skb);
	IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
	return err;

fail:
	kfree_skb(skb);
	IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
	return err;
}
### IPv4IPv6报文结构及字段详解 #### 1. IPv4 报文结构 IPv4 数据报由固定部分和可变选项部分组成。其基本格式如下: | 字段名称 | 长度 (位) | 描述 | |------------------|-----------|----------------------------------------------------------------------| | 版本 | 4 | 表明协议版本号,对于 IPv4 始终为 4[^2]。 | | 头部长度 (IHL) | 4 | 指定头部的长度,单位为 32 位字,最小值为 5(即 20 字节)。 | | 差分服务字段 | 8 | 曾称为 TOS,用于 QoS 控制。 | | 总长度 | 16 | 整个数据报的总长度,包括头部和数据部分,单位为字节。 | | 标识符 | 16 | 用于唯一标识主机发送的数据报。 | | 标志 | 3 | 控制分片行为,通常用来防止进一步分片。 | | 分片偏移量 | 13 | 如果发生分片,则表示该片段相对于原始数据报的位置。 | | 生存时间 (TTL) | 8 | 定义数据报在网络中的最大跳数,每经过一个路由器减 1,直到变为 0 则丢弃。 | | 协议 | 8 | 指定上层协议类型,如 TCP 或 UDP。 | | 首部校验和 | 16 | 对整个首部进行错误检测。 | | 源地址 | 32 | 发送方的 IP 地址。 | | 目标地址 | 32 | 接收方的 IP 地址。 | IPv4 的主要特点是头部较复杂,包含许多固定的字段以及可选字段。 --- #### 2. IPv6 报文结构 IPv6 设计的目标之一是简化头部结构并提升性能。以下是 IPv6 报文的基本格式: | 字段名称 | 长度 (位) | 描述 | |------------------|-----------|----------------------------------------------------------------------| | 版本 | 4 | 表明协议版本号,对于 IPv6 始终为 6。 | | 流类别 | 8 | 类似于 IPv4 中的差分服务字段,用于流量分类。 | | 流标签 | 20 | 可用于标记特定流,便于中间节点优化处理。 | | 载荷长度 | 16 | 不包括基础头部在内的有效载荷长度,单位为字节。 | | 下一头部 | 8 | 指向下一个头部类型,类似于 IPv4 的协议字段。 | | 跳数限制 | 8 | 功能类似于 TTL,控制数据报的最大跳数。 | | 源地址 | 128 | 发送方的 IPv6 地址。 | | 目标地址 | 128 | 接收方的 IPv6 地址。 | 相比 IPv4IPv6 删除了一些冗余字段并将某些功能转移到扩展头部中,从而减少了固定头部的大小到仅 40 字节[^4]。 --- #### 3. 扩展头部 IPv6 支持多种类型的扩展头部,这些头部按顺序附加在基础头部之后。常见的扩展头部包括但不限于以下几种: - **逐跳选项头部**: 影响路径上的每一个节点的行为。 - **路由头部**: 提供显式的源路由机制。 - **分段头部**: 实现分片操作。 - **认证头部 (AH)** 和 **封装安全净荷 (ESP)**: 提供安全性支持。 通过这种模块化设计,IPv6 能够灵活适应不同的应用场景需求。 --- #### 4. IPv4IPv6 报文对比总结 | 属性 | IPv4 | IPv6 | |-------------------|------------------------------------|------------------------------------| | 固定头部长度 | 20 字节 | 40 字节 | | 地址空间大小 | 32 位 (约 43 亿个地址)[^3] | 128 位 (几乎无限多)[^3] | | 首部校验和 | 存在 | 移除 | | 自动配置能力 | 较弱 | 强大 | | 安全特性 | 需要额外协议实现 | 内置 IPSec | | 扩展性 | 使用选项字段 | 使用扩展头部 | 上述差异使得 IPv6 更加适合现代互联网的需求,在地址耗尽、移动性和安全性等方面具有显著优势[^3]。 ```python # 示例 Python 程序展示如何解析简单的 IPv4/IPv6 数据包头 import struct def parse_ipv4_header(packet): version_ihl, tos, total_length, identification, flags_offset, ttl, protocol, checksum, src_ip, dst_ip = \ struct.unpack('!BBHHHBBHII', packet[:20]) ihl = version_ihl & 0xF header_len = ihl * 4 return { 'version': version_ihl >> 4, 'header_length': header_len, 'tos': tos, 'total_length': total_length, 'identification': identification, 'flags_offset': flags_offset, 'ttl': ttl, 'protocol': protocol, 'checksum': checksum, 'src_ip': socket.inet_ntoa(struct.pack('!I', src_ip)), 'dst_ip': socket.inet_ntoa(struct.pack('!I', dst_ip)) } def parse_ipv6_header(packet): ver_tc_flow, payload_length, next_header, hop_limit, src_addr, dst_addr = \ struct.unpack('!IHBB16s16s', packet[:40]) return { 'version': ver_tc_flow >> 28, 'traffic_class': (ver_tc_flow >> 20) & 0xFF, 'flow_label': ver_tc_flow & 0xFFFFF, 'payload_length': payload_length, 'next_header': next_header, 'hop_limit': hop_limit, 'src_address': socket.inet_ntop(socket.AF_INET6, src_addr), 'dst_address': socket.inet_ntop(socket.AF_INET6, dst_addr) } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值