TCP/IP学习(30)——L2数据链路层的数据包处理详细流程

news/2024/7/3 8:57:50

原文地址:TCP/IP学习(30)——L2数据链路层的数据包处理详细流程 作者:GFree_Wind

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

在前面的博文中,我学习了数据包从L2到L5的流程,但是当时因为时间和水平的限制,整个儿流程并没有涉及太多的细节。前两天大致又过了这个流程,发现有不少细节还是需要注意的。所以决定,将之前略过的一些内容,详细的学习一遍。

今天主要是学习L2数据链路层的数据包的处理机制。在Linux kernel中,由网卡驱动完成L1物理层和L2数据链路层的工作。

首先看函数net_dev_init
  1. static int __init net_dev_init(void)
  2. {
  3.     int i, rc = -ENOMEM;

  4.     BUG_ON(!dev_boot_phase);
     /* 
     创建对应的/proc文件,如/proc/net/dev, /proc/net/softnet_stat等
     */
  1.     if (dev_proc_init())
  2.         goto out;
     /* 初始化netdev对应的kobject*/
  1.     if (netdev_kobject_init())
  2.         goto out;
     /* 
     初始化数据链路层的handle上层数据类型表。
     回忆前文《TCP/IP学习(28)——数据包完整接受流程》中,在inet_init中注册了IP包类型到这个表中。
     */
  1.     INIT_LIST_HEAD(&ptype_all);
  2.     for (= 0; i < PTYPE_HASH_SIZE; i++)
  3.         INIT_LIST_HEAD(&ptype_base[i]);
     /*
     注册neddev_net_ops subsystem
     */
  1.     if (register_pernet_subsys(&netdev_net_ops))
  2.         goto out;

  3.     /*
  4.      *    Initialise the packet receive queues.
  5.      */
  6.     /*
  7.     为每个CPU初始化PERCPU的全局变量softnet_data,作为该CPU的接收缓存 
  8.     */
  9.     for_each_possible_cpu(i) {
  10.         struct softnet_data *sd = &per_cpu(softnet_data, i);

  11.         ...... ......
  12.     }

  13.     dev_boot_phase = 0;

  14.     /* The loopback device is special if any other network devices
  15.      * is present in a network namespace the loopback device must
  16.      * be present. Since we now dynamically allocate and free the
  17.      * loopback device ensure this invariant is maintained by
  18.      * keeping the loopback device as the first device on the
  19.      * list of network devices. Ensuring the loopback devices
  20.      * is the first device that appears and the last network device
  21.      * that disappears.
  22.      */
  23.     if (register_pernet_device(&loopback_net_ops))
  24.         goto out;

  25.     if (register_pernet_device(&default_device_ops))
  26.         goto out;
     /*
     enable软中断
     */
  1.     open_softirq(NET_TX_SOFTIRQ, net_tx_action);
  2.     open_softirq(NET_RX_SOFTIRQ, net_rx_action);

  3.     hotcpu_notifier(dev_cpu_callback, 0);
  4.     dst_init();
  5.     dev_mcast_init();
  6.     rc = 0;
  7. out:
  8.     return rc;
  9. }
net_dev_init在系统启动时,在注册网卡之前调用,主要就是初始化net device所需要的一些环境。

下面仍然以Intel PRO/1000的网卡驱动为例,e1000_init_module为该驱动的入口。通过e1000_init_module->pci_register_driver->e1000_probe进入初始化函数。
在e1000_probe中,通过下面这条语句绑定了操作函数。
netdev->netdev_ops = &e1000_netdev_ops;
  1. static const struct net_device_ops e1000_netdev_ops = {
  2.     .ndo_open        = e1000_open,
  3. ...... ......
  4. };
