IP 包分片

        本文以 linux 2.6.27.62 中 UDP 發包過程中重要的一個 IP 層的函數來分析 IP 層是如何分片的。

        科普一下,什麼是 IP 包分片,在某一個鏈路上,比如在以太網鏈路上,每次所能發送最大的包是有限制的,叫做 MTU,也就是 IP 層要想發包,每次包大小必須不大於 MTU,見上一篇文章,但傳輸層很有可能發送大於這個值的數據,此時  IP 層會對這些數據(可以稱爲 IP 包)進行分片,然後在收到時,在 IP 層再進行重組,形成一個 IP 包,交給傳輸層。

        代碼如下:

int ip_append_data(struct sock *sk,
		   int getfrag(void *from, char *to, int offset, int len,
			       int odd, struct sk_buff *skb),
		   void *from, int length, int transhdrlen,
		   struct ipcm_cookie *ipc, struct rtable *rt,
		   unsigned int flags)
{
	struct inet_sock *inet = inet_sk(sk);
	struct sk_buff *skb;

	struct ip_options *opt = NULL;
	int hh_len;
	int exthdrlen;
	int mtu;
	int copy;
	int err;
	int offset = 0;
	unsigned int maxfraglen, fragheaderlen;
	int csummode = CHECKSUM_NONE;

	// 如果只是爲了探測,則不發包,直接返回
	if (flags&MSG_PROBE)
		return 0;

	// 檢查發送隊列是否爲空,如果爲空,則表示這是 IP 包的第一個分片
	if (skb_queue_empty(&sk->sk_write_queue)) {
		/*
		 * setup for corking.
		 */
		opt = ipc->opt;
		if (opt) {
			if (inet->cork.opt == NULL) {
				inet->cork.opt = kmalloc(sizeof(struct ip_options) + 40, sk->sk_allocation);
				if (unlikely(inet->cork.opt == NULL))
					return -ENOBUFS;
			}
			memcpy(inet->cork.opt, opt, sizeof(struct ip_options)+opt->optlen);
			inet->cork.flags |= IPCORK_OPT;
			inet->cork.addr = ipc->addr;
		}
		dst_hold(&rt->u.dst);
		inet->cork.fragsize = mtu = inet->pmtudisc == IP_PMTUDISC_PROBE ?
					    rt->u.dst.dev->mtu :
					    dst_mtu(rt->u.dst.path);
		inet->cork.dst = &rt->u.dst;
		inet->cork.length = 0;
		sk->sk_sndmsg_page = NULL;
		sk->sk_sndmsg_off = 0;
		if ((exthdrlen = rt->u.dst.header_len) != 0) {
			length += exthdrlen;
			transhdrlen += exthdrlen;
		}
	} else {
		rt = (struct rtable *)inet->cork.dst;
		if (inet->cork.flags & IPCORK_OPT)
			opt = inet->cork.opt;

		transhdrlen = 0;		// 如果該 IP 包裏還有分片,那麼就會忽略掉此次的傳輸層頭信息,直接添加到上一個 IP 包
		exthdrlen = 0;
		mtu = inet->cork.fragsize;
	}

	// 計算 L2 層頭部長度,即鏈路層,以太網爲 1500
	hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);

	// 計算該層,即 IP 層頭部長度
	fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);
	// 計算該分片,如果不是最後一片,那麼它的載荷最大爲多少,8 字節對齊的原因,見上一篇《IP層分析》一文
	// maxfraglen 表示如果不是最後一個分片的分片的最大長度
	maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen;

	// 該 IP 包的載荷是否超過了最大限制,總大小爲什麼是 0xFFFF,見上一文
	if (inet->cork.length + length > 0xFFFF - fragheaderlen) {
		ip_local_error(sk, EMSGSIZE, rt->rt_dst, inet->dport, mtu-exthdrlen);
		return -EMSGSIZE;
	}

	/*
	 * transhdrlen > 0 means that this is the first fragment and we wish
	 * it won't be fragmented in the future.
	 */
	if (transhdrlen &&
	    length + fragheaderlen <= mtu &&
	    rt->u.dst.dev->features & NETIF_F_V4_CSUM &&
	    !exthdrlen)
		csummode = CHECKSUM_PARTIAL;             // 讓硬件,即網卡計算校驗和

	// 更新該 IP 包已累積的數據的長度,cork 相當於軟木塞,使小的數據包可以積累成爲一個大的 IP 包
	inet->cork.length += length;
	if (((length> mtu) || !skb_queue_empty(&sk->sk_write_queue)) &&
	    (sk->sk_protocol == IPPROTO_UDP) &&
	    (rt->u.dst.dev->features & NETIF_F_UFO)) {
		err = ip_ufo_append_data(sk, getfrag, from, length, hh_len,
					 fragheaderlen, transhdrlen, mtu,
					 flags);
		if (err)
			goto error;
		return 0;
	}

	/* So, what's going on in the loop below?
	 *
	 * We use calculated fragment length to generate chained skb,
	 * each of segments is IP fragment ready for sending to network after
	 * adding appropriate IP header.
	 */
	// 取出該 IP 包的最後一個 sk_buff,即最後一個分片
	if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL)
		goto alloc_new_skb;

	// 下面開始 IP 分片的主邏輯
	while (length > 0) {
		/* Check if the remaining data fits into current packet. */
		// 檢查最後一個分片的剩餘空間是否可以滿足當前的包,最後一個分片的大小因爲不需要滿足 8 字節對齊
		// 所以它的大小有可能,大於 maxfraglen,但肯定小於 mtu. 所以如果 copy 如果可以滿足 length,那麼
		// 就不用申請新的分片,直接填充到最後一個分片中。但如果 copy 不能滿足 length (copy < length), 
		// 那麼就需要新和分片,此時上次的最後一個分片的大小就需要做 8 字節對齊處理。所以 copy 記錄了能夠從
		// length 中拷貝的數據的大小
		copy = mtu - skb->len;
		if (copy < length)
			copy = maxfraglen - skb->len;

		// 如果最後一個分片不能滿足此次請求,並且 skb->len >= maxfraglen時,此時 copy <= 0, 也就是最後一個
		// 分片有可能需要作處理,移動最後沒有 8 字節對齊的部分
		if (copy <= 0) {
			char *data;
			unsigned int datalen;
			unsigned int fraglen;
			unsigned int fraggap;
			unsigned int alloclen;
			struct sk_buff *skb_prev;
alloc_new_skb:
			// 取出上一個分片,因爲上一個分片在處理時有可能被當作最後一個分片處理,長度可能不是 8 的倍數,此處要處理這種情況
			skb_prev = skb;
			if (skb_prev)
				fraggap = skb_prev->len - maxfraglen;			// 計算最後一個分片是否需要做字節對齊處理
			else
				fraggap = 0;

			/*
			 * If remaining data exceeds the mtu,
			 * we know we need more fragment(s).
			 */
			// 計算需要拷貝到新的分片中的數據長度
			datalen = length + fraggap;
			// 如果不能當作最後一個分片全部處理掉,那麼說明還需要更多的分片,此時將要新申請的分片就需要做對齊處理了
			if (datalen > mtu - fragheaderlen)
				datalen = maxfraglen - fragheaderlen;
			// 將要填充的分片的長度
			fraglen = datalen + fragheaderlen;

			if ((flags & MSG_MORE) &&
			    !(rt->u.dst.dev->features&NETIF_F_SG))
				alloclen = mtu;
			else
				alloclen = datalen + fragheaderlen;

			/* The last fragment gets additional space at tail.
			 * Note, with MSG_MORE we overallocate on fragments,
			 * because we have no idea what fragment will be
			 * the last.
			 */
			// 如果可以在新的分片中全部處理掉,即不需要更多的分片,將作爲最後一個分片處理
			if (datalen == length + fraggap)
				alloclen += rt->u.dst.trailer_len;

			// 如果是第一個分片
			if (transhdrlen) {
				skb = sock_alloc_send_skb(sk,
						alloclen + hh_len + 15,
						(flags & MSG_DONTWAIT), &err);
			} else {
				skb = NULL;
				if (atomic_read(&sk->sk_wmem_alloc) <=
				    2 * sk->sk_sndbuf)
					skb = sock_wmalloc(sk,
							   alloclen + hh_len + 15, 1,
							   sk->sk_allocation);
				if (unlikely(skb == NULL))
					err = -ENOBUFS;
			}
			if (skb == NULL)
				goto error;

			/*
			 *	Fill in the control structures
			 */
			skb->ip_summed = csummode;
			skb->csum = 0;
			// 保留 L2 層,即鏈路層長度,該保留動作不會影響 skb->len, skb->len 只記錄了 IP 層數據的長度,包括 IP 頭信息
			skb_reserve(skb, hh_len);

			/*
			 *	Find where to start putting bytes.
			 */
			data = skb_put(skb, fraglen);
			skb_set_network_header(skb, exthdrlen);
			skb->transport_header = (skb->network_header +
						 fragheaderlen);
			data += fragheaderlen;

			// 處理上次最後一個分片中需要字節對齊的部分
			if (fraggap) {
				skb->csum = skb_copy_and_csum_bits(
					skb_prev, maxfraglen,
					data + transhdrlen, fraggap, 0);
				skb_prev->csum = csum_sub(skb_prev->csum,
							  skb->csum);
				data += fraggap;
				pskb_trim_unique(skb_prev, maxfraglen);
			}

			// 計算能夠從用戶數據中拷貝的字節數,如果是第一個分片,傳進來的載荷其實是包含傳輸層頭大小的
			copy = datalen - transhdrlen - fraggap;
			// 拷貝到新的分片中
			if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {
				err = -EFAULT;
				kfree_skb(skb);
				goto error;
			}
			// 計算偏移
			offset += copy;
			// 計算此次處理掉的用戶數據的字節數,datalen 是可能包含傳輸頭信息的,傳輸頭也相當於被處理掉了
			length -= datalen - fraggap;
			transhdrlen = 0;
			exthdrlen = 0;
			csummode = CHECKSUM_NONE;

			/*
			 * Put the packet on the pending queue.
			 */
			__skb_queue_tail(&sk->sk_write_queue, skb);
			continue;
		}

		// 如果最後一個分片能夠滿足請求
		if (copy > length)
			copy = length;

		// 如果不支持離散聚合 I/O
		if (!(rt->u.dst.dev->features&NETIF_F_SG)) {
			unsigned int off;
			// 拷貝傳輸層的數據到分片中
			off = skb->len;
			if (getfrag(from, skb_put(skb, copy),
					offset, copy, off, skb) < 0) {
				__skb_trim(skb, off);
				err = -EFAULT;
				goto error;
			}
		} else {
			int i = skb_shinfo(skb)->nr_frags;
			skb_frag_t *frag = &skb_shinfo(skb)->frags[i-1];
			struct page *page = sk->sk_sndmsg_page;
			int off = sk->sk_sndmsg_off;
			unsigned int left;

			if (page && (left = PAGE_SIZE - off) > 0) {
				if (copy >= left)
					copy = left;
				if (page != frag->page) {
					if (i == MAX_SKB_FRAGS) {
						err = -EMSGSIZE;
						goto error;
					}
					get_page(page);
					skb_fill_page_desc(skb, i, page, sk->sk_sndmsg_off, 0);
					frag = &skb_shinfo(skb)->frags[i];
				}
			} else if (i < MAX_SKB_FRAGS) {
				if (copy > PAGE_SIZE)
					copy = PAGE_SIZE;
				page = alloc_pages(sk->sk_allocation, 0);
				if (page == NULL)  {
					err = -ENOMEM;
					goto error;
				}
				sk->sk_sndmsg_page = page;
				sk->sk_sndmsg_off = 0;

				skb_fill_page_desc(skb, i, page, 0, 0);
				frag = &skb_shinfo(skb)->frags[i];
			} else {
				err = -EMSGSIZE;
				goto error;
			}
			if (getfrag(from, page_address(frag->page)+frag->page_offset+frag->size, offset, copy, skb->len, skb) < 0) {
				err = -EFAULT;
				goto error;
			}
			sk->sk_sndmsg_off += copy;
			frag->size += copy;
			skb->len += copy;
			skb->data_len += copy;
			skb->truesize += copy;
			atomic_add(copy, &sk->sk_wmem_alloc);
		}
		offset += copy;
		length -= copy;
	}

	return 0;

