看Linux網管員如何進行網絡性能優化

  IT系統的性能永遠是企業IT人員關注熱點,而隨着Linux系統應用的增多,一些應用方面的問題也隨之增多,本文將爲你們介紹Linux內核下如何進行網絡性能優化。css

  首先咱們先看下網絡的行爲,能夠簡單劃分爲3條路徑:發送路徑、轉發路徑、接收路徑,而網絡性能的優化則可基於這3條路徑來考慮。本文集中於發送路徑和接收路徑上的優化方法分析,其中的NAPI本質上是接收路徑上的優化,但由於它在Linux的內核出現時間較早,而它也是後續出現的各類優化方法的基礎,因此將其單獨分析。api

  最爲基本的 NAPI數組

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

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

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

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

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

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

  對NAPI 的使用,通常包括如下的幾個步驟:函數

  一、在中斷處理函數中,先禁止接收中斷,且告訴網絡子系統,將以輪詢方式快速收包,其中禁止接收中斷徹底由硬件功能決定,而告訴內核將以輪詢方式處理包則是使用函數 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);

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

  NAPI 的輪詢方法

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

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

  退出輪詢模式

void netif_rx_complete(struct net_device * dev);

  三、在驅動中建立輪詢函數,須要和實際的網絡設備 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);

  發送路徑上的優化

  TSO (TCP Segmentation Offload)

  TSO (TCP Segmentation Offload) 是一種利用網卡分割大數據包,減少 CPU 負荷的一種技術,也被叫作 LSO (Large segment offload) ,若是數據包的類型只能是 TCP,則被稱之爲 TSO,若是硬件支持 TSO 功能的話,也須要同時支持硬件的 TCP 校驗計算和分散 - 彙集 (Scatter Gather) 功能。

  能夠看到 TSO 的實現,須要一些基本條件,而這些實際上是由軟件和硬件結合起來完成的,對於硬件,具體說來,硬件可以對大的數據包進行分片,分片以後,還要可以對每一個分片附着相關的頭部。TSO 的支持主要有須要如下幾步:

  一、若是網路適配器支持 TSO 功能,須要聲明網卡的能力支持 TSO,這是經過以 NETIF_F_TSO 標誌設置 net_device structure 的 features 字段來代表,例如,在 benet(drivers/net/benet/be_main.c) 網卡的驅動程序中,設置 NETIF_F_TSO 的代碼以下:

  benet 網卡驅動聲明支持 TSO 功能

 static void be_netdev_init(struct net_device * netdev) 

     struct be_adapter
* adapter = netdev_priv(netdev); 

     netdev
-> features | = NETIF_F_SG | NETIF_F_HW_VLAN_RX | NETIF_F_TSO | 
         NETIF_F_HW_VLAN_TX | NETIF_F_HW_VLAN_FILTER | NETIF_F_HW_CSUM | 
         NETIF_F_GRO | NETIF_F_TSO6; 

     netdev
-> vlan_features | = NETIF_F_SG | NETIF_F_TSO | NETIF_F_HW_CSUM; 

     netdev
-> flags | = IFF_MULTICAST; 

     adapter
-> rx_csum = true

    
/* Default settings for Rx and Tx flow control */  
     adapter
-> rx_fc = true
     adapter
-> tx_fc = true

     netif_set_gso_max_size(netdev,
65535 ); 

     BE_SET_NETDEV_OPS(netdev,
& be_netdev_ops); 

     SET_ETHTOOL_OPS(netdev,
& be_ethtool_ops); 

     netif_napi_add(netdev,
& adapter -> rx_eq.napi, be_poll_rx, 
         BE_NAPI_WEIGHT); 
     netif_napi_add(netdev,
& adapter -> tx_eq.napi, be_poll_tx_mcc, 
         BE_NAPI_WEIGHT); 

     netif_carrier_off(netdev); 
     netif_stop_queue(netdev); 
}

  在代碼中,同時也用 netif_set_gso_max_size 函數設置了 net_device 的 gso_max_size 字段。該字段代表網絡接口一次能處理的最大 buffer 大小,通常該值爲 64Kb,這意味着只要 TCP 的數據大小不超過 64Kb,就不用在內核中分片,而只需一次性的推送到網絡接口,由網絡接口去執行分片功能。

  二、當一個 TCP 的 socket 被建立,其中一個職責是設置該鏈接的能力,在網絡層的 socket 的表示是 struck sock,其中有一個字段 sk_route_caps 標示該鏈接的能力,在 TCP 的三路握手完成以後,將基於網絡接口的能力和鏈接來設置該字段。

  網路層對 TSO 功能支持的設定

  /* This will initiate an outgoing connection. */  
