最美情侣中文字幕电影,在线麻豆精品传媒,在线网站高清黄,久久黄色视频

歡迎光臨散文網(wǎng) 會員登陸 & 注冊

25張圖,一萬字,拆解Linux網(wǎng)絡包發(fā)送過程(超級詳細~)【下文】

2022-07-12 14:29 作者:補給站Linux內(nèi)核  | 我要投稿

接上文:25張圖,一萬字,拆解Linux網(wǎng)絡包發(fā)送過程(超級詳細~)【上文】

4.3 網(wǎng)絡層發(fā)送處理

Linux 內(nèi)核網(wǎng)絡層的發(fā)送的實現(xiàn)位于 net/ipv4/ip_output.c 這個文件。傳輸層調(diào)用到的 ip_queue_xmit 也在這里。(從文件名上也能看出來進入到 IP 層了,源文件名已經(jīng)從 tcp_xxx 變成了 ip_xxx。)

在網(wǎng)絡層里主要處理路由項查找、IP 頭設置、netfilter 過濾、skb 切分(大于 MTU 的話)等幾項工作,處理完這些工作后會交給更下層的鄰居子系統(tǒng)來處理。


我們來看網(wǎng)絡層入口函數(shù) ip_queue_xmit 的源碼:

ip_queue_xmit 已經(jīng)到了網(wǎng)絡層,在這個函數(shù)里我們看到了網(wǎng)絡層相關的功能路由項查找,如果找到了則設置到 skb 上(沒有路由的話就直接報錯返回了)。

在 Linux 上通過 route 命令可以看到你本機的路由配置。


在路由表中,可以查到某個目的網(wǎng)絡應該通過哪個 Iface(網(wǎng)卡),哪個 Gateway(網(wǎng)卡)發(fā)送出去。查找出來以后緩存到 socket 上,下次再發(fā)送數(shù)據(jù)就不用查了。

接著把路由表地址也放到 skb 里去。

接下來就是定位到 skb 里的 IP 頭的位置上,然后開始按照協(xié)議規(guī)范設置 IP header。


再通過 ip_local_out 進入到下一步的處理。


在 ip_local_out => __ip_local_out => nf_hook 會執(zhí)行 netfilter 過濾。如果你使用 iptables 配置了一些規(guī)則,那么這里將檢測是否命中規(guī)則。如果你設置了非常復雜的 netfilter 規(guī)則,在這個函數(shù)這里將會導致你的進程 CPU 開銷會極大增加。

還是不多展開說,繼續(xù)只聊和發(fā)送有關的過程 dst_output。

此函數(shù)找到到這個 skb 的路由表(dst 條目) ,然后調(diào)用路由表的 output 方法。這又是一個函數(shù)指針,指向的是 ip_output 方法。

在 ip_output 中進行一些簡單的,統(tǒng)計工作,再次執(zhí)行 netfilter 過濾。過濾通過之后回調(diào) ip_finish_output。

在 ip_finish_output 中我們看到,如果數(shù)據(jù)大于 MTU 的話,是會執(zhí)行分片的。

實際 MTU 大小確定依賴 MTU 發(fā)現(xiàn),以太網(wǎng)幀為 1500 字節(jié)。之前 QQ 團隊在早期的時候,會盡量控制自己數(shù)據(jù)包尺寸小于 MTU,通過這種方式來優(yōu)化網(wǎng)絡性能。因為分片會帶來兩個問題:1、需要進行額外的切分處理,有額外性能開銷。2、只要一個分片丟失,整個包都得重傳。所以避免分片既杜絕了分片開銷,也大大降低了重傳率。

在 ip_finish_output2 中,終于發(fā)送過程會進入到下一層,鄰居子系統(tǒng)中。

4.4 鄰居子系統(tǒng)

鄰居子系統(tǒng)是位于網(wǎng)絡層和數(shù)據(jù)鏈路層中間的一個系統(tǒng),其作用是對網(wǎng)絡層提供一個封裝,讓網(wǎng)絡層不必關心下層的地址信息,讓下層來決定發(fā)送到哪個 MAC 地址。

而且這個鄰居子系統(tǒng)并不位于協(xié)議棧 net/ipv4/ 目錄內(nèi),而是位于 net/core/neighbour.c。因為無論是對于 IPv4 還是 IPv6 ,都需要使用該模塊。


在鄰居子系統(tǒng)里主要是查找或者創(chuàng)建鄰居項,在創(chuàng)造鄰居項的時候,有可能會發(fā)出實際的 arp 請求。然后封裝一下 MAC 頭,將發(fā)送過程再傳遞到更下層的網(wǎng)絡設備子系統(tǒng)。大致流程如圖。

