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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章