分片是网络层的一个重要任务,IPv4需要对两种IP数据包进行分片:
- 本地产生的数据包;
- 转发的数据包;
这两种数据包的长度如果超过了出口设备的MTU(或者PMTU),IPv4就会对数据包进行分片处理,使其适配出口设备的MTU。
重要说明
IPv4使用ip_fragment()函数执行分片处理,在设计时,要求该函数能够处理所有的情况。在实现过程中,可能高层协议已经为分片进行了一些准备工作,所以代码也充分考虑了实际可能的情况,对某些场景进行了优化,下面分情况介绍。
对于本机发送的数据包,TCP在封装skb时就会考虑MTU的限制,它会尽可能的保证每个skb不超过MTU,从而可以避免网络层再进行分段,因为分段对TCP性能的影响较大。考虑UDP,它并不会像TCP一样保证skb不超过MTU,但是其在封装skb时(通过ip_append_data()函数),会将属于同一个IP报文的所有分片都组织成skb列表(非第一个分片都放在第一个分片skb的frag_list链表中),这样网络层在执行分片时将会节省很多工作量。
对于转发的数据包,则无法向本地发送一样,提前做很多的工作,网络层必须依靠自己来兼容所有可能的情况。同样的,天有不测风云,对于一些特殊的异常场景,本机发送的数据包也有可能并没有按照预期情况组织,这时网络层也要能够兼容处理。
综上,网络层在实现分片时,设计了快速路径和慢速路径两个流程来分别对应上面的两种情况。
分片时机: ip_finish_output()
如笔记IPv4之数据包发送流程和IPv4之数据包接收流程介绍,无论是本机发送的数据包,还是转发的数据包,最后在数据包通过Netfilter的POST_ROUTING点后,都会交由ip_finish_output()函数继续处理。
static int ip_finish_output(struct sk_buff *skb)
{
...
// 如果报文长度超过了MTU并且不是GSO场景,那么需要分片,分片后再输出。否则直接输出
if (skb->len > ip_skb_dst_mtu(skb) && !skb_is_gso(skb))
return ip_fragment(skb, ip_finish_output2);
else
return ip_finish_output2(skb);
}
报文分片: ip_fragment()
如注释所述,入参skb代表了一个完整的IP报文,ip_fragement()将其分割成一个个mtu大小的分片。
/*
* 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 = skb->rtable;
int err = 0;
dev = rt->u.dst.dev;
/*
* Point into the IP datagram header.
*/
iph = ip_hdr(skb);
// 需要进行分片,但是报文本身又不允许分片,那么发送失败,向源端发送ICMP需要分片报文
if (unlikely((iph->frag_off & htons(IP_DF)) && !skb->local_df)) {
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_FRAG_NEEDED,
htonl(ip_skb_dst_mtu(skb)));
kfree_skb(skb);
return -EMSGSIZE;
}
/*
* Setup starting values.
*/
// hlen保存IP首部长度,包括选项,每个IP分片都会包含该首部(部分选项不需要在每个分片都存在)
hlen = iph->ihl * 4;
// 在MTU基础上去掉IP首部的开销,所以mtu代表每个IP分片能够容纳的L4载荷
hlen = iph->ihl * 4;
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.
*/
// 如注释,这部分代码利用L4协议准备好的frag_list链表,预期每个skb是一个分片
if (skb_shinfo(skb)->frag_list) {
struct sk_buff *frag;
int first_len = skb_pagelen(skb); // skb线性缓冲区+frags[]数组部分长度
int truesizes = 0;
// 检查L4协议的准备工作是否正确,如果不正确那么走慢速路径
if (first_len - hlen > mtu || // 第一个分片超过了mtu
((first_len - hlen) & 7) || // 第一个分片的数据长度不是8字节对齐的(IP首部偏移字段限制)
(iph->frag_off & htons(IP_MF|IP_OFFSET)) || // 已经包含了分片信息,说明不是第一次进行分片了
skb_cloned(skb)) // skb被共享了
goto slow_path;
// 检查frag_list中每个分片的长度设置是否合理,一旦有一个不合理,那么走慢速路径
for (frag = skb_shinfo(skb)->frag_list; frag; frag = frag->next) {
/* Correct geometry. */
if (frag->len > mtu || // 分片长度不能超过mtu,注意此时分片长度中不能包含IP首部了
((frag->len & 7) && frag->next) || // 非最后一个分片的长度必须是8字节对齐的
skb_headroom(frag) < hlen) // 分片首部要有足够的空间容纳分片报文的IP首部
goto slow_path;
/* Partially cloned skb? */
if (skb_shared(frag)) // 分片的skb也不能是共享的
goto slow_path;
BUG_ON(frag->sk);
if (skb->sk) { // 让每个分片skb都持有sk的引用
sock_hold(skb->sk);
frag->sk = skb->sk;
frag->destructor = sock_wfree;
truesizes += frag->truesize;
}
}
// 所有分片都满足要求,可以按照快速路径进行分片
// 为IP报文的第一个IP分片设置长度、偏移量、校验和信息
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);
// 循环进行后续分片处理,skb指向当前要处理的分片,frag指向其下一个分片
for (;;) {
/* Prepare header of the next frame, before previous one went down. */
if (frag) {
frag->ip_summed = CHECKSUM_NONE;
skb_reset_transport_header(frag);
__skb_push(frag, hlen);
skb_reset_network_header(frag);
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_finish_output2()
err = output(skb);
// 发送失败则结束后续分片过程
if (!err)
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
if (err || !frag)
break;
// 继续下一个分片
skb = frag;
frag = skb->next;
skb->next = NULL;
}
// 所有分片都正确处理完毕,返回0
if (err == 0) {
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
return 0;
}
// 有分片处理失败,将剩余的分片释放掉,返回失败
while (frag) {
skb = frag->next;
kfree_skb(frag);
frag = skb;
}
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
return err;
}
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
*/
// 网桥功能需要在首部预留空间,会占用mtu部分
pad = nf_bridge_pad(skb);
ll_rs = LL_RESERVED_SPACE_EXTRA(rt->u.dst.dev, pad);
mtu -= pad;
/*
* Fragment the datagram.
*/
// 第一个分片的片偏移(字节单位)
offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
not_last_frag = iph->frag_off & htons(IP_MF); // 标记是否是最后一个分片
/*
* Keep copying data until we run out.
*/
// 逐个拷贝分片数据
while (left > 0) {
// 计算本次分片要拷贝的字节数
len = left;
/* IF: it doesn't fit, use 'mtu' - the data space left */
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 */
if (len < left) { // 非最后一个分片长度要8字节对齐
len &= ~7;
}
// 分片一个新的skb保存分片数据
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;
}
// 拷贝skb控制信息兵设置报文长度信息
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;
// 将skb属主设置为对应套接字
if (skb->sk)
skb_set_owner_w(skb2, skb->sk);
// 从skb线性缓冲区拷贝报文首部到分片
skb_copy_from_linear_data(skb, skb_network_header(skb2), hlen);
// 拷贝L4报文内容到分片
if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
BUG();
left -= len;
// 填充分片报文的IP首部
iph = ip_hdr(skb2);
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.
*/
// 第一个报文需要处理选项,后续拷贝即可
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
*/
if (left > 0 || not_last_frag) // 设置MF标记
iph->frag_off |= htons(IP_MF);
ptr += len; // 调整指针为下一个分片拷贝做准备
offset += len;
// 设置IP分片报文长度
iph->tot_len = htons(len + hlen);
// 设置校验和
ip_send_check(iph);
// 输出该分片报文
err = output(skb2);
if (err)
goto fail;
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGCREATES);
}
kfree_skb(skb);
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGOKS);
return err;
fail:
kfree_skb(skb);
IP_INC_STATS(dev_net(dev), IPSTATS_MIB_FRAGFAILS);
return err;
}