IP层输出

来源:互联网 发布:2k16科比捏脸数据 编辑:程序博客网 时间:2024/05/29 15:29
根据《深入理解LINUX网络技术内幕》描述,四层使用IP层输出主要分为两大类处理。函数分别为ip_append_data、ip_push_pending_frames的组合发送,以及ip_queue_xmit的发送。当前仅对这两种输出情况进行分析。其ip_append_data、ip_push_pending_frames组合最常用于UDP报文发送,此时ip_append_data会根据路径MTU将待发送的UDP报文分为多块,以方便后续的IP分片,但此时数据并不发送出去,则是通过调用ip_push_pending_frames才进行数据发送。ip_queue_xmit常用于TCP报文发送,因为TCP已经对报文的大小进行控制,所以ip_queue_xmit中就不用在考虑报文分块的处理,相对比较简单。一、ip_append_data、ip_push_pending_frames的组合发送1、ip_append_datainet_sock *inet = inet_sk(sk);//上层仅仅进行探测,并不真正发送数据。if (flags&MSG_PROBE)return 0;//当前套接口的发送队列为空,先暂存一些信息if (skb_queue_empty(&sk->sk_write_queue))//如果含有待发送的ip选项,则先存储到ip套接对象中opt = ipc->opt;if (opt)inet->cork.flags |= IPCORK_OPT;inet->cork.addr = ipc->addr;dst_hold(&rt->u.dst);//存储路径mtu及路由条目inet->cork.fragsize = mtu = dst_mtu(rt->u.dst.path);inet->cork.rt = rt;inet->cork.length = 0;sk->sk_sndmsg_page = NULL;sk->sk_sndmsg_off = 0;//存在扩展头if ((exthdrlen = rt->u.dst.header_len) != 0)length += exthdrlen;transhdrlen += exthdrlen;elsert = inet->cork.rt;if (inet->cork.flags & IPCORK_OPT)opt = inet->cork.opt;transhdrlen = 0;exthdrlen = 0;mtu = inet->cork.fragsize;//二层硬件头长度hh_len = LL_RESERVED_SPACE(rt->u.dst.dev);//计算ip头部总长度fragheaderlen = sizeof(struct iphdr) + (opt ? opt->optlen : 0);//计算在mtu限制下,除去ip头部,还可以填充的ip负载长度//这里会进行8个字节的对齐,是因为IP的分段偏移字段单位是8字节。maxfraglen = ((mtu - fragheaderlen) & ~7) + fragheaderlen;//如果总长度会超出ip报头中总长度字段可容纳的大小,则返回错误。if (inet->cork.length + length > 0xFFFF - fragheaderlen)ip_local_error(sk, EMSGSIZE, rt->rt_dst, inet->dport, mtu-exthdrlen);return -EMSGSIZE;//如果是第一个报文,同时报文总长度小于MTU,同时硬件支持校验和,同时没//有其它扩展头,则标记使用硬件校验和。if (transhdrlen && length + fragheaderlen <= mtu &&rt->u.dst.dev->features & NETIF_F_ALL_CSUM && !exthdrlen)csummode = CHECKSUM_PARTIAL;//记载接收队列中已经积累的负载长度,不含头部inet->cork.length += length;//如果当前报文长度大于mtu,同时当前是UDP报文,当前设备驱动支持UDP类型//的GSO特性。if (((length > mtu) && (sk->sk_protocol == IPPROTO_UDP)) &&(rt->u.dst.dev->features & NETIF_F_UFO))ip_ufo_append_data(sk, getfrag, from, length, hh_len,fragheaderlen, transhdrlen, mtu,flags);//当前发送列表是空的。if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL)//分配一个新的skb,含硬件头、IP头、UDP头,这个20是预留?skb = sock_alloc_send_skb(sk,hh_len + fragheaderlen + transhdrlen + 20,(flags & MSG_DONTWAIT), &err);//将skb的head到data之间预留硬件头长度。当前data指向IP头位置。skb_reserve(skb, hh_len);//将skb的tail移到UDP的负载位置skb_put(skb,fragheaderlen + transhdrlen);//记载三、四层头的位置skb->nh.raw = skb->data;skb->h.raw = skb->data + fragheaderlen;//记载需要硬件来校验skb->ip_summed = CHECKSUM_PARTIAL;skb->csum = 0;sk->sk_sndmsg_off = 0;//将新收到的UDP负载添加到skb的frags数组中,该数组每个成员都是//一个内存页,在硬件设备支持分散/聚合处理时,就可以把这些零散的//内存页一次性交给硬件发送。skb_append_datato_frags(sk,skb, getfrag, from,(length - transhdrlen));do //只能存放64Kfrg_cnt = skb_shinfo(skb)->nr_frags;if (frg_cnt >= MAX_SKB_FRAGS)return -EFAULT;page = alloc_pages(sk->sk_allocation, 0);//在套接口对象中记载当前使用的页及偏移,方便下次使用。sk->sk_sndmsg_page = page;sk->sk_sndmsg_off = 0;//初始化skb中的frag数组成员skb_fill_page_desc(skb, frg_cnt, page, 0, 0);frag = &skb_shinfo(skb)->frags[i];frag->page  = page;//新建的页地址frag->page_offset  = off;//0frag->size  = size;//0skb_shinfo(skb)->nr_frags = i + 1;//更新frags数组长度skb->truesize += PAGE_SIZE;//更新skb总的内存用量atomic_add(PAGE_SIZE, &sk->sk_wmem_alloc);frg_cnt = skb_shinfo(skb)->nr_frags;frag = &skb_shinfo(skb)->frags[frg_cnt - 1];//left为当前页还可用的空间,如果当前页的空间已经不够用,则//先copy先设置为left长度,否则copy为需要复制的整个数据长度left = PAGE_SIZE - frag->page_offset;copy = (length > left)? left : length;//getfrag是传入的回调函数参数,该回调函数由不同上层协议调用时//传入,主要用于将用户空间的数据复制到当前内存页中。该回调//暂不进行分析。getfrag(from, (page_address(frag->page) +frag->page_offset + frag->size),offset, copy, 0, skb);//数据更新sk->sk_sndmsg_off += copy;frag->size += copy;skb->len += copy;skb->data_len += copy;offset += copy;length -= copy;//当还有待处理的数据时,则循环继续处理。while (length > 0);//这里有BUG,因为只有上面是新建的skb才应该加到队列中,否则//原来的已经存在,不应该再次加入队列,通过查看当前3.4的内核//发现新版内核已经解决了这个BUG,将这里的代码放到了上面//if skb == NULL的判断内部了。skb_shinfo(skb)->gso_size = mtu - fragheaderlen;skb_shinfo(skb)->gso_type = SKB_GSO_UDP;__skb_queue_tail(&sk->sk_write_queue, skb);return 0;//输出队列为空,直接跳过前面检测if ((skb = skb_peek_tail(&sk->sk_write_queue)) == NULL)goto alloc_new_skb;while (length > 0)//当前mtu限制下,输出队列中最后一个skb还可填充的空间。copy = mtu - skb->len;//如果copy大小容纳不下当数据长度,则copy先设置为最后一个skb可填充的//大小。if (copy < length)copy = maxfraglen - skb->len;//最后一个skb没有一点空间了,则需要分配一个新的skbif (copy <= 0)skb_prev = skb;//先前的skb,下面链接需要使用alloc_new_skb:if (skb_prev)//fraggap可能是个负数,这是因为IP报头中分段的偏移量的单位是8个//字节,如果上一个skb剩余空间已经不能8字节对齐,则上一个skb的//多余的数据就会复制到当前新的skb中。如下所示://maxfraglen是8字节对齐的,buf尾部是7个字节,则maxfraglen会//截断。此时skb_prev->len减去maxfraglen就去出现-7。//------------------|--------|//|----已使用--- | 7byte |//------------------|--------|fraggap = skb_prev->len - maxfraglen;elsefraggap = 0;//待处理的新的数据长度。datalen = length + fraggap;//如果数据长度大于一片限制的长度,则先复制一片if (datalen > mtu - fragheaderlen)datalen = maxfraglen - fragheaderlen;fraglen = datalen + fragheaderlen;//如果上层传入MORE标记,则表示上层马上会很新的包传下来,此时如果//硬件不支持分散/聚合,则分配大小设置为mtu,通常mtu会大于等于//datalen+fragheaderlen。if ((flags & MSG_MORE) &&!(rt->u.dst.dev->features&NETIF_F_SG))alloclen = mtu;elsealloclen = datalen + fragheaderlen;//如果到达数据的最后,当前目的对象还有扩冲的尾部信息,则多分配尾//部信息的空间。if (datalen == length + fraggap)alloclen += rt->u.dst.trailer_len;//第一个报文if (transhdrlen)//创建一个skb,同时判断套接口是否有错误、判断套接口已经关闭、//判断发送BUF已经满等处理。skb = sock_alloc_send_skb(sk,alloclen + hh_len + 15,(flags & MSG_DONTWAIT), &err);//非第一个报文,则仅需要分配skb即可elseif (atomic_read(&sk->sk_wmem_alloc) <=2 * sk->sk_sndbuf)skb = sock_wmalloc(sk,alloclen + hh_len + 15, 1,sk->sk_allocation);if (unlikely(skb == NULL))err = -ENOBUFS;skb->ip_summed = csummode;skb->csum = 0;//skb的data和tail下移,给硬件头留出空间。skb_reserve(skb, hh_len);//skb的tail下移到负载的尾部,data指向ip头之后data = skb_put(skb, fraglen);//nh.raw指向ip头,h.raw指向四层头的位置skb->nh.raw = data + exthdrlen;data += fragheaderlen;skb->h.raw = data + exthdrlen;//上面为了8个字节对齐,如果前一个skb不是8字节对齐,则会把前一个//skb多余的数据复制到新的skb中。if (fraggap)skb->csum = skb_copy_and_csum_bits(skb_prev, maxfraglen,data + transhdrlen, fraggap, 0);skb_prev->csum = csum_sub(skb_prev->csum,skb->csum);data += fraggap;//因为前一个skb不够8字节对齐的多余数据已经复制到新的//skb中,所以这里调整前一个skb的tail值,使得指向有效数据//位置。pskb_trim_unique(skb_prev, maxfraglen);//getfrag是传入的回调函数参数,该回调函数由不同上层协议调用时传入,主//要用于将用户空间的数据复制到当前内存页中。该回调暂不进行分析。copy = datalen - transhdrlen - fraggap;if (copy > 0 && getfrag(from, data + transhdrlen, offset, copy, fraggap, skb) < 0)err = -EFAULT;kfree_skb(skb);goto error;//新分配的skb已经完成数据复制。offset += copy;length -= datalen - fraggap;transhdrlen = 0;exthdrlen = 0;csummode = CHECKSUM_NONE;//将新分配的skb加入到套接字的发送队列中__skb_queue_tail(&sk->sk_write_queue, skb);continue;if (copy > length)copy = length;//当前硬件不支持分散/聚合特性,则将数据复制到当前skb的buf中if (!(rt->u.dst.dev->features&NETIF_F_SG))off = skb->len;if (getfrag(from, skb_put(skb, copy),offset, copy, off, skb) < 0)__skb_trim(skb, off);err = -EFAULT;goto error;//否则如果硬件支持分散/聚合功能,则将数据复制到skb的frags页中,这样//可以提高分配效率。因为多次调用ip_append_data时,如果一页没有被填//满,则一直可以使用当前页进行数据复制,同时加入到页中的数据,每次仅//仅需要复制四层负载数据就可以了,如果不支持该特性,则新建的skb还需要//多复制IP头。但请不要将分散/聚合与IP分段搞混了,如果已经到达IP分段//的长度,再来的数据不能再放置到frags页中,必须新分配skb,并进行IP头、//数据处理。else//获取上次未使用完成页地址及偏移量,一页不仅可以单个skb的数据存放,//还可以用于多个skb的数据存放。即使大家把页指针都指向唯一的页,但//因为有偏移量,所以大家之间可以区分开。//|------------|              |-----------|//|  skb_1 |               | skb_2  |//|------------|------> |--------| <-------------//              | page  |//               ---------int i = skb_shinfo(skb)->nr_frags;skb_frag_t *frag = &skb_shinfo(skb)->frags[i-1];struct page *page = sk->sk_sndmsg_page;int off = sk->sk_sndmsg_off;//当前套接口中记载的最后使用页地存在if (page && (left = PAGE_SIZE - off) > 0)if (copy >= left)copy = left;//套接口中记载的页与当前待处理的frags数组最后使用的页不相同//则需要重新分配一页。if (page != frag->page)get_page(page);//分配一个新的frags数组成员,并将页信息填充到新的frag对象中//注意这里i在上面赋值为nr_frags,即frags数组当前长度,C代码//从0开始索引,所以这里i就是一个未使用的新frag对象。skb_fill_page_desc(skb, i, page, sk->sk_sndmsg_off, 0);frag = &skb_shinfo(skb)->frags[i];//当前套接字没有记载最后使用的页,并且当前还没有超过页使用上限else if (i < MAX_SKB_FRAGS)if (copy > PAGE_SIZE)copy = PAGE_SIZE;page = alloc_pages(sk->sk_allocation, 0);//分配新页//套接字记载最后使用的页sk->sk_sndmsg_page = page;sk->sk_sndmsg_off = 0;//将页关联到新的frag对象中skb_fill_page_desc(skb, i, page, 0, 0);frag = &skb_shinfo(skb)->frags[i];//记载当前skb的大小,及内存使用量skb->truesize += PAGE_SIZE;atomic_add(PAGE_SIZE, &sk->sk_wmem_alloc);//页分配数已经到了上限elseerr = -EMSGSIZE;goto error;//将四层负载数据使用传入的函数指针复制到当前页的空余位置getfrag(from, page_address(frag->page)+frag->page_offset+frag->size, offset, copy, skb->len, skb)sk->sk_sndmsg_off += copy;frag->size += copy;skb->len += copy;skb->data_len += copy;offset += copy;length -= copy;2、ip_push_pending_framesip_push_pending_frames//从套接口的发送队列头部取出一个skb,注意sk_write_queue仅仅是链表的哨岗//节点,所以是取sk_write_queue->next数据。skb = __skb_dequeue(&sk->sk_write_queue)tail_skb = &(skb_shinfo(skb)->frag_list);//将skb的data上拉到ip头的位置if (skb->data < skb->nh.raw)__skb_pull(skb, skb->nh.raw - skb->data);//如果套接口的发送列表还有数据,则将后续的skb串接到上面第一个取出的skb//的frag_list上,这里要注意frag_list与frags数组是不同的,frags数组每个成员//都在当前skb的负载数据,是和当前skg构成一个完整的报文,而frag_list中每//个成员都是独立的报文(二层、三层头、数据),frag_list仅仅是用于将发送队//列中所有skb串联到上面第一个取出的skb后,这样通过第一个skb就可以把//所有报文skb都访问到。while ((tmp_skb = __skb_dequeue(&sk->sk_write_queue)) != NULL)__skb_pull(tmp_skb, skb->h.raw - skb->nh.raw);*tail_skb = tmp_skb;tail_skb = &(tmp_skb->next);skb->len += tmp_skb->len;skb->data_len += tmp_skb->len;skb->truesize += tmp_skb->truesize;__sock_put(tmp_skb->sk);tmp_skb->destructor = NULL;tmp_skb->sk = NULL;//如果用户设置路径MTU检测方式不是总是检测,将本地分片标记设置到套接口上//,后续在准备进行分片时,如果ip头设置为DF不能分片标记,但本地local_df为//true,则本地完成分片。if (inet->pmtudisc != IP_PMTUDISC_DO)skb->local_df = 1;//如果当前路径MTU检测功能设置为总是进行,或者当前报文长度小于路径MTU大小//并且不希望进行分片,则在IP头中标记不能分片。if (inet->pmtudisc == IP_PMTUDISC_DO ||(skb->len <= dst_mtu(&rt->u.dst) &&ip_dont_fragment(sk, &rt->u.dst)))df = htons(IP_DF);if (inet->cork.flags & IPCORK_OPT)opt = inet->cork.opt;//如果当前是多播,则设置TTL为多播长度(通常为1),否则设置为单播长度。if (rt->rt_type == RTN_MULTICAST)ttl = inet->mc_ttl;elsettl = ip_select_ttl(inet, &rt->u.dst);//设置版本及IP头长度。iph = (struct iphdr *)skb->data;iph->version = 4;iph->ihl = 5;//进行ip选项设置if (opt)iph->ihl += opt->optlen>>2;ip_options_build(skb, opt, inet->cork.addr, rt, 0);//其它IP头字段的填充iph->tos = inet->tos;iph->tot_len = htons(skb->len);iph->frag_off = df;ip_select_ident(iph, &rt->u.dst, sk);iph->ttl = ttl;iph->protocol = sk->sk_protocol;iph->saddr = rt->rt_src;iph->daddr = rt->rt_dst//校验和ip_send_check(iph);;skb->priority = sk->sk_priority;skb->dst = dst_clone(&rt->u.dst);//暂时先不分析netfilter框架,假设没有被拦截,则调用dst_output,dst_output在//下面已经分析,这里不再重复分析。NF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL,skb->dst->dev, dst_output);//当前一个完整的报文已经发出,则清除套接口中存储的临时数据inet->cork.flags &= ~IPCORK_OPT;kfree(inet->cork.opt);inet->cork.opt = NULL;if (inet->cork.rt)ip_rt_put(inet->cork.rt);inet->cork.rt = NULL;二、ip_queue_xmitsock *sk = skb->sk;inet_sock *inet = inet_sk(sk);ip_options *opt = inet->opt;//已经含有目地条目,则直接跳到后面进行处理。rt = (struct rtable *) skb->dst;if (rt != NULL)goto packet_routed;//检测该套接口之前是否含有sk_dst_cache,当没有过时,则直接使用该目地条目。rt = (struct rtable *)__sk_dst_check(sk, 0);//没有目的对象,需要路由查找if (rt == NULL)//获取套接口中设置的目的地址daddr = inet->daddr;//如果含有严格路由的IP选项,则使用该选项中的远端地址做为目的地址。if(opt && opt->srr)daddr = opt->faddr;security_sk_classify_flow(sk, &fl);//路由选路处理,暂不分析。ip_route_output_flow(&rt, &fl, sk, 0);sk_setup_caps(sk, &rt->u.dst);//将当前获取到的远端条目设置到套接口的缓存中//sk->sk_dst_cache = dst;__sk_dst_set(sk, dst);//保存设备能力集到套接口中sk->sk_route_caps = dst->dev->features;//如果驱动支持GSO特性处理,则加上NETIF_F_GSO_MASK表示所有//的GSO子协议(如TCP、UDP等)都能处理。if (sk->sk_route_caps & NETIF_F_GSO)sk->sk_route_caps |= NETIF_F_GSO_MASK;//检测当前套接口处理的协议类型是否匹配当前驱动支持的GSO子协议if (sk_can_gso(sk))if (dst->header_len)//如果有扩展头部,则不能使用GSO特性,所以需要去除sk->sk_route_caps &= ~NETIF_F_GSO_MASK;else//GSO特性是需要设备硬件必须支持分散聚合功能及校验和功能sk->sk_route_caps |= NETIF_F_SG | NETIF_F_HW_CSUM;//增加目地对象的引用计数skb->dst = dst_clone(&rt->u.dst);atomic_inc(&dst->__refcnt)packet_routed://如果含有严格路由选项,但当前目的地和网关不同,则丢弃。if (opt && opt->is_strictroute && rt->rt_dst != rt->rt_gateway)goto no_route;//把skb->data提升到ip头字段的位置iph = (struct iphdr *) skb_push(skb, sizeof(struct iphdr) + (opt ? opt->optlen : 0));//填充协议版本、IP头长度、TOS字段*((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff));//填充总长度iph->tot_len = htons(skb->len);//如果套接口设置了路径MTU发现设置总是执行则不能进行分片//如果套接口设置了路径MTU发现设置每次路由都进行处理,同时当前目地对象还没有索//定。则也不能进行分片。if (ip_dont_fragment(sk, &rt->u.dst) && !ipfragok)//不能进行分片,则需要在IP头部将分片标记设置为DFiph->frag_off = htons(IP_DF);elseiph->frag_off = 0;//允许分片//设置IP头其它字段iph->ttl      = ip_select_ttl(inet, &rt->u.dst);iph->protocol = sk->sk_protocol;iph->saddr    = rt->rt_src;iph->daddr    = rt->rt_dst;skb->nh.iph   = iph;//如果有IP选项,则进行填充if (opt && opt->optlen)iph->ihl += opt->optlen >> 2;ip_options_build(skb, opt, inet->daddr, rt, 0);//设置IP头ID字段ip_select_ident_more(iph, &rt->u.dst, sk,(skb_shinfo(skb)->gso_segs ?: 1) - 1);//设置校验和ip_send_check(iph);//设置优先级skb->priority = sk->sk_priority;//进行netfileter的LOCAL_OUT链处理,这里不分析netfileter,假设没有被//netfilter拦截,则会触发dst_output调用。//dst_output//skb->dst->output(skb);//这里dsp->output的回调是在ip_route_output_flow中设置的,假设当前发送的目的地//是单播,则output回调被设置为ip_outputNF_HOOK(PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt->u.dst.dev,dst_output);-------------------------------------------------------------------------------------------------------------------ip_outputnet_device *dev = skb->dst->dev;skb->dev = dev;//输出设备skb->protocol = htons(ETH_P_IP);//进行netfileter的POST_ROUTING链处理,这里不分析netfileter,假设没有被//netfilter拦截,则会触发ip_finish_output调用。NF_HOOK_COND(PF_INET, NF_IP_POST_ROUTING, skb, NULL, dev,ip_finish_output,!(IPCB(skb)->flags & IPSKB_REROUTED));-------------------------------------------------------------------------------------------------------------------ip_finish_output//如果当前报文大于路径MTU,同时当前报文并不使用GSO特性处理。则需要进行//分片,否则直接调用ip_finish_output2继续处理输出。支持GSO特性通常是把分片//的处理拖延到最后网卡驱动中处理,以后再单独分析IP报的分片和组装。目前//假设不需要分片。if (skb->len > dst_mtu(skb->dst) && !skb_is_gso(skb))ip_fragment(skb, ip_finish_output2);elseip_finish_output2(skb);-------------------------------------------------------------------------------------------------------------------ip_finish_output2dst_entry *dst = skb->dst;net_device *dev = dst->dev;hh_len = LL_RESERVED_SPACE(dev);//如果当前skb的head到data之间的空间已经不能放下二层数据包头,则需要重新//分配一块空间。if (unlikely(skb_headroom(skb) < hh_len && dev->hard_header))skb2 = skb_realloc_headroom(skb, LL_RESERVED_SPACE(dev));if (skb->sk)skb_set_owner_w(skb2, skb->sk);kfree_skb(skb);skb = skb2;//如果已经含有硬件缓存头,则直接填充硬件头,没有硬件缓存头则需要调用邻居//模块去解析处理,以后再分析这块。if (dst->hh)neigh_hh_output(dst->hh, skb);else if (dst->neighbour)dst->neighbour->output(skb);

0 0
原创粉丝点击