tcp/ip 协议栈Linux源码分析四 IPv4分片 ip_fragment函数分析

内核版本:3.4.39

很多项目涉及到IP分片的时候都是绕过去了,感觉分片挺难的。但是老这么做也不行啊,抽空分析了内核的分片处理函数ip_fragment,也不是特别复杂,感觉挺简单的,看来事情只有实际去做才知道。

int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))
{
	struct iphdr *iph;
	int ptr;
	struct net_device *dev;
	struct sk_buff *skb2;
	unsigned int mtu, hlen, left, len, ll_rs;
	int offset;
	__be16 not_last_frag;
	struct rtable *rt = skb_rtable(skb);
	int err = 0;

    /* 获取路由里面的出口设备 */
	dev = rt->dst.dev;

    /* 获取IP头指针 */
	iph = ip_hdr(skb);

    /* 如果数据包携带不分片标志并且本地开启了pmtu发现(local_df==0),则需要
     * 给发送方返回一个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.
	 */

    /* 获取IP首部长度 */
	hlen = iph->ihl * 4;
	
	/* 获取分片报文数据部分大小(mtu 最大传输单元) */
	mtu = dst_mtu(&rt->dst) - hlen;	/* Size of data space */
	
#ifdef CONFIG_BRIDGE_NETFILTER
	if (skb->nf_bridge)
		mtu -= nf_bridge_mtu_reduction(skb);
#endif

    /* 设置标志位,ip_defrag重组函数会去判断这个标志位 */    
	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.
	 */
	 
	 /* 传输层如果已经进行了分片 */ 
	if (skb_has_frag_list(skb)) {
		struct sk_buff *frag, *frag2;

        /* 同一个页面内的报文长度,不包括其它分片 */
		int first_len = skb_pagelen(skb);

        /*
         * 以下四种情况是不能进行快速分片的
         * 1. 第一个报文长度大于mtu。
         * 2. 第一个报文长度不是8的倍数。
         * 3. 该报文自身就是分片报文,需要走慢速通道。
         * 4. 该报文是克隆的,因为快速分片是直接修改skb的,如果是克隆的
         *    则在其它地方存在引用,因此不能直接修改。
         */
		if (first_len - hlen > mtu ||
		    ((first_len - hlen) & 7) ||
		    ip_is_fragment(iph) ||
		    skb_cloned(skb))
			goto slow_path;

        /* 判断其它分片是否满足快速分片要求 */
		skb_walk_frags(skb, frag) {
			/* 如果SKB分片超过mtu则进入慢速分片。
			 * 如果SKB分片不是8的倍数并且不是最后一个分片也走慢速通道。
			 * 如果SKB分片头部空间无法塞下一个IP头
			 * 符合上述情况就进入慢速通道,和上面的慢速通道略有区别
			 */
			if (frag->len > mtu ||
			    ((frag->len & 7) && frag->next) ||
			    skb_headroom(frag) < hlen)
				goto slow_path_clean;

			/* 如果某个地方在引用skb结构体
			 * 就进入慢速通道。
			 */
			if (skb_shared(frag))
				goto slow_path_clean;

            /* 这里我有点疑问,为啥分片的skb不能有sk */
			BUG_ON(frag->sk);
			if (skb->sk) {
			    /* 将所有片段都关联到同一个套接字
			     * 设置套接字的回调函数
			     */
				frag->sk = skb->sk;
				frag->destructor = sock_wfree;
			}

			/* truesize 是skb的总长度,包括skb结构体和数据部分大小 
			 * 这里将其长度从skb中移除,相当于分离开来
			 */
			skb->truesize -= frag->truesize;
		}

		/* Everything is OK. Generate! */

		err = 0;
		offset = 0;

		/* 保存那些独立分片 */
		frag = skb_shinfo(skb)->frag_list;

        /* 清空首个报文的分片指针,该指针已经保存在frag里 */
		skb_frag_list_init(skb);

		/* 第一个报文非线性数据区长度 */
		skb->data_len = first_len - skb_headlen(skb);

		/* 设置skb数据长度,包括线性区和非线性区 */
		skb->len = first_len;

		/* 设置ip头域里面数据长度 */
		iph->tot_len = htons(first_len);

		/* 设置MF标志位 */
		iph->frag_off = htons(IP_MF);

        /* 设置IP头域校验和 */
		ip_send_check(iph);

        /* 准备其它分片 */
		for (;;) {
			if (frag) {
				frag->ip_summed = CHECKSUM_NONE;

                /* 设置传输层首部指针 */
				skb_reset_transport_header(frag);

                /* 插入IP首部长度 */                				                
				__skb_push(frag, hlen);

				/* 设置网络层首部指针 */
				skb_reset_network_header(frag);
				
                /* 复制IP头部,因为是后续分片,所以要设置
                 * 偏移值,长度和分片标志位,当然,校验和要重新计算。
                 */
				memcpy(skb_network_header(frag), iph, hlen);
				iph = ip_hdr(frag);
				iph->tot_len = htons(frag->len);

                /* 复制第一个分片报文的标志位,包括优先级,出口设备
                 * netfilter子模块使用到的标志位等等。
                 * 所有报文设置相同的属性
                 */
				ip_copy_metadata(frag, skb);

				/* 处理IP扩展选项,分片情况下扩展选项处理还是比较特殊的
				 * 有些选项要求所有分片报文都要携带,有些选项只需要首个分片报文携带。
				 * 详细情况参考RFC791
				 */
				if (offset == 0)
					ip_options_fragment(frag);

				/* 分片偏移值,等于之前报文长度累积和 */	
				/* 这里已经假定之前的报文长度都是一样的 */
				offset += skb->len - hlen;

				/* 一定能被8整除? */
				/* 没错,毕竟大小是之前分片的总和 */
				iph->frag_off = htons(offset>>3);

				/* 如果不是最后一个分片,就设置MF标识 */
				if (frag->next != NULL)
					iph->frag_off |= htons(IP_MF);

				/* Ready, complete checksum */
				/* 重新计算校验和 */
				ip_send_check(iph);
			}

            /* 调用发送接口发送出去 */
			err = output(skb);

			if (!err)
				IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
            /* 如果出错并且没有多余分片 */
			if (err || !frag)
				break;

            /* 当前skb传输完成,指向下一个分片 */
			skb = frag;
			frag = skb->next;
			skb->next = NULL;
		}

        /* 传输正常,增加MIB统计计数 */
		if (err == 0) {
			IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
			return 0;
		}

        /* 当传输结束后,释放skb */
		while (frag) {
			skb = frag->next;
			kfree_skb(frag);
			frag = skb;
		}
		IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
		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:
    /* 先计算纯数据总长度,这个长度不包括IP头长 */
	left = skb->len - hlen;		/* Space per frame */

	/* 指向数据的起始位置 */
	ptr = 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
	 */

	/* (链路层预留空间) */ 
	ll_rs = LL_RESERVED_SPACE_EXTRA(rt->dst.dev, nf_bridge_pad(skb));

	/*
	 *	Fragment the datagram.
	 */

    /* 获取偏移值,不从0开始是因为有可能需要分片的报文自身就是一个分片报文 */ 
	offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;

    /* 可能的取值包括0和1 */
	not_last_frag = iph->frag_off & htons(IP_MF);


    /* 进行分片处理,left的初值就是数据总长 */
	while (left > 0) {
		len = left;

		/* 确保每个报文长度不超过MTU */
		if (len > mtu)
			len = mtu;

		/* 确保分片报文大小长度为8字节的整数倍,最后一个分片除外 */   
		if (len < left)	{
			len &= ~7;
		}
		
        /* 分配一个新的skb buffer
         * 数据长度+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;
		}

        /* 复制一些公共数据 */
		ip_copy_metadata(skb2, skb);

		/* 预留链路层首部空间 */
		skb_reserve(skb2, ll_rs);

		/* 插入数据部分和IP头 */
		skb_put(skb2, len + hlen);

		/* 重置网络头指针 */
		skb_reset_network_header(skb2);

		/* 设置传输层头部指针 */
		skb2->transport_header = skb2->network_header + hlen;

        /** 将每一个分片的skb包都关联到源包的socket */
		if (skb->sk)
			skb_set_owner_w(skb2, skb->sk);

		/*
		 *	Copy the packet header into the new buffer.
		 */
        /* 复制IP报头 */
		skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);

		/*
		 *	Copy a block of the IP datagram.
		 *  从原始报文中的ptr起复制长度为len的数据到skb2中
		 */
		if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
			BUG();

        /* 从总长度中减去这个分片的长度,得到剩余部分的长度 */			
		left -= len;

		/*
		 *  接下来就是设置分片报文的网络层首部
		 */
		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.
		 */

		 /* 处理IP选项,对于分片报文来说有些选项是需要每个报文必须携带,
		  * 有些选项只需第一个分片报文携带就可以了。具体的操作可以参考RFC791
		  */
		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
		 */
		 
		/* 设置分片标识位 
		 * 刚才提到not_last_frag可以是0或者1,取决于它在原始分片报文的值
		 */ 
		if (left > 0 || not_last_frag)
			iph->frag_off |= htons(IP_MF);


        /* 更新数据指针,指向下一个分片报文的起始复制位置 */
		ptr += len;

		/* 更新片偏移字段 */
		offset += len;

        /* 更新长度字段 */
		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);
	}

	/* 分片结束,释放原始报文
	 * 这个和快速分片不同,快速分片是发送原始分片
	 * 慢速分片是新建skb然后复制发送
	 */
	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;
}
EXPORT_SYMBOL(ip_fragment);

参考文档:

1. linux内核ip分片函数ip_fragment解析  https://blog.csdn.net/force_eagle/article/details/4555314

2. SKB结构体分析 http://vger.kernel.org/~davem/skb.html

猜你喜欢

转载自blog.csdn.net/fuyuande/article/details/89206954