linux协议栈 IPv4之发送过程中的分段处理ip

it2025-12-07  14

目录

1 分片位置

2 ip_fragment()代码分析


1 分片位置

分段是网络层的一个重要任务,网络层需要对两方面的IP数据包进行分段:

本地产生的数据包;转发的数据包;

这两种数据包的长度如果超过了出口设备的MTU(或者PMTU),则网络层必须先对数据包进行分段,使其适配出口设备的MTU。

IPv4使用 ip_fragment() 处理分段,在设计时,要求该函数能够处理所有的情况,但是在实现过程中,充分考虑了实际可能的情况,对某些场景的处理进行了优化,下面分情况介绍。

对于本机发送的数据包,TCP在组织skb数据时,本身就会考虑MTU的限制,它会尽可能的保证每个skb携带的数据不会超过MTU,就是为了避免网络层再进行分段,因为分段对TCP性能的影响较大。因为TCP就帮忙做了很多事情,所以对于TCP发送场景,应该是很少有机会执行分片的。考虑UDP,它并不会向TCP一样保证skb长度,但是由于UDP往往是调用ip_append_data()组织skb数据的,该函数在组织skb过程中,会将属于同一个IP报文的所有分片都组织成skb列表(非第一个分片都放在第一个分片skb的frag_list中),这样网络层在执行分片时将会节省很多工作量。

对于转发的数据包,则无法向本地发送一样,提前做很多的工作,网络层必须依靠自己来兼容所有可能的情况。同样的,天有不测风云,对于一些特殊的异常场景,本机发送的数据包也有可能并没有按照预期情况组织,这时网络层也要能够兼容处理。

综上,网络层在实现分段时,设计了快速路径和慢速路径两个流程来分别对应上面的两种情况。通常本地产生的UDP大包都会调用 ip_push_pending_frames 将发包队列的skb整合到 frag_list 上,走快速路径分发出去。

2 ip_fragment()代码分析

