热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

TCP/IP学习(28)——数据包完整接收流程

摘自:http:blog.chinaunix.netuid-23629988-id-272460.html本文的copyleft归gfree.wind@g


摘自:http://blog.chinaunix.net/uid-23629988-id-272460.html

本文的copyleft归gfree.wind@gmail.com所有,使用GPL发布,可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接,严禁用于任何商业用途。
作者:gfree.wind@gmail.com
博客:linuxfocus.blog.chinaunix.net
    

今天的学习目标是TCP/IP数据包的完整接收流程!

入口自然是从driver开始,以Intel(R) PRO/1000 Network Driver对应的e1000_main.c为例。
事先声明,因为本人不是内核工程师,所以难免在kernel的代码理解上献丑,希望大家指正。

这里,我们并不需要关心如何去写driver,如何处理中断,只需要关注网卡如何接收的数据包,并传给上层协议。

函数e1000_clean_rx_irq为网卡接收数据包的处理函数。
  1. static bool e1000_clean_rx_irq(struct e1000_adapter *adapter,
  2.              struct e1000_rx_ring *rx_ring,
  3.              int *work_done, int work_to_do)
  4. {
  5.     ...... ......

  6.     /* 得到了接收缓存buffer */
  7.     i = rx_ring->next_to_clean;
  8.     rx_desc = E1000_RX_DESC(*rx_ring, i);
  9.     buffer_info = &rx_ring->buffer_info[i];
  10.     
  11.     while (rx_desc->status & E1000_RXD_STAT_DD) {
  12.         ...... ......
  13.         
  14.         /* 取得接收缓存buffer的数据包buffer,即skb */
  15.         skb = buffer_info->skb;
  16.         buffer_info->skb = NULL;
         
          ...... ......
          
          /* 确定skb的L2数据链路层协议,以及包的类型:broadcast, multicast, otherhost or host */
          skb->protocol = eth_type_trans(skb, netdev);
          
          /* 将数据包传递给L3 IP层*/
          e1000_receive_skb(adapter, status, rx_desc->special, skb);

  1.          ...... ......
  2.     }
  1.     
  2.     ...... ......
  3. }
从驱动中的这个函数,可以看出,驱动层即为L2 数据链路层,所以只负责对应L2层的工作,如设置L2层协议,设置包的类型等。当将L2层的工作处理完毕后,即将数据包传递给上层协议。这个动作由e1000_receive_skb->netif_receive_skb->__netif_receive_skb。其中netif_receive_skb和__netif_receive_skb的主要用途主要是执行一些与硬件本身非紧密相关的一些L2层的操作,如设置skb中的skb_iif(接收包的网卡索引),vlan的处理等等。

L2数据链路层如何将数据传递给L3 IP层的呢?
请看我前面的文章《TCP/IP学习(27)——协议初始化与简要的发送/接收流程》,在inet_init函数中,这一语句dev_add_pack(&ip_packet_type);将IP协议添加到了L2层的协议类型中。
  1. void dev_add_pack(struct packet_type *pt)
  2. {
  3.     int hash;

  4.     spin_lock_bh(&ptype_lock);
  5.     if (pt->type == htons(ETH_P_ALL))
  6.         list_add_rcu(&pt->list, &ptype_all);
  7.     else {
  8.         hash = ntohs(pt->type) & PTYPE_HASH_MASK;
  9.         list_add_rcu(&pt->list, &ptype_base[hash]);
  10.     }
  11.     spin_unlock_bh(&ptype_lock);
  12. }
inet_init将ip_packet_type挂载了&ptype_base[hash]链表上。


在__netif_receive_skb中,下面的这一段代码遍历了ptype_all链表,找到相符的type,调用其func回调函数。
  1. type = skb->protocol;
  2. list_for_each_entry_rcu(ptype,
  3. &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) {
  4. if (ptype->type == type && (ptype->dev == null_or_orig ||
  5.     ptype->dev == skb->dev || ptype->dev == orig_dev ||
  6.             ptype->dev == orig_or_bond)) {
  7. if (pt_prev)
  8. ret = deliver_skb(skb, pt_prev, orig_dev);
  9. pt_prev = ptype;
  10. }
  11. }

 static inline int deliver_skb(struct sk_buff *skb,
      struct packet_type *pt_prev,
      struct net_device *orig_dev)
 {
     atomic_inc(&skb->users);
     return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
 }

而IP对应的数据结构为
  1. static struct packet_type ip_packet_type __read_mostly = {
  2.     .type = cpu_to_be16(ETH_P_IP),
  3.     .func = ip_rcv,
  4.     .gso_send_check = inet_gso_send_check,
  5.     .gso_segment = inet_gso_segment,
  6.     .gro_receive = inet_gro_receive,
  7.     .gro_complete = inet_gro_complete,
  8. };
现在,答案已经很明显了。L2层通过注册的数据包的类型,找到了ip_packet_type。然后调用ip_rcv。在TCP/IP的源码中,大多数的上层协议都是通过这种,向底层协议注册相应的数据包类型,使底层协议可以根据包的类型,调用相应的上层协议的处理函数。这基本是一种通用的方式。

好了,现在我们进入了L3 IP层。 首先依然是IP层的通用处理,sanity check等。最后ip_rcv调用netfilt,并把ip_rcv_finish作为回调函数传递给netfilter。
  1. return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL,
  2.          ip_rcv_finish);
