现在的位置: 首页 > 综合 > 正文

linux内核学习笔记——ip报文的分片

2014年09月05日 ⁄ 综合 ⁄ 共 4558字 ⁄ 字号 评论关闭

对网络比较熟悉的童鞋都知道,当发送的ip报文长度超出了最大的传输单位MTU,且允许分片的情况下,就会对ip报文进行分片。在上层要发送数据时就会调用dst_output,dst_output就会调用ip_output,而ip_output就会调用ip_finish_output,在ip_finish_output把数据发送出去之前就会判断该报文是否进行分片。

static int ip_finish_output(struct sk_buff *skb)
{
	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);
}

从源码中可以看出,当报文的长度大于mtu,gso的长度不为0就会调用ip_fragment进行分片。否则就会调用ip_finish_output2把数据发送出去。

ip分片目前有两种分片方式:1、快速分片;2、慢速分片。在快速分片中,将数据分割成片段已经由传输层完成,三层只需将这写片段组成ip分片;而慢速分片则需要完成全部的工作,即对一个完整的ip数据报根据mtu值循环进行分片,直至完成。整个分片工作都在ip_fragment中完成。

int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))
{
.......
	struct rtable *rt = skb_rtable(skb);
	int err = 0;

	dev = rt->u.dst.dev;
......
	/*
	 * 如果待分片IP数据包禁止分片,则调用
	 * icmp_send()向发送方发送一个原因为需要
	 * 分片而设置了不分片标志的目的不可达
	 * ICMP报文,并丢弃报文,即设置IP状态
	 * 为分片失败,释放skb,返回消息过长
	 * 错误码。
	 */
	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;
	}

	hlen = iph->ihl * 4;
	mtu = dst_mtu(&rt->u.dst) - hlen;	/* Size of data space */
	/*
	 * 在分片之前先给IP数据包的控制块设置
	 * IPSKB_FRAG_COMPLETE标志,标识完成分片。
	 */
	IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;

	if (skb_has_frags(skb)) {
		/*
		 * 获得此IP数据包第一个分片长度,包括SG类型
		 * 聚合分散I/O数据区中的数据。
		 */
		int first_len = skb_pagelen(skb);

		if (first_len - hlen > mtu ||
		    ((first_len - hlen) & 7) ||
		    (iph->frag_off & htons(IP_MF|IP_OFFSET)) ||
		    skb_cloned(skb))
			goto slow_path;

		skb_walk_frags(skb, frag) {
			if (frag->len > mtu ||
			    ((frag->len & 7) && frag->next) ||
			    skb_headroom(frag) < hlen)
			    goto slow_path;

			if (skb_shared(frag))
				goto slow_path;
			if (skb->sk) {
				frag->sk = skb->sk;
				frag->destructor = sock_wfree;
				truesizes += frag->truesize;
			}
......
		frag = skb_shinfo(skb)->frag_list;
		skb_frag_list_init(skb);
......
		for (;;) {
			if (frag) {
				/*
				 * 设置后一个分片skb中指向三层和四层首部
				 * 的指针。
				 */
				skb_reset_transport_header(frag);
				__skb_push(frag, hlen);
				skb_reset_network_header(frag);
				/*
				 * 将当前分片的IP首部复制给后一个分片,
				 * 并修改后一个分片IP首部的总长度字段。
				 */
				memcpy(skb_network_header(frag), iph, hlen);
				iph = ip_hdr(frag);
				iph->tot_len = htons(frag->len);
				/*
				 * 根据当前分片的skb填充后一个分片
				 * skb中的参数。
				 */
				ip_copy_metadata(frag, skb);
				/*
				 * 如果是在处理第一个分片,则调用ip_options_fragment()
				 * 将第二个分片skb中无需复制到每个分片的IP选项都
				 * 填充为IPOPT_NOOP,此后所有的分片选项部分都简单
				 * 地复制上一个的即可。
				 */
				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);
			err = output(skb);
			skb = frag;
			frag = skb->next;
			skb->next = NULL;

快速分片和慢速分片主要通过skb_has_frags这个来判断,也就是判断该数据的第一个skb中的frag_list是否为空,如果为空就是需要进行慢速分片,否则传输层已经为快速分片做好了准备。上面的代码大部分都有注释,需要注意一种情况

1、要进行快速分片还需要对传输层传递的所有的skb进行判断:

  • 有分片长度大于mtu
  • 除最后一个分片外还有分片长度未与8字节对其
  • ip首部中的MF或片偏移不为0,说明不是一个完整的ip报文
  • 此skb被克隆  

上述四种情况是不能进行ip分片的。上面是快速分片;

当不能进行快速分片时就会转到慢速分片,慢速分片其实就需要对skb数据进行复制,而快速分片就不需要此操作。

slow_path:
	/*
	 * 获取待分片的IP数据包的数据长度,此处减去hlen是
	 * 为二层首部留出空间。
	 */
	left = skb->len - hlen;		/* Space per frame */
	/*
	 * 获取IP数据包中数据区指针
	 */
	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
	 */
	/*
	 * 如果是桥转发基于VLAN的IP数据包,则需
	 * 获得VLAN首部长度,在后面分配skb
	 * 缓冲区时留下相应的空间,同时还需
	 * 修改MTU值。
	 */
	pad = nf_bridge_pad(skb);
	/*
	 * 获得IP首部中的片偏移值,即每个分片
	 * 起始处在原始数据包中位置,该值是
	 * 13位的,因此要乘8.
	 */
	offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
	/*
	 * 取MF位值,MF值除最后一个分片外
	 * 都应该置为1,表示该分片之后还
	 * 有分片。
	 */
	not_last_frag = iph->frag_off & htons(IP_MF);
	/*
	 * 循环对left长度的数据进行分片,为
	 * 每一个分片创建一个新的SKB。
	 */
	while (left > 0) {
		len = left;
		/* IF: it doesn't fit, use 'mtu' - the data space left */
		/*
		 * 如果剩余数据的长度大于MTU,则以MTU为
		 * 分片长度进行分片;否则就以剩余数据
		 * 的长度作为分片长度,显然后一种情况
		 * 只会出现在最后一个分片。
		 */
		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 */
		/*
		 * 除非是最后一个分节,否则分片不包括IP
		 * 首部的数据部分,需8字节对齐。
		 */
		if (len < left)	{
			len &= ~7;
		}
		/*
		 *	Allocate buffer.
		 */
		/*
		 * 为分片分配一个SKB,其长度为分片长、
		 * IP首部长,以及二层首部长之和。
		 */
		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_copy_bits(),是因为skb中的数据存储有多种
		 * 可能性,而skb_copy_bits可以处理这些细节。
		 */
		if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
		/*
		 * 设置分片的片偏移字段,对于第一个分片,
		 * 该值即原始IP数据包的片偏移字段值。
		 */
		iph = ip_hdr(skb2);
		iph->frag_off = htons((offset >> 3));

		if (offset == 0)
			ip_options_fragment(skb);
		 * 如果不是最后一个分节,则设置IP首部中
		 * 标识字段的MF位。
		 */
		if (left > 0 || not_last_frag)
			iph->frag_off |= htons(IP_MF);
		/*
		 * 更新后一个分节在整个原始数据包中的偏移量,
		 * 以及后一个分片在当前被分片数据包中的偏移量。
		 * 这两个偏移量是有区别的,因为一个数据包在
		 * 传输过程中可能被多次分片,因此当前被分片
		 * 数据包也由可能是另外一个数据包的分片。
		 */
		ptr += len;
		offset += len;

		/*
		 *	Put this fragment into the sending queue.
		 */
		/*
		 * 设置分片IP首部中总长度字段。
		 */
		iph->tot_len = htons(len + hlen);
......

上述代码也是有注释的,只提示两点:

1、分片的片偏移

分段偏移用于指明分段起始点相对报文起始点的偏移,长度为13位,以8个位组为单位。若MTU=1500时,一个大小为3000字节的数据经过该接口,会被分为端传输:

第一段长度为1480+20,第二段为1480,第三段为40,那么第一段分段的偏移为0,第二段为1480/8,第三段为185+185,所以在源码中需要乘以8,而在设置ip首部片偏移时又除以8的原因

2、ip选项的处理要注意,有的ip选项需要体现在所有的分片中,而有的不需要。

抱歉!评论已关闭.