1. 程式人生 > >linux核心學習筆記------ip報文的分片

linux核心學習筆記------ip報文的分片

對網路比較熟悉的童鞋都知道,當傳送的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選項需要體現在所有的分片中,而有的不需要。