error:
	inet->cork.length -= length;
	IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS);
	return err;
}

        通過代碼分析,我們不難發現,在使用 UDP 傳輸時,由於有 cork 操作的存在(該操作可由用戶控制),當頻繁發送小數據時,會累積成一個 IP 包,當發送大數據時,如果不超過 IP 層所能接受的最大長度,則 IP 層會對它進行分片,並且很有可能與上一個 IP 包粘連。
        Remark: IP 層的一些特性,爲什麼中間分片需要 8 字節對齊,可參見上一篇中 <<IP層分析>>,最後一個分片是不需要 8 字節對齊的,在代碼中爲了處理這些情況,有非常多的邏輯。還有,此次請求的最後一個分片,很有可能在下次請求時,爲了利用上一次的 IP 包,從而使得本來的最後分片,成爲了中間分片,進而必須處理掉分片中的字節對齊的情況。 sk_buff 是個重要的結構,它是用來描述 IP 包中的 IP 分片的信息的。因爲對 IP 層來講,它只關心自己的頭信息和載荷,但一般能組成一個 UDP 包的載荷其實只需要一個 UDP 頭,所以一般只有第一個分片中帶有 UDP 頭,其它分片中不用傳輸層的頭了,也就相當於把多個傳輸層的包合併了。


這也相當於在網絡層去關心傳輸層的一個特例了吧。
發佈了67 篇原創文章 · 獲贊 813 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章