您的位置:首页 > 运维架构 > Linux

linux内核学习笔记------ip报文的分片

2013-12-29 21:09 344 查看
对网络比较熟悉的童鞋都知道,当发送的ip报文长度超出了最大的传输单位MTU,且允许分片的情况下,就会对ip报文进行分片。在上层要发送数据时就会调用dst_output,dst_output就会调用ip_output,而ip_output就会调用ip_finish_output,在ip_finish_output把数据发送出去之前就会判断该报文是否进行分片。

static int ip_finish_output(struct sk_buff *skb)
{
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);
}从源码中可以看出,当报文的长度大于mtu,gso的长度不为0就会调用ip_fragment进行分片。否则就会调用ip_finish_output2把数据发送出去。
ip分片目前有两种分片方式:1、快速分片;2、慢速分片。在快速分片中,将数据分割成片段已经由传输层完成,三层只需将这写片段组成ip分片;而慢速分片则需要完成全部的工作,即对一个完整的ip数据报根据mtu值循环进行分片,直至完成。整个分片工作都在ip_fragment中完成。

int ip_fragment(struct sk_buff *skb, int (*output)(struct sk_buff *))
{
.......
struct rtable *rt = skb_rtable(skb);
int err = 0;

dev = rt->u.dst.dev;
......
/*
* 如果待分片IP数据包禁止分片,则调用
* icmp_send()向发送方发送一个原因为需要
* 分片而设置了不分片标志的目的不可达
* ICMP报文,并丢弃报文,即设置IP状态
* 为分片失败,释放skb,返回消息过长
* 错误码。
*/
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;
}

hlen = iph->ihl * 4;
mtu = dst_mtu(&rt->u.dst) - hlen; /* Size of data space */
/*
* 在分片之前先给IP数据包的控制块设置
* IPSKB_FRAG_COMPLETE标志,标识完成分片。
*/
IPCB(skb)->flags |= IPSKB_FRAG_COMPLETE;

if (skb_has_frags(skb)) {
/*
* 获得此IP数据包第一个分片长度,包括SG类型
* 聚合分散I/O数据区中的数据。
*/
int first_len = skb_pagelen(skb);

if (first_len - hlen > mtu ||
((first_len - hlen) & 7) ||
(iph->frag_off & htons(IP_MF|IP_OFFSET)) ||
skb_cloned(skb))
goto slow_path;

skb_walk_frags(skb, frag) {
if (frag->len > mtu ||
((frag->len & 7) && frag->next) ||
skb_headroom(frag) < hlen)
goto slow_path;

if (skb_shared(frag))
goto slow_path;
if (skb->sk) {
frag->sk = skb->sk;
frag->destructor = sock_wfree;
truesizes += frag->truesize;
}
......
frag = skb_shinfo(skb)->frag_list;
skb_frag_list_init(skb);
......
for (;;) {
if (frag) {
/*
* 设置后一个分片skb中指向三层和四层首部
* 的指针。
*/
skb_reset_transport_header(frag);
__skb_push(frag, hlen);
skb_reset_network_header(frag);
/*
* 将当前分片的IP首部复制给后一个分片,
* 并修改后一个分片IP首部的总长度字段。
*/
memcpy(skb_network_header(frag), iph, hlen);
iph = ip_hdr(frag);
iph->tot_len = htons(frag->len);
/*
* 根据当前分片的skb填充后一个分片
* skb中的参数。
*/
ip_copy_metadata(frag, skb);
/*
* 如果是在处理第一个分片,则调用ip_options_fragment()
* 将第二个分片skb中无需复制到每个分片的IP选项都
* 填充为IPOPT_NOOP,此后所有的分片选项部分都简单
* 地复制上一个的即可。
*/
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);
err = output(skb);
skb = frag;
frag = skb->next;
skb->next = NULL;快速分片和慢速分片主要通过skb_has_frags这个来判断,也就是判断该数据的第一个skb中的frag_list是否为空,如果为空就是需要进行慢速分片,否则传输层已经为快速分片做好了准备。上面的代码大部分都有注释,需要注意一种情况
1、要进行快速分片还需要对传输层传递的所有的skb进行判断:

有分片长度大于mtu
除最后一个分片外还有分片长度未与8字节对其
ip首部中的MF或片偏移不为0,说明不是一个完整的ip报文
此skb被克隆  

上述四种情况是不能进行ip分片的。上面是快速分片;

当不能进行快速分片时就会转到慢速分片,慢速分片其实就需要对skb数据进行复制,而快速分片就不需要此操作。

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
* 缓冲区时留下相应的空间,同时还需
* 修改MTU值。
*/
pad = nf_bridge_pad(skb);
/*
* 获得IP首部中的片偏移值,即每个分片
* 起始处在原始数据包中位置,该值是
* 13位的,因此要乘8.
*/
offset = (ntohs(iph->frag_off) & IP_OFFSET) << 3;
/*
* 取MF位值,MF值除最后一个分片外
* 都应该置为1,表示该分片之后还
* 有分片。
*/
not_last_frag = iph->frag_off & htons(IP_MF);
/*
* 循环对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;
}
......
/*
* 复制分片数据,并更新原始数据包剩余未分片数据量。
* 此处调用了skb_copy_bits(),是因为skb中的数据存储有多种
* 可能性,而skb_copy_bits可以处理这些细节。
*/
if (skb_copy_bits(skb, ptr, skb_transport_header(skb2), len))
/*
* 设置分片的片偏移字段,对于第一个分片,
* 该值即原始IP数据包的片偏移字段值。
*/
iph = ip_hdr(skb2);
iph->frag_off = htons((offset >> 3));

if (offset == 0)
ip_options_fragment(skb);
* 如果不是最后一个分节,则设置IP首部中
* 标识字段的MF位。
*/
if (left > 0 || not_last_frag)
iph->frag_off |= htons(IP_MF);
/*
* 更新后一个分节在整个原始数据包中的偏移量,
* 以及后一个分片在当前被分片数据包中的偏移量。
* 这两个偏移量是有区别的,因为一个数据包在
* 传输过程中可能被多次分片,因此当前被分片
* 数据包也由可能是另外一个数据包的分片。
*/
ptr += len;
offset += len;

/*
* Put this fragment into the sending queue.
*/
/*
* 设置分片IP首部中总长度字段。
*/
iph->tot_len = htons(len + hlen);
......上述代码也是有注释的,只提示两点:
1、分片的片偏移

分段偏移用于指明分段起始点相对报文起始点的偏移,长度为13位,以8个位组为单位。若MTU=1500时,一个大小为3000字节的数据经过该接口,会被分为端传输:

第一段长度为1480+20,第二段为1480,第三段为40,那么第一段分段的偏移为0,第二段为1480/8,第三段为185+185,所以在源码中需要乘以8,而在设置ip首部片偏移时又除以8的原因

2、ip选项的处理要注意,有的ip选项需要体现在所有的分片中,而有的不需要。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  linux内核 网络