【轉】Linux 下網絡性能優化方法簡析


轉自https://www.ibm.com/developerworks/cn/linux/l-cn-network-pt/index.html

 

 

作者:趙 軍

概述

對於網絡的行為,可以簡單划分為 3 條路徑:1) 發送路徑,2) 轉發路徑,3) 接收路徑,而網絡性能的優化則可基於這 3 條路徑來考慮。由於數據包的轉發一般是具備路由功能的設備所關注,在本文中沒有敘述,讀者如果有興趣,可以自行學習(在 Linux 內核中,分別使用了基於哈希的路由查找和基於動態 Trie 的路由查找算法)。本文集中於發送路徑和接收路徑上的優化方法分析,其中的 NAPI 本質上是接收路徑上的優化,但因為它在 Linux 的內核出現時間較早,而它也是后續出現的各種優化方法的基礎,所以將其單獨分析。

最為基本的 NAPI

NAPI

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

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

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

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

  • 中斷緩和 (Interrupt mitigation),由上面的例子可以看到,在高流量下,網卡產生的中斷可能達到每秒幾千次,而如果每次中斷都需要系統來處理,是一個很大的壓力,而 NAPI 使用輪詢時是禁止了網卡的接收中斷的,這樣會減小系統處理中斷的壓力
  • 數據包節流 (Packet throttling),NAPI 之前的 Linux NIC 驅動總在接收到數據包之后產生一個 IRQ,接着在中斷服務例程里將這個 skb 加入本地的 softnet,然后觸發本地 NET_RX_SOFTIRQ 軟中斷后續處理。如果包速過高,因為 IRQ 的優先級高於 SoftIRQ,導致系統的大部分資源都在響應中斷,但 softnet 的隊列大小有限,接收到的超額數據包也只能丟掉,所以這時這個模型是在用寶貴的系統資源做無用功。而 NAPI 則在這樣的情況下,直接把包丟掉,不會繼續將需要丟掉的數據包扔給內核去處理,這樣,網卡將需要丟掉的數據包盡可能的早丟棄掉,內核將不可見需要丟掉的數據包,這樣也減少了內核的壓力

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

  1. 在中斷處理函數中,先禁止接收中斷,且告訴網絡子系統,將以輪詢方式快速收包,其中禁止接收中斷完全由硬件功能決定,而告訴內核將以輪詢方式處理包則是使用函數 netif_rx_schedule(),也可以使用下面的方式,其中的 netif_rx_schedule_prep 是為了判定現在是否已經進入了輪詢模式 ::
    清單 1. 將網卡預定為輪詢模式
    1
    2
    3
    4
           void netif_rx_schedule(struct net_device *dev);
    或者
           if (netif_rx_schedule_prep(dev))
                   __netif_rx_schedule(dev);
  2. 在驅動中創建輪詢函數,它的工作是從網卡獲取數據包並將其送入到網絡子系統,其原型是:
    清單 2. NAPI 的輪詢方法
    1
    int (*poll)(struct net_device *dev, int *budget);

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

    清單 3. 退出輪詢模式
    1
    void netif_rx_complete(struct net_device *dev);
  3. 在驅動中創建輪詢函數,需要和實際的網絡設備 struct net_device 關聯起來,這一般在網卡的初始化時候完成,示例代碼如下:
    清單 4. 設置網卡支持輪詢模式
    1
    2
    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)

與 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:

清單 5. NAPI 結構體
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/*
* 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 相比較,輪詢函數的注冊有些變化,現在使用的新接口是:

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

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

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

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

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

1
2
3
4
5
6
7
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);

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

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

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

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

1
2
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 的代碼如下:
    清單 6. benet 網卡驅動聲明支持 TSO 功能
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    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 的三路握手完成之后,將基於網絡接口的能力和連接來設置該字段。
    清單 7. 網路層對 TSO 功能支持的設定
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /* 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 函數前。

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

  1. 在傳輸協議中,當構造 skb 用於排隊的時候
  2. 在傳輸協議中,但是使用了 NETIF_F_GSO 功能,當即將傳遞個網卡驅動的時候
  3. 在驅動程序里,此時驅動支持 TSO 功能 ( 設置了 NETIF_F_TSO 標志 )

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

代碼中是在 dev_hard_start_xmit 函數里調用 dev_gso_segment 執行分片,這樣盡量推遲分片的時間以提高性能:

清單 8. GSO 中的分片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 可以有三種方式保存真實的負載:

  1. 數據被保存在 skb->data 指向的由 kmalloc 申請的內存緩沖區中,這個數據區通常被稱為線性數據區,數據區長度由函數 skb_headlen 給出
  2. 數據被保存在緊隨 skb 線性數據區尾部的共享結構體 skb_shared_info 中的成員 frags 所表示的內存頁面中,skb_frag_t 的數目由 nr_frags 給出,skb_frags_t 中有數據在內存頁面中的偏移量和數據區的大小
  3. 數據被保存於 skb_shared_info 中的成員 frag_list 所表示的 skb 分片隊列中

合並了多個 skb 的超級 skb,能夠一次性通過網絡協議棧,而不是多次,這對 CPU 負荷的減輕是顯然的。

LRO 的核心結構體如下:

清單 9. LRO 的核心結構體
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/*
* 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,對於這樣的驅動,應該使用下面的接口:

清單 10. LRO 收包函數
1
2
3
4
5
6
7
8
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,直接傳遞個網絡協議棧:

清單 11. LRO flush 函數
1
2
3
4
5
void lro_flush_all(struct net_lro_mgr *lro_mgr);
 
void lro_flush_pkt(struct net_lro_mgr *lro_mgr,
            struct iphdr *iph,
            struct tcphdr *tcph);

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 方法。

清單 12. 協議層支持 GRO/GSO 的接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 的收包函數了 , 因為大部分的工作實際是在協議層做掉了:

清單 13. GRO 收包接口
1
2
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 保有了如此的活力。

 

相關主題


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM