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

来源:互联网 发布:mac上面逗号怎么打 编辑:程序博客网 时间:2024/06/07 13:17

对网络比较熟悉的童鞋都知道,当发送的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);elsereturn 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选项需要体现在所有的分片中,而有的不需要。




0 0
原创粉丝点击