網卡收包流程

前言

網絡編程中咱們接觸得比較多的是socket api和epoll模型,對於系統內核和網卡驅動接觸得比較少,一方面可能咱們的系統沒有須要深度調優的需求,另外一方面網絡編程涉及到硬件,驅動,內核,虛擬化等複雜的知識,令人望而卻步。網絡上網卡收包相關的資料也比較多,可是比較分散,在此梳理了網卡收包的流程,分享給你們,但願對你們有幫助,文中引用了一些同事的圖表和摘選了網上資料,在文章最後給出了原始的連接,感謝這些做者的分享。javascript

1.總體流程

網卡收包從總體上是網線中的高低電平轉換到網卡FIFO存儲再拷貝到系統主內存(DDR3)的過程,其中涉及到網卡控制器,CPU,DMA,驅動程序,在OSI模型中屬於物理層和鏈路層,以下圖所示。java

2.關鍵數據結構

在內核中網絡數據流涉及到的代碼比較複雜,見圖1(原圖在附件中),其中有3個數據結構在網卡收包的流程中是最主要的角色,它們是:sk_buff,softnet_data,net_device。linux


圖1內核網絡數據流

sk_buff
sk_buff結構是Linux網絡模塊中最重要的數據結構之一。sk_buff能夠在不一樣的網絡協議層之間傳遞,爲了適配不一樣的協議,裏面的大多數成員都是指針,還有一些union,其中data指針和len會在不一樣的協議層中發生改變,在收包流程中,即數據向上層傳遞時,下層的首部就再也不須要了。圖2即演示了數據包發送時指針和len的變化狀況。(linux源碼不一樣的版本有些差異,下面的截圖來自linux 2.6.20)。
算法



圖2 sk_buff在不一樣協議層傳遞時,data指針的變化示例docker

softnet_data編程


softnet_data 結構內的字段就是 NIC 和網絡層之間處理隊列,這個結構是全局的,每一個cpu一個,它從 NIC中斷和 POLL 方法之間傳遞數據信息。圖3說明了softnet_data中的變量的做用。
net_device


net_device中poll方法即在NAPI回調的收包函數。
net_device表明的是一種網絡設備,既能夠是物理網卡,也能夠是虛擬網卡。在sk_buff中有一個net_device * dev變量,這個變量會隨着sk_buff的流向而改變。在網絡設備驅動初始化時,會分配接收sk_buff緩存隊列,這個dev指針會指向收到數據包的網絡設備。當原始網絡設備接收到報文後,會根據某種算法選擇某個合適的虛擬網絡設備,並將dev指針修改成指向這個虛擬設備的net_device結構。

3.網絡收包原理

本節主要引用網絡上的文章,在關鍵的地方加了一些備註,騰訊公司內部主要使用Intel 82576網卡和Intel igb驅動,和下面的網卡和驅動不同,實際上原理是同樣的,只是一些函數命名和處理的細節不同,並不影響理解。
網絡驅動收包大體有3種狀況:
no NAPI:
mac每收到一個以太網包,都會產生一個接收中斷給cpu,即徹底靠中斷方式來收包
缺點是當網絡流量很大時,cpu大部分時間都耗在了處理mac的中斷。
netpoll:
在網絡和I/O子系統尚不能完整可用時,模擬了來自指定設備的中斷,即輪詢收包。
缺點是實時性差
NAPI:
採用中斷 + 輪詢的方式:mac收到一個包來後會產生接收中斷,可是立刻關閉。
直到收夠了netdev_max_backlog個包(默認300),或者收完mac上全部包後,纔再打開接收中斷
經過sysctl來修改 net.core.netdev_max_backlog
或者經過proc修改 /proc/sys/net/core/netdev_max_backlogapi


圖3 softnet_data與接口層和網絡層之間的關係
下面只寫內核配置成使用NAPI的狀況,只寫TSEC驅動。內核版本 linux 2.6.24。

NAPI相關數據結構


