擴充套件Linux網路棧

charlieroro發表於2020-12-07

擴充套件Linux網路棧

來自Linux核心文件。之前看過這篇文章,一直好奇,問什麼一條網路流會固定在一個CPU上進行處理,本文件可以解決這個疑問。為了更好地理解本文章中的功能,將這篇文章穿插入內。

簡介

本文的描述了Linux網路棧中的一組補充技術,用於增加多處理器系統的並行性和提高效能。

描述的結束為:

  • RSS: Receive Side Scaling
  • RPS: Receive Packet Steering
  • RFS: Receive Flow Steering
  • Accelerated Receive Flow Steering
  • XPS: Transmit Packet Steering

RSS: Receive Side Scaling

現代NICs支援多個接收和傳輸描述符佇列(多佇列)。在接收報文時,NIC可以通過將不同的報文傳送到不同的佇列的方式來分配多個CPU處理。NIC通過過濾器來分發報文,過濾器會將報文分配給某條邏輯流。每條流中的報文都被導向不同的接收佇列,然後由不同CPU處理。這個機制稱為“Receive-side Scaling” (RSS)。RSS和其他擴充套件技術的目的是提升效能。多佇列分發技術也可以按照優先順序處理流量,但這不是該技術關注的內容。

RSS中的過濾器通常是一個針對網路和/或傳輸層首部的雜湊函式,如對IP地址的4元組和報文的TCP埠進行雜湊。最常見的RSS的硬體實現是使用一個128個表項的間接表,每個表項儲存一個佇列元素。根據由報文計算出的雜湊值的低7位來決定報文的接收佇列(通常是Toeplitz雜湊),使用該值作為間接表的索引,然後讀取相應的值。

一些先進的NICs允許根據程式設計的過濾器將報文匯入佇列。例如,使用TCP 80埠的web伺服器的報文可以直接匯入其歸屬的接收佇列。可以使用ethtool配置這種n元組過濾器(–config-ntuple)。

RSS配置

對於支援多佇列的NIC,驅動通常會提供一個核心模組引數來配置硬體佇列的數目。例如在bnx2x驅動中,該引數稱為num_queues。一個典型的RSS配置應該給每個CPU分配一個接收佇列(如果驅動支援足夠多佇列的話),或至少給每個記憶體域分配一個接收佇列(記憶體域指共享一個特定記憶體級別(L1, L2, NUMA 節點等)的一組CPUs)。

RSS裝置的間接表(通過掩碼雜湊解析佇列)通常是在驅動初始化時進行程式設計。預設會將佇列平均分佈到表中,但也可以在執行時通過ethtool命令進行檢索和修改(–show-rxfh-indir–set-rxfh-indir)。通過修改間接表,可以給不同的佇列分配不同的相對權重。

RSS IRQ 配置

每個接收佇列都會關聯一個IRQ(中斷請求)。當給定的佇列接收到新的報文後,NIC會觸發中斷,通知CPU。PCIe裝置的信令路徑使用訊息信令中斷(MSI-X),可以將每個中斷路由到一個特定的CPU上。可以在/proc/interrupts中檢視到IRQs的佇列對映。預設情況下,任何CPU都可以處理IRQ。由於報文接收中斷處理中包含一部分不可忽略的處理過程,因此在CPU之間分散處理中斷是有利的(防止新的中斷無法被即時處理)。如果要手動調節IRQ的親和性,參見SMP IRQ affinity。一些系統會執行irqbalance,這是一個守護程式,自動分配IRQ,可能會覆蓋手動設定的結果。

建議配置

當關注延遲或當接收中斷處理成為瓶頸後應該啟用RSS。將負載分擔給多個CPU可以有效減小佇列長度。對於低延遲網路來說,最佳設定是分配與系統中CPU數量一樣多的佇列。最高效的高速率配置可能是接收佇列數量最少的配置,這樣不會由於CPU飽和而導致接收佇列溢位,由於在預設模式下啟用了中斷合併,中斷的總數會隨著每個其他佇列的增長而增加。

可以使用mpstat工具檢視單CPU的負載,但對於啟用了超執行緒(HT)的處理器,每個超執行緒都表示一個單獨的CPU。但對於中斷處理,HT在初始測試中沒有顯示任何好處,因此應該將佇列的數目限制為系統上的CPU core的數目。

