您好,登錄后才能下訂單哦!
本篇內容介紹了“Linux網絡協議棧收消息過程是什么”的有關知識,在實際案例的操作過程中,不少人都會遇到這樣的困境,接下來就讓小編帶領大家學習一下如何處理這些情況吧!希望大家仔細閱讀,能夠學有所成!
如果沒有開啟 RPS 會調用 __netif_receive_skb,進而調用 __netif_receive_skb_core 這就基本上進入 Protocol Layer 了。
如果開啟了 RPS 則還有一大段路要走,先調用enqueue_to_backlog準備將數據包放入 CPU 的 Backlog 中。入隊之前會檢查隊列長度,如果隊列長度大于 net.core.netdev_max_backlog
設置的值,則會丟棄數據包。同時也會檢查 flow limit,超過的話也會丟棄數據包。丟棄的話會記錄在 /proc/net/softnet_stat
中。入隊的時候還會檢查目標 CPU 的 NAPI 處理 backlog 的邏輯是否運行,沒運行的話會通過 __napi_schedule 設置目標 CPU 處理 backlog 邏輯。之后發送 Inter-process Interrupt 去喚醒目標 CPU 來處理 CPU backlog 內的數據。
CPU 處理 backlog 的方式和 CPU 去調用 driver 的 poll
函數拉取 Ring Buffer 數據方法類似,也是注冊了一個 poll
函數,只是這個 “poll” 函數在這里是 process_backlog 并且是操作系統 network 相關子系統啟動時候注冊的。process_backlog
內就是個循環,跟 driver 的 poll
一樣不斷的從 backlog 中取出數據來處理。調用 __netif_receive_skb,進而調用 __netif_receive_skb_core ,跟關閉 RPS 情況下邏輯一樣。并且也會按照 budget 來判斷要處理多久而退出循環。budget 跟之前控制 netif_rx_action
執行時間的 budget 配置一樣,也是 net.core.netdev_budget
這個系統配置來控制。
上面說了將數據包放入 CPU 的 backlog 的時候需要看隊列內當前積壓的數據包有多少,超過 net.core.netdev_max_backlog
后要丟棄數據。所以可以根據需要來調整這個值:
sysctl -w net.core.netdev_max_backlog=2000
需要注意的是,好多地方介紹在做壓測的時候建議把這個值調高一點,但從我們上面的分析能看出來,這個值基本上只有在 RPS 開啟的情況下才有用,沒開啟 RPS 的話設置這個值并沒意義。
如果一個 Flow 或者說連接數據特別多,發送數據速度也快,可能會出現該 Flow 的數據包把所有 CPU 的 Backlog 都占滿的情況,從而導致一些數據量少但延遲要求很高的數據包不能快速的被處理。所以就有了 Flow Limit 機制在排隊比較嚴重的時候啟用,來限制 Large Flow 并且偏向 small flow,讓 small flow 的數據能盡快被處理,不要被 Large Flow 影響。
該機制是每個 CPU 獨立的,各 CPU 之間相互不影響,在稍后能看到開啟這個機制也是能單獨的對某個 CPU 開啟。其原理是當 RPS 開啟且 Flow Limit 開啟后,默認當 CPU 的 backlog 占用超過一半的時候,Flow Limit 機制開始運作。這個 CPU 會對 Last 256 個 Packet 進行統計,如果某個 Flow 的 Packet 在這 256 個 Packet 中占比超過一半,就開始對這個 Flow 做限制,該 Flow 新來的 Packet 全部丟棄,別的 Flow 則正常放入 Backlog 正常處理。被限制的 Flow 連接繼續保持,只是丟包增加。
每個 CPU 在 Flow Limit 啟用的時候會分配一個 Hash 表,為每個 Flow 計算占比的時候就是在收到 Packet 時候提取 Packet 內一些信息做 Hash,映射到這個 Hash 表中。Hash Function 跟 RPS 機制下為 Packet 找 CPU 用的 Hash Function 一樣。Hash 表中的值是個 Counter,記錄了在當前 Backlog 中這個 Flow 有多少 Packet 在排隊。這里能看到,Hash 表的大小是有限的,其大小能夠進行配置,如果配置的過小,而當前機器承載的 Flow 又很多,就會出現多個不同的 Flow Hash 到同一個 Counter 的情況,所以可能出現 False Positive 的情況。不過一般還好,因為一般機器同時處理的 Flow 不會特別多,多個 CPU 下能同時處理的 Flow 就更多了。
開啟 Flow Limit 首先要設置 Flow Limit 使用的 Hash 表大小:
sysctl -w net.core.flow_limit_table_len=8192
默認值是 4096。
之后需要為單個 CPU 開啟 Flow Limit,這兩個配置先后順序不能搞錯:
echo f > /proc/sys/net/core/flow_limit_cpu_bitmap
這個跟開啟 RPS 的配置類似,也是個 bitmap 來標識哪些 CPU 開啟 Flow Limit。如果希望所有 CPU 都開啟就設置個大一點的值,不管有多少 CPU 都能覆蓋。
如果因為 backlog 不夠或者 flow limit 不夠數據包被丟棄的話會將丟包信息計入 /proc/net/softnet_stat
。我們也能在這里看到有沒有丟包發生:
cat /proc/net/softnet_stat 930c8a79 00000000 0000270b 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 280178c6 00000000 00000001 00000000 00000000 00000000 00000000 00000000 00000000 0cbbd3d4 00000000
一個 CPU 一行數據。但比較麻煩的是每一列具體表示的是什么意思沒有明確文檔,可能不同版本的 kernel 打印的數據不同。需要看 softnet_seq_show 這個函數是怎么打印的。一般來說第二列是丟包數。
seq_printf(seq, "%08x %08x %08x %08x %08x %08x %08x %08x %08x %08x %08x\n", sd->processed, sd->dropped, sd->time_squeeze, 0, 0, 0, 0, 0, /* was fastroute */ sd->cpu_collision, sd->received_rps, flow_limit_count);
time_squeeze 是 net_rx_action 執行的時候因為 budget 不夠而停止的次數。這說明數據包多而 budget 小,增大 budget 有助于更快的處理數據包
cpu_collision 是發消息時候 CPU 去搶 driver 的鎖沒搶到的次數
received_rps 是 CPU 被通過 Inter-processor Interrupt 喚醒來處理 Backlog 數據的次數。上面例子中看到只有 CPU1 被喚醒過,因為這個 NIC 只有一個 Ring Buffer,IRQ 都是 CPU0 在處理,所以開啟 RPS 后都是 CPU0 將數據發到 CPU1 的 Backlog 然后喚醒 CPU1;
flow_limit_count 表示觸碰 flow limit 的次數
前面介紹到不管開啟還是關閉 RPS 都會通過 __netif_receive_skb_core 將數據包傳入上層。傳入前先會將數據包交到 pcap,tcpdump 就是基于 libcap 實現的,libcap 之所以能捕捉到所有的數據包就是在 __netif_receive_skb_core
實現的。具體位置在:http://elixir.free-electrons.com/linux/v4.4/source/net/core/dev.c#L3850
可以看到這個時候還是在 softirq 的 handler 中呢,所以 tcpdump 這種工具一定是會在一定程度上延長 softirq 的處理時間。
之后就是在 __netif_receive_skb_core
里會遍歷 ptype_base 鏈表,找出 Protocol Layer 中能處理當前數據包的 packet_type 來接著處理數據。所有能處理鏈路層數據包的協議都會注冊到 ptype_base 中。拿 ipv4 來說,在初始化的時候會執行 inet_init,看到在這里會構造 ip_packet_type 并執行 dev_add_pack
。ip_packet_type 指的就是 ipv4。進入dev_add_pack 能看到是將 ip_packet_type 加入到 ptype_head 指向的鏈表中,這里 ptype_head 取到的就是 ptype_base。
回到 ip_packet_type 我們看到其定義為:
static struct packet_type ip_packet_type __read_mostly = { .type = cpu_to_be16(ETH_P_IP), .func = ip_rcv, };
在 __netif_receive_skb_core
中找到 sk_buff 對應的 protocol 是 ETH_P_IP 時就會執行 ip_packet_type 下的 func 函數,即 ip_rcv,從而將數據包交給 Protocol Layer 開始處理了。
ip_rcv
能看出來邏輯比較簡單,基本就是在做各種檢查以及為 transport 層做一些數據準備。最后如果各種檢查都能過,就執行 NF_HOOK。如果有檢查不過需要丟棄數據包就會返回 NET_RX_DROP 并在之后會對丟數據包這個事情進行計數。
NF_HOOK 比較神,它實際是 HOOK 到一個叫做 Netfilter 的東西,在這里你可以根據各種規則對數據包做過濾以及對數據包做一些修改。如果 HOOK 執行后返回 1 表示 Netfilter 允許繼續處理該數據包,就會進入 ip_rcv_finish,HOOK 沒有返回 1 則會返回 Netfilter 的結果,數據包不會繼續被處理。
ip_rcv_finish
負責為 sk_buff 從 IP Route System 中找到路由目標,如果是路由到本機則在下一個處理這個 sk_buff 的協議內(比如上層的 TCP/UDP 協議)還需要從 sk_buff 中找到對應的 socket。也就是說每個收到的數據包都會有兩次 demux (解多路復用)工作(一次找到這個數據包該路由到哪里,一次是如果路由到本機需要將數據包路由到對應的 Socket)。但是對于類似 TCP 這種協議當 socket 處在 ESTABLISHED 狀態后,協議棧不會出現變化,后來的數據包的路由路徑跟握手時數據包的路由路徑完全相同,所以就有了 Early Demux 機制,用于在收到數據包的時候根據 IP Header 中的 protocol 字段找到上一層網絡協議,用上一層網絡協議來解析數據包的路由路徑,以減少一次查詢。拿 TCP 來說,簡單來講就是收到數據包后去 TCP 層查找這個數據包有沒有對應的處在 ESTABLISHED 狀態的 Socket,有的話直接使用這個 Socket 已經 Cache 住的路由目標作為當前 Packet 的路由目標。從而不用再查找 IP Route System,因為根據 Packet 查找 Socket 是怎么都省不掉的。
具體細節是這樣,TCP 會將自己的處理函數在 IP 層初始化的時候注冊在 IP 層的 inet_protos 中。TCP 注冊的這些處理函數中就有 early_demux 函數 tcp_v4_early_demux。在 tcp_v4_early_demux
中我們看到主要是根據 sk_buff 的 source addr、dest addr 等信息從 ESTABLISHED 連接列表中找到當前數據包所屬的 Socket,并獲取 Socket 中的 sk_rx_dst 即 struct dst_entry,這個就是當前 Socket 緩存住的路由路徑,設置到 sk_buff 中。之后這個 sk_buff 就會被路由到 sk_rx_dst 所指的位置。除了路由信息之外,還會將找到的 Socket 的 struct sock 指針存入 sk_buff,這樣數據包被路由到 TCP 層的時候就不需要重復的查找連接列表了。
如果找不到 ESTABLISHED 狀態的 Socket,就會走跟 IP Early Demux 未開啟時一樣的路徑。后面會看到 TCP 新建立的 Socket 會從 sk_buff 中讀取 dst_entry 設置到 struct sock 的 sk_rx_dst 中。struct sock 中的 sk_rx_dst 在這里:linux/include/net/sock.h - Elixir - Free Electrons。
如果 IP Early Demux 沒有起作用,比如當前 sk_buff 可能是 Flow 的第一個數據包,Socket 還未處在 ESTABLISHED 狀態,所以還未找到這個 Socket 也就無法進行 Early Demux。則需要調用 ip_route_input_noref經過 IP Route System 去處理 sk_buff 查找這個 sk_buff 該由誰處理,是不是當前機器處理,還是要轉發出去。這個路由機制看上去還挺復雜的,怪不得需要 Early Demux 機制來省略該步驟呢。如果 IP Route System 找了一圈之后發現這個 sk_buff 確實是需要當前機器處理,最終會設置 dst_entry 指向的函數為 ip_local_deliver。
需要補充一下 Early Demux 對 Socket 還未處在 ESTABLISHED 狀態的 TCP 連接無效。這就導致這種數據包不但會查一次 IP Route System 還會到 TCP ESTABLISHED 連接表中查一次,之后路由到 TCP 層又要再查一次 Socket 表。總體開銷就會比只查一次 IP Route System 還要大。所以 Early Demux 并不是無代價的,只是大多數場景可能開啟后會對性能有提高,所以 Linux 默認是開啟的。但在某些場景下,目前來看應該是大量短連接的場景,連接要不斷建立斷開,有大量的數據包都是在 TCP ESTABLISHED 表中查不到東西,這個機制開啟后性能會有損耗,所以 Linux 提供了關閉該機制的辦法:
sysctl -w net.ipv4.ip_early_demux=0
有人測試在特定場景下這個機制會帶來最大 5% 的損耗:https://patchwork.ozlabs.org/patch/166441/
Early Demux 和查詢 IP Route System 都是為了設置 sk_buff 中的 dst_entry,通過 dst_entry 來跳到下一個負責處理該 sk_buff 的函數。這個跳轉由 ip_rcv_finish
最后的 dst_input 來完成。dst_input
實現很簡單:
return skb_dst(skb)->input(skb);
就是從 sk_buff 中讀出來之前構造好的 struct dst_entry,執行里面的 input 指向的函數并將 sk_buff 交進去。
如果 sk_buff 就是發給當前機器的話,Early Demux 和查詢 IP Route System 都會最終走到 ip_local_deliver
。
做三個事情:
判斷是否有 IP Fragment,有的話就先存下這個 sk_buff 直接返回,等后續數據包來了之后進行組裝;
通過和 ip_rcv
里一樣的 NET_HOOK 將數據包發到 Netfilter 做過濾
如果數據包被過濾掉了,就直接丟棄數據包返回,沒過濾掉最終會執行 ip_local_deliver_finish
ip_local_deliver_finish
內會取出 IP Header 中的 protocol 字段,根據該字段在上面提到過的 inet_protos中找到 IP 層初始化時注冊過的上層協議處理函數。拿 TCP 來說,TCP 注冊的信息在這里: linux/net/ipv4/af_inet.c - Elixir - Free Electrons。ip_local_deliver_finish
會調用注冊的 handler 函數,對 TCP 來說就是 tcp_v4_rcv
。
IP 層在處理數據過程中會更新很多計數,在 snmp.h 這個文件中可以看看。基本上 proc/net/netstat
中展示的帶有 IP 字樣的統計都是這個文件中定義的。
“Linux網絡協議棧收消息過程是什么”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識可以關注億速云網站,小編將為大家輸出更多高質量的實用文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。