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

TCP实现之:IP分片内核实现

程序员文章站 2022-03-07 09:33:13
...

TCP实现之:IP分片内核实现

一、前言

先来回顾一下基本概念吧。啥是分片?啥是分段?

报文在网络设备间传输的时候,一次能够传输单个报文的尺寸是有限制的,这个限制被称为MTU:最大传输单元(Maximum Transmission Unit)。不同类型的网络的MTU大小可能有差异,如以太网的MTU为1500。这个MTU指的是报文所能携带的有效数据,因此并不是以太网数据帧的真实大小,其真实大小为:
1500 + 14 + 4 = 1518 1500 + 14 + 4 = 1518 1500+14+4=1518
其中14为以太网报头,4位为尾部校验和FCS

IP报文的长度大于MTU时,需要将报文拆分为多个长度不大于MTU的数据片,这个过程称为IP层的分片。由于单个数据片的丢失会导致整个IP报文的失效与重传,因此IP的分片要尽量避免。因此,对于TCP报文,在将报文交给IP协议之前,会将报文拆分成长度不大于MTU的数据段,这个过程称为TCP层的分段。

二、IP分片的实现

下面我们主要以原始套接字在进行报文发送时的IP分片为例来进行讲解。

2.1 原始套接字

针对于IP分片,原始套接字有两种情况:是否设置了IP_HDRINCL套接字选项。该选项用来控制内核是否生成IP报头:当设置该选项时,意味着IP头部已经包含在了用户数据里,不需要内核添加IP头部;否则,内核将根据套接字中的信息来为报文添加IP头部。

当设置了该选项时,内核将不会为该报文分片,报文长度如果大于MTU将直接返回错误。官方可能认为,在这种模式下,用户态将负责报文的全权处理,包括分片等操作。

当未设置该选项的时候,内核会调用ip_append_data来将用户数据添加到套接字的待发送队列中。下面我们来详细看一下这个函数的实现。

ip_append_data