RSS 是一個網路卡特性,其使用的是硬體佇列。

為了確定一個介面支援RSS,可以在/proc/interrupts中檢視一個介面是否對應了多箇中斷請求佇列。如下NIC為p1p1介面建立了6個接收佇列(p1p1-0到p1p1-5)。

# egrep 'CPU|p1p1' /proc/interrupts
CPU0    CPU1    CPU2    CPU3    CPU4    CPU5
89:   40187       0       0       0       0       0   IR-PCI-MSI-edge   p1p1-0
90:       0     790       0       0       0       0   IR-PCI-MSI-edge   p1p1-1
91:       0       0     959       0       0       0   IR-PCI-MSI-edge   p1p1-2
92:       0       0       0    3310       0       0   IR-PCI-MSI-edge   p1p1-3
93:       0       0       0       0     622       0   IR-PCI-MSI-edge   p1p1-4
94:       0       0       0       0       0    2475   IR-PCI-MSI-edge   p1p1-5

可以在/sys/class/net/<dev>/queues目錄中看到一個介面上現有的佇列,如下面的eth0有兩組佇列:

# ll /sys/class/net/eth0/queues
total 0
drwxr-xr-x 3 root root 0 Aug 19 16:23 rx-0
drwxr-xr-x 3 root root 0 Aug 19 16:23 rx-1
drwxr-xr-x 3 root root 0 Aug 19 16:23 tx-0
drwxr-xr-x 3 root root 0 Aug 19 16:23 tx-1

也可以使用ethtool -l 檢視。如下面的eth0最多支援2組佇列,當前啟用了2組(就是上述的兩組),可以使用ethtool -L eth0 combined 10修改當前啟用的佇列數,需要注意的的是,目前很多環境都是雲化的虛擬環境,大部分ethtool操作功能和許可權都受到了限制。

# ethtool -l eth0
Channel parameters for eth0:
Pre-set maximums:
RX:             0
TX:             0
Other:          0
Combined:       2
Current hardware settings:
RX:             0
TX:             0
Other:          0
Combined:       2

如上所述,下圖的硬體過濾器就是個根據4元組或5元組等進行雜湊的雜湊函式。可以使用ethtool -x <dev>命令檢視RSS使用的雜湊函式(但大部分虛擬環境不支援該命令,可以在/proc/sys/net/core/netdev_rss_key中檢視RSS使用的雜湊key)。

RPS: Receive Packet Steering

Receive Packet Steering (RPS)是RSS的一個軟體邏輯實現。作為一個軟體實現,需要在資料路徑的後端呼叫它。鑑於RSS會給流量選擇CPU佇列,因此會觸發CPU執行硬體中斷處理程式,RPS會在中斷處理程式之上選擇CPU來執行協議處理。整個過程是通過將報文放到期望的CPU backlog佇列中,然後喚醒該CPU進行處理來實現的。RPS相比RSS有一些優勢:

  1. 可以使用任何NIC
  2. 可以方便地新增軟體過濾器來雜湊新的協議
  3. 不會增加硬體裝置的中斷頻率(雖然它會引入內部處理中斷(IPIs))

在接收中斷處理程式的下半部分會呼叫RPS(當一個驅動使用netif_rx()netif_receive_skb()傳送一個報文到網路棧時)。這些函式會呼叫get_rps_cpu() 函式,get_rps_cpu() 會選擇一個處理報文的佇列。

RPS的第一步是通過對一條流中的報文的地址或埠(2元組或4元組,具體取決於協議)進行雜湊來確定目標CPU。雜湊操作會涉及相關流中的所有報文。可以通過硬體或棧來支援對報文的雜湊。支援報文雜湊的硬體會在接收的報文描述符中傳入雜湊值,通常與RSS使用的雜湊相同(如Toeplitz 雜湊)。雜湊值會儲存在skb->hash中,並且可以在棧的其他位置用作報文流的雜湊值。