关于netfilter的处理和为什么要用netfilter,不是本文的重点——其实熟悉iptable的朋友,基本上就明白原因了,请回忆iptable的处理链或者说检测点。在netfilter的PREROUTING链对数据包处理完毕后,则调用ip_rcv_finish。这才是IP层真正的处理函数。(在TCP/IP源码中,有很多类似的函数,一个名字叫做xxx,另外一个叫做xxx_finish。其中xxx主要做sanity check,而xxx_finish才是真正的工作函数)。

到了L3层,这里就有一个问题了。在L2层,正常的情况下,接收到的L2层的数据包一定是发给本机的,不然我们是收不到这个L2数据包的——这里不考虑网卡的混杂模式或者作为bridge等情况。但是到L3层,也就是IP层,如果linux是作为一个switch,那么这个IP包的目的地址就很可能不是发给linux本机的。那么linux是如何处理这个的呢?

Linux在内核维护了两个路由表,一个是本机地址的路由表,另外一个是转发地址的路由表。在L3层,会查询这两个路由表。首先是查本机地址的路由表,如果成功,表明这个IP报文是发给本机的。如果是失败,则继续搜索转发地址的路由表。如果依然失败,那么就表示该包无法转发,直接drop掉。至于是否回复icmp,则根据配置而定。对于接受数据包来说,这两个路由表的查询动作,是封装在一个函数ip_route_input_slow里的(不考虑route cache的情况)。当查找成功时,返回的是一个与协议无关的struct dst_entry结构。对于接收到的IP数据包,则调用dst->input,如是发送的IP数据包,则调用dst->output。这基本上是Linux对于IP数据处理的机制。这里只是简要的描述了一下。以后我会针对这个问题,专门讲解一下的。

在此,我们假设该数据包是发送给linux本机的。ip_rcv_finish在做了IP层的处理,如解析IPoption等, 肯定可以在本机地址的路由表查找成功。然后调用dst->input,也就是ip_local_deliver。
  1. int ip_local_deliver(struct sk_buff *skb)
  2. {
  3.     /*
  4.      *    Reassemble IP fragments.
  5.      */

  6.     if (ip_hdr(skb)->frag_off & htons(IP_MF | IP_OFFSET)) {
  7.         if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER))
  8.             return 0;
  9.     }

  10.     return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL,
  11.          ip_local_deliver_finish);
  12. }
与ip_rcv类似,ip_local_deliver同样是做些预处理的操作,如处理IP分片。然后将数据包交给netfilter。最后数据包将传至ip_local_deliver_finish。
  1. static int ip_local_deliver_finish(struct sk_buff *skb)
  2. {
  3.     ...... ......
  4.     /* 将数据包传递给匹配的raw socket */
  5.     raw = raw_local_deliver(skb, protocol);
  6.     hash = protocol & (MAX_INET_PROTOS - 1);
  7.     /* 查找对应的proto */
  8.     ipprot = rcu_dereference(inet_protos[hash]);
  9.     if (ipprot) {
  10.         ...... ......
  11.         /* 调用proto对应的处理函数 */
  12.         ret = ipprot->handler(skb);
  13.         ...... ......
  14.     }    
  1. }
在ip_local_deliver_finish中,首先将数据包clone传递给匹配的raw socket。注意,这里是将skb clone了一份,然后将clone传给raw socket。这里说明raw socket在不影响其它socket的情况下,可以收到所有匹配其条件的数据包的clone——具体说明,参加我前面的博文《大师的错误?》。在传递给raw socket后,根据IP头部的protocol字段,从全局的数组inet_protos中选择出正确的L4层协议。这里,最好可以参照前文《协议的初始化》,在inet_init中,使用inet_add_protocol将L4层协议添加到inet_protos中的,如inet_add_protocol(&udp_protocol, IPPROTO_UDP) 。

假设数据包为UDP数据包,udp_protocol的声明如下:
  1. static const struct net_protocol udp_protocol = {
  2.     .handler =    udp_rcv,
  3.     .err_handler =    udp_err,
  4.     .gso_send_check = udp4_ufo_send_check,
  5.     .gso_segment = udp4_ufo_fragment,
  6.     .no_policy =    1,
  7.     .netns_ok =    1,
  8. };
那么现在数据包就传递到了L4层,udp_rcv中。udp_rcv直接调用__udp4_lib_rcv。大家都知道Linux的TCP/IP与OSI的7层协议模型不同,只有5层。到了L4 transport层后,在往上层传递就到了应用层,也就是kernel需要把数据包传递给正确的socket。就完成了整个儿的数据包接收过程。对于UDP数据包,__udp4_lib_rcv调用__udp4_lib_lookup_skb找到最佳匹配的socket,然后调用udp_queue_rcv_skb将这个数据包添加到该socket的接收队列中。


好了,这就是一个发往linux本地的数据包包的完整接收流程。在这个过程中,还是省略了不少细节。但是大体的处理流程基本上就是这样。在处理过程中,虽然是C语言,但是仍然体现了面向对象的思想。每种协议为一个structure,也就是一个object。通过上层协议向下层协议注册,然后调用其回调函数的方式。实现了上层对下层的透明,下层协议无须关系上层的处理。另外,linux对于IP包的处理,通过查找路由的方式,实现了处理的统一接口。这个需要再进行专门的研究和分析。


推荐阅读
author-avatar
飞上天的鱼
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有