对于今天的主题来说,只需关心e1000_open即可。因为该函数是在激活该网卡时被调用,完成资源的申请,中断的注册,即e1000_intr。
  1. static irqreturn_t e1000_intr(int irq, void *data)
  2. {
  3.     ...... ...... 
  4.     /*
  5.     检测是否可以调度NAPI:
  6.     当没有disable NAPI且没有该网卡对应的NAPI在运行时(保证对应一个网卡的NAPI只有一个实例在运行),即可调度一个新的NAPI。
  7.     NAPI是一种新的网卡数据检查处理方式。基本上是interrupt+poll。详细信息问google
  8.     */
  9.     if (likely(napi_schedule_prep(&adapter->napi))) {
  10.         /* 
  11.         清楚单次的统计信息。
  12.         刚看到这里时,我也奇怪,为什么total的统计信息要被清零。
  13.         实际上这些统计信息只是一次NAPI运行的统计信息,并不是网卡总的统计信息。
  14.         网卡的统计信息为netdev->stats。NAPI运行完会将下面的值加到网卡的统计信息上的。
  15.         */
  16.         adapter->total_tx_bytes = 0;
  17.         adapter->total_tx_packets = 0;
  18.         adapter->total_rx_bytes = 0;
  19.         adapter->total_rx_packets = 0;
  20.         /* 要求调度对应的NAPI实例 */
  21.         __napi_schedule(&adapter->napi);
  22.     } else {
  23.         /* this really should not if it does it is basically a
  24.          * bug, but not a hard error, so enable ints and continue */
  25.         if (!test_bit(__E1000_DOWN, &adapter->flags))
  26.             e1000_irq_enable(adapter);
  27.     }

  28.     return IRQ_HANDLED;
  29. }
上面为中断的关键流程,其中要求调度对应的NAPI实例时,实际上是引发一个软中断。
__raise_softirq_irqoff(NET_RX_SOFTIRQ)。这个中断函数的主要功能就是要求调度一个NAPI——这里跟以前理解的中断函数不太一样。按照教科书式的概念,网卡的中断函数,应该将数据包从网卡的缓冲中取出放到一个系统缓冲中,然后在引发软中断去做剩下的工作。

下面看 NET_RX_SOFTIRQ软中断对应的处理函数net_rx_action。
  1. static void net_rx_action(struct softirq_action *h)
  2. {
  3.     struct softnet_data *sd = &__get_cpu_var(softnet_data);
  4.     unsigned long time_limit = jiffies + 2;
  5.     int budget = netdev_budget;
  6.     void *have;

  7.     local_irq_disable();
     /* 开始顺序poll所有需要poll的网卡 */
  1.     while (!list_empty(&sd->poll_list)) {
  2.         struct napi_struct *n;
  3.         int work, weight;

  4.         /* If softirq window is exhuasted then punt.
  5.          * Allow this to run for 2 jiffies since which will allow
  6.          * an average latency of 1.5/HZ.
  7.          */
  8.         if (unlikely(budget <= 0 || time_after(jiffies, time_limit)))
  9.             goto softnet_break;

  10.         local_irq_enable();

  11.         /* Even though interrupts have been re-enabled, this
  12.          * access is safe because interrupts can only add new
  13.          * entries to the tail of this list, and only ->poll()
  14.          * calls can remove this head entry from the list.
  15.          */
  16.         /* 取得一个网卡的NAPI实例 */
  17.         n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list);
         /* 给这个实例上锁 */
  1.         have = netpoll_poll_lock(n);

  2.         weight = n->weight;

  3.         /* This NAPI_STATE_SCHED test is for avoiding a race
  4.          * with netpoll's poll_napi(). Only the entity which
  5.          * obtains the lock and sees NAPI_STATE_SCHED set will
  6.          * actually make the ->poll() call. Therefore we avoid
  7.          * accidently calling ->poll() when NAPI is not scheduled.
  8.          */
  9.         work = 0;
  10.         if (test_bit(NAPI_STATE_SCHED, &n->state)) {
  11.             /* poll这个网卡 */
  12.             work = n->poll(n, weight);
  13.             trace_napi_poll(n);
  14.         }

  15.         WARN_ON_ONCE(work > weight);

  16.         budget -= work;

  17.         local_irq_disable();

  18.         /* Drivers must not modify the NAPI state if they
  19.          * consume the entire weight. In such cases this code
  20.          * still "owns" the NAPI instance and therefore can
  21.          * move the instance around on the list at-will.
  22.          */
  23.         if (unlikely(work == weight)) {
  24.             /* 该NAPI的weight消耗完毕,需要处理下一个 */
  25.             if (unlikely(napi_disable_pending(n))) {
  26.                 local_irq_enable();
  27.                 napi_complete(n);
  28.                 local_irq_disable();
  29.             } else
  30.                 list_move_tail(&n->poll_list, &sd->poll_list);
  31.         }

  32.         netpoll_poll_unlock(have);
  33.     }
  34. out:
  35.     net_rps_action_and_irq_enable(sd);

  36. #ifdef CONFIG_NET_DMA
  37.     /*
  38.      * There may not be any more sk_buffs coming right now, so push
  39.      * any pending DMA copies to hardware
  40.      */
  41.     dma_issue_pending_all();
  42. #endif

  43.     return;

  44. softnet_break:
  45.     sd->time_squeeze++;
  46.     __raise_softirq_irqoff(NET_RX_SOFTIRQ);
  47.     goto out;
  48. }
