溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Linux網(wǎng)絡(luò)協(xié)議棧收消息過(guò)程是什么

發(fā)布時(shí)間:2021-11-16 14:00:33 來(lái)源:億速云 閱讀:181 作者:iii 欄目:大數(shù)據(jù)

本篇內(nèi)容介紹了“Linux網(wǎng)絡(luò)協(xié)議棧收消息過(guò)程是什么”的有關(guān)知識(shí),在實(shí)際案例的操作過(guò)程中,不少人都會(huì)遇到這樣的困境,接下來(lái)就讓小編帶領(lǐng)大家學(xué)習(xí)一下如何處理這些情況吧!希望大家仔細(xì)閱讀,能夠?qū)W有所成!

如果沒(méi)有開(kāi)啟 RPS 會(huì)調(diào)用 __netif_receive_skb,進(jìn)而調(diào)用 __netif_receive_skb_core 這就基本上進(jìn)入 Protocol Layer 了。

如果開(kāi)啟了 RPS 則還有一大段路要走,先調(diào)用enqueue_to_backlog準(zhǔn)備將數(shù)據(jù)包放入 CPU 的 Backlog 中。入隊(duì)之前會(huì)檢查隊(duì)列長(zhǎng)度,如果隊(duì)列長(zhǎng)度大于 net.core.netdev_max_backlog 設(shè)置的值,則會(huì)丟棄數(shù)據(jù)包。同時(shí)也會(huì)檢查 flow limit,超過(guò)的話(huà)也會(huì)丟棄數(shù)據(jù)包。丟棄的話(huà)會(huì)記錄在 /proc/net/softnet_stat 中。入隊(duì)的時(shí)候還會(huì)檢查目標(biāo) CPU 的 NAPI 處理 backlog 的邏輯是否運(yùn)行,沒(méi)運(yùn)行的話(huà)會(huì)通過(guò) __napi_schedule 設(shè)置目標(biāo) CPU 處理 backlog 邏輯。之后發(fā)送 Inter-process Interrupt 去喚醒目標(biāo) CPU 來(lái)處理 CPU backlog 內(nèi)的數(shù)據(jù)。

CPU 處理 backlog 的方式和 CPU 去調(diào)用 driver 的 poll 函數(shù)拉取 Ring Buffer 數(shù)據(jù)方法類(lèi)似,也是注冊(cè)了一個(gè) poll 函數(shù),只是這個(gè) “poll” 函數(shù)在這里是 process_backlog 并且是操作系統(tǒng) network 相關(guān)子系統(tǒng)啟動(dòng)時(shí)候注冊(cè)的。process_backlog 內(nèi)就是個(gè)循環(huán),跟 driver 的 poll 一樣不斷的從 backlog 中取出數(shù)據(jù)來(lái)處理。調(diào)用 __netif_receive_skb,進(jìn)而調(diào)用 __netif_receive_skb_core ,跟關(guān)閉 RPS 情況下邏輯一樣。并且也會(huì)按照 budget 來(lái)判斷要處理多久而退出循環(huán)。budget 跟之前控制 netif_rx_action執(zhí)行時(shí)間的 budget 配置一樣,也是 net.core.netdev_budget 這個(gè)系統(tǒng)配置來(lái)控制。

net.core.netdev_max_backlog

上面說(shuō)了將數(shù)據(jù)包放入 CPU 的 backlog 的時(shí)候需要看隊(duì)列內(nèi)當(dāng)前積壓的數(shù)據(jù)包有多少,超過(guò) net.core.netdev_max_backlog 后要丟棄數(shù)據(jù)。所以可以根據(jù)需要來(lái)調(diào)整這個(gè)值:

sysctl -w net.core.netdev_max_backlog=2000

需要注意的是,好多地方介紹在做壓測(cè)的時(shí)候建議把這個(gè)值調(diào)高一點(diǎn),但從我們上面的分析能看出來(lái),這個(gè)值基本上只有在 RPS 開(kāi)啟的情況下才有用,沒(méi)開(kāi)啟 RPS 的話(huà)設(shè)置這個(gè)值并沒(méi)意義。

Flow Limit

