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 头,其它分片中不用传输层的头了,也就相当于把多个传输层的包合并了。

这也相当于在网络层去关心传输层的一个特例了吧。

时间: 2024-08-26 06:02:10

IP 包分片的相关文章

Lwip IP包分片重组

1. 开发环境 操作系统:SylixOS 编程环境:RealEvo-IDE3.1 硬件平台:AT9x25开发板 2. 技术实现 SylixOS系统使用的网络协议栈是Lwip协议栈.Lwip是Light Weight (轻型)IP协议,有无操作系统的支持都可以运行.Lwip实现的重点是在保持TCP协议主要功能的基础上减少对RAM 的占用,它只需十几KB的RAM和40K左右的ROM就可以运行,这使Lwip协议栈适合在低端的嵌入式系统中使用. Lwip协议栈主要关注的是怎么样减少内存的使用和代码的大小

IP包、TCP报文、UDP数据段格式的汇总

一.IP包格式 IP数据包是一种可变长分组,它由首部和数据负载两部分组成.首部长度一般为20-60字节(Byte),其中后40字节是可选的,长度不固定,前20字节格式为固定.数据负载部分的长度一般可变,整个IP数据包的最大长度为65535B. 1.版本号(Version) 长度为4位(bit),IP v4的值为0100,IP v6的值为0110. 2.首部长度 指的是IP包头长度,用4位(bit)表示,十进制值就是[0,15],一个IP包前20个字节是必有的,后40个字节根据情况可能有可能没有.

《TCP/IP详解卷2:实现》笔记--IP的分片和重装

IP首部内有三个字段实现分片和重装:标识字段(ip_id).标志字段(ip_off的3个高位比特)和偏移字段(ip_off的13个低位 比特).标志字段由3个1bit标志组成.比特0是保留的必须为0,:比特1是"不分片"(DF)标志:比特2是"更多分片"(MF)标志. Net/3中,标志和偏移字段结合起来,由ip_off访问,如下图所示: ip_off的其他13bit指出在原始数据报内分片的位置,以8字节为单位计算.因此,除最后一个分片外,其他的分片都希望是一个 8

TCP的分段和IP的分片

写在前面: 分组可以发生在运输层和网络层,运输层中的TCP会分段,网络层中的IP会分片.IP层的分片更多的是为运输层的UDP服务的,由于TCP自己会避免IP的分片,所以使用TCP传输在IP层都不会发生分片的现象. 我们在学习TCP/IP协议时都知道,TCP报文段如果很长的话,会在发送时发生分段,在接受时进行重组,同样IP数据报在长度超过一定值时也会发生分片,在接收端再将分片重组. 我们先来看两个与TCP报文段分段和IP数据报分片密切相关的概念. MYU(最大传输单元) MTU前面已经说过了,是链

C++ 捕获本机网卡的IP包并对其解析的实现

编程要求:捕获本机网卡的IP包,对捕获的IP包进行解析.要求必须输出以下字段:版本号.总长度.标志位.片偏移.协议.源地址和目的地址. TCP/IP协议定义了一个在因特网上传输的包,称为IP数据报(IP Datagram).这是一个与硬件无关的虚拟包,由首部和数据两部分组成.首部的前一部分是固定长度,共 20 字节,是所有IP数据报必须具有的.在首部的固定部分的后面是一些可选字段,其长度是可变的.下面我们看一下IP数据包的格式: 具体的说明: 各个字段说明 版本 IP协议版本号, IPv4此字段

IP包格式

网络层提供的服务就是在不同网段之间转发数据包. Ip包结构 1,格式(每行4byte*5) 2,版本 V4 V6 3,首部长度 20(固定)+可变长度 ?,区分服务 Win2008开始:gpedit.msc 设置DSCP值 5,总长度 设置头长度的意义:因为ip头有可变部分.头长度丈量头部长度,总长度-头长度=数据长度. 6,数据包分片 为什么要分片? 数据链路层最大允许1500byte. Ip层最大允许2的12次方(总长度2byte)65535字节. ID 数据包尺寸: 数据链路层数据:150

TCP/IP具体解释--TCP的分段和IP的分片

写在前面: 分组能够发生在运输层和网络层.运输层中的TCP会分段,网络层中的IP会分片.IP层的分片很多其它的是为运输层的UDP服务的,因为TCP自己会避免IP的分片,所以使用TCP传输在IP层都不会发生分片的现象. 我们在学习TCP/IP协议时都知道.TCP报文段假设非常长的话,会在发送时发生分段.在接受时进行重组,相同IP数据报在长度超过一定值时也会发生分片,在接收端再将分片重组. 我们先来看两个与TCP报文段分段和IP数据报分片密切相关的概念. MYU(最大传输单元) MTU前面已经说过了

以太网数据包、IP包、TCP/UDP 包的结构(转)

源:以太网数据包.IP包.TCP/UDP 包的结构 版本号(Version):长度4比特.标识目前采用的IP协议的版本号.一般的值为0100(IPv4),0110(IPv6). IP包头长度(Header Length):长度4比特.这个字段的作用是为了描述IP包头的长度,因为在IP包头中有变长的可选部分.该部分占4个bit位,单位为32bit(4个字节),即本区域值 = IP头部长度(单位为bit)/ (8*4),因此,一个IP包头的长度最长为“1111”,即15*4=60个字节.IP包头最小

IP报文分片

1. 最大传输单元(Maximum Transmission Unit,MTU). 以太网帧中的数据长度规定最小46 字节,最大1500 字节,MTU 指数据帧中有效载荷的最大长度,不包括帧首部的长度. 2. IP帧格式, 4 位首部长度的数值是以4 字节为单位的,最小值为5,也就是说首部长度最小是4x5=20 字节,也就是不带任何选项的IP 首部,4 位能表示的最大值是15,也就是说首部长度最大是60 字节. 标识,16位,用来唯一地标识主机发送的每一份数据报.IP软件会在存储器中维持一个计数