int tcp_v4_connect(struct sock * sk, struct sockaddr * uaddr, int addr_len) 

         ……

    
/* OK, now commit destination to socket.   */  
     sk
-> sk_gso_type = SKB_GSO_TCPV4; 
     sk_setup_caps(sk,
& rt -> dst); 

         ……
 }

  代碼中的 sk_setup_caps() 函數則設置了上面所說的 sk_route_caps 字段,同時也檢查了硬件是否支持分散 - 彙集功能和硬件校驗計算功能。須要這 2 個功能的緣由是:Buffer 可能不在一個內存頁面上,因此須要分散 - 彙集功能,而分片後的每一個分段須要從新計算 checksum,所以須要硬件支持校驗計算。

  三、如今,一切的準備工做都已經作好了,當實際的數據須要傳輸時,須要使用咱們設置好的 gso_max_size,咱們知道,TCP 向 IP 層發送數據會考慮 mss,使得發送的 IP 包在 MTU 內,不用分片。而 TSO 設置的 gso_max_size 就影響該過程,這主要是在計算 mss_now 字段時使用。若是內核不支持 TSO 功能,mss_now 的最大值爲「MTU – HLENS」,而在支持 TSO 的狀況下,mss_now 的最大值爲「gso_max_size -HLENS」,這樣,從網絡層帶驅動的路徑就被打通了。

  GSO (Generic Segmentation Offload)

  TSO 是使得網絡協議棧可以將大塊 buffer 推送至網卡,而後網卡執行分片工做,這樣減輕了 CPU 的負荷,但 TSO 須要硬件來實現分片功能;而性能上的提升,主要是由於延緩分片而減輕了 CPU 的負載,所以,能夠考慮將 TSO 技術通常化,由於其本質實際是延緩分片,這種技術,在 Linux 中被叫作 GSO(Generic Segmentation Offload),它比 TSO 更通用,緣由在於它不須要硬件的支持分片就可以使用,對於支持 TSO 功能的硬件,則先通過 GSO 功能,而後使用網卡的硬件分片能力執行分片;而對於不支持 TSO 功能的網卡,將分片的執行,放在了將數據推送的網卡的前一刻,也就是在調用驅動的 xmit 函數前。

  咱們再來看看內核中數據包的分片都有可能在哪些時刻:

  一、在傳輸協議中,當構造 skb 用於排隊的時候

  二、在傳輸協議中,可是使用了 NETIF_F_GSO 功能,立即將傳遞個網卡驅動的時候

  三、在驅動程序裏,此時驅動支持 TSO 功能 ( 設置了 NETIF_F_TSO 標誌 )

  對於支持 GSO 的狀況,主要使用了狀況 2 或者是狀況 2.、3,其中狀況二是在硬件不支持 TSO 的狀況下,而狀況 二、3 則是在硬件支持 TSO 的狀況下。

  代碼中是在 dev_hard_start_xmit 函數裏調用 dev_gso_segment 執行分片,這樣儘可能推遲分片的時間以提升性能:

  GSO 中的分片

  int dev_hard_start_xmit(struct sk_buff * skb, struct net_device * dev, 
             struct netdev_queue
* txq) 
{
……
        
if (netif_needs_gso(dev, skb)) { 
            
if (unlikely(dev_gso_segment(skb))) 
                
goto out_kfree_skb; 
            
if (skb -> next
                
goto gso; 
         }
else
            ……

         } 

        ……

 }

  接收路徑上的優化

  LRO (Large Receive Offload)

  Linux 在 2.6.24 中加入了支持 IPv4 TCP 協議的 LRO (Large Receive Offload) ,它經過將多個 TCP 數據聚合在一個 skb 結構,在稍後的某個時刻做爲一個大數據包交付給上層的網絡協議棧,以減小上層協議棧處理 skb 的開銷,提升系統接收 TCP 數據包的能力。

  固然,這一切都須要網卡驅動程序支持。理解 LRO 的工做原理,須要理解 sk_buff 結構體對於負載的存儲方式,在內核中,sk_buff 能夠有三種方式保存真實的負載:

  一、數據被保存在 skb->data 指向的由 kmalloc 申請的內存緩衝區中,這個數據區一般被稱爲線性數據區,數據區長度由函數 skb_headlen 給出

  二、數據被保存在緊隨 skb 線性數據區尾部的共享結構體 skb_shared_info 中的成員 frags 所表示的內存頁面中,skb_frag_t 的數目由 nr_frags 給出,skb_frags_t 中有數據在內存頁面中的偏移量和數據區的大小

  三、數據被保存於 skb_shared_info 中的成員 frag_list 所表示的 skb 分片隊列中

  合併了多個 skb 的超級 skb,可以一次性經過網絡協議棧,而不是屢次,這對 CPU 負荷的減輕是顯然的。

  LRO 的核心結構體以下:

  LRO 的核心結構體

  /*  
* Large Receive Offload (LRO) Manager 
*  
* Fields must be set by driver 
*/  

 struct net_lro_mgr { 
     struct net_device
* dev; 
     struct net_lro_stats stats; 

    
/* LRO features */  
     unsigned
long features; 
#define LRO_F_NAPI            
1    /* Pass packets to stack via NAPI */  
#define LRO_F_EXTRACT_VLAN_ID
2    /* Set flag if VLAN IDs are extracted 
                    from received packets
and eth protocol 
                    
is still ETH_P_8021Q */  

    
/*  
    
* Set for generated SKBs that are not added to  
    
* the frag list in fragmented mode 
    
*/  
     u32 ip_summed; 
     u32 ip_summed_aggr;
/* Set in aggregated SKBs: CHECKSUM_UNNECESSARY 
                
* or CHECKSUM_NONE */  

    
int max_desc; /* Max number of LRO descriptors   */  
    
int max_aggr; /* Max number of LRO packets to be aggregated */  

    
int frag_align_pad; /* Padding required to properly align layer 3  
                
* headers in generated skb when using frags */  

     struct net_lro_desc
* lro_arr; /* Array of LRO descriptors */  

    
/*  
    
* Optimized driver functions 
    
*  
    
* get_skb_header: returns tcp and ip header for packet in SKB 
    
*/  
    
int ( * get_skb_header)(struct sk_buff * skb, void ** ip_hdr, 
                  void
** tcpudp_hdr, u64 * hdr_flags, void * priv); 

    
/* hdr_flags: */  
#define LRO_IPV4
1 /* ip_hdr is IPv4 header */  
#define LRO_TCP  
2 /* tcpudp_hdr is TCP header */  

    
/*  
    
* get_frag_header: returns mac, tcp and ip header for packet in SKB 
    
*  
    
* @hdr_flags: Indicate what kind of LRO has to be done 
    
*              (IPv4 / IPv6 / TCP / UDP) 
    
*/  
    
int ( * get_frag_header)(struct skb_frag_struct * frag, void ** mac_hdr, 
                   void
** ip_hdr, void ** tcpudp_hdr, u64 * hdr_flags, 
                   void
* priv); 
};

  在該結構體中:

  dev:指向支持 LRO 功能的網絡設備

  stats:包含一些統計信息,用於查看 LRO 功能的運行狀況

  features:控制 LRO 如何將包送給網絡協議棧,其中的 LRO_F_NAPI 代表驅動是 NAPI 兼容的,應該使用 netif_receive_skb() 函數,而 LRO_F_EXTRACT_VLAN_ID 代表驅動支持 VLAN

  ip_summed:代表是否須要網絡協議棧支持 checksum 校驗

  ip_summed_aggr:代表彙集起來的大數據包是否須要網絡協議棧去支持 checksum 校驗

  max_desc:代表最大數目的 LRO 描述符,注意,每一個 LRO 的描述符描述了一路 TCP 流,因此該值代表了作多同時能處理的 TCP 流的數量

  max_aggr:是最大數目的包將被彙集成一個超級數據包

  lro_arr:是描述符數組,須要驅動本身提供足夠的內存或者在內存不足時處理異常

  get_skb_header()/get_frag_header():用於快速定位 IP 或者 TCP 的頭,通常驅動只提供其中的一個實現

  通常在驅動中收包,使用的函數是 netif_rx 或者 netif_receive_skb,但在支持 LRO 的驅動中,須要使用下面的函數,這兩個函數將進來的數據包根據 LRO 描述符進行分類,若是能夠進行彙集,則彙集爲一個超級數據包,否者直接傳遞給內核,走正常途徑。須要 lro_receive_frags 函數的緣由是某些驅動直接將數據包放入了內存頁,以後去構造 sk_buff,對於這樣的驅動,應該使用下面的接口:

  LRO 收包函數

 void lro_receive_skb(struct net_lro_mgr * lro_mgr, 
                  struct sk_buff
* skb, 
                  void
* priv); 

 void lro_receive_frags(struct net_lro_mgr
* lro_mgr, 
                       struct skb_frag_struct
* frags, 
              
int len , int true_size, 
               void
* priv, __wsum sum);

  由於 LRO 須要彙集到 max_aggr 數目的數據包,但有些狀況下可能致使延遲比較大,這種狀況下,能夠在彙集了部分包以後,直接傳遞給網絡協議棧處理,這時可使用下面的函數,也能夠在收到某個特殊的包以後,不通過 LRO,直接傳遞個網絡協議棧:

  LRO flush 函數

 void lro_receive_skb(struct net_lro_mgr * lro_mgr, 
                  struct sk_buff
* skb, 
                  void
* priv); 

 void lro_receive_frags(struct net_lro_mgr
* lro_mgr, 
                       struct skb_frag_struct
* frags, 
              
int len , int true_size, 
               void
* priv, __wsum sum);

  GRO (Generic Receive Offload)

  前面的 LRO 的核心在於:在接收路徑上,將多個數據包聚合成一個大的數據包,而後傳遞給網絡協議棧處理,但 LRO 的實現中存在一些瑕疵:

  一、數據包合併可能會破壞一些狀態;

  二、數據包合併條件過於寬泛,致使某些狀況下原本須要區分的數據包也被合併了,這對於路由器是不可接收的;

  三、在虛擬化條件下,須要使用橋接功能,但 LRO 使得橋接功能沒法使用;

  四、實現中,只支持 IPv4 的 TCP 協議。

  而解決這些問題的辦法就是新提出的 GRO(Generic Receive Offload),首先,GRO 的合併條件更加的嚴格和靈活,而且在設計時,就考慮支持全部的傳輸協議,所以,後續的驅動,都應該使用 GRO 的接口,而不是 LRO,內核可能在全部先有驅動遷移到 GRO 接口以後將 LRO 從內核中移除。而 Linux 網絡子系統的維護者 David S. Miller 就明確指出,如今的網卡驅動,有 2 個功能須要使用,一是使用 NAPI 接口以使得中斷緩和 (interrupt mitigation) ,以及簡單的互斥,二是使用 GRO 的 NAPI 接口去傳遞數據包給網路協議棧。

  在 NAPI 實例中,有一個 GRO 的包的列表 gro_list,用堆積收到的包,GRO 層用它來將彙集的包分發到網絡協議層,而每一個支持 GRO 功能的網絡協議層,則須要實現 gro_receive 和 gro_complete 方法。

  協議層支持 GRO/GSO 的接口

 struct packet_type { 
     __be16              type;      
/* This is really htons(ether_type). */  
     struct net_device      
* dev;       /* NULL is wildcarded here           */  
    
int               ( * func) (struct sk_buff *
                     struct net_device
*
                     struct packet_type
*
                     struct net_device
* ); 
     struct sk_buff          
* ( * gso_segment)(struct sk_buff * skb, 
                        
int features); 
    
int               ( * gso_send_check)(struct sk_buff * skb); 
     struct sk_buff          
** ( * gro_receive)(struct sk_buff ** head, 
                           struct sk_buff
* skb); 
    
int               ( * gro_complete)(struct sk_buff * skb); 
     void              
* af_packet_priv; 
     struct list_head      list; 
};

  其中,gro_receive 用於嘗試匹配進來的數據包到已經排隊的 gro_list 列表,而 IP 和 TCP 的頭部則在匹配以後被丟棄;而一旦咱們須要向上層協議提交數據包,則調用 gro_complete 方法,將 gro_list 的包合併成一個大包,同時 checksum 也被更新。在實現中,並沒要求 GRO 長時間的去實現聚合,而是在每次 NAPI 輪詢操做中,強制傳遞 GRO 包列表跑到上層協議。GRO 和 LRO 的最大區別在於,GRO 保留了每一個接收到的數據包的熵信息,這對於像路由器這樣的應用相當重要,而且實現了對各類協議的支持。以 IPv4 的 TCP 爲例,匹配的條件有:

  一、源 / 目的地址匹配;

  二、TOS/ 協議字段匹配;

  三、源 / 目的端口匹配。

  而不少其它事件將致使 GRO 列表向上層協議傳遞聚合的數據包,例如 TCP 的 ACK 不匹配或者 TCP 的序列號沒有按序等等。

  GRO 提供的接口和 LRO 提供的接口很是的相似,但更加的簡潔,對於驅動,明確可見的只有 GRO 的收包函數了 , 由於大部分的工做實際是在協議層作掉了:

  GRO 收包接口

 gro_result_t napi_gro_receive(struct napi_struct * napi, struct sk_buff * skb) 
gro_result_t napi_gro_frags(struct napi_struct
* napi)

  小結

  從上面的分析,能夠看到,Linux 網絡性能優化方法,就像一部進化史,但每步的演化,都讓解決問題的辦法更加的通用,更加的靈活;從 NAPI 到 Newer newer NAPI,從 TSO 到 GSO,從 LRO 到 GRO,都是一個從特例到一個更通用的解決辦法的演化,正是這種漸進但連續的演化,讓 Linux 保有了如此的活力。

相關文章
相關標籤/搜索