/* * This IP datagram is too large to be sent in one piece. Break it up into * smaller pieces (each of size equal to IP header plus * a block of the data of the original IP data part) that will yet fit in a * single device frame, and queue such a frame for sending. */ int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff*)) { struct iphdr *iph; int raw = 0; int ptr; struct net_device *dev; struct sk_buff *skb2; unsigned int mtu, hlen, left, len, ll_rs, pad; int offset; __be16 not_last_frag; struct rtable *rt = (struct rtable*)skb->dst; int err = 0; dev = rt->u.dst.dev; /* * Point into the IP datagram header. */ iph = ip_hdr(skb); // 一旦进入该函数,说明skb过大,需要进行IP分片,但是又设置了DF标记或者本身是抑制分片的, // 那么发送失败,向源端发送ICMP报文,这里主要是为forward数据所判断。 if (unlikely((iph->frag_off & htons(IP_DF)) && !skb->local_df)) { IP_INC_STATS(IPSTATS_MIB_FRAGFAILS); icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED, htonl(ip_skb_dst_mtu(skb))); kfree_skb(skb); return -EMSGSIZE; } // hlen保存IP首部长度 hlen = iph->ihl * 4; // mtu代表每个IP片段能够容纳的L4载荷,所以需要在MTU基础上去掉IP首部的开销 mtu = dst_mtu(&rt->u.dst) - hlen; // 设置 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. */ /* * 如果4层将数据包分片了,那么就会把这些数据包放到skb的frag_list链表中, * 因此这里首先先判断frag_list链表是否为空,为空的话将会进行slow 分片 */ if (skb_shinfo(skb)->frag_list) { struct sk_buff *frag; /* * 取得第一个数据报的len.当sk_write_queue队列被flush后, * 除了第一个切好包的另外的包都会加入到frag_list中(接口ip_push_pending_frames), * 而这里需要得到的第一个包(也就是本身这个sk_buff)的长度。 * first_len是第一个skb中所有数据的总长度,包括线性缓冲区和frags[]数组 */ int first_len = skb_pagelen(skb); int truesizes = 0; /* * 接下来的判断都是为了确定能进行fast分片。分片不能被共享, * 这是因为在fast path 中,需要加给每个分片不同的ip头(而并 * 不会复制每个分片)。因此在fast path中是不可接受的。而在 * slow path中,就算有共享也无所谓,因为他会复制每一个分片, * 使用一个新的buff。 */ /* * 判断第一个包长度是否符合一些限制(包括mtu,mf位等一些限制). * 如果第一个数据报的len没有包含mtu的大小这里之所以要把第一个 * 切好片的数据包单独拿出来检测,是因为一些域是第一个包所独有 * 的(比如IP_MF要为1)。这里由于这个mtu是不包括hlen的mtu,因此 * 需要减去一个hlen。 */ // 1. 条件1说明高层协议切割的skb的长度还是太长了; // 2. 第一个片段长度必须是8字节对齐的(IP片段偏移量是8字节对齐决定的); // 3. 偏移量在下面的分段过程中才会进行设置,这里不应该有值; // 4. skb不能是被共享的,因为快速路径上不会进行skb拷贝,而是直接修改skb; // 上述4个条件有任何一个不满足,那么就用慢速路径完成分片 if (first_len - hlen > mtu || ((first_len - hlen) & 7) || (iph->frag_off & htons(IP_MF|IP_OFFSET)) || skb_cloned(skb)) { goto slow_path; } // 遍历frag_list列表,检查是否所有的分片是否符合快速分片处理 for (frag = skb_shinfo(skb)->frag_list; frag; frag = frag->next) { // 这4个条件和前面对第一个片段的检查类似 if (frag->len > mtu || ((frag->len & 7) && frag->next) || skb_headroom(frag) < hlen) goto slow_path; if (skb_shared(frag)) goto slow_path; BUG_ON(frag->sk); // 设置skb的属主 if (skb->sk) { sock_hold(skb->sk); frag->sk = skb->sk; frag->destructor = sock_wfree; truesizes += frag->truesize; } } // 对第一个分片的特殊处理 err = 0; offset = 0; frag = skb_shinfo(skb)->frag_list; skb_shinfo(skb)->frag_list = NULL; skb->data_len = first_len - skb_headlen(skb); skb->truesize -= truesizes; skb->len = first_len; iph->tot_len = htons(first_len); iph->frag_off = htons(IP_MF); ip_send_check(iph); // 循环处理后面所有的分片 for (;;) { if (frag) { frag->ip_summed = CHECKSUM_NONE; skb_reset_transport_header(frag); __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); ip_copy_metadata(frag, skb); 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); } // 继续发送过程,先发送首片ip的skb自己,后面继续发送frag。 err = output(skb); if (!err) IP_INC_STATS(IPSTATS_MIB_FRAGCREATES); if (err || !frag) break; // 继续处理下一个分片 skb = frag; frag = skb->next; skb->next = NULL; } // 一切正常,分片过程从这里返回 if (err == 0) { IP_INC_STATS(IPSTATS_MIB_FRAGOKS); return 0; } // 分片或者发送过程失败了,释放所有的skb分片 while (frag) { skb = frag->next; kfree_skb(frag); frag = skb; } // 快速路径结束 IP_INC_STATS(IPSTATS_MIB_FRAGFAILS); return err; }//end of (skb_shinfo(skb)->frag_list) slow_path: // left保存整个IP报文中剩余需要分段的报文长度,在下面分段过程中会逐渐减小, // 直到为0说明分段过程结束 left = skb->len - hlen; /* Space per frame */ // ptr指向L4载荷的偏移,初始值指向L4报文的开头 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 */ // L2的特殊使用,处理桥接、VLAN、PPPOE相关MTU,这里认为pad=0即可 pad = nf_bridge_pad(skb); // link layer reserved space,即链路层保留长度,是指应该在skb线性缓冲区的首部 // 应该为L2保留的长度,主要包括mac层首部 ll_rs = LL_RESERVED_SPACE_EXTRA(rt->u.dst.dev, pad); mtu -= pad; // offset为偏移量,不包括DF、MF标记 offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3; // 顾名思义,标识是否是IP报文的最后一个片段,因为最后一个片段的MF标记是0 not_last_frag = iph->frag_off & htons(IP_MF); // 循环创建新的skb2,然后拷贝数据,完成分段(left==0) while (left > 0) { // 调整len为本轮循环分片中能够容纳的L4报文载荷长度 len = left; /* IF: it doesn't fit, use 'mtu' - the data space left */ if (len > mtu) len = mtu; // IP报文首部的片偏移字段格式决定了非最后一个IP片段的偏移量必须是8字节对齐的 if (len < left) { len &= ~7; } // 分配skb2,长度包括三部分:len代表的L4报文部分;hlen代表的IP报文首部;ll_rs代表的L2报文首部 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; } // 首先设置skb2中的各个字段 ip_copy_metadata(skb2, skb); skb_reserve(skb2, ll_rs); skb_put(skb2, len + hlen); skb_reset_network_header(skb2); skb2->transport_header = skb2->network_header + hlen; // 设置owner,内存的消耗将会记录到owner的账上 if (skb->sk) skb_set_owner_w(skb2, skb->sk); // 先从源skb的线性缓冲区将IP报文首部拷贝到skb2的线性缓冲区,因为内存上是连续的, // 所以直接使用memcpy拷贝即可 skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen); // 下面的数据拷贝就复杂了一些,因为慢速路径要能够处理任何可能的skb的数据组织方式。 // skb_copy_bits()将从skb->data开始偏移的ptr位置开始拷贝数据,共拷贝len字节到 // skb2传输层开始的位置,注意这里在处理ptr偏移会考虑页缓冲区和frag_list两种情况 if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len)) BUG(); // 分段完成,left减去1个片段的长度 left -= len; // 填充IP首部,主要是选项和偏移字段 iph = ip_hdr(skb2); iph->frag_off = htons((offset >> 3)); // ip_options_fragment()会在第一个IP片段的基础上将后面片段不需要的IP选项删除, // 这样后面的IP片段直接继承即可(前面已经拷贝),无需重新设定选项,所以这里只 // 在第一个片段分片过程中调用ip_options_fragment() if (offset == 0) ip_options_fragment(skb); //不是最后一个包,因此设置mf位 if (left > 0 || not_last_frag) iph->frag_off |= htons(IP_MF); // 更新ptr和offset,为下一个分片做好准备 ptr += len; offset += len; // IP首部的total字段表示的是IP片段的长度 iph->tot_len = htons(len + hlen); // 计算IP首部校验和 ip_send_check(iph); // 调用发送接口继续发送过程 err = output(skb2); if (err) goto fail; IP_INC_STATS(IPSTATS_MIB_FRAGCREATES); } // 原有的IP报文都已经被分割成一个个新的skb,所以处理结束后,原来的skb需要释放 kfree_skb(skb); IP_INC_STATS(IPSTATS_MIB_FRAGOKS); return err; fail: kfree_skb(skb); IP_INC_STATS(IPSTATS_MIB_FRAGFAILS); return err; }

 

最新回复(0)