通过上面这个软中断处理函数,对应每个网卡来说,又需要跳回驱动,去学习对应的poll函数。对于本文的这个驱动来说,poll函数就是e1000_clean->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.    ...... ......
     
     /* 得到当前需要处理buffer*/
  1.     i = rx_ring->next_to_clean;
  2.     rx_desc = E1000_RX_DESC(*rx_ring, i);
  3.     buffer_info = &rx_ring->buffer_info[i];

  4.     while (rx_desc->status & E1000_RXD_STAT_DD) {
  5.         struct sk_buff *skb;
  6.         u8 status;

  7.         if (*work_done >= work_to_do//如果已经poll到足够的包,可以跳出返回
  8.             break;
  9.         (*work_done)++;
  10.         rmb(); /* read descriptor and rx_buffer_info after status DD */
          
         /* 得到数据包buffer对应的skb buffer结构地址 */
  1.         status = rx_desc->status;
  2.         skb = buffer_info->skb;
  3.         buffer_info->skb = NULL;
  
 /* 
         然后做一些网卡硬件相关,及一些sanity check
         */
         ...... ......
  1.        /* 
  2.        设置skb->pkt_type:PACKET_BROADCAST等;
  3.        即数据链路层协议类型
  4.        */
  5.         skb->protocol = eth_type_trans(skb, netdev);
         /* 将数据包传递给上层,并做一些通用数据链路层的处理 */
  1.         e1000_receive_skb(adapter, status, rx_desc->special, skb);

  2. next_desc:
  3.         /* 处理下一个数据包 */
  4.         ...... ......
  5.     }

     /* 更新统计信息等*/
  1.     ...... ...... 

  2.     return cleaned;
  3. }
在这个函数中,真正的从网卡buffer中取出数据包,然后根据硬件的特性做一些特定处理,并简单的设置了数据包的一些field,完成L1的操作,设置好L2的报头。这时,数据包已经为TCP/IP协议栈所需要的skb_buff结构。
然后调用e1000_receive_skb->netif_receive_skb->__netif_receive_skb
  1. static int __netif_receive_skb(struct sk_buff *skb)
  2. {
  3.     struct packet_type *ptype, *pt_prev;
  4.     rx_handler_func_t *rx_handler;
  5.     struct net_device *orig_dev;
  6.     struct net_device *master;
  7.     struct net_device *null_or_orig;
  8.     struct net_device *orig_or_bond;
  9.     int ret = NET_RX_DROP;
  10.     __be16 type;
      /* 为skb打时间戳 */
  1.     if (!netdev_tstamp_prequeue)
  2.         net_timestamp_check(skb);
     /* vlan下硬件加速处理 */
  1.     if (vlan_tx_tag_present(skb) && vlan_hwaccel_do_receive(skb))
  2.         return NET_RX_SUCCESS;

  3.     /* if we've gotten here through NAPI, check netpoll */
  4.     if (netpoll_receive_skb(skb))
  5.         return NET_RX_DROP;
     /* 设置skb的iif为接收网卡的索引 */
  1.     if (!skb->skb_iif)
  2.         skb->skb_iif = skb->dev->ifindex;
  1.     /*
  2.      * bonding note: skbs received on inactive slaves should only
  3.      * be delivered to pkt handlers that are exact matches. Also
  4.      * the deliver_no_wcard flag will be set. If packet handlers
  5.      * are sensitive to duplicate packets these skbs will need to
  6.      * be dropped at the handler. The vlan accel path may have
  7.      * already set the deliver_no_wcard flag.
  8.      */
  9.     /*关于网卡的bond的处理, 这个feature我只是了解,所以略过 */
  10.     null_or_orig = NULL;
  11.     orig_dev = skb->dev;
  12.     master = ACCESS_ONCE(orig_dev->master);
  13.     if (skb->deliver_no_wcard)
  14.         null_or_orig = orig_dev;
  15.     else if (master) {
  16.         if (skb_bond_should_drop(skb, master)) {
  17.             skb->deliver_no_wcard = 1;
  18.             null_or_orig = orig_dev; /* deliver only exact match */
  19.         } else
  20.             skb->dev = master;
  21.     }

  22.     __this_cpu_inc(softnet_data.processed);
  23.     /* 初始化l3 header 和 l4 header 的地址*/
  24.     skb_reset_network_header(skb);
  25.     skb_reset_transport_header(skb);
  26.     /* 得到mac地址长度,准确来说是2层地址的长度 */
  27.     skb->mac_len = skb->network_header - skb->mac_header;

  28.     pt_prev = NULL;

  29.     rcu_read_lock();

  30.     /*
  31.     省略一些不太相关的代码 
  32.     */
  33.     ...... ......
  34.    
  35.     /*
  36.     通过2层协议类型作为key,得到相应链表。
  37.     */
  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//找到匹配的协议类型,上传给L3层
  8.                 ret = deliver_skb(skb, pt_prev, orig_dev);
  9.             pt_prev = ptype;
  10.         }
  11.     }

  12.     if (pt_prev) {
  13.         ret = pt_prev->func(skb, skb->dev, pt_prev, orig_dev);
  14.     } else {
  15.         kfree_skb(skb);
  16.         /* Jamal, now you will not able to escape explaining
  17.          * me how you were going to use this. :-)
  18.          */
  19.         ret = NET_RX_DROP;
  20.     }

  21. out:
  22.     rcu_read_unlock();
  23.     return ret;
  24. }
现在基本上已经比较详细的学习了L2层的数据包处理流程。当然,还有很多很多的细节没有涉及,道路还很漫长啊。

http://www.niftyadmin.cn/n/1965488.html

相关文章

一次性搞清楚equals和hashCode

在程序设计中&#xff0c;有很多的“公约”&#xff0c;遵守约定去实现你的代码&#xff0c;会让你避开很多坑&#xff0c;这些公约是前人总结出来的设计规范。 Object类是Java中的万类之祖&#xff0c;其中&#xff0c;equals和hashCode是2个非常重要的方法。 这2个方法总是被…

硬科技企业大放异彩,Nibiru、柔宇斩获国家优质投资项目特别奖

2016—2017年度国家优质投资项目推介表彰大会在京举行。 近日&#xff0c;由国家发展改革委主管&#xff0c;中国投资协会主办的2016—2017年度国家优质投资项目推介表彰活动的审定结果在北京钓鱼台国宾馆揭晓。中国投资协会会长杨庆蔚&#xff0c;中国投资协会副会长、创投委…

VirtualBox虚拟机Ubuntu设置共享文件夹,并自动挂载

一、环境 Win10系统&#xff0c;VirtualBox-5.1.22-115126Ubuntu16.04&#xff08;64位&#xff09;虚拟机二、目的在Ubuntu中能够共享Win10中的某个文件夹&#xff0c;而且能够自动挂载三、设置共享文档夹1) 安装好Ubuntu后&#xff0c;运行并登录2) 点击"设备(Devices) …

笨方法学习Python31-40

31、作出决定复习了raw_input("> ")if 变量 "Vaule":执行语句32、循环和列表count [1, 2, 3, 4, 5]for i in count:print "This is %d" % i #循环打印出count的值count.append(6) #从最后增加值count.insert(1,44) #从第…

Shell脚本入门-4

2019独角兽企业重金招聘Python工程师标准>>> 我们可以在执行 Shell 脚本时&#xff0c;向脚本传递参数&#xff0c;脚本内获取参数的格式为&#xff1a;$n。n 代表一个数字&#xff0c;1 为执行脚本的第一个参数&#xff0c;2 为执行脚本的第二个参数&#xff0c;以…

Docker初体验——踩过的那些坑!

2018‎年‎3‎月‎6‎日 Docker安装 环境&#xff1a;windows7 安装包&#xff1a;DockerToolbox-17.10.0-ce.exe &#xff08;下载地址&#xff1a;http://mirrors.aliyun.com/doc...&#xff09; 坑&#xff1a; //启动包错&#xff1a; Running pre-create checks... (def…

双11购物节火热,谨防木马乘机而入

腾讯电脑管家 2015/11/12 16:240x00 概况近期11.11购物节&#xff0c;无数的网页、软件都充斥着“血拼双11”的广告&#xff0c;这时的电脑桌面如果多了几个双11相关的快捷方式&#xff0c;或者浏览器主页被锁定成推送网购内容的导航网站&#xff0c;你会不会认为这也是正常的…

Android应用安全开发之防范无意识的数据泄露

gh0stbo 2016/01/29 10:160x00 简介OWASP移动安全漏洞Top 10中第4个就是无意识的数据泄漏。当应用程序存储数据的位置本身是脆弱的时&#xff0c;就会造成无意识的数据泄漏。这些位置可能包括剪贴板&#xff0c;URL缓存&#xff0c;浏览器的Cookies&#xff0c;HTML5数据存储&…