欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2024-03-19 22:49:22
...

内核版本: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

相关标签: ip分片