NETDEV 协议

来源:互联网 发布:大酒神淘宝店 编辑:程序博客网 时间:2024/06/06 07:10

什么是NAPI

NAPI是linux一套最新的处理网口数据的API,linux 2.5引入的,所以很多驱动并不支持这种操作方式。简单来说,NAPI是综合中断方式与轮询方式的技术。数据量很低与很高时,NAPI可以发挥中断方式与轮询方式的优点,性能较好。如果数据量不稳定,且说高不高说低不低,则NAPI会在两种方式切换上消耗不少时间,效率反而较低一些。

 

下面会用到netdev_priv()这个函数,这里先讲解下,每个网卡驱动都有自己的私有的数据,来维持网络的正常运行,而这部分私有数据放在网络设备数据后面(内存概念上),这个函数就是通过dev来取得这部分私有数据,注间这部分私有数据不在dev结构体中,而是紧接在dev内存空间后。

static inline void *netdev_priv(const struct net_device *dev)

{

 return (char *)dev + ALIGN(sizeof(struct net_device), NETDEV_ALIGN);

}

 

弄清这个函数还得先清楚dev这个结构的分配

alloc_netdev() -> alloc_netdev_mq()

struct net_device *alloc_netdev_mq(int sizeof_priv, const char *name,

             void (*setup)(struct net_device *), unsigned int queue_count)

{

    ……

 

    alloc_size = sizeof(struct net_device);

    if (sizeof_priv) {

             /* ensure 32-byte alignment of private area */

             alloc_size = ALIGN(alloc_size, NETDEV_ALIGN);

             alloc_size += sizeof_priv;

    }

    /* ensure 32-byte alignment of whole construct */

    alloc_size += NETDEV_ALIGN - 1;

 

    p = kzalloc(alloc_size, GFP_KERNEL);

    if (!p) {

             printk(KERN_ERR "alloc_netdev: Unable to allocate device./n");

             return NULL;

    }

 

    ……….

}

可以看到,dev在分配时,即在它的后面分配了private的空间,需要注意的是,这两部分都是32字节对齐的,如下图所示,padding是加入的的补齐字节:

  

 

举个例子,假设sizeof(net_device)大小为31B,private大小45B,则实际分配空间如图所示:

 

   

    b44_interrupt():当有数据包收发或发生错误时,会产生硬件中断,该函数被触发

struct b44 *bp = netdev_priv(dev);

取出网卡驱动的私有数据private,该部分数据位于dev数据后面

istat = br32(bp, B44_ISTAT);

imask = br32(bp, B44_IMASK);