static int __ip_append_data(struct sock *sk,
			    struct flowi4 *fl4,
			    struct sk_buff_head *queue,
			    struct inet_cork *cork,
			    struct page_frag *pfrag,
			    int getfrag(void *from, char *to, int offset,
					int len, int odd, struct sk_buff *skb),
			    void *from, int length, int transhdrlen,
			    unsigned int flags)
{
	struct inet_sock *inet = inet_sk(sk);
	struct sk_buff *skb;

	struct ip_options *opt = cork->opt;
	int hh_len;
	int exthdrlen;
	int mtu;
	int copy;
	int err;
	int offset = 0;
	unsigned int maxfraglen, fragheaderlen, maxnonfragsize;
	int csummode = CHECKSUM_NONE;
	struct rtable *rt = (struct rtable *)cork->dst;
	u32 tskey = 0;

	skb = skb_peek_tail(queue); //取出发送队列最后一个skb

	exthdrlen = !skb ? rt->dst.header_len : 0;
	mtu = cork->fragsize; //cork里面保存了IP分片过程中需要用到的很多参数,fragsize为MTU
	if (cork->tx_flags & SKBTX_ANY_SW_TSTAMP &&
	    sk->sk_tsflags & SOF_TIMESTAMPING_OPT_ID)
		tskey = sk->sk_tskey++;

	hh_len = LL_RESERVED_SPACE(rt->dst.dev); //以太网头部长度

	fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0); //IP头部长度
	maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen; //最大片段长度,指的是IP报文总长度。
	maxnonfragsize = ip_sk_ignore_df(sk) ? 0xFFFF : mtu; //最大允许的未分片的数据的尺寸。也就是说,数据的总长度不能超过这个值。如果sk设置的ignore_df(忽略禁止分片),那么其大小为0xFFFF;否则为MTU。

    /* cork->length为已经添加的数据长度。若数据长度超过允许的长度,返回EMSGSIZE。 */
	if (cork->length + length > maxnonfragsize - fragheaderlen) {
		ip_local_error(sk, EMSGSIZE, fl4->daddr, inet->inet_dport,
			       mtu - (opt ? opt->optlen : 0));
		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->dst.dev->features & (NETIF_F_HW_CSUM | NETIF_F_IP_CSUM) &&
	    !(flags & MSG_MORE) &&
	    !exthdrlen)
		csummode = CHECKSUM_PARTIAL;

	cork->length += length;
    /* 对于GSO标志的报文,或者是支持UFO的网卡驱动(网卡驱动进行的UDP分段),使用ip_ufo_append_data方法来处理。该方法不会对报文进行分片。 */
	if ((skb && skb_is_gso(skb)) ||
	    ((length > mtu) &&
	    (skb_queue_len(queue) <= 1) &&
	    (sk->sk_protocol == IPPROTO_UDP) &&
	    (rt->dst.dev->features & NETIF_F_UFO) && !rt->dst.header_len &&
	    (sk->sk_type == SOCK_DGRAM) && !sk->sk_no_check_tx)) {
		err = ip_ufo_append_data(sk, queue, getfrag, from, length,
					 hh_len, fragheaderlen, transhdrlen,
					 maxfraglen, 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.
	 */

	if (!skb)
		goto alloc_new_skb;
    
    /* 开始循环将报文数据添加到套接口的发送队列中。 */
    while (length > 0) {
		/* 检查当前的skb可以容纳的报文数据长度。 */
		copy = mtu - skb->len;
		if (copy < length)
			copy = maxfraglen - skb->len;
        /* 当前skb满了,重新分配一个新的skb。 */
		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:
			skb_prev = skb;
			if (skb_prev)
				fraggap = skb_prev->len - maxfraglen;
			else
				fraggap = 0;

			/* datalen为要拷贝的数据的长度(不包括报文头部) */
			datalen = length + fraggap;
			if (datalen > mtu - fragheaderlen)
				datalen = maxfraglen - fragheaderlen;
			fraglen = datalen + fragheaderlen;

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

            /* 计算需要分配的skb线性缓存区的大小。该大小由报文长度、headroom和tailroom组成。 */
			alloclen += exthdrlen;

			/* 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->dst.trailer_len;

            /* 进行skb的创建 */
			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))
					err = -ENOBUFS;
			}
			if (!skb)
				goto error;

			/*
			 *	Fill in the control structures
			 */
			skb->ip_summed = csummode;
			skb->csum = 0;
            /* 开辟headroom。在此之前,head、data、tail指向同一位置:线性缓存区的头部。 */
			skb_reserve(skb, hh_len);

			/* only the initial fragment is time stamped */
			skb_shinfo(skb)->tx_flags = cork->tx_flags;
			cork->tx_flags = 0;
			skb_shinfo(skb)->tskey = tskey;
			tskey = 0;

            /* 开辟data空间,将tail往下移动,并增加len计数。 */
			data = skb_put(skb, fraglen + exthdrlen);
            /* 设置三层协议报头偏移。 */
			skb_set_network_header(skb, exthdrlen);
            /* 设置四层协议报头偏移。 */
			skb->transport_header = (skb->network_header +
						 fragheaderlen);
            /* 此时data指向了IP数据区 */
			data += fragheaderlen + exthdrlen;

            /* fraggap代表上一个skb多余的数据长度,要转移到当前的skb。不太懂这种操作。 */
			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);
			}

            /* 计算要拷贝的数据长度,去除fraggap和transhdrlen。 */
			copy = datalen - transhdrlen - fraggap;
            
            /*
             * getfrag是用于对from进行数据分片的函数,其可以是raw_getfrag、udp_getfrag等。
             * 对于raw_getfrag,它所做的事情是把msg迭代器中的copy长度的报文数据拷贝到skb的报文区。
             * 其实这里的offset没有多大的意义,因为msg迭代器拷贝时会从上一次结束的地方重新开始。
             */
			if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0) {
				err = -EFAULT;
				kfree_skb(skb);
				goto error;
			}

			offset += copy;
			length -= datalen - fraggap;
			transhdrlen = 0;
			exthdrlen = 0;
			csummode = CHECKSUM_NONE;

			/*
			 * 将新创建的skb添加到发送队列。从这里我们可以看出,其并没有对IP头部进行初始化,只是保留了IP头部空间。
			 */
			__skb_queue_tail(queue, skb);
			continue;
		}

        /* 之前的skb有足够的空间,不需要重新分配skb。 */
		if (copy > length)
			copy = length;

        /* 设备不支持分散聚合IO,并且skb的tailroom有足够的空间。 */
		if (!(rt->dst.dev->features&NETIF_F_SG) &&
		    skb_tailroom(skb) >= copy) {
			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 {
            /* 使用frags。此时,skb里存的数据量还没有达到MTU,但是线性缓存区已经满了 */
			int i = skb_shinfo(skb)->nr_frags;

			err = -ENOMEM;
			if (!sk_page_frag_refill(sk, pfrag))
				goto error;

			if (!skb_can_coalesce(skb, i, pfrag->page,
					      pfrag->offset)) {
				err = -EMSGSIZE;
				if (i == MAX_SKB_FRAGS)
					goto error;

				__skb_fill_page_desc(skb, i, pfrag->page,
						     pfrag->offset, 0);
				skb_shinfo(skb)->nr_frags = ++i;
				get_page(pfrag->page);
			}
			copy = min_t(int, copy, pfrag->size - pfrag->offset);
			if (getfrag(from,
				    page_address(pfrag->page) + pfrag->offset,
				    offset, copy, skb->len, skb) < 0)
				goto error_efault;

			pfrag->offset += copy;
			skb_frag_size_add(&skb_shinfo(skb)->frags[i - 1], 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_efault:
	err = -EFAULT;g
        
error:
	cork->length -= length;
	IP_INC_STATS(sock_net(sk), IPSTATS_MIB_OUTDISCARDS);
	return err;

总结一下,ip_append_data是一个相对比较通用的函数。它的作用是把数据添加到套接字发送队列中的skb中去。如果数据量很小,它不会创建新的skb,而是将数据添加到最后一个skbfrag中(skb的数据量没有超过MTU)。如果数据量很大,那么它会将其进行拆分。需要注意的是,这里的拆分并不是IP的分片,具体的拆分过程会根据传递进去的getfrag函数而不同。这里创建的skb单个数据量都不会超过MTU,而且skb中没有构建IP报头。

ip_push_pending_frames

这个函数一般都是与上面那个ip_append_data配合使用的,即前者想将用户态的报文数据整理好存储到套接字发送队列中,此函数再将队列中的数据一次性发送出去。值得一提的是,当前队列中的所有数据都是一块的,也就是说他们都属于同一个上层(用户态)传递下来的数据,比如说一个大的UDP数据块。

int ip_push_pending_frames(struct sock *sk, struct flowi4 *fl4)
{
	struct sk_buff *skb;

	skb = ip_finish_skb(sk, fl4);
	if (!skb)
		return 0;

	/* Netfilter gets whole the not fragmented skb. */
	return ip_send_skb(sock_net(sk), skb);
}

该函数首先调用了ip_finish_skb,这个函数是__ip_make_skb的封装。而__ip_make_skb这个函数的作用我们之前也已经讲过了,它是用来将套接字发送队列中的skb组装成一个skb的。具体是怎么个逻辑呢?它取出发送队列queue中的第一个skb,并将其余的skb一个接一个的连接到第一个skbfrag_list中,为后面的IP分片做准备。第一个skb的长度为所有skb的数据部分总长度,且其余的skbdata均指向IP数据部分(不包含IP头部),只有第一个skb拥有IP头部。

struct sk_buff *__ip_make_skb(struct sock *sk,
			      struct flowi4 *fl4,
			      struct sk_buff_head *queue,
			      struct inet_cork *cork)
{
	struct sk_buff *skb, *tmp_skb;
	struct sk_buff **tail_skb;
	struct inet_sock *inet = inet_sk(sk);
	struct net *net = sock_net(sk);
	struct ip_options *opt = NULL;
	struct rtable *rt = (struct rtable *)cork->dst;
	struct iphdr *iph;
	__be16 df = 0;
	__u8 ttl;

    /* 取出队列中的第一个skb元素 */
	skb = __skb_dequeue(queue);
	if (!skb)
		goto out;
    /* 取出第一个元素的frag_list */
	tail_skb = &(skb_shinfo(skb)->frag_list);

	/* 将skb的data设置到IP头部 */
	if (skb->data < skb_network_header(skb))
		__skb_pull(skb, skb_network_offset(skb));
    
    /* 循环取出queue中的所有skb,并逐个添加到第一个skb的frag_list中。 */
	while ((tmp_skb = __skb_dequeue(queue)) != NULL) {
        /* 将tmp_skb的data移动到IP报文的数据区域(排除IP头部) */
		__skb_pull(tmp_skb, skb_network_header_len(skb));
		*tail_skb = tmp_skb;
		tail_skb = &(tmp_skb->next);
		skb->len += tmp_skb->len;
		skb->data_len += tmp_skb->len;
		skb->truesize += tmp_skb->truesize;
		tmp_skb->destructor = NULL;
		tmp_skb->sk = NULL;
	}

	/* Unless user demanded real pmtu discovery (IP_PMTUDISC_DO), we allow
	 * to fragment the frame generated here. No matter, what transforms
	 * how transforms change size of the packet, it will come out.
	 */
	skb->ignore_df = ip_sk_ignore_df(sk);

	/* DF bit is set when we want to see DF on outgoing frames.
	 * If ignore_df is set too, we still allow to fragment this frame
	 * locally. */
	if (inet->pmtudisc == IP_PMTUDISC_DO ||
	    inet->pmtudisc == IP_PMTUDISC_PROBE ||
	    (skb->len <= dst_mtu(&rt->dst) &&
	     ip_dont_fragment(sk, &rt->dst)))
		df = htons(IP_DF);

	if (cork->flags & IPCORK_OPT)
		opt = cork->opt;

	if (cork->ttl != 0)
		ttl = cork->ttl;
	else if (rt->rt_type == RTN_MULTICAST)
		ttl = inet->mc_ttl;
	else
		ttl = ip_select_ttl(inet, &rt->dst);

    /* 构造IP头部 */
	iph = ip_hdr(skb);
	iph->version = 4;
	iph->ihl = 5;
	iph->tos = (cork->tos != -1) ? cork->tos : inet->tos;
	iph->frag_off = df;
	iph->ttl = ttl;
	iph->protocol = sk->sk_protocol;
	ip_copy_addrs(iph, fl4);
	ip_select_ident(net, skb, sk);

	if (opt) {
		iph->ihl += opt->optlen>>2;
		ip_options_build(skb, opt, cork->addr, rt, 0);
	}

	skb->priority = (cork->tos != -1) ? cork->priority: sk->sk_priority;
	skb->mark = cork->mark;
	skb->tstamp = cork->transmit_time;
	/*
	 * Steal rt from cork.dst to avoid a pair of atomic_inc/atomic_dec
	 * on dst refcount
	 */
	cork->dst = NULL;
	skb_dst_set(skb, &rt->dst);

	if (iph->protocol == IPPROTO_ICMP)
		icmp_out_count(net, ((struct icmphdr *)
			skb_transport_header(skb))->type);

	ip_cork_release(cork);
out:
	return skb;
}

构造出来一个完整的skb后,ip_send_skb负责将这个报文发送出去。后面的发送流程与我们之前的关于IP协议实现部分就是一致的了,流程为:

ip_send_skb -> ip_local_out -> __ip_local_out -> dst_output -> ip_output -> ip_finish_output

值得注意的是,在ip_finish_output函数中会根据skb的长度来选择是否需要对skb进行分片,即真正的IP分片是在这里进行的,具体的分片函数为:ip_fragment

ip_fragment

分片函数会先进行简单的检查,主要为DFDon't Fragment)标志位的检查。

static int ip_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
		       unsigned int mtu,
		       int (*output)(struct net *, struct sock *, struct sk_buff *))
{
	struct iphdr *iph = ip_hdr(skb);

    /* 检查IP头部是否设置了DF,没有的话直接进行分片。 */
	if ((iph->frag_off & htons(IP_DF)) == 0)
		return ip_do_fragment(net, sk, skb, output);

    /* IP头部设置了DF,且skb没有设置ignore_df,则返回错误。 */
	if (unlikely(!skb->ignore_df ||
		     (IPCB(skb)->frag_max_size &&
		      IPCB(skb)->frag_max_size > mtu))) {
		IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
		icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
			  htonl(mtu));
		kfree_skb(skb);
		return -EMSGSIZE;
	}

	return ip_do_fragment(net, sk, skb, output);
}

检查都通过后,调用ip_do_fragment来对报文进行真正的分片。其实分片要做的工作不多,因为skb的构造在之前就已经完成了,主要包括以下几点:

  1. 对于具有片段的skb,将第一个skbIP头部拷贝到frag_list中所有的skb里;
  2. 计算IP报头中分片的偏移量;
  3. 调用output将报文发送出去。

以下代码为针对frag_listIP分片。如果存在frag_list,就意味着队列中的每个skb的长度都不会大于MTU,只需要进行IP头部的封装就行了。

int ip_do_fragment(struct net *net, struct sock *sk, struct sk_buff *skb,
		   int (*output)(struct net *, struct sock *, struct sk_buff *))
{
	struct iphdr *iph;
	struct sk_buff *skb2;
	struct rtable *rt = skb_rtable(skb);
	unsigned int mtu, hlen, ll_rs;
	struct ip_fraglist_iter iter;
	ktime_t tstamp = skb->tstamp;
	struct ip_frag_state state;
	int err = 0;

	/* for offloaded checksums cleanup checksum before fragmentation */
	if (skb->ip_summed == CHECKSUM_PARTIAL &&
	    (err = skb_checksum_help(skb)))
		goto fail;

	/*
	 *	提取出第一个skb的IP头部地址,作为所有片段的IP头部.
	 */

	iph = ip_hdr(skb);

	/* 计算分片的MTU。 */
	mtu = ip_skb_dst_mtu(sk, skb);
	if (IPCB(skb)->frag_max_size && IPCB(skb)->frag_max_size < mtu)
		mtu = IPCB(skb)->frag_max_size;

	/*
	 *	Setup starting values.
	 */

	hlen = iph->ihl * 4;
	mtu = mtu - hlen;	/* Size of data space */
	IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;
	ll_rs = LL_RESERVED_SPACE(rt->dst.dev);

	/* 
	 * 检查到skb存在frag_list,进入frag_list分片流程。
	 */
	if (skb_has_frag_list(skb)) {
		struct sk_buff *frag, *frag2;
		/* 第一个skb线性和frags数据长度的总和,不包括分片队列的长度。 */
		unsigned int first_len = skb_pagelen(skb);

		/* 如果去除了分片队列的长度后,skb的长度还是大于MTU,则进入到slow_path流程,即线性数据IP分片流程 */
		if (first_len - hlen > mtu ||
		    ((first_len - hlen) & 7) ||
		    ip_is_fragment(iph) ||
		    skb_cloned(skb) ||
		    skb_headroom(skb) < ll_rs)
			goto slow_path;

        /* 遍历skb的分片队列,检查每个片段是否有效。 */
		skb_walk_frags(skb, frag) {
			/* 对于长度超标或者headroom不足的报文,进入到slow_path_clean流程。该流程会恢复skb片段的初始状态,并进入slow_path处理流程。 */
			if (frag->len > mtu ||
			    ((frag->len & 7) && frag->next) ||
			    skb_headroom(frag) < hlen + ll_rs)
				goto slow_path_clean;

			/* Partially cloned skb? */
			if (skb_shared(frag))
				goto slow_path_clean;

			BUG_ON(frag->sk);
			if (skb->sk) {
				frag->sk = skb->sk;
				frag->destructor = sock_wfree;
			}
			skb->truesize -= frag->truesize;
		}

		/* 生成IP头部校验码、设置MF标志、设置IP头部正确的数据长度等。 */
		ip_fraglist_init(skb, iph, hlen, &iter);

        /* 使用迭代器进入片段发送流程。 */
		for (;;) {
			/* Prepare header of the next frame,
			 * before previous one went down. */
			if (iter.frag) {
                /* 拷贝上一个skb的cb部分内容到当前片段。 */
				ip_fraglist_ipcb_prepare(skb, &iter);
                /* 拷贝上一个skb的IP头部部分内容到当前片段,设置分片偏移量,计算校验码等。 */
				ip_fraglist_prepare(skb, &iter);
			}

            /* 设置时间戳,并调用output进行报文的发送。 */
			skb->tstamp = tstamp;
			err = output(net, sk, skb);

			if (!err)
				IP_INC_STATS(net, IPSTATS_MIB_FRAGCREATES);
			if (err || !iter.frag)
				break;

			skb = ip_fraglist_next(&iter);
		}

		if (err == 0) {
			IP_INC_STATS(net, IPSTATS_MIB_FRAGOKS);
			return 0;
		}

		kfree_skb_list(iter.frag);

		IP_INC_STATS(net, 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:
	
	/* 初始化state对象,该对象用于存储分片过程中用到的一些信息,包括MTU、剩余数据长度、分片偏移等。 */
	ip_frag_init(skb, hlen, ll_rs, mtu, IPCB(skb)->flags & IPSKB_FRAG_PMTU,
		     &state);

	/* 循环拷贝skb中的数据 */
	while (state.left > 0) {
        /* 根据偏移量判断当前是否是第一个IP分片。 */
		bool first_frag = (state.offset == 0);

        /* 分配一个新的skb2,并将skb中的元数据、IP报头、以及剩余的数据(长度不超过MTU)拷贝进去,设置分片偏移量、计算校验码等。注意,在进行数据拷贝时,会依次从线性区域、frags和frag_list中取数据。 */
		skb2 = ip_frag_next(skb, &state);
		if (IS_ERR(skb2)) {
			err = PTR_ERR(skb2);
			goto fail;
		}
        /* 拷贝IP标志和选项。 */
		ip_frag_ipcb(skb, skb2, first_frag, &state);

		/*
		 *	Put this fragment into the sending queue.
		 */
		skb2->tstamp = tstamp;
        /* 发送出去。 */
		err = output(net, sk, skb2);
		if (err)
			goto fail;

		IP_INC_STATS(net, IPSTATS_MIB_FRAGCREATES);
	}
	consume_skb(skb);
	IP_INC_STATS(net, IPSTATS_MIB_FRAGOKS);
	return err;

fail:
	kfree_skb(skb);
	IP_INC_STATS(net, IPSTATS_MIB_FRAGFAILS);
	return err;
}

至此,整个IP的分片过程就完成了。

从以上的内容中我们可以看出,虽然IP_HDRINCL类型的套接字不支持IP分片,那也只是在一开始长度检查的时候进行了限制,如果把raw_send_hdrinc里长度检查部分去掉,后面的流程里是可以进行正常的IP分片的。

2.2 UDP报文分片

UDP报文发送的流程为:

udp_sendmsg -> __ip_append_data -> __ip_make_skb -> ip_send_skb -> ip_local_out -> __ip_local_out -> dst_output -> ip_output -> ip_finish_output

可以看出来,UDP的发送与原始套接字的流程基本上相似,只不过在udp_sendmsg函数中,内核会对用户态传递进来的UDP数据封装UDP头部。

相关标签: 协议栈