理解了大致流程,我們再回頭看源碼。在上面小節(jié) ip_finish_output2 源碼中調(diào)用了 __ipv4_neigh_lookup_noref。它是在 arp 緩存中進行查找,其第二個參數(shù)傳入的是路由下一跳 IP 信息。

如果查找不到,則調(diào)用 __neigh_create 創(chuàng)建一個鄰居。

有了鄰居項以后,此時仍然還不具備發(fā)送 IP 報文的能力,因為目的 MAC 地址還未獲取。調(diào)用 dst_neigh_output 繼續(xù)傳遞 skb。

調(diào)用 output,實際指向的是 neigh_resolve_output。在這個函數(shù)內(nèi)部有可能會發(fā)出 arp 網(wǎng)絡請求。

當獲取到硬件 MAC 地址以后,就可以封裝 skb 的 MAC 頭了。最后調(diào)用 dev_queue_xmit 將 skb 傳遞給 Linux 網(wǎng)絡設備子系統(tǒng)。


【文章福利】小編推薦自己的Linux內(nèi)核技術交流群:【891587639】整理了一些個人覺得比較好的學習書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ??


4.5 網(wǎng)絡設備子系統(tǒng)


鄰居子系統(tǒng)通過 dev_queue_xmit 進入到網(wǎng)絡設備子系統(tǒng)中來。

開篇第二節(jié)網(wǎng)卡啟動準備里我們說過,網(wǎng)卡是有多個發(fā)送隊列的(尤其是現(xiàn)在的網(wǎng)卡)。上面對 netdev_pick_tx 函數(shù)的調(diào)用就是選擇一個隊列進行發(fā)送。

netdev_pick_tx 發(fā)送隊列的選擇受 XPS 等配置的影響,而且還有緩存,也是一套小復雜的邏輯。這里我們只關注兩個邏輯,首先會獲取用戶的 XPS 配置,否則就自動計算了。代碼見 netdev_pick_tx => __netdev_pick_tx。

然后獲取與此隊列關聯(lián)的 qdisc。在 linux 上通過 tc 命令可以看到 qdisc 類型,例如對于我的某臺多隊列網(wǎng)卡機器上是 mq disc。

大部分的設備都有隊列(回環(huán)設備和隧道設備除外),所以現(xiàn)在我們進入到 __dev_xmit_skb。

上述代碼中分兩種情況,1 是可以 bypass(繞過)排隊系統(tǒng)的,另外一種是正常排隊。我們只看第二種情況。

先調(diào)用 q->enqueue 把 skb 添加到隊列里。然后調(diào)用 __qdisc_run 開始發(fā)送。

在上述代碼中,我們看到 while 循環(huán)不斷地從隊列中取出 skb 并進行發(fā)送。注意,這個時候其實都占用的是用戶進程的系統(tǒng)態(tài)時間(sy)。只有當 quota 用盡或者其它進程需要 CPU 的時候才觸發(fā)軟中斷進行發(fā)送。

所以這就是為什么一般服務器上查看 /proc/softirqs,一般 NET_RX 都要比 NET_TX 大的多的第二個原因。對于讀來說,都是要經(jīng)過 NET_RX 軟中斷,而對于發(fā)送來說,只有系統(tǒng)態(tài)配額用盡才讓軟中斷上。

我們來把精力在放到 qdisc_restart 上,繼續(xù)看發(fā)送過程。

qdisc_restart 從隊列中取出一個 skb,并調(diào)用 sch_direct_xmit 繼續(xù)發(fā)送。

4.6 軟中斷調(diào)度

在 4.5 咱們看到了如果系統(tǒng)態(tài) CPU 發(fā)送網(wǎng)絡包不夠用的時候,會調(diào)用 __netif_schedule 觸發(fā)一個軟中斷。該函數(shù)會進入到 __netif_reschedule,由它來實際發(fā)出 NET_TX_SOFTIRQ 類型軟中斷。

軟中斷是由內(nèi)核線程來運行的,該線程會進入到 net_tx_action 函數(shù),在該函數(shù)中能獲取到發(fā)送隊列,并也最終調(diào)用到驅(qū)動程序里的入口函數(shù) dev_hard_start_xmit。


在該函數(shù)里在軟中斷能訪問到的 softnet_data 里設置了要發(fā)送的數(shù)據(jù)隊列,添加到了 output_queue 里了。緊接著觸發(fā)了 NET_TX_SOFTIRQ 類型的軟中斷。(T 代表 transmit 傳輸)

我們直接從 NET_TX_SOFTIRQ softirq 注冊的回調(diào)函數(shù) net_tx_action講起。用戶態(tài)進程觸發(fā)完軟中斷之后,會有一個軟中斷內(nèi)核線程會執(zhí)行到 net_tx_action。

牢記,這以后發(fā)送數(shù)據(jù)消耗的 CPU 就都顯示在 si 這里了,不會消耗用戶進程的系統(tǒng)時間了。

軟中斷這里會獲取 softnet_data。前面我們看到進程內(nèi)核態(tài)在調(diào)用 __netif_reschedule 的時候把發(fā)送隊列寫到 softnet_data 的 output_queue 里了。軟中斷循環(huán)遍歷 sd->output_queue 發(fā)送數(shù)據(jù)幀。

來看 qdisc_run,它和進程用戶態(tài)一樣,也會調(diào)用到 __qdisc_run。

然后一樣就是進入 qdisc_restart => sch_direct_xmit,直到驅(qū)動程序函數(shù) dev_hard_start_xmit。

4.7 igb 網(wǎng)卡驅(qū)動發(fā)送

我們前面看到,無論是對于用戶進程的內(nèi)核態(tài),還是對于軟中斷上下文,都會調(diào)用到網(wǎng)絡設備子系統(tǒng)中的 dev_hard_start_xmit 函數(shù)。在這個函數(shù)中,會調(diào)用到驅(qū)動里的發(fā)送函數(shù) igb_xmit_frame。

在驅(qū)動函數(shù)里,將 skb 會掛到 RingBuffer上,驅(qū)動調(diào)用完畢后,數(shù)據(jù)包將真正從網(wǎng)卡發(fā)送出去。


我們來看看實際的源碼:

其中 ndo_start_xmit 是網(wǎng)卡驅(qū)動要實現(xiàn)的一個函數(shù),是在 net_device_ops 中定義的。

在 igb 網(wǎng)卡驅(qū)動源碼中,我們找到了。

也就是說,對于網(wǎng)絡設備層定義的 ndo_start_xmit, igb 的實現(xiàn)函數(shù)是 igb_xmit_frame。這個函數(shù)是在網(wǎng)卡驅(qū)動初始化的時候被賦值的。具體初始化過程參見《圖解Linux網(wǎng)絡包接收過程》一文中的 2.4 節(jié),網(wǎng)卡驅(qū)動初始化。

所以在上面網(wǎng)絡設備層調(diào)用 ops->ndo_start_xmit 的時候,會實際上進入 igb_xmit_frame 這個函數(shù)中。我們進入這個函數(shù)來看看驅(qū)動程序是如何工作的。

在這里從網(wǎng)卡的發(fā)送隊列的 RingBuffer 中取下來一個元素,并將 skb 掛到元素上。


igb_tx_map 函數(shù)處理將 skb 數(shù)據(jù)映射到網(wǎng)卡可訪問的內(nèi)存 DMA 區(qū)域。

當所有需要的描述符都已建好,且 skb 的所有數(shù)據(jù)都映射到 DMA 地址后,驅(qū)動就會進入到它的最后一步,觸發(fā)真實的發(fā)送。

4.8 發(fā)送完成硬中斷

當數(shù)據(jù)發(fā)送完成以后,其實工作并沒有結(jié)束。因為內(nèi)存還沒有清理。當發(fā)送完成的時候,網(wǎng)卡設備會觸發(fā)一個硬中斷來釋放內(nèi)存。

在發(fā)送完成硬中斷里,會執(zhí)行 RingBuffer 內(nèi)存的清理工作,如圖。



再回頭看一下硬中斷觸發(fā)軟中斷的源碼。

這里有個很有意思的細節(jié),無論硬中斷是因為是有數(shù)據(jù)要接收,還是說發(fā)送完成通知,從硬中斷觸發(fā)的軟中斷都是 NET_RX_SOFTIRQ。這個我們在第一節(jié)說過了,這是軟中斷統(tǒng)計中 RX 要高于 TX 的一個原因。

好我們接著進入軟中斷的回調(diào)函數(shù) igb_poll。在這個函數(shù)里,我們注意到有一行 igb_clean_tx_irq,參見源碼:

我們來看看當傳輸完成的時候,igb_clean_tx_irq 都干啥了。

無非就是清理了 skb,解除了 DMA 映射等等。到了這一步,傳輸才算是基本完成了。

為啥我說是基本完成,而不是全部完成了呢?因為傳輸層需要保證可靠性,所以 skb 其實還沒有刪除。它得等收到對方的 ACK 之后才會真正刪除,那個時候才算是徹底的發(fā)送完畢。

最后

用一張圖總結(jié)一下整個發(fā)送過程


了解了整個發(fā)送過程以后,我們回頭再來回顧開篇提到的幾個問題。

1.我們在監(jiān)控內(nèi)核發(fā)送數(shù)據(jù)消耗的 CPU 時,是應該看 sy 還是 si ?

