tcp/ip 协议栈Linux内核源码分析九 IPv6分片ip6_fragment 分析

博客介绍内核版本3.4.39下IPv6的分片流程,其与IPv4基本一致。分片时会判断是否满足快速分片,满足则加分片扩展选项逐个发送;不满足则走慢速通道,需重新分配skb并复制原始SKB报文数据发送。

内核版本:3.4.39

IPv6的分片流程和IPv4基本一致,这一点内核源码作者也说了。流程比较简单,分片的时候判断是否满足快速分片,满足的话直接一个接一个加上分片扩展选项发送出去,不满足的话就只能走慢速分片通道了,这时候需要重新分配每一个skb,然后从原始SKB报文那里复制数据发送出去。

int ip6_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))
{
	struct sk_buff *frag;
	struct rt6_info *rt = (struct rt6_info*)skb_dst(skb);
	struct ipv6_pinfo *np = skb->sk ? inet6_sk(skb->sk) : NULL;
	struct ipv6hdr *tmp_hdr;
	struct frag_hdr *fh;
	unsigned int mtu, hlen, left, len;
	int hroom, troom;
	__be32 frag_id = 0;
	int ptr, offset = 0, err=0;
	u8 *prevhdr, nexthdr = 0;
	struct net *net = dev_net(skb_dst(skb)->dev);

    //获取不能分片的扩展选项长度,不是每个扩展选项都能够分片的
	hlen = ip6_find_1stfragopt(skb, &prevhdr);
	nexthdr = *prevhdr;

	//获取mtu大小
	mtu = ip6_skb_dst_mtu(skb);

	/* We must not fragment if the socket is set to force MTU discovery
	 * or if the skb it not generated by a local socket.
	 */
    //检查下是否允许分片
	if (!skb->local_df && skb->len > mtu) {
		skb->dev = skb_dst(skb)->dev;
		icmpv6_send(skb, ICMPV6_PKT_TOOBIG, 0, mtu);
		IP6_INC_STATS(net, ip6_dst_idev(skb_dst(skb)),
			      IPSTATS_MIB_FRAGFAILS);
		kfree_skb(skb);
		return -EMSGSIZE;
	}

	//获取mtu大小
	if (np && np->frag_size < mtu) {
		if (np->frag_size)
			mtu = np->frag_size;
	}

	//hlen表示每个分片报文都必须携带的扩展选项头
	mtu -= hlen + sizeof(struct frag_hdr);

	//判断是否存在分片队列,存在的话说明应用层可以已经帮忙分片了,这时候只需要检查
	//分片的长度是否合法,合法的话则可以使用快速分片。
	if (skb_has_frag_list(skb)) {
		int first_len = skb_pagelen(skb);
		struct sk_buff *frag2;

		//如果第一个报文长度大于MTU或者长度不是8字节的整数倍
		//克隆的报文也不能使用快速分片。
		if (first_len - hlen > mtu ||
		    ((first_len - hlen) & 7) ||
		    skb_cloned(skb))
			goto slow_path;

		//检查分片是否满足快速分片要求
		skb_walk_frags(skb, frag) {
			/* Correct geometry. */
			//首先是长度不能大于MTU
			//此外除了最后一个报文其它报文长度必须是8字节整数倍
			//当然首部空间不够的话也不行
			if (frag->len > mtu ||
			    ((frag->len & 7) && frag->next) ||
			    skb_headroom(frag) < hlen)
				goto slow_path_clean;

			/* Partially cloned skb? */
			if (skb_shared(frag))
				goto slow_path_clean;

			BUG_ON(frag->sk);
			if (skb->sk) {
				frag->sk = skb->sk;
				frag->destructor = sock_wfree;
			}

			//报文要各自为战了,所以从第一个报文那个分出来。
			//这么做是因为发送的时候需要将报文所占大小还给系统。
			//每个报文返还自己的。
			skb->truesize -= frag->truesize;
		}

		err = 0;
		offset = 0;

		//将分片从skb链表上挂载到frag上。
		frag = skb_shinfo(skb)->frag_list;
		skb_frag_list_init(skb);
		/* BUILD HEADER */

		*prevhdr = NEXTHDR_FRAGMENT;
		tmp_hdr = kmemdup(skb_network_header(skb), hlen, GFP_ATOMIC);
		if (!tmp_hdr) {
			IP6_INC_STATS(net, ip6_dst_idev(skb_dst(skb)),
				      IPSTATS_MIB_FRAGFAILS);
			return -ENOMEM;
		}

		//追加一个分片头
		__skb_pull(skb, hlen);
		fh = (struct frag_hdr*)__skb_push(skb, sizeof(struct frag_hdr));
		__skb_push(skb, hlen);
		skb_reset_network_header(skb);
		memcpy(skb_network_header(skb), tmp_hdr, hlen);

		//选择一个报文ID标识
		ipv6_select_ident(fh, rt);

		//设置分片扩展选项
		fh->nexthdr = nexthdr;
		fh->reserved = 0;
		fh->frag_off = htons(IP6_MF);
		frag_id = fh->identification;

		//重新调整长度
		first_len = skb_pagelen(skb);
		skb->data_len = first_len - skb_headlen(skb);
		skb->len = first_len;
		ipv6_hdr(skb)->payload_len = htons(first_len -
						   sizeof(struct ipv6hdr));

		//增加路由统计计数
		dst_hold(&rt->dst);

		for (;;) {
			/* Prepare header of the next frame,
			 * before previous one went down. */
			//处理其它分片报文 
			if (frag) {
				frag->ip_summed = CHECKSUM_NONE;
				skb_reset_transport_header(frag);

				//添加分片扩展选项
				fh = (struct frag_hdr*)__skb_push(frag, sizeof(struct frag_hdr));
				__skb_push(frag, hlen);
				skb_reset_network_header(frag);
				memcpy(skb_network_header(frag), tmp_hdr,
				       hlen);

				//调整选项头       
				offset += skb->len - hlen - sizeof(struct frag_hdr);
				fh->nexthdr = nexthdr;
				fh->reserved = 0;
				fh->frag_off = htons(offset);
				if (frag->next != NULL)
					fh->frag_off |= htons(IP6_MF);
				fh->identification = frag_id;
				ipv6_hdr(frag)->payload_len =
						htons(frag->len -
						      sizeof(struct ipv6hdr));
				ip6_copy_metadata(frag, skb);
			}

			//先将上一个报文发送出去
			//分片报文按照处理的顺序发送出去
			err = output(skb);
			if(!err)
				IP6_INC_STATS(net, ip6_dst_idev(&rt->dst),
					      IPSTATS_MIB_FRAGCREATES);

			if (err || !frag)
				break;

			skb = frag;
			frag = skb->next;
			skb->next = NULL;
		}

		kfree(tmp_hdr);

		//分片成功则返回
		if (err == 0) {
			IP6_INC_STATS(net, ip6_dst_idev(&rt->dst),
				      IPSTATS_MIB_FRAGOKS);
			dst_release(&rt->dst);
			return 0;
		}

		//走到这里说明发送那里出现了错误,功亏一篑,剩下的报文原地解散回家去吧
		while (frag) {
			skb = frag->next;
			kfree_skb(frag);
			frag = skb;
		}

		//失败的统计计数
		IP6_INC_STATS(net, ip6_dst_idev(&rt->dst),
			      IPSTATS_MIB_FRAGFAILS);

		//释放占用的路由指针	      
		dst_release(&rt->dst);
		return err;

slow_path_clean:
		skb_walk_frags(skb, frag2) {
			if (frag2 == frag)
				break;
			frag2->sk = NULL;
			frag2->destructor = NULL;
			skb->truesize += frag2->truesize;
		}
	}

slow_path:
	//慢速通道,需要重新分配skb

	//left表示分片报文数据部分总长度
	left = skb->len - hlen;		/* Space per frame */
	ptr = hlen;			/* Where to start from */

	/*
	 *	Fragment the datagram.
	 */

	*prevhdr = NEXTHDR_FRAGMENT;

	//预留链路层头部长度
	hroom = LL_RESERVED_SPACE(rt->dst.dev);
	troom = rt->dst.dev->needed_tailroom;

	/*
	 *	Keep copying data until we run out.
	 */
	while(left > 0)	{
		len = left;
		/* IF: it doesn't fit, use 'mtu' - the data space left */
		//分片长度最大时mtu
		if (len > mtu)
			len = mtu;
		/* IF: we are not sending up to and including the packet end
		   then align the next start on an eight byte boundary */

		//除了最后一个分片报文,其它报文长度必须是8字节整数倍   
		if (len < left)	{
			len &= ~7;
		}
		/*
		 *	Allocate buffer.
		 */
		//分配一个SKB,果然是慢速,快速已经分配好了直接发送就可以了。
		if ((frag = alloc_skb(len + hlen + sizeof(struct frag_hdr) +
				      hroom + troom, GFP_ATOMIC)) == NULL) {
			NETDEBUG(KERN_INFO "IPv6: frag: no memory for new fragment!\n");
			IP6_INC_STATS(net, ip6_dst_idev(skb_dst(skb)),
				      IPSTATS_MIB_FRAGFAILS);
			err = -ENOMEM;
			goto fail;
		}

		/*
		 *	Set up data on packet
		 */

		//重新分配数据
		ip6_copy_metadata(frag, skb);
		skb_reserve(frag, hroom);
		skb_put(frag, len + hlen + sizeof(struct frag_hdr));
		skb_reset_network_header(frag);
		fh = (struct frag_hdr *)(skb_network_header(frag) + hlen);
		frag->transport_header = (frag->network_header + hlen +
					  sizeof(struct frag_hdr));

		/*
		 *	Charge the memory for the fragment to any owner
		 *	it might possess
		 */
		if (skb->sk)
			skb_set_owner_w(frag, skb->sk);

		/*
		 *	Copy the packet header into the new buffer.
		 */
		skb_copy_from_linear_data(skb, skb_network_header(frag), hlen);

		/*
		 *	Build fragment header.
		 */
		//配置分片选项头 
		fh->nexthdr = nexthdr;
		fh->reserved = 0;
		if (!frag_id) {
			ipv6_select_ident(fh, rt);
			frag_id = fh->identification;
		} else
			fh->identification = frag_id;

		/*
		 *	Copy a block of the IP datagram.
		 */
		//复制其它数据 
		if (skb_copy_bits(skb, ptr, skb_transport_header(frag), len))
			BUG();
		left -= len;

		//设置分片选项头
		fh->frag_off = htons(offset);
		if (left > 0)
			fh->frag_off |= htons(IP6_MF);
		ipv6_hdr(frag)->payload_len = htons(frag->len -
						    sizeof(struct ipv6hdr));

		ptr += len;
		offset += len;

		/*
		 *	Put this fragment into the sending queue.
		 */
		//发送报文 
		err = output(frag);
		if (err)
			goto fail;

		IP6_INC_STATS(net, ip6_dst_idev(skb_dst(skb)),
			      IPSTATS_MIB_FRAGCREATES);
	}
	IP6_INC_STATS(net, ip6_dst_idev(skb_dst(skb)),
		      IPSTATS_MIB_FRAGOKS);
	kfree_skb(skb);
	return err;

fail:
	IP6_INC_STATS(net, ip6_dst_idev(skb_dst(skb)),
		      IPSTATS_MIB_FRAGFAILS);
	kfree_skb(skb);
	return err;
}

 

