NAPI之(二)——機制分析

NAPI 的核心在於:在一個繁忙網絡,每次有網絡數據包到達時,不需要都引發中斷,因爲高頻率的中斷可能會影響系統的整體效率,假象一個場景,我們此時使用標準的 100M 網卡,可能實際達到的接收速率爲 80MBits/s,而此時數據包平均長度爲 1500Bytes,則每秒產生的中斷數目爲:

  80M bits/s / (8 Bits/Byte * 1500 Byte) = 6667 箇中斷 /s

  每秒 6667 箇中斷,對於系統是個很大的壓力,此時其實可以轉爲使用輪詢 (polling) 來處理,而不是中斷;但輪詢在網絡流量較小的時沒有效率,因此低流量時,基於中斷的方式則比較合適,這就是 NAPI 出現的原因,在低流量時候使用中斷接收數據包,而在高流量時候則使用基於輪詢的方式接收。

  現在內核中 NIC 基本上已經全部支持 NAPI 功能,由前面的敘述可知,NAPI 適合處理高速率數據包的處理,而帶來的好處則是:

  1、中斷緩和 (Interrupt mitigation),由上面的例子可以看到,在高流量下,網卡產生的中斷可能達到每秒幾千次,而如果每次中斷都需要系統來處理,是一個很大的壓力,而 NAPI 使用輪詢時是禁止了網卡的接收中斷的,這樣會減小系統處理中斷的壓力;

  2、數據包節流 (Packet throttling),NAPI 之前的 Linux NIC 驅動總在接收到數據包之後產生一個 IRQ,接着在中斷服務例程裏將這個 skb 加入本地的 softnet,然後觸發本地 NET_RX_SOFTIRQ 軟中斷後續處理。如果包速過高,因爲 IRQ 的優先級高於 SoftIRQ,導致系統的大部分資源都在響應中斷,但 softnet 的隊列大小有限,接收到的超額數據包也只能丟掉,所以這時這個模型是在用寶貴的系統資源做無用功。而 NAPI 則在這樣的情況下,直接把包丟掉,不會繼續將需要丟掉的數據包扔給內核去處理,這樣,網卡將需要丟掉的數據包儘可能的早丟棄掉,內核將不可見需要丟掉的數據包,這樣也減少了內核的壓力。

  對NAPI 的使用,一般包括以下的幾個步驟:

  1、在中斷處理函數中,先禁止接收中斷,且告訴網絡子系統,將以輪詢方式快速收包,其中禁止接收中斷完全由硬件功能決定,而告訴內核將以輪詢方式處理包則是使用函數 netif_rx_schedule(),也可以使用下面的方式,其中的 netif_rx_schedule_prep 是爲了判定現在是否已經進入了輪詢模式:

  將網卡預定爲輪詢模式

void netif_rx_schedule(struct net_device *dev);
或者
if (netif_rx_schedule_prep(dev))
__netif_rx_schedule(dev);

   2、在驅動中創建輪詢函數,它的工作是從網卡獲取數據包並將其送入到網絡子系統,其原型是:

  NAPI 的輪詢方法

int (*poll)(struct net_device *dev, int *budget);

  這裏的輪詢函數用於在將網卡切換爲輪詢模式之後,用 poll() 方法處理接收隊列中的數據包,如隊列爲空,則重新切換爲中斷模式。切換回中斷模式需要先關閉輪詢模式,使用的是函數 netif_rx_complete (),接着開啓網卡接收中斷 .。

  退出輪詢模式

void netif_rx_complete(struct net_device *dev);

  3、在驅動中創建輪詢函數,需要和實際的網絡設備 struct net_device 關聯起來,這一般在網卡的初始化時候完成,示例代碼如下:

  設置網卡支持輪詢模式

dev->poll = my_poll;
dev
->weight = 64;

  裏面另外一個字段爲權重 (weight),該值並沒有一個非常嚴格的要求,實際上是個經驗數據,一般 10Mb 的網卡,我們設置爲 16,而更快的網卡,我們則設置爲 64。

  NAPI的一些相關Interface

  下面是 NAPI 功能的一些接口,在前面都基本有涉及,我們簡單看看:

  netif_rx_schedule(dev)

  在網卡的中斷處理函數中調用,用於將網卡的接收模式切換爲輪詢

  netif_rx_schedule_prep(dev)

  在網卡是 Up 且運行狀態時,將該網卡設置爲準備將其加入到輪詢列表的狀態,可以將該函數看做是 netif_rx_schedule(dev) 的前半部分

  __netif_rx_schedule(dev)

  將設備加入輪詢列表,前提是需要 netif_schedule_prep(dev) 函數已經返回了 1

  __netif_rx_schedule_prep(dev)

  與 netif_rx_schedule_prep(dev) 相似,但是沒有判斷網卡設備是否 Up 及運行,不建議使用

  netif_rx_complete(dev)

  用於將網卡接口從輪詢列表中移除,一般在輪詢函數完成之後調用該函數。

  __netif_rx_complete(dev)

  Newer newer NAPI

  其實之前的 NAPI(New API) 這樣的命名已經有點讓人忍俊不禁了,可見 Linux 的內核極客們對名字的掌控,比對代碼的掌控差太多,於是乎,連續的兩次對 NAPI 的重構,被戲稱爲 Newer newer NAPI 了。

  與 netif_rx_complete(dev) 類似,但是需要確保本地中斷被禁止

  Newer newer NAPI

  在最初實現的 NAPI 中,有 2 個字段在結構體 net_device 中,分別爲輪詢函數 poll() 和權重 weight,而所謂的 Newer newer NAPI,是在 2.6.24 版內核之後,對原有的 NAPI 實現的幾次重構,其核心是將 NAPI 相關功能和 net_device 分離,這樣減少了耦合,代碼更加的靈活,因爲 NAPI 的相關信息已經從特定的網絡設備剝離了,不再是以前的一對一的關係了。例如有些網絡適配器,可能提供了多個 port,但所有的 port 卻是共用同一個接受數據包的中斷,這時候,分離的 NAPI 信息只用存一份,同時被所有的 port 來共享,這樣,代碼框架上更好地適應了真實的硬件能力。Newer newer NAPI 的中心結構體是napi_struct:

  NAPI 結構體

/* 
 
* Structure for NAPI scheduling similar to tasklet but with weighting 
*/ 
 struct napi_struct { 
    
/* The poll_list must only be managed by the entity which 
    
* changes the state of the NAPI_STATE_SCHED bit.  This means 
     
* whoever atomically sets that bit can add this napi_struct 
     
* to the per-cpu poll_list, and whoever clears that bit 
     
* can remove from the list right before clearing the bit. 
     
*/ 
     struct list_head      poll_list; 

     unsigned 
long          state; 
     
int              weight; 
     
int              (*poll)(struct napi_struct *int); 
 #ifdef CONFIG_NETPOLL 
     spinlock_t          poll_lock; 
     
int              poll_owner; 
 #endif 

     unsigned 
int          gro_count; 

     struct net_device      
*dev; 
     struct list_head      dev_list; 
     struct sk_buff          
*gro_list; 
     struct sk_buff          
*skb; 
 };

  熟悉老的 NAPI 接口實現的話,裏面的字段 poll_list、state、weight、poll、dev、沒什麼好說的,gro_count 和 gro_list 會在後面講述 GRO 時候會講述。需要注意的是,與之前的 NAPI 實現的最大的區別是該結構體不再是 net_device 的一部分,事實上,現在希望網卡驅動自己單獨分配與管理 napi 實例,通常將其放在了網卡驅動的私有信息,這樣最主要的好處在於,如果驅動願意,可以創建多個 napi_struct,因爲現在越來越多的硬件已經開始支持多接收隊列 (multiple receive queues),這樣,多個 napi_struct 的實現使得多隊列的使用也更加的有效。

  與最初的 NAPI 相比較,輪詢函數的註冊有些變化,現在使用的新接口是:

 void netif_napi_add(struct net_device *dev, struct napi_struct *napi, 
            
int (*poll)(struct napi_struct *int), int weight)

   熟悉老的 NAPI 接口的話,這個函數也沒什麼好說的。

  值得注意的是,前面的輪詢 poll() 方法原型也開始需要一些小小的改變:

    int (*poll)(struct napi_struct *napi, int budget);

   大部分 NAPI 相關的函數也需要改變之前的原型,下面是打開輪詢功能的 API:

    void netif_rx_schedule(struct net_device *dev, 
                           struct napi_struct 
*napi); 
    
/* ...or... */ 
    
int netif_rx_schedule_prep(struct net_device *dev, 
                   struct napi_struct 
*napi); 
    void __netif_rx_schedule(struct net_device 
*dev, 
                        struct napi_struct 
*napi);

   輪詢功能的關閉則需要使用:

    void netif_rx_complete(struct net_device *dev, 
               struct napi_struct 
*napi);

  因爲可能存在多個 napi_struct 的實例,要求每個實例能夠獨立的使能或者禁止,因此,需要驅動作者保證在網卡接口關閉時,禁止所有的 napi_struct 的實例。

  函數 netif_poll_enable() 和 netif_poll_disable() 不再需要,因爲輪詢管理不再和 net_device 直接管理,取而代之的是下面的兩個函數:

    void napi_enable(struct napi *napi); 
    void napi_disable(struct napi 
*napi);
發佈了4 篇原創文章 · 獲贊 38 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章