每個接收硬體佇列都有相關的CPU列表,RPS會將報文入佇列並進行處理。對於每個接收到的報文,會根據流雜湊以列表大小為模來計算列表的索引。索引到的CPU就是處理報文的CPU,且報文會放到CPU backlog佇列的末尾。在下半部分的例程處理結束之後,會給有報文進入backlog佇列的CPU傳送IPIs。IPI會喚醒遠端CPU對backlog的處理,後續佇列中的報文會在網路棧中進行處理。

RPS配置

RPS需要在核心編譯時啟用CONFIG_RPS 選項(SMP上預設啟用,可以使用 cat /boot/config-$(uname -r)|grep CONFIG_RPS命令檢視)。但即使在編譯時指定了該功能,後續也需要通過明確配置才能啟用該功能。可以通過sys檔案系統中的檔案來配置RPS可以為接收佇列轉發流量的CPU列表。

/sys/class/net/<dev>/queues/rx-<n>/rps_cpus

該檔案實現了CPU點陣圖。當上述值為0時(預設為0),不會啟用RPS,這種情況下,報文將在中斷的CPU上進行處理。SMP IRQ affinity解釋瞭如何將CPU分配給點陣圖。

建議配置

對於一個單佇列裝置,典型的RPS配置會將rps_cpus 設定為與中斷CPU相同的記憶體域中的CPUs。如果NUMA的本地性不是問題,則也可以設定為系統上的所有CPUs。在高中斷率的情況下,最好從點陣圖中排除該CPU(因為該CPU已經足夠繁忙)。

對於一個多佇列系統,如果配置了RSS,則硬體接收佇列會對映到每個CPU上,此時RPS可能會冗餘。但如果硬體佇列的數目少於CPU,那麼,如果rps_cpus為每個佇列指定的CPU與中斷該佇列的CPU共享相同的記憶體域時,則RPS可能是有用的。

RPS使用/sys/class/net/<dev>/queues/rx-<n>/rps_cpus來設定某個接收佇列使用的CPU。如果要使用CPU 1~3,則點陣圖為0 0 0 0 0 1 1 1,即0x7,將7 寫入rps_cpus即可,後續rx-0將會使用CPU 1~3來接收報文。

# echo 7 > /sys/class/net/eth0/queues/rx-0/rps_cpus

在驅動將報文封裝到sk_buff之後,將會經過 netif_rx_internal()netif_receive_skb_internal(),然後呼叫get_rps_cpu()將雜湊值對映到rps_map中的某個表項,即CPU id。在獲取到CPU id之後,enqueue_to_backlog()會將sk_buff 放到指定的CPU佇列中(後續處理)。為每個CPU分配的佇列是一個per-cpu變數,softnet_data

如果已經啟用了RSS,則可以不啟用RPS。但如果系統上CPU的數目大於佇列的數目時,可以啟用RPS,給佇列關聯更多的CPU,這樣一個佇列的報文就可以在多個CPU上處理。

RPS流限制

RPS擴充套件了核心跨CPU接收處理的能力,而不會引入重排。如果報文的速率不同,則將同一流上的所有報文傳送到同一CPU會導致CPU負載失衡。在極端情況下,單條流會主導流量。特別是在存在很多並行連線的通用伺服器上,這類行為可能表現為配置錯誤或欺騙源拒絕服務攻擊。

流限制是RPS的一個可選特性,在CPU競爭期間,通過提前一點丟棄大流量的報文來為小流量騰出處理的機會。只有當RPS或RFS目標CPU達到飽和時,才會啟用此功能。一旦一個CPU的輸入報文佇列超過最大佇列長度(即,net.core.netdev_max_backlog)的一半,核心會從最近的256個報文開始按流計數,如果接收到一個新的報文,且此時這條流的報文數超過了設定的比率(預設為一半,即超過了256/2個),則會丟棄新報文。其他流的報文只有在輸入報文佇列達到netdev_max_backlog時才會丟棄報文。如果報文佇列長度低於閾值,則不會丟棄報文,因此流量限制不會完全切斷連線:即使是大流量也可以保持連線。

如果因為 CPU backlog 不夠或者 flow limit 不夠,被丟棄的報文會將丟包資訊計入 /proc/net/softnet_stat

介面