读出当前中断状态和中断屏蔽字

    if (istat) {

             ……

             if (napi_schedule_prep(&bp->napi)) {

                      bp->istat = istat;

                      __b44_disable_ints(bp);

                      __napi_schedule(&bp->napi);

             }

设置NAPI为SCHED状态,记录当前中断状态,关闭中断,执行调度

void __napi_schedule(struct napi_struct *n)

{

    unsigned long flags;

 

    local_irq_save(flags);

    list_add_tail(&n->poll_list, &__get_cpu_var(softnet_data).poll_list);

    __raise_softirq_irqoff(NET_RX_SOFTIRQ);

    local_irq_restore(flags);

}

__get_cpu_var():得到当前CPU的偏移量,与多CPU有关

napipoll_list加入到softnet_data队列尾部,然后引起软中断NET_RX_SOFTIRQ

 

似乎还没有真正的收发函数出现,别急;关于软中断的机制请参考资料,在net_dev_init()[dev.c]中,注册了两个软中断处理函数,所以引起软中断后,最终调用了net_rx_action()

open_softirq(NET_TX_SOFTIRQ, net_tx_action);

open_softirq(NET_RX_SOFTIRQ, net_rx_action);

下面来看下net_rx_action()函数实现:

static void net_rx_action(struct softirq_action *h)

{

    struct list_head *list = &__get_cpu_var(softnet_data).poll_list; // [1]

    ……

    n = list_first_entry(list, struct napi_struct, poll_list);    // [2]

    ……

    work = 0;

    if (test_bit(NAPI_STATE_SCHED, &n->state)) {

             work = n->poll(n, weight);       // [3]

             trace_napi_poll(n);

    }

……

}

__get_cpu_var是不是很熟悉,在b44_interrupt()中才向它的poll_list中加入了一个napi_struct;代码[2]很简单了,从poll_list的头中取出一个napi_struct,然后执行代码[3],调用poll()函数;注意到这里在interrupt时,会向poll_list尾部加入一个napi_struct,并引起软中断,在软中断处理函数中,会从poll_list头部移除一个napi_struct,进行处理,理论上说,硬件中断加入的数据在其引起的软中断中被处理。

poll函数实际指向的是b44_poll(),这是显而易见的,但具体怎样调用的呢?在网卡驱动初始化函数b44_init_one()有这样一行代码:

netif_napi_add(dev, &bp->napi, b44_poll, 64);

而netif_napi_add()中初始化napi并将其加入dev的队列,

    napi->poll = poll;

这行代码就是b44_poll赋给napi_poll,所以在NET_RX_SOFTIRQ软中断处理函数net_rx_action()中执行的b44_poll()

怎么到这里都还没有收发数据包的函数呢!b44_poll()就是轮询中断向量,查找出引起本次操作的中断;

static int b44_poll(struct napi_struct *napi, int budget)

{

    ……

    if (bp->istat & (ISTAT_TX | ISTAT_TO))

             b44_tx(bp);

    ……

    if (bp->istat & ISTAT_RX)

             work_done += b44_rx(bp, budget);

    if (bp->istat & ISTAT_ERRORS)

             ……

}

可以看到,查询了四种中断:ISTAT_TXISTAT_TOISTAT_RXISTAT_ERRORS

#define ISTAT_TO              0x00000080 /* General Purpose Timeout */

#define ISTAT_RX              0x00010000 /* RX Interrupt */

#define ISTAT_TX               0x01000000 /* TX Interrupt */

#define ISTAT_ERRORS (ISTAT_DSCE|ISTAT_DATAE|ISTAT_DPE|ISTAT_RDU|ISTAT_RFO|ISTAT_TFU)

如果是TX中断,则调用b44_tx发送数据包;如果是RX中断,则调用b44_rx接收数据包。至此,从网卡驱动收发数据包的调用就是如此了。

最后,给个总结性的图:

  

纠结了好多天,终于弄懂了B440X的处理。

上篇讲到通过中断,最终网卡调用了b44_rx()来接收报文

 

对这个函数中的一些参数,可以这样理解:

bp->rx_cons – 处理器处理到的缓冲区号

bp->rx_pending – 分配的缓冲区个数

bp->rx_prod – 当前缓冲区的最后一个缓冲号

 

这里要参数B440X的手册了解下寄存器的作用:

#define B44_DMARX_ADDR        0x0214UL /* DMA RX Descriptor Ring Address */

#define B44_DMARX_PTR  0x0218UL /* DMA RX Last Posted Descriptor */

#define B44_DMARX_STAT 0x021CUL /* DMA RX Current Active Desc. + Status */

b44_rx()来说,B44_DMARX_ADDR储存了环形缓冲的基地址,B44_DMARX_PTR存储了环形缓冲最后一个缓冲区号,这两个寄存器都由处理来设置;B44_DMARX_STAT储存了状态及网卡当前处理到的缓冲区号,这个寄存器只能由网卡来设置。

 

网卡中DMA也很重要:

在网卡初始化阶段,b44_open() -> b44_alloc_consistent()

bp->rx_buffers = kzalloc(size, gfp);  // size = B44_RX_RING_SIZE * sizeof(struct ring_info)

bp->rx_ring = ssb_dma_alloc_consistent(bp->sdev, size, &bp->rx_ring_dma, gfp);

     // size = DMA_TABLE_BYTES

rx_ringDMA映射的虚拟地址,rx_rind_dmaDMA映射的总线地址,这个地址将会写入B44_DMARX_ADDR寄存器,作为环形缓冲的基地址。

bw32(bp, B44_DMARX_ADDR, bp->rx_ring_dma + bp->dma_offset);

稍后在rx_init_rings() -> b44_alloc_rx_skb()

mapping = ssb_dma_map_single(bp->sdev, skb->data,RX_PKT_BUF_SZ,DMA_FROM_DEVICE);

rx_buffers进行DMA映射,并将映射地址存储在rx_ring

dp->addr = cpu_to_le32((u32) mapping + bp->dma_offset); // dprx_ring中一个

 

DMA的大致流程:

       不准确,但可以参考下大致意思

 

网卡读取B44_DMARX_ADDRB44_DMARX_STAT寄存器,得到下一个处理的struct dma_desc,然后根据dma_desc中的addr找到报文缓冲区,通过DMA处理器将网卡收到报文拷贝到addr地址处,这个过程CPU是不参与的。

 

        prod – 网卡[硬件]处理到的缓冲区号

prod  = br32(bp, B44_DMARX_STAT) & DMARX_STAT_CDMASK;

prod /= sizeof(struct dma_desc);

cons = bp->rx_cons;

根据上面分析,prod读取B44_DMARX_STAT寄存器,存储网卡当前处理到的缓冲区号;cons存储处理器处理到的缓冲区号。

 

while (cons != prod && budget > 0) {

处理报文当前时刻网卡接收到的所有报文,每处理一个报文cons都会加1,由于是环形缓冲,因此这里用相等,而不是大小比较。

 

struct ring_info *rp = &bp->rx_buffers[cons];

struct sk_buff *skb = rp->skb;

dma_addr_t map = rp->mapping;

skbmap保存了当关地址,下面在交换缓冲区后会用到。

 

ssb_dma_sync_single_for_cpu(bp->sdev, map,RX_PKT_BUF_SZ,DMA_FROM_DEVICE);

CPU取得rx_buffer[cons]的控制权,此时网卡不能再处理该缓冲区。

 

rh = (struct rx_header *) skb->data;

len = le16_to_cpu(rh->len);

….

len -= 4;

CPU取得控制权后,取得报文头,再从报文头取出报文长度lenlen-=4表示忽略了最后4节字的CRC,从这里可以看出,B440X网卡驱动不会检查CRC校验。而每个报文数据最前面添加了网卡的头部信息struct rx_header,这里是28字节。

 

struct sk_buff *copy_skb;

b44_recycle_rx(bp, cons, bp->rx_prod);

copy_skb = netdev_alloc_skb(bp->dev, len + 2);

copy_skb作为传送报文的中间量,在第三句为其分配了len + 2的空间(为了IP头对齐,稍后提到)b44_recycle_rx()函数很关键,它作了如下工作:

1.       将缓冲区号cons赋值给缓冲区号rx_prod

2.       rx_buffers[cons].skb = NULL

3.       将缓冲区号rx_prod控制权给网卡

简单来说,就是将cons号缓冲区交由CPU处理,而用rx_prod号缓冲区代替其给网卡使用。

                        

a.       b44_recycle_rx                          b. b44_recycle_rx

以起始状态为例,缓冲区rx_ring分配了512个,但rx_buffers仅分配了200个,此时cons = 0rx_prod = 200。执行b44_recycle_rx()后,网卡处理缓冲区变为1~200,而0号缓冲区交由CPU处理,将报文拷贝,并向上送至协议栈。注意rx_ringrx_buffer是不同的。

这样做的好处在于,不用等待CPU处理完0号缓冲区,网卡的缓冲区数保持200,而不会减少,这也是rx_pending = 200的原因所在。

 

skb_reserve(copy_skb, 2);

skb_put(copy_skb, len);

关于skb的操作自己去了解,这里skb_reserve()在报文头部保留了两个字节,我们知道链路层报头是14字节,正常IP报文会从14字节开始,这样就不是4字节对齐了,所以在头部保留2字节,使IP报文从16字节开始。

 

 

skb_copy_from_linear_data_offset(skb, RX_PKT_OFFSET,copy_skb->data, len);

skb = copy_skb;

CPU将报文从skb拷贝到copy_skb中,跳过了网卡报头的额外信息,因为这部分信息在上层协议站是没用的,所以去掉。在函数开始时说过skb是保存了cons号的地址,因为在b44_recycle_rx()cons号不再引用skb指向的空间,而仅由skb引用,这样便可以向上层传送,而不用额外复制。

 

netif_receive_skb(skb);

received++;

budget--;

next_pkt:

     bp->rx_prod = (bp->rx_prod + 1) & (B44_RX_RING_SIZE - 1);

     cons = (cons + 1) & (B44_RX_RING_SIZE - 1);

netif_receive_skb()将报文交由上层协议栈处理,这是下一节的内容,然后CPU处理下一个报文,rx_prodcons各加1,它们代表的含义开头有说明。

 

如此循环,直到cons == prod,此时网卡收到的报文都已被CPU处理,更新变量:

 

bp->rx_cons = cons;

bw32(bp, B44_DMARX_PTR, cons * sizeof(struct dma_desc));

 

netif_receive_skb()函数中,可以看出处理的是像ARPIP这些链路层以上的协议,那么,链路层报头是在哪里去掉的呢?答案是网卡驱动中,在调用netif_receive_skb()前,

skb->protocol = eth_type_trans(skb, bp->dev);

该函数对处理后skb>data跳过以太网报头,由mac_header指示以太网报头:

进入netif_receive_skb()函数

list_for_each_entry_rcu(ptype,&ptype_base[ntohs(type) & PTYPE_HASH_MASK], list)

按照协议类型依次由相应的协议模块进行处理,而所以的协议模块处理都会注册在ptype_base中,实际是链表结构。

net/core/dev.c

static struct list_head ptype_base __read_mostly;  /* Taps */

 

而相应的协议模块是通过dev_add_pack()函数加入的:

void dev_add_pack(struct packet_type *pt)

{

     int hash;

 

     spin_lock_bh(&ptype_lock);

     if (pt->type == htons(ETH_P_ALL))

              list_add_rcu(&pt->list, &ptype_all);

     else {

              hash = ntohs(pt->type) & PTYPE_HASH_MASK;

              list_add_rcu(&pt->list, &ptype_base[hash]);

     }

     spin_unlock_bh(&ptype_lock);

}

 

ARP处理为例

该模块的定义,它会在arp_init()中注册进ptype_base链表中:

static struct packet_type arp_packet_type __read_mostly = {

     .type =      cpu_to_be16(ETH_P_ARP),

     .func =      arp_rcv,

};

 

然后在根据报文的TYPE来在ptype_base中查找相应协议模块进行处理时,实际调用arp_rcv()进行接收

arp_rcv() --> arp_process()

arp = arp_hdr(skb);

……

arp_ptr= (unsigned char *)(arp+1);

sha= arp_ptr;

arp_ptr += dev->addr_len;

memcpy(&sip, arp_ptr, 4);

arp_ptr += 4;

arp_ptr += dev->addr_len;

memcpy(&tip, arp_ptr, 4);

操作后这指针位置:

然后判断是ARP请求报文,这时先查询路由表ip_route_input()

if (arp->ar_op == htons(ARPOP_REQUEST) &&

         ip_route_input(skb, tip, sip, 0, dev) == 0)

ip_route_input()函数中,先在cache中查询是否存在相应的路由表项:

hash = rt_hash(daddr, saddr, iif, rt_genid(net));

缓存的路由项在内核中组织成hash表的形式,因此在查询时,先算出的hash值,再用该项- rt_hash_table[hash].chain即可。这里可以看到,缓存路由项包括了源IP地址、目的IP地址、网卡号。

 

如果在缓存中没有查到匹配项,或指定不查询cache,则查询路由表ip_route_input_slow()

进入ip_route_input_slow()函数,最终调用fib_lookup()得到查询结果fib_result

if ((err = fib_lookup(net, &fl, &res)) != 0)

如果结果fib_result合法,则需要更新路由缓存,将此次查询结果写入缓存

hash = rt_hash(daddr, saddr, fl.iif, rt_genid(net));

err = rt_intern_hash(hash, rth, NULL, skb, fl.iif);

 

在查找完路由表后,回到arp_process()函数,如果路由项指向本地,则应由本机接收该报文:

if (addr_type == RTN_LOCAL) {

              ……

              if (!dont_send) {

                       n = neigh_event_ns(&arp_tbl, sha, &sip, dev);

                       if (n) {

                                 arp_send(ARPOP_REPLY,ETH_P_ARP,sip,dev,tip,sha,dev->dev_addr,sha);

                                 neigh_release(n);

                       }

              }

              goto out;

     }

首先更新邻居表neigh_event_ns(),然后发送ARP响应 – arp_send

至此,大致的ARP流程完成。由于ARP部分涉及到路由表以及邻居表,这都是很大的概念,在下一篇中介绍,这里直接略过了。

 

 

 

 

 

0 0
原创粉丝点击