每一個網絡設備(MAC層)都有本身的net_device數據結構,這個結構上有napi_struct。每當收到數據包時,網絡設備驅動會把本身的napi_struct掛到CPU私有變量上。這樣在軟中斷時,net_rx_action會遍歷cpu私有變量的poll_list,執行上面所掛的napi_struct結構的poll鉤子函數,將數據包從驅動傳到網絡協議棧。

內核啓動時的準備工做

3.1 初始化網絡相關的全局數據結構,並掛載處理網絡相關軟中斷的鉤子函數

start_kernel()
--> rest_init()
        --> do_basic_setup()
            --> do_initcall
               -->net_dev_init

__init  net_dev_init(){
    //每一個CPU都有一個CPU私有變量 _get_cpu_var(softnet_data)
    //_get_cpu_var(softnet_data).poll_list很重要,軟中斷中須要遍歷它的
    for_each_possible_cpu(i) {
        struct softnet_data *queue;
        queue = &per_cpu(softnet_data, i);
        skb_queue_head_init(&queue->input_pkt_queue);
        queue->completion_queue = NULL;
        INIT_LIST_HEAD(&queue->poll_list);
        queue->backlog.poll = process_backlog;
        queue->backlog.weight = weight_p;
}
   //在軟中斷上掛網絡發送handler
    open_softirq(NET_TX_SOFTIRQ, net_tx_action, NULL); 
//在軟中斷上掛網絡接收handler
    open_softirq(NET_RX_SOFTIRQ, net_rx_action, NULL);
}複製代碼

softirq
中斷處理「下半部」機制
中斷服務程序通常都是在中斷請求關閉的條件下執行的,以免嵌套而使中斷控制複雜化。可是,中斷是一個隨機事件,它隨時會到來,若是關中斷的時間太長,CPU就不能及時響應其餘的中斷請求,從而形成中斷的丟失。
所以,Linux內核的目標就是儘量快的處理完中斷請求,盡其所能把更多的處理向後推遲。例如,假設一個數據塊已經達到了網線,當中斷控制器接受到這個中斷請求信號時,Linux內核只是簡單地標誌數據到來了,而後讓處理器恢復到它之前運行的狀態,其他的處理稍後再進行(如把數據移入一個緩衝區,接受數據的進程就能夠在緩衝區找到數據)。
所以,內核把中斷處理分爲兩部分:上半部(top-half)和下半部(bottom-half),上半部(就是中斷服務程序)內核當即執行,而下半部(就是一些內核函數)留着稍後處理。
2.6內核中的「下半部」處理機制:
1) 軟中斷請求(softirq)機制(注意不要和進程間通訊的signal混淆)
2) 小任務(tasklet)機制
3) 工做隊列機制
咱們能夠經過top命令查看softirq佔用cpu的狀況:數組


softirq實際上也是一種註冊回調的機制,ps –elf 能夠看到註冊的函數由一個守護進程(ksoftirgd)專門來處理,並且是每一個cpu一個守護進程。

3.2 加載網絡設備的驅動

NOTE:這裏的網絡設備是指MAC層的網絡設備,即TSEC和PCI網卡(bcm5461是phy)在網絡設備驅動中建立net_device數據結構,並初始化其鉤子函數 open(),close() 等掛載TSEC的驅動的入口函數是 gfar_probe緩存

// 平臺設備 TSEC 的數據結構
static struct platform_driver gfar_driver = {
.probe = gfar_probe,
.remove = gfar_remove,
.driver = {
      .name = "fsl-gianfar",
  },
};

int gfar_probe(struct platform_device *pdev)
{
dev = alloc_etherdev(sizeof (*priv)); // 建立net_device數據結構
    dev->open = gfar_enet_open;
dev->hard_start_xmit = gfar_start_xmit;
dev->tx_timeout = gfar_timeout;
dev->watchdog_timeo = TX_TIMEOUT;
#ifdef CONFIG_GFAR_NAPI
    netif_napi_add(dev, &priv->napi,gfar_poll,GFAR_DEV_WEIGHT); //軟中斷裏會調用poll鉤子函數
#endif
#ifdef CONFIG_NET_POLL_CONTROLLER
    dev->poll_controller = gfar_netpoll;
#endif
  dev->stop = gfar_close;
  dev->change_mtu = gfar_change_mtu;
  dev->mtu = 1500;
  dev->set_multicast_list = gfar_set_multi;
  dev->set_mac_address = gfar_set_mac_address;
  dev->ethtool_ops = &gfar_ethtool_ops;
}複製代碼

