网站首页 > 技术文章 正文
本文分析下ip的分片。
当发送的 IP 数据报的长度超过了最大传输单元 MTU,且允许分片时,就会对数据报进行 IP 分片。
IP 分片通常发生在网络环境中,比如以太网环境中 MTU 为 1500B,若传输的数据长度超过 1500B,就会进行分片,经过分片后才能传输此数据报。
通常情况下,UDP 协议发送的数据报很容易导致 IP 分片,而 TCP 由于是基于流的传输,因此通常不会产生分片。
IP 数据报被分片后,各个分片( fragment )分别组成一个具有IP首部的分组,并各自独立的选择路由,待其分别到达目的主机后,目的主机的IP层会在传送给传输层前把所有分片组装成一个完整的 IP 数据报。所以,IP 数据报是通信双方 IP 层之间的传输单元,对于传输层是透明的。
尽管分片对于传输层是透明的,但是在快速分片中,传输层由于提前将要传送的 IP 层数据报按 MTU 分割完成,所以,传输层似乎也能感知到分片的存在。
ip分片
在 ip_finish_output() 将数据报发送之前,该数据报包括本地发送的数据报也包括转发的数据报,会对 skb 缓存区的数据长度进行校验,若超出了输出设备的MTU值,且又符合其他一些条件,则调用 ip_fragment() 对数据报进行分片;否则直接调用 ip_finish_output2() 输出到链路层。
目前分片有两种处理方式:快速分片和慢速分片。
整个分片的过程中,除了将三层的有效负载根据 MTU 分成一个个片段外,还要对每个分片设置 IP 首部,更新 IP 校验和等。
在快速分片中,将数据进行分割成片段已由传输层完成,三层只需要将这些片段组成 IP 分片即可;而慢速路径则需要完成所有的工作,也即是对一个完整的 IP 数据报根据 MTU 循环进行分片,直至完成。
正是由于快速分片需要传输层的协助,因此只对本地发送的数据报才有可能。
发送前 skb 的缓冲区如下:
有关 IP 分片具体实现如下:
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.
*/
//得到待分片的ip首部
iph = skb->nh.iph;
/*若禁止分片,则发送一个原因为需要分片而设置了不分片标志的目的不可达的ICMP报文,并丢弃该报文*/
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(dst_mtu(&rt->u.dst)));
kfree_skb(skb);
//返回消息过长的错误码
return -EMSGSIZE;
}
/*
* Setup starting values.
*/
/*获取待分片ip数据报的ip首部长度*/
hlen = iph->ihl * 4;
//获取扣除了ip首部长度的mtu值
mtu = dst_mtu(&rt->u.dst) - hlen; /* Size of data space */
//分片前先给控制块设置标志,标识完成分片
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.
*/
/*当传输层已将数据块分块,则将这些块链接在skb_shinfo(skb)->frag_list 中,此时可以通过快速分片进行处理.
因此frag_list有分片的skb,则说明传输层已经为快速分片做好了准备*/
if (skb_shinfo(skb)->frag_list) {
//快速分片处理:
struct sk_buff *frag;
/*获得此ip数据报第一个分片数据长度,包括SG类型聚合分散I/O数据区的数据*/
int first_len = skb_pagelen(skb);
/* 对第一个分片做检测,因为要进行快速分片,还需要对传输层传递的所有SKB做一些判断
以下四种情况是不能进行快速分片的 */
if (first_len - hlen > mtu || //分片长度大于MTU
((first_len - hlen) & 7) || //除最后一个分片外还有分片长度未8字节对齐
(iph->frag_off & htons(IP_MF|IP_OFFSET)) || //ip首部中的MF或分片偏移不为0,说明skb不是一个完整的ip数据报
skb_cloned(skb)) //此skb被克隆
goto slow_path;
for (frag = skb_shinfo(skb)->frag_list; frag; frag = frag->next) {
/* Correct geometry. */
//遍历分片时,对每个分片做校验
if (frag->len > mtu || //分片长度是否超过MTU
((frag->len & 7) && frag->next) || //除最后一个分片外还有分片长度有没有8字节对齐
skb_headroom(frag) < hlen) //缓冲区头部是否有足够的空间存放ip首部
goto slow_path;
/* Partially cloned skb? */
//判断是否被克隆,被克隆的不适合进行快速分片
if (skb_shared(frag))
goto slow_path;
BUG_ON(frag->sk);
if (skb->sk) {
//递增数据报所属传输控制块的引用计数
sock_hold(skb->sk);
//当前分片的传输控制块指向该传输控制块
frag->sk = skb->sk;
//设置skb释放回调函数
frag->destructor = sock_wfree;
//修改第一个分片的缓冲区总长度,减去当前分片的长度
skb->truesize -= frag->truesize;
}
}
/* Everything is OK. Generate! */
//初始化错误码和分片偏移
err = 0;
offset = 0;
//保存frag_list指针,并将指针设置为NULL
frag = skb_shinfo(skb)->frag_list;
skb_shinfo(skb)->frag_list = NULL;
//重新设置第一个分片的数据长度和缓冲区长度
skb->data_len = first_len - skb_headlen(skb);
skb->len = first_len;
//设置第一个分片ip首部中的总长度字段和MF标志位
iph->tot_len = htons(first_len);
iph->frag_off = htons(IP_MF);
//重新计算第一个分片IP首部校验和
ip_send_check(iph);
/*从第二个分片开始循环设置每个分片的skb及IP首部(此时第一个分片已设置完成),然后将所有分片(包括第一个分片)发送出去*/
for (;;) {
/* Prepare header of the next frame,
* before previous one went down. */
//在发送当前skb前,需先完成对后一个分片的相应设置。因为其中一些是根据当前分片来设置的
if (frag) {
//设置后一个分片的校验和完全有软件处理
frag->ip_summed = CHECKSUM_NONE;
//设置后一个分片skb中指向三层和四层首部的指针
frag->h.raw = frag->data;
frag->nh.raw = __skb_push(frag, hlen);
//将当前分片的ip首部复制给后一个分片,并修改后一个分片ip首部的总长度字段
memcpy(frag->nh.raw, iph, hlen);
iph = frag->nh.iph;
iph->tot_len = htons(frag->len);
//根据当前分片的skb填充后一个分片skb中的参数
ip_copy_metadata(frag, skb);
/*若是在处理第一个分片,则将第二个分片skb中无需复制到每个分片的ip选项都填充为IPOPT_NOOP,此后所有的分片选项部分都简单的复制上一个的即可。*/
if (offset == 0)
ip_options_fragment(frag);
//设置后一个分片ip首部的片偏移、MF标志以及校验和
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); // ip_finish_output2
//对MIB的IPSTATS_MIB_FRAGCREATES数据进行统计
if (!err)
IP_INC_STATS(IPSTATS_MIB_FRAGCREATES);
/*若发送当前分片失败或已无后续分片,则结束分片和发送。
也即是一旦有一个分片发送失败,则剩余分片都不再发送*/
if (err || !frag)
break;
//从链表中取下一个待处理分片
skb = frag;
frag = skb->next;
skb->next = NULL;
}
//成功发送所有分片,对MIB的IPSTATS_MIB_FRAGOKS数据进行统计后返回
if (err == 0) {
IP_INC_STATS(IPSTATS_MIB_FRAGOKS);
return 0;
}
/*若有分片发送失败,则释放所有未发送ip分片*/
while (frag) {
skb = frag->next;
kfree_skb(frag);
frag = skb;
}
IP_INC_STATS(IPSTATS_MIB_FRAGFAILS);
return err;
}
//当不支持快速分片时,只能进行慢速分片处理,尽管性能会低一些
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缓冲区时留下相应的空间*/
pad = nf_bridge_pad(skb);
//获取二层首部长度
ll_rs = LL_RESERVED_SPACE_EXTRA(rt->u.dst.dev, pad);
//还需修改MTU值
mtu -= pad;
/*
* Fragment the datagram.
*/
// 获取ip首部中的片偏移量,即每个分片起始处在原始数据报中的位置,该值是13位的,因此要乘以8
offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
//获取MF位值,MF值除最后一个分片为都应该为1,表示该分片后还有分片
not_last_frag = iph->frag_off & htons(IP_MF);
/*
* Keep copying data until we run out.
*/
/*循环对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;
}
/*
* Set up data on packet
*/
//根据原始数据报skb填充分片新分配的skb,包括类型、优先级、协议等
ip_copy_metadata(skb2, skb);
//分别为二层首部、分片加ip首部留出相应的空间和数据空间,以备后面填充数据
skb_reserve(skb2, ll_rs);
skb_put(skb2, len + hlen);
//设置新SKB中三层首部指针和四层首部指针
skb2->nh.raw = skb2->data;
skb2->h.raw = skb2->data + hlen;
/*
* Charge the memory for the fragment to any owner
* it might possess
*/
/*设置新的skb的宿主,包括递增数据报传输控制块的引用数,设置skb的释放回调函数等。*/
if (skb->sk)
skb_set_owner_w(skb2, skb->sk);
/*
* Copy the packet header into the new buffer.
*/
//复制ip首部到新skb中
memcpy(skb2->nh.raw, skb->data, hlen);
/*
* Copy a block of the IP datagram.
*/
/*复制分片数据,并更新原始数据报剩余未分片数量。*/
if (skb_copy_bits(skb, ptr, skb2->h.raw, len))
BUG();
left -= len;
/*
* Fill in the new header fields.
*/
/*设置分片的片偏移字段,对于第一个分片,该值即原始ip数据报的片偏移字段值*/
iph = skb2->nh.iph;
iph->frag_off = htons((offset >> 3));
/* ANK: dirty, but effective trick. Upgrade options only if
* the segment to be fragmented was THE FIRST (otherwise,
* options are already fixed) and make it ONCE
* on the initial skb, so that all the following fragments
* will inherit fixed options.
*/
//根据条件清理掉ip选项中的一些选项,设置为 IPOPT_NOOP
if (offset == 0)
ip_options_fragment(skb);
/*
* Added AC : If we are fragmenting a fragment that's not the
* last fragment then keep MF on each bit
*/
//若不是最后一个分片,则设置ip首部中标识字段的MF位
if (left > 0 || not_last_frag)
iph->frag_off |= htons(IP_MF);
/*更新后一个分片在整个原始数据报中的偏移量,以及后一个分片在当前被分片数据报中的偏移量。
这2个偏移量是有区别的,因为一个数据报在传输的过程中可能被多次分片,因此当前被分片的数据报也有可能
是另一个数据包的分片*/
ptr += len;
offset += len;
/*
* Put this fragment into the sending queue.
*/
//设置分片ip首部中总长度字段
iph->tot_len = htons(len + hlen);
//重新计算该分片的ip首部校验和
ip_send_check(iph);
//发送报文
err = output(skb2); // ip_finish_output2
if (err)
goto fail;
IP_INC_STATS(IPSTATS_MIB_FRAGCREATES);
}
//完成所有分片及发送之后,释放被分片的ip数据包
kfree_skb(skb);
IP_INC_STATS(IPSTATS_MIB_FRAGOKS);
return err;
fail:
kfree_skb(skb);
IP_INC_STATS(IPSTATS_MIB_FRAGFAILS);
return err;
}
在 sk_write_queue 的 sk_buff 队列中,每个 sk_buff 的 len = x (也即是每一个第一个切片的包的L4 payload长度)+S(这里表示所有frags域的数据的总大小,也即是 data_len 的长度),
static inline int skb_pagelen(const struct sk_buff *skb)
{
int i, len = 0;
// 我们知道如果设备支持S/G IO的话,nr_frags会包含一些L4 payload,因此我们需要先遍历nr_frags.然后加入它的长度。
for (i = (int)skb_shinfo(skb)->nr_frags - 1; i >= 0; i--)
len += skb_shinfo(skb)->frags[i].size;
// 最后加上skb_headlen,而skb_headlen = skb->len - skb->data_len;因此这里就会返回这个数据包的len。
return len + skb_headlen(skb);
}
关系图如下:
有关分片的流程如下:
猜你喜欢
- 2024-10-12 「观潮」4K HDR高动态范围制作技术(下)
- 2024-10-12 如何利用eBPF程序监控Kubernetes 如何使用ebsco
- 2024-10-12 济南广播电视台4K IP HDR超高清电视转播车 应邀参加国际性体育赛事转播
- 2024-10-12 IP头情景分析 ip头部分析
- 2024-10-12 深入理解高性能网络开发路上的绊脚石 - 同步阻塞网络 IO
- 2024-10-12 Linux下AF-PACKET的V3版本 linux afs
- 2024-10-12 3000万像素与4K HDR视频?索尼A7 IV全幅微单最新消息汇总
- 2024-10-12 Linux 网络协议栈 linux网络协议栈是按照OSI网络模型实现的
- 2024-10-12 利用“socket”编程实现网络攻防 socket网络编程步骤
- 2024-10-12 揭秘 BPF map 前生今世 bp from
你 发表评论:
欢迎- 最近发表
- 标签列表
-
- oraclesql优化 (66)
- 类的加载机制 (75)
- feignclient (62)
- 一致性hash算法 (71)
- dockfile (66)
- 锁机制 (57)
- javaresponse (60)
- 查看hive版本 (59)
- phpworkerman (57)
- spark算子 (58)
- vue双向绑定的原理 (68)
- springbootget请求 (58)
- docker网络三种模式 (67)
- spring控制反转 (71)
- data:image/jpeg (69)
- base64 (69)
- java分页 (64)
- kibanadocker (60)
- qabstracttablemodel (62)
- java生成pdf文件 (69)
- deletelater (62)
- com.aspose.words (58)
- android.mk (62)
- qopengl (73)
- epoch_millis (61)
本文暂时没有评论,来添加一个吧(●'◡'●)