NETDEV 協議

什麼是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 篇原創文章 · 獲贊 7 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章