如果一個(gè) Flow 或者說(shuō)連接數(shù)據(jù)特別多,發(fā)送數(shù)據(jù)速度也快,可能會(huì)出現(xiàn)該 Flow 的數(shù)據(jù)包把所有 CPU 的 Backlog 都占滿(mǎn)的情況,從而導(dǎo)致一些數(shù)據(jù)量少但延遲要求很高的數(shù)據(jù)包不能快速的被處理。所以就有了 Flow Limit 機(jī)制在排隊(duì)比較嚴(yán)重的時(shí)候啟用,來(lái)限制 Large Flow 并且偏向 small flow,讓 small flow 的數(shù)據(jù)能盡快被處理,不要被 Large Flow 影響。

該機(jī)制是每個(gè) CPU 獨(dú)立的,各 CPU 之間相互不影響,在稍后能看到開(kāi)啟這個(gè)機(jī)制也是能單獨(dú)的對(duì)某個(gè) CPU 開(kāi)啟。其原理是當(dāng) RPS 開(kāi)啟且 Flow Limit 開(kāi)啟后,默認(rèn)當(dāng) CPU 的 backlog 占用超過(guò)一半的時(shí)候,F(xiàn)low Limit 機(jī)制開(kāi)始運(yùn)作。這個(gè) CPU 會(huì)對(duì) Last 256 個(gè) Packet 進(jìn)行統(tǒng)計(jì),如果某個(gè) Flow 的 Packet 在這 256 個(gè) Packet 中占比超過(guò)一半,就開(kāi)始對(duì)這個(gè) Flow 做限制,該 Flow 新來(lái)的 Packet 全部丟棄,別的 Flow 則正常放入 Backlog 正常處理。被限制的 Flow 連接繼續(xù)保持,只是丟包增加。

每個(gè) CPU 在 Flow Limit 啟用的時(shí)候會(huì)分配一個(gè) Hash 表,為每個(gè) Flow 計(jì)算占比的時(shí)候就是在收到 Packet 時(shí)候提取 Packet 內(nèi)一些信息做 Hash,映射到這個(gè) Hash 表中。Hash Function 跟 RPS 機(jī)制下為 Packet 找 CPU 用的 Hash Function 一樣。Hash 表中的值是個(gè) Counter,記錄了在當(dāng)前 Backlog 中這個(gè) Flow 有多少 Packet 在排隊(duì)。這里能看到,Hash 表的大小是有限的,其大小能夠進(jìn)行配置,如果配置的過(guò)小,而當(dāng)前機(jī)器承載的 Flow 又很多,就會(huì)出現(xiàn)多個(gè)不同的 Flow Hash 到同一個(gè) Counter 的情況,所以可能出現(xiàn) False Positive 的情況。不過(guò)一般還好,因?yàn)橐话銠C(jī)器同時(shí)處理的 Flow 不會(huì)特別多,多個(gè) CPU 下能同時(shí)處理的 Flow 就更多了。

開(kāi)啟 Flow Limit 首先要設(shè)置 Flow Limit 使用的 Hash 表大小:

sysctl -w net.core.flow_limit_table_len=8192

默認(rèn)值是 4096。

之后需要為單個(gè) CPU 開(kāi)啟 Flow Limit,這兩個(gè)配置先后順序不能搞錯(cuò):

echo f > /proc/sys/net/core/flow_limit_cpu_bitmap

這個(gè)跟開(kāi)啟 RPS 的配置類(lèi)似,也是個(gè) bitmap 來(lái)標(biāo)識(shí)哪些 CPU 開(kāi)啟 Flow Limit。如果希望所有 CPU 都開(kāi)啟就設(shè)置個(gè)大一點(diǎn)的值,不管有多少 CPU 都能覆蓋。

丟棄數(shù)據(jù)包統(tǒng)計(jì)

如果因?yàn)?backlog 不夠或者 flow limit 不夠數(shù)據(jù)包被丟棄的話(huà)會(huì)將丟包信息計(jì)入 /proc/net/softnet_stat。我們也能在這里看到有沒(méi)有丟包發(fā)生:

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

一個(gè) CPU 一行數(shù)據(jù)。但比較麻煩的是每一列具體表示的是什么意思沒(méi)有明確文檔,可能不同版本的 kernel 打印的數(shù)據(jù)不同。需要看 softnet_seq_show 這個(gè)函數(shù)是怎么打印的。一般來(lái)說(shuō)第二列是丟包數(shù)。

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 執(zhí)行的時(shí)候因?yàn)?budget 不夠而停止的次數(shù)。這說(shuō)明數(shù)據(jù)包多而 budget 小,增大 budget 有助于更快的處理數(shù)據(jù)包
cpu_collision 是發(fā)消息時(shí)候 CPU 去搶 driver 的鎖沒(méi)搶到的次數(shù)
received_rps 是 CPU 被通過(guò) Inter-processor Interrupt 喚醒來(lái)處理 Backlog 數(shù)據(jù)的次數(shù)。上面例子中看到只有 CPU1 被喚醒過(guò),因?yàn)檫@個(gè) NIC 只有一個(gè) Ring Buffer,IRQ 都是 CPU0 在處理,所以開(kāi)啟 RPS 后都是 CPU0 將數(shù)據(jù)發(fā)到 CPU1 的 Backlog 然后喚醒 CPU1;
flow_limit_count 表示觸碰 flow limit 的次數(shù)

Internet Protocol Layer

前面介紹到不管開(kāi)啟還是關(guān)閉 RPS 都會(huì)通過(guò) __netif_receive_skb_core 將數(shù)據(jù)包傳入上層。傳入前先會(huì)將數(shù)據(jù)包交到 pcap,tcpdump 就是基于 libcap 實(shí)現(xiàn)的,libcap 之所以能捕捉到所有的數(shù)據(jù)包就是在 __netif_receive_skb_core 實(shí)現(xiàn)的。具體位置在:http://elixir.free-electrons.com/linux/v4.4/source/net/core/dev.c#L3850

可以看到這個(gè)時(shí)候還是在 softirq 的 handler 中呢,所以 tcpdump 這種工具一定是會(huì)在一定程度上延長(zhǎng) softirq 的處理時(shí)間。

之后就是在 __netif_receive_skb_core 里會(huì)遍歷 ptype_base 鏈表,找出 Protocol Layer 中能處理當(dāng)前數(shù)據(jù)包的 packet_type 來(lái)接著處理數(shù)據(jù)。所有能處理鏈路層數(shù)據(jù)包的協(xié)議都會(huì)注冊(cè)到 ptype_base 中。拿 ipv4 來(lái)說(shuō),在初始化的時(shí)候會(huì)執(zhí)行 inet_init,看到在這里會(huì)構(gòu)造 ip_packet_type 并執(zhí)行 dev_add_pack。ip_packet_type 指的就是 ipv4。進(jìn)入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 對(duì)應(yīng)的 protocol 是 ETH_P_IP 時(shí)就會(huì)執(zhí)行 ip_packet_type 下的 func 函數(shù),即 ip_rcv,從而將數(shù)據(jù)包交給 Protocol Layer 開(kāi)始處理了。

ip_rcv

ip_rcv 能看出來(lái)邏輯比較簡(jiǎn)單,基本就是在做各種檢查以及為 transport 層做一些數(shù)據(jù)準(zhǔn)備。最后如果各種檢查都能過(guò),就執(zhí)行 NF_HOOK。如果有檢查不過(guò)需要丟棄數(shù)據(jù)包就會(huì)返回 NET_RX_DROP 并在之后會(huì)對(duì)丟數(shù)據(jù)包這個(gè)事情進(jìn)行計(jì)數(shù)。

NF_HOOK 比較神,它實(shí)際是 HOOK 到一個(gè)叫做 Netfilter 的東西,在這里你可以根據(jù)各種規(guī)則對(duì)數(shù)據(jù)包做過(guò)濾以及對(duì)數(shù)據(jù)包做一些修改。如果 HOOK 執(zhí)行后返回 1 表示 Netfilter 允許繼續(xù)處理該數(shù)據(jù)包,就會(huì)進(jìn)入 ip_rcv_finish,HOOK 沒(méi)有返回 1 則會(huì)返回 Netfilter 的結(jié)果,數(shù)據(jù)包不會(huì)繼續(xù)被處理。