在網(wǎng)絡包的發(fā)送過程中,用戶進程(在內(nèi)核態(tài))完成了絕大部分的工作,甚至連調(diào)用驅(qū)動的事情都干了。只有當內(nèi)核態(tài)進程被切走前才會發(fā)起軟中斷。發(fā)送過程中,絕大部分(90%)以上的開銷都是在用戶進程內(nèi)核態(tài)消耗掉的。

只有一少部分情況下才會觸發(fā)軟中斷(NET_TX 類型),由軟中斷 ksoftirqd 內(nèi)核進程來發(fā)送。

所以,在監(jiān)控網(wǎng)絡 IO 對服務器造成的 CPU 開銷的時候,不能僅僅只看 si,而是應該把 si、sy 都考慮進來。

2. 在服務器上查看 /proc/softirqs,為什么 NET_RX 要比 NET_TX 大的多的多?

之前我認為 NET_RX 是讀取,NET_TX 是傳輸。對于一個既收取用戶請求,又給用戶返回的 Server 來說。這兩塊的數(shù)字應該差不多才對,至少不會有數(shù)量級的差異。但事實上,飛哥手頭的一臺服務器是這樣的:



經(jīng)過今天的源碼分析,發(fā)現(xiàn)這個問題的原因有兩個。

第一個原因是當數(shù)據(jù)發(fā)送完成以后,通過硬中斷的方式來通知驅(qū)動發(fā)送完畢。但是硬中斷無論是有數(shù)據(jù)接收,還是對于發(fā)送完畢,觸發(fā)的軟中斷都是 NET_RX_SOFTIRQ,而并不是 NET_TX_SOFTIRQ。

第二個原因是對于讀來說,都是要經(jīng)過 NET_RX 軟中斷的,都走 ksoftirqd 內(nèi)核進程。而對于發(fā)送來說,絕大部分工作都是在用戶進程內(nèi)核態(tài)處理了,只有系統(tǒng)態(tài)配額用盡才會發(fā)出 NET_TX,讓軟中斷上。

綜上兩個原因,那么在機器上查看 NET_RX 比 NET_TX 大的多就不難理解了。

3.發(fā)送網(wǎng)絡數(shù)據(jù)的時候都涉及到哪些內(nèi)存拷貝操作?

這里的內(nèi)存拷貝,我們只特指待發(fā)送數(shù)據(jù)的內(nèi)存拷貝。

第一次拷貝操作是內(nèi)核申請完 skb 之后,這時候會將用戶傳遞進來的 buffer 里的數(shù)據(jù)內(nèi)容都拷貝到 skb 中。如果要發(fā)送的數(shù)據(jù)量比較大的話,這個拷貝操作開銷還是不小的。

第二次拷貝操作是從傳輸層進入網(wǎng)絡層的時候,每一個 skb 都會被克隆一個新的副本出來。網(wǎng)絡層以及下面的驅(qū)動、軟中斷等組件在發(fā)送完成的時候會將這個副本刪除。傳輸層保存著原始的 skb,在當網(wǎng)絡對方?jīng)]有 ack 的時候,還可以重新發(fā)送,以實現(xiàn) TCP 中要求的可靠傳輸。

第三次拷貝不是必須的,只有當 IP 層發(fā)現(xiàn) skb 大于 MTU 時才需要進行。會再申請額外的 skb,并將原來的 skb 拷貝為多個小的 skb。

這里插入個題外話,大家在網(wǎng)絡性能優(yōu)化中經(jīng)常聽到的零拷貝,我覺得這有點點夸張的成分。TCP 為了保證可靠性,第二次的拷貝根本就沒法省。如果包再大于 MTU 的話,分片時的拷貝同樣也避免不了。

看到這里,相信內(nèi)核發(fā)送數(shù)據(jù)包對于你來說,已經(jīng)不再是一個完全不懂的黑盒了。本文哪怕你只看懂十分之一,你也已經(jīng)掌握了這個黑盒的打開方式。這在你將來優(yōu)化網(wǎng)絡性能時你就會知道從哪兒下手了。



25張圖,一萬字,拆解Linux網(wǎng)絡包發(fā)送過程(超級詳細~)【下文】的評論 (共 條)

分享到微博請遵守國家法律
东港市| 贡山| 新营市| 睢宁县| 开原市| 会泽县| 内黄县| 渭源县| 岱山县| 越西县| 上高县| 呼图壁县| 南涧| 和龙市| 池州市| 宁武县| 河源市| 镇坪县| 玉山县| 沙田区| 德安县| 罗定市| 象山县| 温泉县| 鲁甸县| 安溪县| 文山县| 平泉县| 宿州市| 客服| 江西省| 周宁县| 辰溪县| 茶陵县| 申扎县| 邵阳县| 县级市| 万州区| 郸城县| 若尔盖县| 营口市|