<think> 嗯,用户这次的问题是关于TCP/IP协议中IP数据包头标识字段的具体工作机制,特别是标识加1是否代表数据到达网关。这是个非常专业的网络协议问题,需要准确解析IP协议规范。 用户可能正在学习计算机网络课程,或者在工作中遇到了相关调试需求。从提问的精准度来看,ta应该已经具备基础的TCP/IP知识,但可能混淆了IP层和数据链路层的功能差异。需要特别注意区分“标识字段”和“数据到达”这两个关键概念的关系。 首先回顾RFC 791标准:IP标识字段是16位计数器,由源主机为每个发出的数据包分配,主要用于分片重组。这个字段的维护完全由发送端控制,中间设备(如网关)不会修改它。用户提到的“标识加1”其实是发送端的行为,和传输路径无关。 关于网关的角色要特别澄清:作为三层设备,网关只检查IP头部中的目标地址进行转发,可能修改TTL和校验和字段,但绝不会改动标识字段。数据包到达网关仅代表通过了一跳(hop),距离最终目标可能还有多跳。 用户可能存在的认知误区在于:把IP标识当成了类似TCP序列号的确认机制。实际上IP是无连接的,没有端到端的送达确认功能。这个误解需要重点纠正,可以补充TCP和IP层级关系的对比。 在回答结构上,考虑分四个部分:字段定义、发送端维护方式、网关转发行为、常见误区。最后附加的实践问题能帮助用户验证理解深度,比如分片场景的标识使用,这正是网络工程师面试的经典题型。 注意到用户上次的quick排序示例提到过“性能分析”,这次回答特意加入了标识字段设计的性能考量(16位溢出不重置),保持知识深度的一致性。参考文献直接锚定RFC文件,确保权威性。 </think> 关于IP数据包头标识字段的维护机制及"加1"的含义,以下是专业解析: ### 一、IP标识字段核心机制 1. **字段定义** 在IPv4头部中,16位的**标识字段(Identification)** 用于唯一标记同一数据报的分片。每个原始IP数据报分配独立标识值,其格式为: $$ \text{IPv4 Header} = \begin{array}{|c|c|c|c|} \hline \text{Version} & \text{IHL} & \text{TOS} & \text{Total Length} \\ \hline \text{Identification} & \text{Flags} & \text{Fragment Offset} \\ \hline ... \\ \hline \end{array} $$ 2. **维护方式** - **发送端维护**:由源主机生成初始值,遵循以下规则: - 每发出**一个完整的原始数据报**,标识值增加1(非每个分片单独增加) - 同一数据报的所有分片共享相同标识值 ```c // 伪代码示例:发送端维护逻辑 static uint16_t ip_id_counter = 0; void send_ip_packet() { ip_header.identification = ip_id_counter++; fragment_if_needed(); // 分片时复制相同标识值 } ``` - **传输过程不变**:路由器/网关等中间设备**绝不修改**该字段值(RFC 791) ### 二、标识值增加的含义 1. **仅表示新数据报发出** 当观察到标识值 +1 时,仅说明源主机发出了**一个新的完整数据报**(可能是TCP分段或独立UDP包等),与传输路径无关 2. **与网关无关** - 网关(路由器)仅执行以下操作: - 检查TTL并减1 - 重新计算头部校验和 - 根据路由表转发 - **标识字段始终透明传输**(如下图所示): ``` 源主机 [标识=X] → 网关1 → 网关2 → 目标主机 [仍为X] ``` ### 三、常见误解澄清 1. **≠ 数据到达网关** 标识值变化仅发生在源主机创建新数据报时,网关既不会修改该值,也不会触发其增加 2. **≠ 传输进度指示** 该字段仅用于接收端重组分片(通过相同标识值关联分片),无路径跟踪功能 3. **交付确认由高层协议实现** IP层本身无确认机制,数据到达最终目标需依赖: - TCP的ACK序号(传输层) - ICMP回显(网络层诊断工具) > **关键结论**:标识值增加仅反映**发送端生成新数据报**,与网关操作或数据传输进度无关。网关对IP标识字段保持"只读不写"原则,这是IP协议无连接特性的核心设计[^1]。 --- ### 相关问题扩展 1. **IP分片重组时如何利用标识字段?** > 接收端通过**相同标识值 + 分片偏移量 + MF标志**重组原始数据报(需解析Fragment Offset字段) 2. **为什么16位标识可能导致重复?如何处理?** > 因取值空间 $[0, 2^{16}-1]$ 会循环,但实际中: > - TTL限制数据报存活时间(通常 $< 4$ 分钟) > - 高速网络通过随机初始值避免冲突(如Linux内核默认随机化) 3. **IPv6是否保留标识字段?机制有何不同?** > IPv6移除分片字段(标识/标志/偏移量),分片功能由端主机通过**扩展头**实现(路由器不分片)[^2] 4. **如何通过Wireshark观察标识字段变化?** > 在抓包分析中: > - 过滤`ip.id`字段 > - 对比相同五元组流中的值变化规律 > - 分片数据报显示`[IP Fragments]`标识 [^1]: IP协议规范RFC 791: Identification Field [^2]: IPv6分片机制RFC 8200: Fragment Header
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值