流控制功能預設會編譯到核心中(CONFIG_NET_FLOW_LIMIT),但不會啟用。它是為每個CPU獨立實現的(以避免鎖和快取競爭),並通過在sysctl net.core.flow_limit_cpu_bitmap中設定相關位來切換CPU,它的CPU點陣圖介面與rps_cpus 相同。

/proc/sys/net/core/flow_limit_cpu_bitmap

通過將每個報文雜湊到一個雜湊表bucket中,並增加每個bucket計數器來計算每條流的速率。雜湊函式與RPS選擇CPU時使用的相同,但由於bucket的數目要遠大於CPU的數目,因此流控制可更精細地識別大流量,並減少誤報。預設的表大小為4096個bucket,可以通過sysctl工具修改:

net.core.flow_limit_table_len

只有在分配新表時才會查詢該值。修改該值不會更新現有的表。

建議配置

流控制在系統上有很多並行流時有用,如果單個連線佔用了CPU的50%,則表明存在問題。這種環境下,可以為所有的CPU啟用流控制特性,來處理網路rx中斷(/proc/irq/N/smp_affinity可以設定中斷親和性)。

該特性依賴於輸入報文佇列長度超過流限制閾值(50%)+流歷史長度(256)。在實驗中將net.core.netdev_max_backlog設定為1000或10000效果很好。

RPS/RFS主要是針對單佇列網路卡多CPU環境。而/proc/irq/{IRQ}/smp_affinity/proc/irq/{IRQ}/smp_affinity_list指定了哪些CPU能夠關聯到一個給定的IRQ源。RPS只是單純把資料包均衡到不同的cpu,這個時候如果應用程式所在的cpu和軟中斷處理的cpu不是同一個,此時對cpu cache的影響會很大。目前大多數SMP系統會使用smp_affinity功能,預設不啟用RPS。

RFS: Receive Flow Steering

雖然基於雜湊的RPS可以很好地分配處理報文時的負載,但沒有考慮到應用所在的位置。Receive Flow Steering (RFS)擴充套件了這一點。RFS的目的是通過將報文的處理引導到正在消耗報文的應用程式執行緒所在的CPU上來提高資料快取命中率。RFS依賴與RPS相同的機制來將入佇列的報文導向另外一個CPU的backlog佇列,並喚醒該CPU。

在RFS中,報文不會根據雜湊結果進行轉發,雜湊結果會作為流查詢表的索引。該表會將流對映到正在處理這些流的CPU上。流雜湊(見RPS)用於計算該表的索引。記錄在表項中的CPU就是上次處理該條流的CPUs。如果一個表項中不包含有效的CPU,則對映到該表項的報文將會完全使用RPS。多個表項可能對映到相同的CPU上(存在很多條流,但僅有很少的CPU,且一個應用可能會使用很多流雜湊來處理流)。

rps_sock_flow_table 是一個全域性的流表,包含流期望的CPUs:處理當前在使用者空間中使用的流的CPU。每個表的值都對應一個CPU索引,並在執行recvmsgsendmsg (準確地講是 inet_recvmsg(), inet_sendmsg(), inet_sendpage()tcp_splice_read())時更新。

當排程器將一個執行緒轉移到一個新的CPU,但它在舊CPU上有未處理的接收報文時,收到的報文可能會亂序。為了防止發生這種情況,RFS使用一個秒流表來跟蹤每個流中未處理的報文:rps_dev_flow_table 是針對每個裝置的每個硬體接收佇列的表。每個表中的值都儲存了一個CPU索引和一個計數器。CPU索引表示入佇列(cpu的backlog佇列)流報文的當前CPU,後續由核心處理。理想情況下,核心和使用者空間的處理會發生在相同的CPU上,此時兩個表(rps_sock_flow_tablerps_dev_flow_table)中的CPU索引是相同的。但如果排程器最近轉移了使用者執行緒,而報文的入佇列和處理仍舊發生在老的CPU上,此時就會發生CPU不一致的情況。

當CPU處理的流有新報文入佇列時,rps_dev_flow_table 中的計數器會記錄當前CPU的backlog的長度。每個backlog佇列都有一個頭計數器,在報文出佇列時增加。尾計數器的計算方式為:頭計數器+佇列長度。即rps_dev_flow[i]中的計數器記錄了流i中的最後一個元素,該元素入佇列到為流i分配的CPU中(當然,表項i實際上是通過雜湊選擇的,多條流可能會雜湊到同一表項i)。

現在,避免出現亂序資料包的技巧是:當選擇處理報文的CPU時(通過get_rps_cpu()),會比較接收到資料包的佇列的rps_sock_flow表和rps_dev_flow表。如果流的rps_sock_flow 表中期望的CPU與rps_dev_flow 表中記錄的當前CPU匹配的話,報文會進入該CPU的backlog佇列。如果不同,當下面任一條成立時,會更新CPU,使其與期望的CPU匹配:

  • 當前CPU的佇列頭計數器 >= rps_dev_flow[i]中記錄的尾計數器
  • 當前CPU未設定(>= nr_cpu_ids)
  • 當前CPU下線

在上述檢查之後,報文會傳送(可能會更新CPU)到當前CPU。上述規則用於保證只有當老CPU上不存在未處理的報文時才會將一個流轉移到一個新的CPU上(因為未處理的報文可能晚於將要在新CPU上處理的報文)。

RFS 配置

只有啟用了核心引數CONFIG_RPS (SMP預設會啟用)之後才能使用RFS。該功能只有在明確配置之後才能使用。可以通過如下引數設定全域性流表rps_sock_flow_table的表項數:

/proc/sys/net/core/rps_sock_flow_entries

每個佇列的rps_dev_flow_table 流表中的表項數可以通過如下引數設定:

/sys/class/net/<dev>/queues/rx-<n>/rps_flow_cnt
建議配置

上述配置都需要在為接收佇列啟用RFS前完成配置。兩者的值會四捨五入到最接近的2的冪。建議的流數應該取決於任意時間活動的連線數,這可能大大少於開啟的連線數。我們發現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。

rps_sock_flow_table的結構如下:

struct rps_sock_flow_table {
 u32 mask;
 u32 ents[0];
};

mask用於將雜湊值對映到表的索引。由於表大小會四捨五入為2的冪,因此mask設為table_size - 1,並且很容易通過hash & scok_table->mask索引到一個sk_buff

表項通過rps_cpu_mask分為流id和CPU id。低位為CPU id,高位為流id。當應用在socket上進行操作 (inet_recvmsg(), inet_sendmsg(), inet_sendpage(), tcp_splice_read())時,會呼叫sock_rps_record_flow() 更新 rps_sock_flow_table 表。

當接收到一個報文時,會呼叫get_rps_cpu()來決定報文發到哪個CPU佇列,下面是其計算方式:

ident = sock_flow_table->ents[hash & sock_flow_table->mask];
if ((ident ^ hash) & ~rps_cpu_mask)
  goto try_rps;
next_cpu = ident & rps_cpu_mask;

get_rps_cpu()使用流表的mask欄位來獲取表項的索引,然後檢查匹配到的表項是否存在高位元位,如果存在,則使用表項中的CPU,並將其分配給該報文。否則使用RPS對映。

RFS使用pre-queue的rps_dev_flow_table 來跟蹤未處理的報文,解決在CPU切換之後,應用可能接收到亂序報文的問題。rps_dev_flow_table 的結構如下:

struct rps_dev_flow {
 u16 cpu;
 u16 filter; /* For aRFS */
 unsigned int last_qtail;
};

struct rps_dev_flow_table {
 unsigned int mask;
 struct rcu_head rcu;
 struct rps_dev_flow flows[0];
};

與sock流表一樣,rps_dev_flow_table 也使用table_size-1作為掩碼,而表大小也必須四捨五入為2的冪。當一個報文入佇列後,last_qtail 更新為CPU佇列的末尾。如果應用遷移到一個新的CPU,則sock流表會反應這種變化,且get_rps_cpu()會為流選擇新的CPU。在設定新CPU之前,get_rps_cpu()會檢查當前佇列的首部是否經過了last_qtail,如果是,則表示佇列中沒有未處理的報文,可以安全地切換CPU,否則get_rps_cpu()會使用rps_dev_flow->cpu中記錄的老CPU。

上圖展示了RFS是如何工作的。核心獲取一個藍色的報文,屬於藍色的流。 per-queue流表 (rps_dev_flow_table)檢測到藍色的報文屬於CPU2(老的CPU),而此時socket將sock流表更新為使用CPU1,即新的CPU。get_rps_cpu()會對兩個表進行檢查,發現CPU發生了遷移,因此會更新 per-queue流表(假設此時在CPU2上沒有未處理的報文) ,並將CPU1分配給藍色的報文。

加速(Accelerated )RFS

加速RFS 對RFS來說,就像RSS對RPS:它是一個硬體加速負載均衡機制,基於消耗流報文的應用所執行的位置來使用軟狀態進行導流。加速RFS的效能要比RFS好,因為報文會直接傳送到消耗該報文的執行緒所在的CPU上。目標CPU可能是應用執行的CPU,或在快取結構中接近應用執行緒所在的CPU的CPU。

為了啟用加速RFS,網路棧會帶呼叫ndo_rx_flow_steer 驅動函式來與期望(匹配特定流)的硬體佇列進行互動。網路棧會在rps_dev_flow_table 中的流表項更新之後呼叫該函式。驅動會使用一個裝置特定的方法來程式設計NIC,使其引導報文。

流的硬體佇列是從rps_dev_flow_table中記錄的CPU派生的。網路棧會查詢CPU到硬體佇列的對映,該對映由NIC驅動程式維護,它是自動生成(/proc/interrupts中展示)的IRQ親和表的反向對映。驅動可以使用核心庫cpu_rmap (“CPU affinity reverse map”)來生成對映。對於每個CPU,對映中相應的佇列設定為最接近快取位置的CPU的佇列。

加速RFS 配置

只有在核心編譯時啟用了CONFIG_RFS_ACCEL且NIC裝置和驅動同時支援的情況下才能使用加速RFS功能。同時它還要求通過ethtool啟用ntuple過濾功能。驅動程式會為每個接收佇列配置的IRQ親和性自動推導CPU到佇列的對映,不需要額外的配置。

建議配置

如果希望使用RFS且NIC支援硬體加速,就可以啟用該技術。

為了啟用aRFS,需要一張帶有可程式設計ntupter過濾器的網路卡,和驅動程式的支援。可以使用如下方式啟用ntuple過濾器:

# ethtool -K eth0 ntuple on

對於支援aRFS的驅動,需要實現ndo_rx_flow_steer 來幫助set_rps_cpu()配置硬體過濾器。當get_rps_cpu()決定為一條流分配CPU時,它會呼叫set_rps_cpu()set_rps_cpu()首先會檢查網路卡是否支援ntuple過濾器,如果支援,它會請求rx_cpu_rmap來為流找一個合適的RX佇列。rx_cpu_rmap是一個由驅動維護的特殊的對映,該對映用來為CPU查詢一個合適的RX佇列,找到的佇列可是與給定CPU直接關聯的佇列,也可能是臨近的快取位置上的佇列。在獲取到RX佇列索後,set_rps_cpu()會呼叫ndo_rx_flow_steer()來通知驅動為給定的流建立一個新的過濾器。ndo_rx_flow_steer()會返回過濾器id,過濾器id會被儲存到per-queue流表中。

除了實現ndo_rx_flow_steer(),驅動還需要週期性地呼叫rps_may_expire_flow()檢查過濾器是否有效,並移除過期的過濾器。

XPS: Transmit Packet Steering

Transmit Packet Steering是一種在多佇列裝置上傳輸報文時,為報文智慧選擇傳輸佇列的機制。可以通過記錄兩種對映來實現,即將CPU對映到硬體佇列或將接收佇列對映到傳輸佇列。

  1. XPS使用CPU對映

    該對映的目的是佇列專門分配給 CPU 的一個子集,在此佇列上的CPU會完成這些佇列的報文傳輸。這種方式提供了兩種好處:首先,由於競爭同一個佇列的cpu更少,因此大大降低了裝置佇列的鎖競爭(當每個CPU完成自己的傳輸佇列時會釋放鎖);其次,降低了傳輸競爭導致的快取miss率,特別是對那些儲存了sk_buff結構的資料快取行。

  2. XPS使用接收佇列對映

    該對映用於基於管理員設定的接收佇列對映選擇傳輸佇列。一組接收佇列可以對映到一組傳輸佇列(多對多,但通常會使用1:1對映)。這將允許在相同的佇列上下文(如CPU和快取等)中對報文進行傳輸和接收。這種方式可以用於繁忙的輪詢多執行緒工作負載,在這些工作負載中,很難將特定的CPU與特定的應用程式執行緒關聯起來。應用執行緒不會固定執行在某些CPU上,且每個執行緒會基於一個單獨的佇列接收報文。socket會快取接收佇列的數目。在這種模型下,將傳輸佇列與相關的接收佇列關聯起來,可以有效降低CPU開銷。此時傳輸工作會鎖定到給定應用執行輪詢的上下文中,避免觸發其他CPU造成的開銷。當應用程式在繁忙的輪詢期間清理報文時,可能會在與應用相同的執行緒上下文中完成傳輸,從而減少延遲。

可以通過設定一個CPUs/接收佇列點陣圖來為每個傳輸佇列配置XPS。每個網路裝置會計算並維護從CPUs到傳輸佇列或從接收佇列到傳輸佇列的反向對映。當在一條流中傳輸首個報文時,會呼叫get_xps_queue()選擇一個佇列。該函式會為每個socket連線使用的接收佇列的ID來匹配"接收佇列到傳輸佇列"的查詢表。另外,該函式也可以使用執行的CPU ID作為key來匹配"CPU到佇列"的查詢表。如果這個ID匹配到一個佇列,則使用該佇列傳輸報文。如果匹配到多個佇列,則通過流雜湊計算出的索引來選擇一個佇列。當基於接收佇列對映選擇傳輸佇列時,傳輸裝置不會針對接收裝置進行驗證,因為這需要在資料路徑中進行代價高昂的查詢操作。

為特定傳輸流選擇的佇列會儲存在對應的流(如TCP)socket結構體中。該傳輸佇列會用於這條流上的後續報文的傳輸,方式傳送亂序(ooo)報文。這種方式還分攤了流中所有報文呼叫get_xps_queues()的開銷。為了防止亂序報文,只有設定了流報文的skb->ooo_okay欄位,才能變更這條流使用的佇列。這個標誌位標識這條流中沒有未處理的報文,這樣就可以切換傳輸佇列,而不用擔心生成亂序報文的風險。傳輸層會負責正確處理亂序報文。如TCP,當確認一個連線上的所有資料後就會設定該標誌。

XPS配置

只有在核心啟用了CONFIG_XPS 符號時才能使用XPS功能。如果核心編譯了該功能,由驅動決定是否以及如何在裝置初始化時配置XPS。使用sfsfs來檢查和配置CPUs/接收佇列到傳輸佇列的對映。

對於基於CPUs的對映:

xps_cpus的意義與rx-/rps_cpus類似,確定佇列使用的CPU。

/sys/class/net/<dev>/queues/tx-<n>/xps_cpus

對於基於接收佇列的對映:

/sys/class/net/<dev>/queues/tx-<n>/xps_rxqs
建議配置

對於一個只有一條傳輸佇列的網路裝置,由於這種情況下無法選擇傳輸對,此時XPS是不起作用的。在多佇列系統中,建議配置XPS,這樣每個CPU會對映到一個傳輸佇列。如果系統中傳輸佇列的數目等於CPUs的數目,則每個佇列仍然會對映到一個CPU,此時不會產生多佇列競爭CPU的情況。如果佇列的數目少於CPUs的數目,那麼,共享給特定佇列的最佳CPU可能是與處理該佇列的傳輸完成(傳輸中斷)的CPU共享快取記憶體的CPU。

對於基於接收佇列選擇的傳輸佇列,XPS需要明確配置接收佇列到傳輸佇列的對映關係。如果使用者配置的接受佇列對映沒有生效,則會使用基於CPU對映來選擇傳輸佇列。

單傳輸佇列速率限制

這是由硬體實現的速率限制機制,當前支援設定最大速率熟悉,使用如下值設定一個Mbps值:

/sys/class/net/<dev>/queues/tx-<n>/tx_maxrate

0值表示不啟用,預設為0.

更多資訊

RPS 和RFS是核心2.6.35引入的,XPS是2.6.38引入的。

加速RFS是2.6.35引入的。

參考:

相關文章