免费注册 查看新帖 |

Chinaunix

  平台 论坛 博客 文库
最近访问板块 发新帖
查看: 4198 | 回复: 7
打印 上一主题 下一主题

Linux 网络栈数据的接收 [复制链接]

论坛徽章:
0
跳转到指定楼层
1 [收藏(0)] [报告]
发表于 2006-11-14 15:59 |只看该作者 |倒序浏览
最近看Linux的网络栈,把接收这一部份的流程记录下来:

内核版本:2.6.12

一、网卡驱动程序
当然,网卡驱动程序完成了从网卡接收数据的第一部份工作,以以3com501 的驱动
linux/drivers/net/3c501.c为例(因为它排在了第一个):

设备初始化函数中,依次调用:
  1. int init_module(void)
  2. ->el1_probe()
  3. -->el1_probe1()
复制代码


先向内核申请注册一个以太设备,并设定设备的open函数为:
  1. dev->open = &el_open;(line 316)
复制代码


在el_open函数中,注册了自己的中断处理程序:(line 350)
  1. if ((retval = request_irq(dev->irq, &el_interrupt, 0, dev->name, dev)))
复制代码


el_interruupt为中断处理程序,中断实际上是一个电信号,网卡收到数据包后,就触发这个电信号给中断控制器的某个引脚,中断控制器向CPU某引脚发送相应的信号,CPU检测到此信号后,进入内存中操作系统预设的位置的代码,即中断处理程序入口点,这样,操作系统设用do_IRQ()处理中断,该中断函数根据中断号和dev_id,确定中断处理函数,对于我们的例子,也就是调用对应中断函数el_interrupt(),它判断网卡传递的信号,如果是接收数据,于是读网卡寄存器……XXXXXXXXX
完成读取读取后,调用接收函数:(p654)
  1.                 else if (rxsr & RX_GOOD)
  2.                 {
  3.                         /*
  4.                          *        Receive worked.
  5.                          */
  6.                         el_receive(dev);
  7.                 }
复制代码


参数dev 就是本身网卡的设备描述,el_receive()函数最重要的工作就是分配缓存:
  1. skb = dev_alloc_skb(pkt_len+2);(line724)
复制代码

并关联设备:
  1. skb->dev = dev;(line740)
复制代码

然后调用:
  1. netif_rx(skb);(line748)
复制代码


netif_rx是网络栈向驱动程序提供的接收数据包的接口,对于驱动程序的编写者而言,只需要知道这个函数即可,不过我们要分析整个接收流程,还要来进一步剖析它。

PS:网卡驱动的整个实现比较复杂,我这里只是做了简单的描述,对于没有网络驱动编写经验的朋友,可以参考《Linux设备驱动程序》第十七章网卡驱动方面的内容,九贱对作者提供的例子做了进一步的脚注:
http://bbs.chinaunix.net/viewthread.php?tid=845422&highlight=独孤九贱

二、下半部
因为中断处理程序本身的一些局限性,为了提高其效率,Linux将中断处理分为两部份:上半部(中断处理程序)和下半部。
一些时间任务紧迫,与硬件相关的任务,被放在了中断处理程序中,而另外一些可以“稍微延迟”的任务被放在了下半部中。例如,响应网卡中断,读取接收数据,由网卡中断处理程序完成,而对数据包的接收的进一步处理,就由下半部来完成。
Linux中下半部的完成,有许多种实现,对于网络栈来说,采用了“软中断”机制。也就是说,当从网卡接收到数据后,网络子系统触发软中断,网卡中断处理程序就返回去继续它的工作了,而内核会在尽可能快的时间内,调用网络子系统的软中断函数,进一步处理数据包。
关于下半部和软中断的介绍,《Linux内核设计与实现》第二版第七章中,有详细的介绍。

三、队列
由于中断处理的接收函数netif_rx同软中断函数不是连续处理的。也就是说,netif_rx不是等上层协议栈处理完一个数据包后,再返回处理另一个数据包,所以,在netif_rx与软中断函数中间,自然需要一个缓冲区:netif_rx往里面写,而软中断函数从里边取。所以,从性能的角度考虑,建立这样一个缓冲区,是非常必要的。Linux实现这一缓冲的算法是队列。

Linux用struct softnet_data结构描述了队列:
  1. struct softnet_data
  2. {
  3.         int                        throttle;
  4.         int                        cng_level;
  5.         int                        avg_blog;
  6.         struct sk_buff_head        input_pkt_queue;
  7.         struct list_head        poll_list;
  8.         struct net_device        *output_queue;
  9.         struct sk_buff                *completion_queue;

  10.         struct net_device        backlog_dev;        /* Sorry. 8) */
  11. };
复制代码


throttle 用于拥塞控制,当拥塞发生时,throttle将被设置,后续进入的数据包将被丢弃;
cng_level描述一个拥塞级别,由netif_rx函数返回。
input_pkt_queue成员即为包的输入队列,在后面的分析中,我们可以看它网络子系统,如果进行入队和出队操作;
poll_list维护一个设备列表,特别的,我们并不需要针对每一个物理设备进行特别处理,比如,eth0接收一个包,eth1也接收一个包,eth0->eth1,再分别调用它们的处理函数poll,从队列中取出数据包并送住上层。而且采用了一个通用伪设备backlog_dev(struct softnet_data的最后一个成员),来进行处理,它的poll函数是process_backlog,在后面队列初始化中,我们会看到。

特别地,队列是一个per_cpu变量,也就是说,第一个CPU都有一个:
  1. DECLARE_PER_CPU(struct softnet_data,softnet_data);(net/core/dev.c line:576)
复制代码


这样,每一次,都可以通过:
  1.         for (i = 0; i < NR_CPUS; i++) {
  2.                 struct softnet_data *queue;

  3.                 queue = &per_cpu(softnet_data, i);
  4.                ……
  5.                }
复制代码

来遍历每一个CPU的队列,并处理,或者是:
  1. struct softnet_data *queue;
  2. queue = &__get_cpu_var(softnet_data);
复制代码

取得当前CPU的队列。

四、上层协议的处理
最后一个要讨论的问题是,如何把相应的协议送往相应的上层栈,如IP协议交由tcp/ip栈,arp协议交由arp处理程序……
以太网帧头有一个类型字段,最简单的办法莫过于
switch(type)
case 0x0800
   do_ip();
case 0x0806:
   do_arp();
……

呵呵,当然Linux不会用我的笨办法了
内核用ruct packet_type结构来描述每一类协议:
  1. struct packet_type {
  2.         __be16                        type;        /* This is really htons(ether_type).        */
  3.         struct net_device                *dev;        /* NULL is wildcarded here                */
  4.         int                        (*func) (struct sk_buff *, struct net_device *,
  5.                                          struct packet_type *);
  6.         void                        *af_packet_priv;
  7.         struct list_head        list;
  8. };
复制代码

type即为协议类型值,func函数指针为对应的包处理句柄,list成员用于维护协议链表。
以ip协议(net/ipv4/af_inet.c)为例:
  1. static struct packet_type ip_packet_type = {
  2.         .type = __constant_htons(ETH_P_IP),
  3.         .func = ip_rcv,
  4. };
复制代码


对于IP协议来讲,类型是ETH_P_IP,接收处理函数是ip_rcv。接下来,就是要把ip_packet_type添加进协议链表中去:

  1. inet_init(void)(line:1012)
  2. -> ip_init();

  3. void __init ip_init(void)
  4. {
  5.         dev_add_pack(&ip_packet_type);
  6. ……
  7. }

  8. void dev_add_pack(struct packet_type *pt)
  9. {
  10.         int hash;

  11.         spin_lock_bh(&ptype_lock);
  12.         if (pt->type == htons(ETH_P_ALL)) {
  13.                 netdev_nit++;
  14.                 list_add_rcu(&pt->list, &ptype_all);
  15.         } else {
  16.                 hash = ntohs(pt->type) & 15;
  17.                 list_add_rcu(&pt->list, &ptype_base[hash]);
  18.         }
  19.         spin_unlock_bh(&ptype_lock);
  20. }
复制代码


可见,最后添加进的是一个以ptype_bash数组为首的链表,用协议值& 15,来取hash值,定位数组入口。

OK,有了这些基础,我们就可以来分析netif_rx函数了!!

未完,待续……

[ 本帖最后由 独孤九贱 于 2006-11-14 16:01 编辑 ]

论坛徽章:
1
2015年辞旧岁徽章
日期:2015-03-03 16:54:15
2 [报告]
发表于 2006-11-14 16:15 |只看该作者
支持九贱发文!

论坛徽章:
0
3 [报告]
发表于 2006-11-14 16:22 |只看该作者
多谢楼上的支持,我也是初看内核栈,以前都一直在看netfilter,写出来是为了和大家一起交流学习,水平有限,错误,不详之处众多,众人拾材火焰高呀!

一、网络子系统的初始化
实始化是在net/core/dev.c的init 函数中完成的

  1. static int __init net_dev_init(void)
  2. {
  3.         int i, rc = -ENOMEM;

  4.         BUG_ON(!dev_boot_phase);

  5.         net_random_init();

  6.         if (dev_proc_init())
  7.                 goto out;

  8.         if (netdev_sysfs_init())
  9.                 goto out;

  10. /*ptype_all与ptype_base类似,不过它用于混杂模式*/
  11.         INIT_LIST_HEAD(&ptype_all);
  12. /*初始化hash链表首部,我们前面已经讲过它的链表的建立了,现在才看到初始化,先后顺序有问题*/
  13.         for (i = 0; i < 16; i++)
  14.                 INIT_LIST_HEAD(&ptype_base[i]);

  15.         for (i = 0; i < ARRAY_SIZE(dev_name_head); i++)
  16.                 INIT_HLIST_HEAD(&dev_name_head[i]);

  17.         for (i = 0; i < ARRAY_SIZE(dev_index_head); i++)
  18.                 INIT_HLIST_HEAD(&dev_index_head[i]);

  19.         /*
  20.          *        初始化每一个CPU的包接收队列.
  21.          */

  22.         for (i = 0; i < NR_CPUS; i++) {
  23.                 struct softnet_data *queue;

  24. /*逐次取得第i个CPU的队列*/
  25.                 queue = &per_cpu(softnet_data, i);
  26. /*初始化队列链表*/
  27.                 skb_queue_head_init(&queue->input_pkt_queue);
  28. /*初始化各个成员变量*/
  29.                 queue->throttle = 0;
  30.                 queue->cng_level = 0;
  31.                 queue->avg_blog = 10; /* arbitrary non-zero */
  32.                 queue->completion_queue = NULL;
  33. /*初始化设备列表*/
  34.                 INIT_LIST_HEAD(&queue->poll_list);
  35. /*设置backlog_dev设备的状态,“我已经可以接收数据了”*/
  36.                 set_bit(__LINK_STATE_START, &queue->backlog_dev.state);
  37.                 queue->backlog_dev.weight = weight_p;
  38. /*这一步很重要,指明backlog_dev的poll函数为process_backlog*/
  39.                 queue->backlog_dev.poll = process_backlog;
  40.                 atomic_set(&queue->backlog_dev.refcnt, 1);
  41.         }

  42. #ifdef OFFLINE_SAMPLE
  43.         samp_timer.expires = jiffies + (10 * HZ);
  44.         add_timer(&samp_timer);
  45. #endif

  46.         dev_boot_phase = 0;
  47. /*接收和发送的软中断,在这里安装*/
  48.         open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL);
  49.         open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);

  50.         hotcpu_notifier(dev_cpu_callback, 0);
  51.         dst_init();
  52.         dev_mcast_init();
  53.         rc = 0;
  54. out:
  55.         return rc;
  56. }
复制代码

  1. 二、netif_rx
复制代码


netif_rx有两个很重要的工作:
1、把backlog_dev加入进设备列表;
2、把从网卡驱动接收来的数据添加进接收包队列中;

  1. int netif_rx(struct sk_buff *skb)
  2. {
  3.         int this_cpu;
  4.         struct softnet_data *queue;
  5.         unsigned long flags;

  6.         /* if netpoll wants it, pretend we never saw it */
  7.         if (netpoll_rx(skb))
  8.                 return NET_RX_DROP;

  9. /*如果没有设置接收的时间,更新之*/
  10.         if (!skb->stamp.tv_sec)
  11.                 net_timestamp(&skb->stamp);

  12.         /*
  13.          * The code is rearranged so that the path is the most
  14.          * short when CPU is congested, but is still operating.
  15.          */
  16.         local_irq_save(flags);
  17.         this_cpu = smp_processor_id();
  18. /*取得当前CPU的队列*/
  19.         queue = &__get_cpu_var(softnet_data);

  20. /*接收计数器累加*/
  21.         __get_cpu_var(netdev_rx_stat).total++;
  22. /*队列总数没有超过定义的最大值*/
  23.         if (queue->input_pkt_queue.qlen <= netdev_max_backlog) {
  24. /*这个if判断的含义是,如果阶列中没有数据包,则先添加设备,再跳转回来,将数据包入队,否则直接入队,在入队之前,还要判断throttle成员的值,即拥塞是否已经发生,如果发生,则要丢弃当前数据包*/
  25.                 if (queue->input_pkt_queue.qlen) {
  26.                         if (queue->throttle)
  27.                                 goto drop;

  28. enqueue:
  29.                         dev_hold(skb->dev);
  30. /*将数据包入队,并累加队列计算器*/
  31.                         __skb_queue_tail(&queue->input_pkt_queue, skb);
  32. #ifndef OFFLINE_SAMPLE
  33.                         get_sample_stats(this_cpu);
  34. #endif
  35.                         local_irq_restore(flags);
  36.                         return queue->cng_level;
  37.                 }

  38.                 if (queue->throttle)
  39.                         queue->throttle = 0;
  40. /*将backlog_dev设备添加进设备列表,并进一步入理*/
  41.                 netif_rx_schedule(&queue->backlog_dev);
  42.                 goto enqueue;
  43.         }

  44.         if (!queue->throttle) {
  45.                 queue->throttle = 1;
  46.                 __get_cpu_var(netdev_rx_stat).throttled++;
  47.         }

  48. drop:
  49. /*丢弃数据包,先累加计数器,再释放skb*/
  50.         __get_cpu_var(netdev_rx_stat).dropped++;
  51.         local_irq_restore(flags);

  52.         kfree_skb(skb);
  53.         return NET_RX_DROP;
  54. }
复制代码


三、netif_rx_schedule
  1. static inline void netif_rx_schedule(struct net_device *dev)
  2. {
  3.         if (netif_rx_schedule_prep(dev))
  4.                 __netif_rx_schedule(dev);
  5. }
复制代码


netif_rx_schedule函数只是一个包裹函数,它将主动权交到了__netif_rx_schedule手中:

  1. static inline void __netif_rx_schedule(struct net_device *dev)
  2. {
  3.         unsigned long flags;

  4.         local_irq_save(flags);
  5.         dev_hold(dev);
  6. /*添加设备进列表*/
  7.         list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
  8.         if (dev->quota < 0)
  9.                 dev->quota += dev->weight;
  10.         else
  11.                 dev->quota = dev->weight;
  12. /*触发接收软中断*/
  13.         __raise_softirq_irqoff(NET_RX_SOFTIRQ);
  14.         local_irq_restore(flags);
  15. }
复制代码


接收处理函数触发软中断,负责“下半部”处理的软中断函数net_rx_action函数将会被执行。

未完,待续……

[ 本帖最后由 独孤九贱 于 2006-11-14 16:25 编辑 ]

论坛徽章:
0
4 [报告]
发表于 2006-11-14 16:50 |只看该作者
  1. 一、接收的软中断函数net_rx_action
复制代码

  1. static void net_rx_action(struct softirq_action *h)
  2. {
  3. /*取得当前CPU上的队列*/
  4.         struct softnet_data *queue = &__get_cpu_var(softnet_data);
  5.         unsigned long start_time = jiffies;
  6. /*netdev_max_backlog是一个sysctl常量,它决定了最大的排队等候的数据包*/
  7.         int budget = netdev_max_backlog;

  8.        
  9.         local_irq_disable();
  10. /*处理每一个设备上接收到的数据包,直接遍历完所有的设备,其中之一,就是netif_rx函数添加进的伪设备:backlog_dev*/
  11.         while (!list_empty(&queue->poll_list)) {
  12.                 struct net_device *dev;

  13.                 if (budget <= 0 || jiffies - start_time > 1)
  14.                         goto softnet_break;

  15.                 local_irq_enable();
  16. /*取得每一个设备*/
  17.                 dev = list_entry(queue->poll_list.next,
  18.                                  struct net_device, poll_list);
  19.                 netpoll_poll_lock(dev);
  20. /*调用设备的poll函数*/
  21.                 if (dev->quota <= 0 || dev->poll(dev, &budget)) {
  22.                         netpoll_poll_unlock(dev);
  23.                         local_irq_disable();
  24.                         list_del(&dev->poll_list);
  25.                         list_add_tail(&dev->poll_list, &queue->poll_list);
  26.                         if (dev->quota < 0)
  27.                                 dev->quota += dev->weight;
  28.                         else
  29.                                 dev->quota = dev->weight;
  30.                 } else {
  31.                         netpoll_poll_unlock(dev);
  32.                         dev_put(dev);
  33.                         local_irq_disable();
  34.                 }
  35.         }
  36. out:
  37.         local_irq_enable();
  38.         return;

  39. softnet_break:
  40.         __get_cpu_var(netdev_rx_stat).time_squeeze++;
  41.         __raise_softirq_irqoff(NET_RX_SOFTIRQ);
  42.         goto out;
  43. }
复制代码


对于软中断函数,我们主要关心的是:
遍历设备列表,取得每一个设备的poll函数,在netif_rx函数中,添加进队列的是back_log设备,它的poll函数是process_backlog。

二、process_backlog
  1. static int process_backlog(struct net_device *backlog_dev, int *budget)
  2. {
  3.         int work = 0;
  4.         int quota = min(backlog_dev->quota, *budget);
  5. /*同样地,取得当前CPU的队列*/
  6.         struct softnet_data *queue = &__get_cpu_var(softnet_data);
  7.         unsigned long start_time = jiffies;

  8.         backlog_dev->weight = weight_p;
  9. /*这个循环中,将队列中的数据包逐一出队*/
  10.         for (;;) {
  11.                 struct sk_buff *skb;
  12.                 struct net_device *dev;

  13. /*数据包出队,当然,为了保护队列不至于混乱,必须关闭中断*/
  14.                 local_irq_disable();
  15.                 skb = __skb_dequeue(&queue->input_pkt_queue);
  16.                 if (!skb)
  17.                         goto job_done;
  18.                 local_irq_enable();

  19.                 dev = skb->dev;
  20. /*将从队列中取出的数据包,进一步交给上层函数处得*/
  21.                 netif_receive_skb(skb);

  22.                 dev_put(dev);

  23.                 work++;

  24.                 if (work >= quota || jiffies - start_time > 1)
  25.                         break;

  26.         }

  27.         backlog_dev->quota -= work;
  28.         *budget -= work;
  29.         return -1;

  30. job_done:
  31.         backlog_dev->quota -= work;
  32.         *budget -= work;

  33.         list_del(&backlog_dev->poll_list);
  34.         smp_mb__before_clear_bit();
  35.         netif_poll_enable(backlog_dev);

  36.         if (queue->throttle)
  37.                 queue->throttle = 0;
  38.         local_irq_enable();
  39.         return 0;
  40. }
复制代码


三、netif_receive_skb
netif_receive_skb函数最重要的工作,就是把数据包交给上层协议栈:


int netif_receive_skb(struct sk_buff *skb)
{
/*ptype和pt_prev用来遍历packet_type结构的链表,我们前面讨论过上层协议的封装与注册*/
        struct packet_type *ptype, *pt_prev;
        int ret = NET_RX_DROP;
        unsigned short type;

        /* 处理NAPI的情况*/
        if (skb->dev->poll && netpoll_rx(skb))
                return NET_RX_DROP;

        if (!skb->stamp.tv_sec)
                net_timestamp(&skb->stamp);

        skb_bond(skb);
/*接收计数器累加*/
        __get_cpu_var(netdev_rx_stat).total++;
/*更新传输层头和网络层头指针*/
        skb->h.raw = skb->nh.raw = skb->data;
        skb->mac_len = skb->nh.raw - skb->mac.raw;

        pt_prev = NULL;

        rcu_read_lock();

#ifdef CONFIG_NET_CLS_ACT
        if (skb->tc_verd & TC_NCLS) {
                skb->tc_verd = CLR_TC_NCLS(skb->tc_verd);
                goto ncls;
        }
#endif
/*处理混杂模式的情况*/
        list_for_each_entry_rcu(ptype, &ptype_all, list) {
                if (!ptype->dev || ptype->dev == skb->dev) {
                        if (pt_prev)
                                ret = deliver_skb(skb, pt_prev);
                        pt_prev = ptype;
                }
        }

#ifdef CONFIG_NET_CLS_ACT
        if (pt_prev) {
                ret = deliver_skb(skb, pt_prev);
                pt_prev = NULL; /* noone else should process this after*/
        } else {
                skb->tc_verd = SET_TC_OK2MUNGE(skb->tc_verd);
        }

        ret = ing_filter(skb);

        if (ret == TC_ACT_SHOT || (ret == TC_ACT_STOLEN)) {
                kfree_skb(skb);
                goto out;
        }

        skb->tc_verd = 0;
ncls:
#endif

        handle_diverter(skb);
/*进入网桥*/
        if (handle_bridge(&skb, &pt_prev, &ret))
                goto out;
/*取得上层协议类型,这个类型值,是在网卡驱动中设置的*/
        type = skb->protocol;
/*遍历每一个注册协议,调用deliver_skb函数来调用协议封装的处理函数*/
        list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type)&15], list) {
                if (ptype->type == type &&
                    (!ptype->dev || ptype->dev == skb->dev)) {
                        if (pt_prev)
                                ret = deliver_skb(skb, pt_prev);
                        pt_prev = ptype;
                }
        }

        if (pt_prev) {
                ret = pt_prev->func(skb, skb->dev, pt_prev);
        } else {
                kfree_skb(skb);
                /* Jamal, now you will not able to escape explaining
                 * me how you were going to use this.
                 */
                ret = NET_RX_DROP;
        }

out:
        rcu_read_unlock();
        return ret;
}

对于IP协议而言,早已注册的处理函数是ip_recv……

未完,待续!

论坛徽章:
0
5 [报告]
发表于 2006-11-14 16:58 |只看该作者
支持&羡慕ing
一直都想好好看看网络的源码,可是总是偷懒了
这篇文章不错,推荐一下
CSDN技术中心 Linux网络接口源码导读

[ 本帖最后由 duanjigang 于 2006-11-14 17:11 编辑 ]

论坛徽章:
0
6 [报告]
发表于 2006-12-11 17:37 |只看该作者
又读一遍,醍醐灌顶!

[ 本帖最后由 nkchenz 于 2006-12-11 21:32 编辑 ]

论坛徽章:
0
7 [报告]
发表于 2006-12-12 09:24 |只看该作者
原帖由 nkchenz 于 2006-12-11 17:37 发表
又读一遍,醍醐灌顶!

其实这个没有写清楚,比如:
1、网卡处理数据的细节,现现一般都用流式DMA映射来处理的,比如Intel的e100;
2、队列层基本略过了;

呵呵,这两天打算抽时间把路由处理拿下来,到时候整理一下,一起发……

论坛徽章:
0
8 [报告]
发表于 2006-12-12 12:42 |只看该作者
支持一下,向你的认真态度学习
您需要登录后才可以回帖 登录 | 注册

本版积分规则 发表回复

  

北京盛拓优讯信息技术有限公司. 版权所有 京ICP备16024965号-6 北京市公安局海淀分局网监中心备案编号:11010802020122 niuxiaotong@pcpop.com 17352615567
未成年举报专区
中国互联网协会会员  联系我们:huangweiwei@itpub.net
感谢所有关心和支持过ChinaUnix的朋友们 转载本站内容请注明原作者名及出处

清除 Cookies - ChinaUnix - Archiver - WAP - TOP