分片是网络层的一个重要任务,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