IPv4之报文分片


分片是网络层的一个重要任务,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;
}

版权声明:本文为fanxiaoyu321原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。