目录
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;
}