歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux技術 >> linux協議棧中網卡相關的名詞解釋

linux協議棧中網卡相關的名詞解釋

日期:2017/3/3 12:48:33   编辑:Linux技術
摘自:http://blog.csdn.net/w_s_xin/article/details/11632089 和 http://blog.csdn.net/w_s_xin/article/details/11636651
這個文檔介紹了Linux網絡協議棧中一系列互補的技術。
這些技術用來增加多處理器系統的並行性和改善性能。
這些技術包括:
RSS: Receive Side Scaling (接收側的縮放)
RPS: Receive Packet Steering (接收端包的控制)
RFS: Receive Flow Steering (接收端流的控制)
Accelerated Receive Flow Steering (加速的接收端流的控制)
XPS: Transmit Packet Steering(發送端包的控制)
(1)
RSS: Receive Side Scaling
=========================
當代的NICs支持多個接收和傳輸隊列,即多隊列。接收的時候,一個網卡
能夠發送不同的包到不同的隊列,在不同的CPU之間分散處理。
NIC針對每一個包,通過一個過濾器來指定這個包屬於哪一個流。
每個流中的數據包被控制在一個單獨的接收隊列中,CPU進行輪回
處理。這種機制就叫做RSS。RSS的目標和其他控制技術目的都是為了增加性能。
多隊列也可以被用於流量優先控制,但那不是這些技術的目的。
RSS中的過濾器是一個基於L3和L4層頭部的hash函數,
例如,基於IP地址和TCP端口的4元組的hash函數。最常見的RSS硬件實現中,使用了128個間接表,
其中每個表存儲一個隊列號(注,網卡的隊列數比較少,比如igb是8個,bnx2是5個)。
針對某個包而言,使用這個包計算出的hash值(hash是Toeplitz算法)的低7位先確定
間接表,再從間接表中的值訪問隊列。
一些高級的NICs允許使用可編程的過濾器來控制包屬於哪個隊列。
例如,綁定TCP端口80的webserver,數據包能被指向他們自己的隊列。
“n-tuple”過濾器可以通過ethtool的 --config-ntuple來配置。(注: 2.6.36開始引入!)
==== RSS Configuration
多隊列網卡的驅動提供了一個內核模塊參數,用來指定硬件隊列個數。
例如,bnx2x驅動使用的參數是num_queues. 如果設備支持足夠多的隊列,
一個典型的RSS配置中,最好的是一個CPU一個接收隊列。或者至少每個內存域一個接收隊列,
一個內存域包含一系列的CPU,並共享一個特殊的內存級別(L1,L2,NUMA節點等等)。
RSS設備的間接表,在驅動初始化的時候被映射。默認的映射是隊列均勻的發布在間接表中。
但是,在運行的時候,使用ethtool命令 (--show-rxfh-indir and --set-rxfh-indir),
間接表可以被查看,也可以被修改。修改間接表,可以給不同的隊列不同比例的權重。
== RSS IRQ Configuration
每個接收隊列有一個單獨的IRQ,即中斷號。NIC通過IRQ來通知CPU什麼時候新的數據包到達了指定的隊列。
PCIe設備使用MSI-X來路由每個中斷到CPU。有效的隊列到IRQ的映射是由/proc/interrupts來制定的。
默認,一個中斷能被任何一個CPU處理。因為一個重要的包處理部分發生在接收中斷處理函數函數中,
在CPU中平分接收中斷是有優點的。如果要手動的調節每個中斷的親和性,可以參考Documentation/IRQ-affinity.txt。
一些系統會運行irqbalance服務,這個服務會動態的優化IRQ的親和性,因此會覆蓋任何手動設置。
== Suggested Configuration
當關注低延時或者接收中斷處理稱為瓶頸時,應該啟用RSS。分擔負載在不同的CPU之間,
減少了隊列長度。對於低延時的網絡,最佳的設置是創建和CPU個數一樣多的隊列。
最高效的配置是擁有最少的隊列,並且沒有隊列溢出。這是因為,默認下
中斷聚合啟用的情況下,中斷的總數目戶隨著每個增加的隊列而增加。
每個cpu的負載可以使用mpstat工具來觀測到。但是,注意,啟用超線程的處理器,
每一個超線程代筆了單獨一個cpu。對於中斷處理,在最初的測試中顯示超線程
並沒有產生優勢。所以,根據CPU的核個數,而不是邏輯cpu個數,來限制隊列數目。
(2)
RPS: Receive Packet Steering
============================
RPS,邏輯上是一種以軟件的方式來實現RSS。在數據路徑上,稍後被調用。
介於RSS選擇了隊列和CPU(這個cpu會處理硬中斷),RPS選擇CPU來執行硬件中斷處理之後的協議處理。
通過把數據包放在目標CPU的backlog隊列,並喚醒CPU來處理。
RPS相比RSS有幾個好處:
1) RPS能夠被任何NIC使用。
2) 軟件過濾器能夠輕易的被添加,用來hash新的協議。
3) 它不會增加硬件設備的中斷。盡管,引入了IPIs(inter-processor interrupts)。
當一個設備使用 netif_rx() 函數和netif_receive_skb()函數,(從網卡驅動)向網絡協議棧傳遞數據包時,
RPS在底半環境(通過軟中斷來實現的,在硬中斷處理函數之後。)中被調用。
這2個函數調用get_rps_cpu() 函數,來選擇應該執行包的隊列。
決定目標CPU的第一步是基於包的地址和端口(有的協議是2元組,有的協議是4元組)
來計算hash值。這個值與這個包的流保持一致。這個hash值要麼是由硬件來提供的,
要麼是由協議棧來計算的。厲害的硬件能夠在包的接收描述符中傳遞hash值,這個值
與RSS計算的值是相等的。這個hash值保存在skb->rx_hash中,並且這個值可以作為流的hash值
可以被使用在棧的其他任何地方。
每一個接收硬件隊列有一個相關的CPU列表,RPS可以將包放到這個隊列中進行處理。
對於每一個接收到的包,指向這個列表的索引是通過流hash值對列表大小取模來計算的。
被指向的CPU是處理 數據包的目標CPU,並且這個包被加到CPU的backlog隊列的尾部。
最底半處理的最後,IPI被發送到這個包所插到的那個CPU。IPI喚醒遠程CPU來處理backlog隊列,
之後隊列中數據包被發送到網絡協議棧進行處理。
==== RPS Configuration
RPS要求內核編譯了CONFIG_RPS選項(SMP上默認是打開的)。盡管編譯到內核,直到
被配置了才能啟用。對於某個接收隊列,RPS可以轉發流量到哪個CPU,是由
/sys/class/net/<dev>/queues/rx-<n>/rps_cpus來控制的。這個文件實現了
CPU的位圖。默認,當值是0,RPS是無效的,數據包是由中斷的CPU來處理的。
Documentation/IRQ-affinity.txt 解釋了CPU是怎麼由位圖來設置的。
== Suggested Configuration
對於單個隊列的設備,一個典型的RPS配置是設置rps_cpus指向與中斷CPU屬於相同內存域的
CPU列表。如果NUMA位置不是一個問題,也可以設置所有的CPUs。如果高中斷率,
從cpu位圖中排除高中斷率的CPU是明智的,因為那個CPU已經執行了太多的工作。
對於一個多隊列的系統,如果RSS已經配置了,導致一個硬件接收隊列已經映射到每一個CPU。
那麼RPS就是多余的和不必要的。如果只有很少的硬件中斷隊列(比CPU個數少),每個隊列
的rps_cpus 指向的CPU列表與這個隊列的中斷CPU共享相同的內存域,那RPS將會是有效的。
(3)
RFS: Receive Flow Steering
===============
===========
RPS只依靠hash來控制數據包,提供了好的負載平衡,但是它沒有考慮應用程序的位置(注:這個位置是指程序在哪個cpu上執行)。RFS則考慮到了應用程序的位置。RFS的目標是通過指派應用線程正在運行的CPU來進行數據包處理,以此來增加數據緩存的命中率。RFS依靠RPS的機制插入數據包到指定CPU的backlog隊列,並喚醒那個CPU來執行。
RFS中,數據包並不會直接的通過數據包的hash值被轉發,但是hash值將會作為流查詢表的索引。這個表映射數據流與處理這個流的CPU。這個數據流的hash值(就是這個流中的數據包的hash值)將被用來計算這個表的索引。流查詢表的每條記錄中所記錄的CPU是上次處理數據流的CPU。如果記錄中沒有CPU,那麼數據包將會使用RPS來處理。多個記錄會指向相同的CPU。確實,當流很多而CPU很少時,很有可能一個應用線程處理多個不同hash值的數據流。
rps_sock_flow_table是一個全局的數據流表,這個表中包含了數據流渴望運行的CPU。這個CPU是當前正在用戶層處理流的CPU。每個數據流表項的值是CPU號,這個會在調recvmsg,sendmsg (特別是inet_accept(), inet_recvmsg(), inet_sendmsg(), inet_sendpage() and tcp_splice_read()),被更新。(注:使用sock_rps_record_flow()來記錄rps_sock_flow_table表中每個數據流表項的CPU號。)
當調度器移動一個線程到一個新的CPU,而內核正在舊的CPU上處理接收到的數據包,這會導致數據包的亂序。為了避免這個, RFS使用了第二個數據流表來為每個數據流跟蹤數據包:rps_dev_flow_table 是一個表,被指定到每個設備的每個硬件接收隊列。每個表值存儲了CPU號和一個計數值。這個CPU號表示了數據流中的數據包將被內核進一步處理的CPU。理想狀態下,內核和用戶處理發生正在同一個CPU上,由此在這兩個表中這個CPU號是相同的。如果調度器已經遷移用戶進程,而內核仍然有數據包被加到舊的CPU上,那麼這兩個值就不等了。
當這個流中的數據包最終被加到隊列中, rps_dev_flow_table中的計數值記錄了當前CPU的backlog隊列的長度。每個backlog隊列有一個隊列頭,當數據包從隊列中出去後,這個隊列頭就會增加。隊列尾部則等於隊列頭加上隊列長度。換句話說,rps_dev_flow[i] 中的計數值記錄了流i中的最後一個數據包,這個數據包已經添加到了目標CPU的backlog隊列。當然,流i是由hash值選擇的,並且多個數據流可以hash到同一個流i.
下面描述避免數據包亂序的技巧,當從get_rps_cpu()選擇CPU來進行數據包處理,rps_sock_flow 和rps_dev_flow 將會進行比較。如果數據流的理想CPU(found in therps_sock_flow table)和當前CPU(found in the rps_dev_flow table)匹配,這個包將會加到這個CPU的backlog隊列。如果他們不同,並且下面規則中任一個為真,則當前的CPU將會被更新,去匹配理想CPU。
- 當前CPU的隊列頭部大於等於rps_dev_flow[i]中記錄的尾部計數值,這個計數值指向了CPU的隊列的尾部。(說明當前cpu中沒有多余的數據包未處理。)
- 當前CPU是未設置的。(等於NR_CPUS,RPS_NO_CPU=0xffff)
- 當前CPU是離線的。(注:應該是沒有啟用。)
(注:如果他們不同,並且當前CPU是有效的,則會繼續用當前的CPU來處理。)檢查了之後,數據包被發送到(可能)更新後的CPU.這些規則目標是當舊的CPU上沒有接收到的數據包,才會移動數據流移動到一個新的CPU上。接收到的數據包能夠在新的CPU切換後到達。
==== RFS Configuration
RFS需要內核編譯CONFIG_RPS選項,直到明顯的配置,RFS才起作用。全局數據流表(rps_sock_flow_table)的總數可以通過下面的參數來設置:
/proc/sys/net/core/rps_sock_flow_entries
每個隊列的數據流表總數可以通過下面的參數來設置:
/sys/class/net/<dev>/queues/rx-<n>/rps_flow_cnt
== Suggested Configuration
針對每個接收隊列啟用RFS,上面的兩個參數需要被設置。參數的值會被進位到最近的2的冪次方值。(參數的值是7,則實際有效值是8. 參數是值32,則實際值就是32.)建議的流計數依賴於期待的有效的連接數,這個值顯著的小於連接總數。我們發現rps_sock_flow_entries設置成32768,在中等負載的服務器上,工作的很好。對於單隊列設備,單隊列的rps_flow_cnt值被配置成與 rps_sock_flow_entries相同。對於一個多隊列設備,每個隊列的rps_flow_cnt被配置成rps_sock_flow_entries/N, N是隊列總數。例如,如果rps_sock_flow_entries設置成32768,並且有16個接收隊列,每個隊列的rps_flow_cnt最好被配置成2048.
(4)
Accelerated RFS(加速RFS)
===============
加速RFS對於RFS而言,就像RSS對於RPS。 加速RFS是一個硬件加速的負載平衡機制。加速RFS基於應用線程正在運行的CPU,使用“soft state”來控制流。加速RFS應該比RFS執行的好,因為數據包直接發送到CPU,而消耗數據包的線程也在這個cpu上。目標CPU要麼是和應用線程相同的CPU,要麼至少是和應用線程在同一緩存層次的CPU(注:意思可能是共享同個cache的其他CPU)。
要啟用加速RFS,網絡協議棧調用ndo_rx_flow_steer驅動函數為數據包通訊理想的硬件隊列,這個隊列匹配數據流。當rps_dev_flow_table中的每個流被更新了,網絡協議棧自動調用這個函數。驅動輪流地使用一種設備特定的方法指定NIC去控制數據包。
一個數據流的硬件隊列是從rps_dev_flow_table的CPU記錄中推斷出來的。協議棧需要向NIC驅動咨詢CPU到硬件隊列的映射,因為這個映射是由NIC驅動來維護的。這個是自動從IRQ親和性表(通過/proc/interrupts顯示)生成的反轉表。驅動可以使用cpu_rmap (“CPU affinity reverse map”) 內核庫函數來填充這個映射。For each CPU, the corresponding queue
in the map isset to be one whose processing CPU is closest in cache locality.(不知道怎麼翻譯了 :-0)
==== Accelerated RFS Configuration
加速RFS需要內核編譯CONFIG_RFS_ACCEL,並且需要NIC設備和驅動都支持。並且要求ntuple過濾已經通過ethtool啟用。CPU到隊列的映射是自動從每個接收隊列的IRQ親和性配置推斷出來的,所以無需格外的配置。
== Suggested Configuration
不管什麼時候,只要你想用RFS並且NIC支持硬件加速,這個技術都需要被啟用。
(支持這個的硬件有哪些??)
(5)
XPS: Transmit Packet Steering
=============================
XPS 是一種機制,用來智能的選擇多隊列設備的隊列來發送數據包。為了達到這個目標,從CPU到硬件隊列的映射需要被記錄。這個映射的目標是專門地分配隊列到一個CPU列表,這些CPU列表中的某個CPU來完成隊列中的數據傳輸。這個有兩點優勢,第一點,設備隊列上的鎖競爭會被減少,因為只有很少的CPU對相同的隊列進行競爭。(如果每個CPU只有自己的傳輸隊列,鎖的競爭就完全沒有了。)第二點,傳輸時的緩存不命中的概率就減少,特別是持有sk_buff的數據緩存。
XPS通過設置使用隊列進行傳輸的CPU位圖,對每一個隊列進行配置。相反的映射,從CPU到傳輸隊列,是由網絡設備計算並維護的。當傳輸數據流的第一個數據包時,函數get_xps_queue()被調用來選擇一個隊列。這個函數使用正在運行的CPU的ID號作為指向CPU-到-隊列的查找表的key值。如果這個ID匹配一個單獨的隊列,那麼這個隊列被用來傳輸。如果多個隊列被匹配,通過數據流的hash值作為key值來選擇隊列。
選擇傳輸特殊數據流的隊列被存儲在相應的數據流的socket結構體(sk_tx_queue_mapping)。
這個傳輸隊列被用來傳輸接下來的數據包,以防亂序(OOO)的包。這個選擇也分擔了為這個流中的所有數據包調用 get_xps_queues() 的開銷。為了避免亂序的包,只有這個數據流中的某個包的skb->ooo_okay標志被設置了,這個數據流所使用的隊列才能改變。這個標志表示數據流中沒有待解決的數據包(注:被解決的數據包應該是指tcp_packets_in_flight()等於0。也就是說發送出去的數據包都被應答了),所以,這個傳輸隊列才能安全的改變,而不會有產生亂序包的危險。傳輸層即L4層相應地有責任來設置ooo_okay標志位。例如,當一個連接的所有數據包被應答了,tcp才設置這個標志位。(UDP協議沒有流的概念,所以沒有必要設置這個標志。)
==== XPS Configuration
XPS要求內核編譯了CONFIG_XPS選項(SMP上默認是打開的)。盡管編譯到內核,直到被配置了才能啟用。為了使用XPS,需要使用sysfs來配置傳輸隊列的CPU位圖:
/sys/class/net/<dev>/queues/tx-<n>/xps_cpus
== Suggested Configuration
對於只有一個傳輸隊列的網絡設置而言,XPS的配置沒有任何效果,因為這種情況下沒有選擇。對於一個多隊列系統,XPS更好的配置是每個CPU映射到一個隊列中。如果有CPU一樣多的隊列,那麼每個隊列可以映射到每個CPU上,這就導致沒有競爭的專一配對。如果隊列比CPU少,共享指定隊列的CPU最好是與處理傳輸硬中斷(這個中斷用來清理隊列傳輸結束後的工作)的CPU共享緩存的CPU。
Further Information
===================
RPS和RFS在內核2.6.35中被引入。XPS在2.6.38中被引入。原始的patches是由Tom Herbert
([email protected])來提交的。
加速RFS在2.6.35中被引入,原始的patches是由Ben Hutchings ([email protected])提交的
--------------------------------------------------------------------------------------------
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 功能
[cpp] view
plaincopy
  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 功能支持的設定
