分享

【转】Linux内核网络协议栈笔记4:接收网络数据包详细过程

 kylin_1983 2014-05-04

网络数据接收过程,从数据包到达网卡的物理接口开始,然后由网卡的驱动程序交给网络协议栈,最后经过协议栈的一层层处理之后交给应用程序。大致上是这样的过程,但实际上有更多的细节。本文中主要介绍第一个和第二个步骤。

我们本文中依然以一个Realtek 8139网卡为例(驱动程序为/drivers/net/8139too.c)。请注意在内核代码中receive都是用rx简写的。

(1)注册与激活软中断

在生成net_device对象及初始化的函数rtl8139_init_one中已经初始化dev->open方法为rtl8139_open函数(在本系列文章2:初始化中的net_device对象中已经介绍,点这里查看)。在rtl8139_open函数(这个函数在网卡启动时被调用)中注册了一个中断函数rtl8139_interrupt:

retval = request_irq (dev->irq, rtl8139_interrupt, SA_SHIRQ, dev->name, dev);

所以只要当网卡开启后(状态为up),当网络数据包到达时,都会产生一个硬件中断(这不同于后面的软中断)。这个硬件中断由内核调用中断处理程序rtl8139_interrupt函数处理。这个函数比较重要,网卡发送或者接收数据时内核都会调用这个函数处理中断,而中断的类型是根据网卡状态寄存器的不同而确定的。本文中仅涉及接收数据的中断,因此只给出了接收的代码:

static irqreturn_t rtl8139_interrupt (int irq, void *dev_instance, struct pt_regs *regs)
{
    if (status & RxAckBits){
        if(netif_rx_schedule_prep(dev))
                 __netif_rx_schedule (dev);
     }
}

主要函数为__netif_rx_schedule(函数名意为:network interface receive schedule,即网络接口接收调度),因为当网卡接收到数据包之后,马上告知CPU在合适的时间去启动调度程序,轮询(poll)网卡。

请注意:Linux接收网络数据实际上有两种方式。
(a)中断。每个数据包到达都会产生一个中断,然后由内核调用中断处理程序处理。
(b)NAPI(New API)。Linux内核2.6版本之后加入的新机制,核心方法是:不采用中断的方式读取数据,而代之以首先采用中断唤醒数据接收的服务程序,然后以POLL的方法来轮询数据。
因此本文中只介绍NAPI的接收方式。我们不再详细介绍这种机制,网上可找到比较多的资料,可以参考IBM的技术文章:NAPI 技术在 Linux 网络驱动上的应用和完善。

__netif_rx_schedule函数的定义如下:

static inline void __netif_rx_schedule(struct net_device *dev)
{
         local_irq_save(flags);//disable interrupt
    //Add interface to tail of rx poll list
         list_add_tail(&dev->poll_list, &__get_cpu_var(softnet_data).poll_list);
    //activate network rx softirq
         __raise_softirq_irqoff(NET_RX_SOFTIRQ);
         local_irq_restore(flags);
}

这个函数最核心的就是三步:

(a)local_irq_save:禁用中断

(b)list_add_tail:将设备添加到softnet_data的poll_list中。

(c)激活一个软中断NET_RX_SOFTIRQ。

======================================

说到这里我们必须介绍一个关键数据结构softnet_data,每个CPU都拥有一个这样的网络数据队列(所以函数中使用了__get_cpu_var函数取得),定义如下:

struct softnet_data
{
    int             throttle;    /*为 1 表示当前队列的数据包被禁止*/
    int             cng_level;    /*表示当前处理器的数据包处理拥塞程度*/
    int             avg_blog;    /*某个处理器的平均拥塞度*/
    struct sk_buff_head     input_pkt_queue;    /*接收缓冲区的sk_buff队列*/
    struct list_head             poll_list;    /*POLL设备队列头*/
    struct net_device              output_queue;     /*网络设备发送队列的队列头*/
    struct sk_buff         completion_queue; /*完成发送的数据包等待释放的队列*/
    struct net_device     backlog_dev;    /*表示当前参与POLL处理的网络设备*/
};

大致说明一下这个数据结构的意义。某个网卡产生中断之后,内核就把这个网卡挂载到轮询列表(poll_list)中。一个CPU会轮询自己的列表中的每一个网卡,看看它们是不是有新的数据包可以处理。我们需要先用一个比喻说明这个数据结构与轮询的关系:网卡就是佃户,CPU就是地主。佃户有自己种的粮食(网络数据包),但地主家也有粮仓(softnet_data)。地主要收粮的时候,就会挨家挨户的去催佃户交粮,放到自己的粮仓里。

=======================================

(2)软中断处理

我们知道:激活软中断之后,并不是马上会被处理的。只有当遇到软中断的检查点时,系统才会调用相应的软中断处理函数。

所有的网络接收数据包的软中断处理函数都是net_rx_action。这个函数的详细注释可以看IBM的那篇技术文章。其核心语句就是一个轮询的函数:

dev->poll

就调用了相应设备的poll函数。也就是说,当CPU处理软中断时,才去轮询网卡,把数据放入softnet_data中。

下面是整个中断和轮询过程的一个示意图:

下面我们解释一下poll函数具体干了什么事情。

而我们知道,在Realtek 8139网卡的net_device对象中我们已经注册了一个poll函数:

dev->poll = rtl8139_poll

那么一次poll就表示从网卡缓冲区取出一定量的数据。而rtl8139_poll函数中调用的主要函数就是rtl8139_rx函数。这个函数是完成从网卡取数据,分配skb缓冲区的核心函数。其核心代码如下:

static int rtl8139_rx(struct net_device *dev, struct rtl8139_private *tp, int budget)
{
     skb = dev_alloc_skb (pkt_size + 2);
     eth_copy_and_sum (skb, &rx_ring[ring_offset + 4], pkt_size, 0);//memcpy
     skb->protocol =eth_type_trans (skb, dev);
     netif_receive_skb (skb);
}

工作主要分为4部分:

(a)给sk_buff数据结构(skb)分配空间。

(b)从网卡的环形缓冲区rx_ring中拷贝出网络数据包放到sk_buff对象skb中。这个函数实质上就是一个memcpy函数。

(c)在skb中标识其协议为以太网帧。

(d)调用netif_receice_skb函数。
netif_receive_skb函数相对比较重要。函数主体是两个循环:

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;
         }
}

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;
     }
}

两个循环分别遍历了两个链表:ptype_all和ptype_base。前者是内核中注册的sniffer,后者则是注册到内核协议栈中的网络协议类型。如果skb中的协议类型type与ptype_base中的类型一致,那么使用deliver_skb函数发送给这个协议一份,定义如下:

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

这个函数只是一个封装函数,实际上调用了每个packet type结构中注册的处理函数func。

struct packet_type {
         unsigned short           type;   
        struct net_device               *dev;   
        int                      (*func) (struct sk_buff *,
                          struct net_device *, struct packet_type *);
        void                    *af_packet_priv;
        structlist_head         list;
};

例如:IP包类型的处理函数就是ip_rcv(定义在/net/ipv4/ip_output.c文件中),定义如下:

static struct packet_type ip_packet_type = {
         .type = __constant_htons(ETH_P_IP),
         .func =ip_rcv,
};

这个包的类型是在ip_init协议初始化时添加到全局的ptype_base哈希数组中的:

void __init ip_init(void)
{
         dev_add_pack(&ip_packet_type);

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多