ip_rcv_finish 負(fù)責(zé)為 sk_buff 從 IP Route System 中找到路由目標(biāo),如果是路由到本機(jī)則在下一個(gè)處理這個(gè) sk_buff 的協(xié)議內(nèi)(比如上層的 TCP/UDP 協(xié)議)還需要從 sk_buff 中找到對(duì)應(yīng)的 socket。也就是說(shuō)每個(gè)收到的數(shù)據(jù)包都會(huì)有兩次 demux (解多路復(fù)用)工作(一次找到這個(gè)數(shù)據(jù)包該路由到哪里,一次是如果路由到本機(jī)需要將數(shù)據(jù)包路由到對(duì)應(yīng)的 Socket)。但是對(duì)于類(lèi)似 TCP 這種協(xié)議當(dāng) socket 處在 ESTABLISHED 狀態(tài)后,協(xié)議棧不會(huì)出現(xiàn)變化,后來(lái)的數(shù)據(jù)包的路由路徑跟握手時(shí)數(shù)據(jù)包的路由路徑完全相同,所以就有了 Early Demux 機(jī)制,用于在收到數(shù)據(jù)包的時(shí)候根據(jù) IP Header 中的 protocol 字段找到上一層網(wǎng)絡(luò)協(xié)議,用上一層網(wǎng)絡(luò)協(xié)議來(lái)解析數(shù)據(jù)包的路由路徑,以減少一次查詢(xún)。拿 TCP 來(lái)說(shuō),簡(jiǎn)單來(lái)講就是收到數(shù)據(jù)包后去 TCP 層查找這個(gè)數(shù)據(jù)包有沒(méi)有對(duì)應(yīng)的處在 ESTABLISHED 狀態(tài)的 Socket,有的話(huà)直接使用這個(gè) Socket 已經(jīng) Cache 住的路由目標(biāo)作為當(dāng)前 Packet 的路由目標(biāo)。從而不用再查找 IP Route System,因?yàn)楦鶕?jù) Packet 查找 Socket 是怎么都省不掉的。

具體細(xì)節(jié)是這樣,TCP 會(huì)將自己的處理函數(shù)在 IP 層初始化的時(shí)候注冊(cè)在 IP 層的 inet_protos 中。TCP 注冊(cè)的這些處理函數(shù)中就有 early_demux 函數(shù) tcp_v4_early_demux。在 tcp_v4_early_demux 中我們看到主要是根據(jù) sk_buff 的 source addr、dest addr 等信息從 ESTABLISHED 連接列表中找到當(dāng)前數(shù)據(jù)包所屬的 Socket,并獲取 Socket 中的 sk_rx_dst 即 struct dst_entry,這個(gè)就是當(dāng)前 Socket 緩存住的路由路徑,設(shè)置到 sk_buff 中。之后這個(gè) sk_buff 就會(huì)被路由到 sk_rx_dst 所指的位置。除了路由信息之外,還會(huì)將找到的 Socket 的 struct sock 指針存入 sk_buff,這樣數(shù)據(jù)包被路由到 TCP 層的時(shí)候就不需要重復(fù)的查找連接列表了。

如果找不到 ESTABLISHED 狀態(tài)的 Socket,就會(huì)走跟 IP Early Demux 未開(kāi)啟時(shí)一樣的路徑。后面會(huì)看到 TCP 新建立的 Socket 會(huì)從 sk_buff 中讀取 dst_entry 設(shè)置到 struct sock 的 sk_rx_dst 中。struct sock 中的 sk_rx_dst 在這里:linux/include/net/sock.h - Elixir - Free Electrons。

如果 IP Early Demux 沒(méi)有起作用,比如當(dāng)前 sk_buff 可能是 Flow 的第一個(gè)數(shù)據(jù)包,Socket 還未處在 ESTABLISHED 狀態(tài),所以還未找到這個(gè) Socket 也就無(wú)法進(jìn)行 Early Demux。則需要調(diào)用 ip_route_input_noref經(jīng)過(guò) IP Route System 去處理 sk_buff 查找這個(gè) sk_buff 該由誰(shuí)處理,是不是當(dāng)前機(jī)器處理,還是要轉(zhuǎn)發(fā)出去。這個(gè)路由機(jī)制看上去還挺復(fù)雜的,怪不得需要 Early Demux 機(jī)制來(lái)省略該步驟呢。如果 IP Route System 找了一圈之后發(fā)現(xiàn)這個(gè) sk_buff 確實(shí)是需要當(dāng)前機(jī)器處理,最終會(huì)設(shè)置 dst_entry 指向的函數(shù)為 ip_local_deliver。