[cpp] view
plaincopy
 /* This will initiate an outgoing connection. */
nt 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”,這樣,從網絡層帶驅動的路徑就被打通了。
(2)
  GSO (Generic Segmentation Offload)
  TSO 是使得網絡協議棧能夠將大塊 buffer 推送至網卡,然後網卡執行分片工作,這樣減輕了 CPU 的負荷,但 TSO 需要硬件來實現分片功能;而性能上的提高,主要是因為延緩分片而減輕了 CPU 的負載,因此,可以考慮將 TSO 技術一般化,因為其本質實際是延緩分片,這種技術,在 Linux 中被叫做 GSO(Generic Segmentation Offload),它比 TSO 更通用,原因在於它不需要硬件的支持分片就可使用,對於支持
TSO 功能的硬件,則先經過 GSO 功能,然後使用網卡的硬件分片能力執行分片;而對於不支持 TSO 功能的網卡,將分片的執行,放在了將數據推送的網卡的前一刻,也就是在調用驅動的 xmit 函數前。
  我們再來看看內核中數據包的分片都有可能在哪些時刻:
  在傳輸協議中,當構造 skb 用於排隊的時候
  在傳輸協議中,但是使用了 NETIF_F_GSO 功能,當即將傳遞個網卡驅動的時候
[cpp] view
plaincopy
  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 { …… }
…… }
  在驅動程序裡,此時驅動支持 TSO 功能 ( 設置了 NETIF_F_TSO 標志 )
  對於支持 GSO 的情況,主要使用了情況 2 或者是情況 2.、3,其中情況二是在硬件不支持 TSO 的情況下,而情況 2、3 則是在硬件支持 TSO 的情況下。
  代碼中是在 dev_hard_start_xmit 函數裡調用 dev_gso_segment 執行分片,這樣盡量推遲分片的時間以提高性能:
  清單 8. GSO 中的分片
(4)
  接收路徑上的優化
  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 的核心結構體
  
[cpp] view
plaincopy
/* * 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_flush_all(struct net_lro_mgr *lro_mgr);
void lro_flush_pkt(struct net_lro_mgr *lro_mgr, struct iphdr *iph, struct tcphdr *tcph);
(4)  
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 的接口
 
[cpp] view
plaincopy
 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)
Copyright © Linux教程網 All Rights Reserved