3.3啓用網絡設備

3.3.1 用戶調用ifconfig等程序,而後經過ioctl系統調用進入內核
socket的ioctl()系統調用服務器

--> sock_ioctl()
        --> dev_ioctl()                              //判斷SIOCSIFFLAGS
          --> __dev_get_by_name(net, ifr->ifr_name)  //根據名字選net_device
             --> dev_change_flags()                  //判斷IFF_UP
                --> dev_open(net_device)             //調用open鉤子函數複製代碼

對於TSEC來講,掛的鉤子函數是 gfar_enet_open(net_device)

3.3.2 在網絡設備的open鉤子函數裏,分配接收bd,掛中斷ISR(包括rx、tx、err),對於TSEC來講

gfar_enet_open

-->給Rx Tx Bd 分配一致性DMA內存 
-->把Rx Bd的「EA地址」賦給數據結構,物理地址賦給TSEC寄存器
-->把Tx Bd的「EA地址」賦給數據結構,物理地址賦給TSEC寄存器
-->給 tx_skbuff 指針數組分配內存,並初始化爲NULL
-->給 rx_skbuff 指針數組分配內存,並初始化爲NULL

-->初始化Tx Bd
-->初始化Rx Bd,提早分配存儲以太網包的skb,這裏使用的是一次性dma映射
   (注意:`#define DEFAULT_RX_BUFFER_SIZE 1536`保證了skb能存一個以太網包)複製代碼
rxbdp = priv->rx_bd_base;
        for (i = 0; i < priv->rx_ring_size; i++) {
            struct sk_buff *skb = NULL;
            rxbdp->status = 0;
            //這裏真正分配skb,而且初始化rxbpd->bufPtr, rxbdpd->length
            skb = gfar_new_skb(dev, rxbdp);   
             priv->rx_skbuff[i] = skb;
            rxbdp++;
        }
        rxbdp--;
        rxbdp->status |= RXBD_WRAP; // 給最後一個bd設置標記WRAP標記複製代碼
-->註冊TSEC相關的中斷handler:錯誤,接收,發送複製代碼
request_irq(priv->interruptError, gfar_error, 0, "enet_error", dev)
        request_irq(priv->interruptTransmit, gfar_transmit, 0, "enet_tx", dev)//包發送完
        request_irq(priv->interruptReceive, gfar_receive, 0, "enet_rx", dev)  //包接收完

    -->gfar_start(net_device)
        // 使能Rx、Tx
        // 開啓TSEC的 DMA 寄存器
        // Mask 掉咱們不關心的中斷event複製代碼

最終,TSEC相關的Bd等數據結構應該是下面這個樣子的

3.4中斷裏接收以太網包

TSEC的RX已經使能了,網絡數據包進入內存的流程爲:

網線 --> Rj45網口 --> MDI 差分線
--> bcm5461(PHY芯片進行數模轉換) --> MII總線
--> TSEC的DMA Engine 會自動檢查下一個可用的Rx bd
-->把網絡數據包 DMA 到 Rx bd 所指向的內存,即skb->data

接收到一個完整的以太網數據包後,TSEC會根據event mask觸發一個 Rx 外部中斷。
cpu保存現場,根據中斷向量,開始執行外部中斷處理函數do_IRQ()

do_IRQ 僞代碼
上半部處理硬中斷
查看中斷源寄存器,得知是網絡外設產生了外部中斷
執行網絡設備的rx中斷handler(設備不一樣,函數不一樣,但流程相似,TSEC是gfar_receive)

  1. mask 掉 rx event,再來數據包就不會產生rx中斷
  2. 給napi_struct.state加上 NAPI_STATE_SCHED 狀態
  3. 掛網絡設備本身的napi_struct結構到cpu私有變量_get_cpu_var(softnet_data).poll_list
  4. 觸發網絡接收軟中斷( __raise_softirq_irqoff(NET_RX_SOFTIRQ); ——> wakeup_softirqd() )
    下半部處理軟中斷
    依次執行全部軟中斷handler,包括timer,tasklet等等
    執行網絡接收的軟中斷handler net_rx_action
  5. 遍歷cpu私有變量_get_cpu_var(softnet_data).poll_list
  6. 取出poll_list上面掛的napi_struct 結構,執行鉤子函數napi_struct.poll()
    (設備不一樣,鉤子函數不一樣,流程相似,TSEC是gfar_poll
  7. 若poll鉤子函數處理完全部包,則打開rx event mask,再來數據包的話會產生rx中斷
  8. 調用napi_complete(napi_struct *n)
  9. 把napi_struct 結構從_get_cpu_var(softnet_data).poll_list 上移走,同時去掉 napi_struct.state 的 NAPI_STATE_SCHED 狀態
    3.4.1 TSEC的接收中斷處理函數
    gfar_receive{
    #ifdef CONFIG_GFAR_NAPI
    // test_and_set當前net_device的napi_struct.state 爲 NAPI_STATE_SCHED
    // 在軟中斷裏調用 net_rx_action 會檢查狀態 napi_struct.state
    if (netif_rx_schedule_prep(dev, &priv->napi)) {  
     tempval = gfar_read(&priv->regs->imask);            
     tempval &= IMASK_RX_DISABLED; //mask掉rx,再也不產生rx中斷
     gfar_write(&priv->regs->imask, tempval);    
     // 將當前net_device的 napi_struct.poll_list 掛到
     // CPU私有變量__get_cpu_var(softnet_data).poll_list 上,並觸發軟中斷
      // 因此,在軟中斷中調用 net_rx_action 的時候,就會執行當前net_device的
      // napi_struct.poll()鉤子函數,即 gfar_poll()
      __netif_rx_schedule(dev, &priv->napi);   
    } 
    #else
    gfar_clean_rx_ring(dev, priv->rx_ring_size);
    #endif
    }複製代碼
    3.4.2 網絡接收軟中斷net_rx_action
net_rx_action(){
  struct list_head *list = &__get_cpu_var(softnet_data).poll_list;    
//經過 napi_struct.poll_list,將N多個 napi_struct 連接到一條鏈上 
//經過 CPU私有變量,咱們找到了鏈頭,而後開始遍歷這個鏈

   int budget = netdev_budget; //這個值就是 net.core.netdev_max_backlog,經過sysctl來修改

   while (!list_empty(list)) {
        struct napi_struct *n;
        int work, weight;
        local_irq_enable();
        //從鏈上取一個 napi_struct 結構(接收中斷處理函數里加到鏈表上的,如gfar_receive)
        n = list_entry(list->next, struct napi_struct, poll_list);
        weight = n->weight;
        work = 0;
        if (test_bit(NAPI_STATE_SCHED, &n->state)) //檢查狀態標記,此標記在接收中斷里加上的 
              //使用NAPI的話,使用的是網絡設備本身的napi_struct.poll 
//對於TSEC是,是gfar_poll
            work = n->poll(n, weight);                                      

          WARN_ON_ONCE(work > weight);
          budget -= work;
          local_irq_disable();

          if (unlikely(work == weight)) {
                if (unlikely(napi_disable_pending(n)))
//操做napi_struct,把去掉NAPI_STATE_SCHED狀態,從鏈表中刪去
                  __napi_complete(n); 
            else
                  list_move_tail(&n->poll_list, list);
          }
         netpoll_poll_unlock(have);
}
out:
        local_irq_enable();
}
static int gfar_poll(struct napi_struct *napi, int budget){
     struct gfar_private *priv = container_of(napi, struct gfar_private, napi);
struct net_device *dev = priv->dev;  //TSEC對應的網絡設備
    int howmany;  
 //根據dev的rx bd,獲取skb並送入協議棧,返回處理的skb的個數,即以太網包的個數
    howmany = gfar_clean_rx_ring(dev, budget);
// 下面這個判斷比較有講究的
    // 收到的包的個數小於budget,表明咱們在一個軟中斷裏就全處理完了,因此打開 rx中斷
    // 要是收到的包的個數大於budget,表示一個軟中斷裏處理不完全部包,那就不打開rx 中斷,
    // 待到下一個軟中斷裏再接着處理,直到把全部包處理完(即howmany<budget),再打開rx 中斷
    if (howmany < budget) {        
        netif_rx_complete(dev, napi);
        gfar_write(&priv->regs->rstat, RSTAT_CLEAR_RHALT);
        //打開 rx 中斷,rx 中斷是在gfar_receive()中被關閉的
        gfar_write(&priv->regs->imask, IMASK_DEFAULT); 
}
  return howmany;
}複製代碼
gfar_clean_rx_ring(dev, budget){
bdp = priv->cur_rx;
    while (!((bdp->status & RXBD_EMPTY) || (--rx_work_limit < 0))) {
        rmb();
       skb = priv->rx_skbuff[priv->skb_currx]; //從rx_skbugg[]中獲取skb
        howmany++;
        dev->stats.rx_packets++;
        pkt_len = bdp->length - 4;  //從length中去掉以太網包的FCS長度
        gfar_process_frame(dev, skb, pkt_len);
        dev->stats.rx_bytes += pkt_len;
        dev->last_rx = jiffies;
        bdp->status &= ~RXBD_STATS;  //清rx bd的狀態

        skb = gfar_new_skb(dev, bdp); // Add another skb for the future
        priv->rx_skbuff[priv->skb_currx] = skb;
        if (bdp->status & RXBD_WRAP)  //更新指向bd的指針
            bdp = priv->rx_bd_base;   //bd有WARP標記,說明是最後一個bd了,須要「繞回來」       
else
         bdp++;
 priv->skb_currx = (priv->skb_currx + 1) & RX_RING_MOD_MASK(priv->rx_ring_size);
  }

priv->cur_rx = bdp; /* Update the current rxbd pointer to be the next one */
return howmany;
}

 gfar_process_frame()  
-->RECEIVE(skb) //調用netif_receive_skb(skb)進入協議棧

#ifdef CONFIG_GFAR_NAPI
#define RECEIVE(x) netif_receive_skb(x)
#else
#define RECEIVE(x) netif_rx(x)
#endif複製代碼

在軟中斷中使用NAPI
上面net_rx_action的主要流程如圖4所示,執行一次網絡軟中斷過程當中,網卡自己的Rx中斷已經關閉了,即不會產生新的接收中斷了。local_irq_enable和local_irq_disable設置的是cpu是否接收中斷。進入網絡軟中斷net_rx_action的時候,會初始一個budget(預算),即最多處理的網絡包個數,若是有多個網卡(放在poll_list裏),是共享該budget,同時每一個網卡也一個權重weight或者說是配額quota,一個網卡處理完輸入隊列裏包後有兩種狀況,一次收到的包不少,quota用完了,則把收包的poll虛函數又掛到poll_list隊尾,從新設置一下quota值,等待while輪詢;另一種狀況是,收到的包很少,quota沒有用完,表示網卡比較空閒,則把本身從poll_list摘除,退出輪詢。整個net_rx_action退出的狀況有兩種:budget所有用完了或者是時間超時了。


圖4net_rx_action主要執行流程

3.5 DMA 8237A

在網卡收包中涉及到DMA的操做,DMA的主要做用是讓外設間(如網卡和主內存)傳輸數據而不須要CPU的參與(即不須要CPU使用專門的IO指令來拷貝數據),下面簡單介紹一下DMA的原理,如圖5所示。


圖5 DMA系統組成
網卡採用DMA方式(DMA控制器通常在系統板上,有的網卡也內置DMA控制器),ISR經過CPU對DMA控制器編程(由DMA的驅動完成,此時DMA至關於一個普通的外設,編程主要指設置DMA控制器的寄存器),DMA控制器收到ISR請求後,向主CPU發出總線HOLD請求,獲取CPU應答後便向LAN發出DMA應答並接管總線,同時開始網卡緩衝區與內存之間的數據傳輸,這個時候CPU能夠繼續執行其餘的指令,當DMA操做完成後,DMA則釋放對總線的控制權。

4.網卡多隊列

網卡多隊列是硬件的一種特性,同時也須要內核支持,騰訊公司使用的Intel 82576是支持網卡多隊列的,並且內核版本要大於2.6.20。對於單隊列的網卡,只能產生一箇中斷信號,而且只能由一個cpu來處理,這樣會致使多核系統中一個核(默認是cpu0)負載很高。網卡多隊列在網卡的內部維持多個收發隊列,併產生多箇中斷信號使不一樣的cpu都能處理網卡收到的包,從而提高了性能,如圖6所示。


圖6多隊列網卡工做收包流程示意圖
MSI-X :一個設備能夠產生多箇中斷,以下圖中的54-61號中斷eth1-TxRx-[0-7],實際是eth1網卡佔用的中斷號。


CPU 親和性:每一箇中斷號配置只有一個cpu進行處理,其中的值:01,02,04等爲16進制,相應bit爲1的值代碼cpu的編號。

5.I/O虛擬化SR-IOV

服務器虛擬化技術在分佈式系統中很常見,它能提升設備利用率和運營效率。服務器虛擬化包括處理器虛擬化,內存虛擬化和I/0設備的虛擬化,與網絡有關的虛擬化屬於I/0虛擬化,I/0設備虛擬化的做用是單個I/O設備能夠被多個虛擬機共享使用。對於客戶機操做系統中的應用程序來講,它發起 I/O 操做的流程和在真實硬件平臺上的操做系統是同樣的,整個 I/O 流程有所不一樣的在於設備驅動訪問硬件這個部分。I/0虛擬化通過多年的發展,主要模型如表1所示,早期的設備仿真如圖7所示,能夠看到網絡數據包從物理網卡到虛擬機中的進程須要通過不少額外的處理,效率很低。SR-IOV則直接從硬件上支持虛擬化,如圖8所示,設備劃分爲一個物理功能單元PF(Physical Functions)和多個虛擬功能單元 VF(Virtual Function),每一個虛擬功能單元均可以做爲一個輕量級的 I/O 設備供虛擬機使用,這樣一個設備就能夠同時被分配給多個虛擬機,解決了因設備數量限制給虛擬化系統帶來的可擴展性差的問題。每一個 VF 都有一個惟一的 RID(Requester Identifier,請求標識號)和收發數據包的關鍵資源,如發送隊列、接收隊列、DMA 通道等,所以每一個 VF 都具備獨立收發數據包的功能。全部的 VF 共享主要的設備資源,如數據鏈路層的處理和報文分類。


表1幾種I/0虛擬化計算對比


圖7設備仿真


圖8支持SR-IOV的設備結構
SR-IOV須要網卡支持:


須要有專門的驅動來支持:


VF的驅動實際上和普通的網卡差很少,最後都會執行到netif_receive_skb中,而後將接收到的包發給它的vlan虛擬子設備進行處理。

docker中使用SR-IOV
激活VF

#echo "options igb max_vfs=7" >>/etc/modprobe.d/igb.conf
#reboot

設置VF的VLAN
#ip link set eth1 vf 0 vlan 12
將VF移到container network namespace
#ip link set eth4 netns $pid
#ip netns exec $pid ip link set dev eth4 name eth1
#ip netns exec $pid ip link set dev eth1 up
In container:
設置IP
#ip addr add 10.217.121.107/21 dev eth1
網關
#ip route add default via 10.217.120.1

6.參考資料

  1. Linux內核源碼剖析——TCP/IP實現,上冊
  2. Understanding Linux Network Internals
  3. 基於SR-IOV技術的網卡虛擬化研究和實現
  4. 82576 sr-iov driver companion guide
  5. 網卡工做原理及高併發下的調優
  6. 多隊列網卡簡介
  7. Linux內核NAPI機制分析
  8. 網絡數據包收發流程(一):從驅動到協議棧
  9. 中斷處理「下半部」機制
  10. Linux 上的基礎網絡設備詳解
  11. DMA operating system
相關文章
相關標籤/搜索