需要補(bǔ)充一下 Early Demux 對(duì) Socket 還未處在 ESTABLISHED 狀態(tài)的 TCP 連接無(wú)效。這就導(dǎo)致這種數(shù)據(jù)包不但會(huì)查一次 IP Route System 還會(huì)到 TCP ESTABLISHED 連接表中查一次,之后路由到 TCP 層又要再查一次 Socket 表??傮w開(kāi)銷(xiāo)就會(huì)比只查一次 IP Route System 還要大。所以 Early Demux 并不是無(wú)代價(jià)的,只是大多數(shù)場(chǎng)景可能開(kāi)啟后會(huì)對(duì)性能有提高,所以 Linux 默認(rèn)是開(kāi)啟的。但在某些場(chǎng)景下,目前來(lái)看應(yīng)該是大量短連接的場(chǎng)景,連接要不斷建立斷開(kāi),有大量的數(shù)據(jù)包都是在 TCP ESTABLISHED 表中查不到東西,這個(gè)機(jī)制開(kāi)啟后性能會(huì)有損耗,所以 Linux 提供了關(guān)閉該機(jī)制的辦法:

sysctl -w net.ipv4.ip_early_demux=0

有人測(cè)試在特定場(chǎng)景下這個(gè)機(jī)制會(huì)帶來(lái)最大 5% 的損耗:https://patchwork.ozlabs.org/patch/166441/

Early Demux 和查詢(xún) IP Route System 都是為了設(shè)置 sk_buff 中的 dst_entry,通過(guò) dst_entry 來(lái)跳到下一個(gè)負(fù)責(zé)處理該 sk_buff 的函數(shù)。這個(gè)跳轉(zhuǎn)由 ip_rcv_finish 最后的 dst_input 來(lái)完成。dst_input 實(shí)現(xiàn)很簡(jiǎn)單:

return skb_dst(skb)->input(skb);

就是從 sk_buff 中讀出來(lái)之前構(gòu)造好的 struct dst_entry,執(zhí)行里面的 input 指向的函數(shù)并將 sk_buff 交進(jìn)去。

如果 sk_buff 就是發(fā)給當(dāng)前機(jī)器的話(huà),Early Demux 和查詢(xún) IP Route System 都會(huì)最終走到 ip_local_deliver。

ip_local_deliver

做三個(gè)事情:

  1. 判斷是否有 IP Fragment,有的話(huà)就先存下這個(gè) sk_buff 直接返回,等后續(xù)數(shù)據(jù)包來(lái)了之后進(jìn)行組裝;

  2. 通過(guò)和 ip_rcv 里一樣的 NET_HOOK 將數(shù)據(jù)包發(fā)到 Netfilter 做過(guò)濾

  3. 如果數(shù)據(jù)包被過(guò)濾掉了,就直接丟棄數(shù)據(jù)包返回,沒(méi)過(guò)濾掉最終會(huì)執(zhí)行 ip_local_deliver_finish

ip_local_deliver_finish 內(nèi)會(huì)取出 IP Header 中的 protocol 字段,根據(jù)該字段在上面提到過(guò)的 inet_protos中找到 IP 層初始化時(shí)注冊(cè)過(guò)的上層協(xié)議處理函數(shù)。拿 TCP 來(lái)說(shuō),TCP 注冊(cè)的信息在這里: linux/net/ipv4/af_inet.c - Elixir - Free Electrons。ip_local_deliver_finish 會(huì)調(diào)用注冊(cè)的 handler 函數(shù),對(duì) TCP 來(lái)說(shuō)就是 tcp_v4_rcv。

IP 層在處理數(shù)據(jù)過(guò)程中會(huì)更新很多計(jì)數(shù),在 snmp.h 這個(gè)文件中可以看看?;旧?nbsp;proc/net/netstat中展示的帶有 IP 字樣的統(tǒng)計(jì)都是這個(gè)文件中定義的。

“Linux網(wǎng)絡(luò)協(xié)議棧收消息過(guò)程是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識(shí)可以關(guān)注億速云網(wǎng)站,小編將為大家輸出更多高質(zhì)量的實(shí)用文章!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI