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

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

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

2022-07-11 21:37 作者:補給站Linux內(nèi)核  | 我要投稿

半年前我以源碼的方式描述了網(wǎng)絡(luò)包的接收過程。之后不斷有粉絲提醒我還沒聊發(fā)送過程呢。好,安排!

在開始今天的文章之前,我先來請大家思考幾個小問題。

  • 問1:我們在查看內(nèi)核發(fā)送數(shù)據(jù)消耗的 CPU 時,是應(yīng)該看 sy 還是 si ?

  • 問2:為什么你服務(wù)器上的 /proc/softirqs 里 NET_RX 要比 NET_TX 大的多的多?

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

這些問題雖然在線上經(jīng)??吹?,但我們似乎很少去深究。如果真的能透徹地把這些問題理解到位,我們對性能的掌控能力將會變得更強。

帶著這三個問題,我們開始今天對 Linux 內(nèi)核網(wǎng)絡(luò)發(fā)送過程的深度剖析。還是按照我們之前的傳統(tǒng),先從一段簡單的代碼作為切入。如下代碼是一個典型服務(wù)器程序的典型的縮微代碼:

今天我們來討論上述代碼中,調(diào)用 send 之后內(nèi)核是怎么樣把數(shù)據(jù)包發(fā)送出去的。本文基于Linux 3.10,網(wǎng)卡驅(qū)動采用Intel的igb網(wǎng)卡舉例。


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


我覺得看 Linux 源碼最重要的是得有整體上的把握,而不是一開始就陷入各種細(xì)節(jié)。

我這里先給大家準(zhǔn)備了一個總的流程圖,簡單闡述下 send 發(fā)送了的數(shù)據(jù)是如何一步一步被發(fā)送到網(wǎng)卡的。


在這幅圖中,我們看到用戶數(shù)據(jù)被拷貝到內(nèi)核態(tài),然后經(jīng)過協(xié)議棧處理后進入到了 RingBuffer 中。隨后網(wǎng)卡驅(qū)動真正將數(shù)據(jù)發(fā)送了出去。當(dāng)發(fā)送完成的時候,是通過硬中斷來通知 CPU,然后清理 RingBuffer。

因為文章后面要進入源碼,所以我們再從源碼的角度給出一個流程圖。


雖然數(shù)據(jù)這時已經(jīng)發(fā)送完畢,但是其實還有一件重要的事情沒有做,那就是釋放緩存隊列等內(nèi)存。

那內(nèi)核是如何知道什么時候才能釋放內(nèi)存的呢,當(dāng)然是等網(wǎng)絡(luò)發(fā)送完畢之后。網(wǎng)卡在發(fā)送完畢的時候,會給 CPU 發(fā)送一個硬中斷來通知 CPU。更完整的流程看圖:


注意,我們今天的主題雖然是發(fā)送數(shù)據(jù),但是硬中斷最終觸發(fā)的軟中斷卻是 NET_RX_SOFTIRQ,而并不是 NET_TX_SOFTIRQ ?。。。═ 是 transmit 的縮寫,R 表示 receive)

意不意外,驚不驚喜???

所以這就是開篇問題 1 的一部分的原因(注意,這只是一部分原因)。

問1:在服務(wù)器上查看 /proc/softirqs,為什么 NET_RX 要比 NET_TX 大的多的多?

傳輸完成最終會觸發(fā) NET_RX,而不是 NET_TX。所以自然你觀測 /proc/softirqs 也就能看到 NET_RX 更多了。

好,現(xiàn)在你已經(jīng)對內(nèi)核是怎么發(fā)送網(wǎng)絡(luò)包的有一個全局上的把握了。不要得意,我們需要了解的細(xì)節(jié)才是更有價值的地方,讓我們繼續(xù)!!

二、網(wǎng)卡啟動準(zhǔn)備

現(xiàn)在的服務(wù)器上的網(wǎng)卡一般都是支持多隊列的。每一個隊列上都是由一個 RingBuffer 表示的,開啟了多隊列以后的的網(wǎng)卡就會對應(yīng)有多個 RingBuffer。



網(wǎng)卡在啟動時最重要的任務(wù)之一就是分配和初始化 RingBuffer,理解了 RingBuffer 將會非常有助于后面我們掌握發(fā)送。因為今天的主題是發(fā)送,所以就以傳輸隊列為例,我們來看下網(wǎng)卡啟動時分配 RingBuffer 的實際過程。

在網(wǎng)卡啟動的時候,會調(diào)用到 __igb_open 函數(shù),RingBuffer 就是在這里分配的。

在上面 __igb_open 函數(shù)調(diào)用 igb_setup_all_tx_resources 分配所有的傳輸 RingBuffer, 調(diào)用 igb_setup_all_rx_resources 創(chuàng)建所有的接收 RingBuffer。

真正的 RingBuffer 構(gòu)造過程是在 igb_setup_tx_resources 中完成的。

從上述源碼可以看到,實際上一個 RingBuffer 的內(nèi)部不僅僅是一個環(huán)形隊列數(shù)組,而是有兩個。

1)igb_tx_buffer 數(shù)組:這個數(shù)組是內(nèi)核使用的,通過 vzalloc 申請的。 2)e1000_adv_tx_desc 數(shù)組:這個數(shù)組是網(wǎng)卡硬件使用的,硬件是可以通過 DMA 直接訪問這塊內(nèi)存,通過 dma_alloc_coherent 分配。

這個時候它們之間還沒有啥聯(lián)系。將來在發(fā)送的時候,這兩個環(huán)形數(shù)組中相同位置的指針將都將指向同一個 skb。這樣,內(nèi)核和硬件就能共同訪問同樣的數(shù)據(jù)了,內(nèi)核往 skb 里寫數(shù)據(jù),網(wǎng)卡硬件負(fù)責(zé)發(fā)送。



最后調(diào)用 netif_tx_start_all_queues 開啟隊列。另外,對于硬中斷的處理函數(shù) igb_msix_ring 其實也是在 __igb_open 中注冊的。

三、accept 創(chuàng)建新 socket

在發(fā)送數(shù)據(jù)之前,我們往往還需要一個已經(jīng)建立好連接的 socket。

我們就以開篇服務(wù)器縮微源代碼中提到的 accept 為例,當(dāng) accept 之后,進程會創(chuàng)建一個新的 socket 出來,然后把它放到當(dāng)前進程的打開文件列表中,專門用于和對應(yīng)的客戶端通信。

假設(shè)服務(wù)器進程通過 accept 和客戶端建立了兩條連接,我們來簡單看一下這兩條連接和進程的關(guān)聯(lián)關(guān)系。



其中代表一條連接的 socket 內(nèi)核對象更為具體一點的結(jié)構(gòu)圖如下。




今天我們還是把重點放到數(shù)據(jù)發(fā)送過程上。

四、發(fā)送數(shù)據(jù)真正開始

4.1 send 系統(tǒng)調(diào)用實現(xiàn)

send 系統(tǒng)調(diào)用的源碼位于文件 net/socket.c 中。在這個系統(tǒng)調(diào)用里,內(nèi)部其實真正使用的是 sendto 系統(tǒng)調(diào)用。整個調(diào)用鏈條雖然不短,但其實主要只干了兩件簡單的事情,

  • 第一是在內(nèi)核中把真正的 socket 找出來,在這個對象里記錄著各種協(xié)議棧的函數(shù)地址。

  • 第二是構(gòu)造一個 struct msghdr 對象,把用戶傳入的數(shù)據(jù),比如 buffer地址、數(shù)據(jù)長度啥的,統(tǒng)統(tǒng)都裝進去.

剩下的事情就交給下一層,協(xié)議棧里的函數(shù) inet_sendmsg 了,其中 inet_sendmsg 函數(shù)的地址是通過 socket 內(nèi)核對象里的 ops 成員找到的。大致流程如圖。



有了上面的了解,我們再看起源碼就要容易許多了。源碼如下:

從源碼可以看到,我們在用戶態(tài)使用的 send 函數(shù)和 sendto 函數(shù)其實都是 sendto 系統(tǒng)調(diào)用實現(xiàn)的。send 只是為了方便,封裝出來的一個更易于調(diào)用的方式而已。

在 sendto 系統(tǒng)調(diào)用里,首先根據(jù)用戶傳進來的 socket 句柄號來查找真正的 socket 內(nèi)核對象。接著把用戶請求的 buff、len、flag 等參數(shù)都統(tǒng)統(tǒng)打包到一個 struct msghdr 對象中。

接著調(diào)用了 sock_sendmsg => __sock_sendmsg ==> ?__sock_sendmsg_nosec。在__sock_sendmsg_nosec 中,調(diào)用將會由系統(tǒng)調(diào)用進入到協(xié)議棧,我們來看它的源碼。

通過第三節(jié)里的 socket 內(nèi)核對象結(jié)構(gòu)圖,我們可以看到,這里調(diào)用的是 sock->ops->sendmsg 實際執(zhí)行的是 inet_sendmsg。這個函數(shù)是 AF_INET 協(xié)議族提供的通用發(fā)送函數(shù)。

4.2 傳輸層處理

1)傳輸層拷貝

在進入到協(xié)議棧 inet_sendmsg 以后,內(nèi)核接著會找到 socket 上的具體協(xié)議發(fā)送函數(shù)。對于 TCP 協(xié)議來說,那就是 tcp_sendmsg(同樣也是通過 socket 內(nèi)核對象找到的)。

在這個函數(shù)中,內(nèi)核會申請一個內(nèi)核態(tài)的 skb 內(nèi)存,將用戶待發(fā)送的數(shù)據(jù)拷貝進去。注意這個時候不一定會真正開始發(fā)送,如果沒有達到發(fā)送條件的話很可能這次調(diào)用直接就返回了。大概過程如圖:


我們來看 inet_sendmsg 函數(shù)的源碼。

在這個函數(shù)中會調(diào)用到具體協(xié)議的發(fā)送函數(shù)。同樣參考第三節(jié)里的 socket 內(nèi)核對象結(jié)構(gòu)圖,我們看到對于 TCP 協(xié)議下的 socket 來說,來說 sk->sk_prot->sendmsg 指向的是 tcp_sendmsg(對于 UPD 來說是 udp_sendmsg)。

tcp_sendmsg 這個函數(shù)比較長,我們分多次來看它。先看這一段

理解對 socket 調(diào)用 tcp_write_queue_tail 是理解發(fā)送的前提。如上所示,這個函數(shù)是在獲取 socket 發(fā)送隊列中的最后一個 skb。skb 是 struct sk_buff 對象的簡稱,用戶的發(fā)送隊列就是該對象組成的一個鏈表。

我們再接著看 tcp_sendmsg 的其它部分。

這個函數(shù)比較長,不過其實邏輯并不復(fù)雜。其中 msg->msg_iov 存儲的是用戶態(tài)內(nèi)存的要發(fā)送的數(shù)據(jù)的 buffer。接下來在內(nèi)核態(tài)申請內(nèi)核內(nèi)存,比如 skb,并把用戶內(nèi)存里的數(shù)據(jù)拷貝到內(nèi)核態(tài)內(nèi)存中。這就會涉及到一次或者幾次內(nèi)存拷貝的開銷。


至于內(nèi)核什么時候真正把 skb 發(fā)送出去。在 tcp_sendmsg 中會進行一些判斷。

只有滿足 forced_push(tp) 或者 skb == tcp_send_head(sk) 成立的時候,內(nèi)核才會真正啟動發(fā)送數(shù)據(jù)包。其中 forced_push(tp) 判斷的是未發(fā)送的數(shù)據(jù)數(shù)據(jù)是否已經(jīng)超過最大窗口的一半了。

條件都不滿足的話,這次的用戶要發(fā)送的數(shù)據(jù)只是拷貝到內(nèi)核就算完事了!

2)傳輸層發(fā)送

假設(shè)現(xiàn)在內(nèi)核發(fā)送條件已經(jīng)滿足了,我們再來跟蹤一下實際的發(fā)送過程。對于上小節(jié)函數(shù)中,當(dāng)滿足真正發(fā)送條件的時候,無論調(diào)用的是 __tcp_push_pending_frames 還是 tcp_push_one 最終都實際會執(zhí)行到 tcp_write_xmit。

所以我們直接從 tcp_write_xmit 看起,這個函數(shù)處理了傳輸層的擁塞控制、滑動窗口相關(guān)的工作。滿足窗口要求的時候,設(shè)置一下 TCP 頭然后將 skb 傳到更低的網(wǎng)絡(luò)層進行處理。


我們來看下 tcp_write_xmit 的源碼。

可以看到我們之前在網(wǎng)絡(luò)協(xié)議里學(xué)的滑動窗口、擁塞控制就是在這個函數(shù)中完成的,這部分就不過多展開了,感興趣同學(xué)自己找這段源碼來讀。我們今天只看發(fā)送主過程,那就走到了 tcp_transmit_skb。

第一件事是先克隆一個新的 skb,這里重點說下為什么要復(fù)制一個 skb 出來呢?

是因為 skb 后續(xù)在調(diào)用網(wǎng)絡(luò)層,最后到達網(wǎng)卡發(fā)送完成的時候,這個 skb 會被釋放掉。而我們知道 TCP 協(xié)議是支持丟失重傳的,在收到對方的 ACK 之前,這個 skb 不能被刪除。所以內(nèi)核的做法就是每次調(diào)用網(wǎng)卡發(fā)送的時候,實際上傳遞出去的是 skb 的一個拷貝。等收到 ACK 再真正刪除。

第二件事是修改 skb 中的 TCP header,根據(jù)實際情況把 TCP 頭設(shè)置好。這里要介紹一個小技巧,skb 內(nèi)部其實包含了網(wǎng)絡(luò)協(xié)議中所有的 header。在設(shè)置 TCP 頭的時候,只是把指針指向 skb 的合適位置。后面再設(shè)置 IP 頭的時候,在把指針挪一挪就行,避免頻繁的內(nèi)存申請和拷貝,效率很高。

tcp_transmit_skb 是發(fā)送數(shù)據(jù)位于傳輸層的最后一步,接下來就可以進入到網(wǎng)絡(luò)層進行下一層的操作了。調(diào)用了網(wǎng)絡(luò)層提供的發(fā)送接口icsk->icsk_af_ops->queue_xmit()。

在下面的這個源碼中,我們的知道了 queue_xmit 其實指向的是 ip_queue_xmit 函數(shù)。

自此,傳輸層的工作也就都完成了。數(shù)據(jù)離開了傳輸層,接下來將會進入到內(nèi)核在網(wǎng)絡(luò)層的實現(xiàn)里。

文章篇幅過長,下文繼續(xù)講解


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

分享到微博請遵守國家法律
陇南市| 泗阳县| 惠水县| 息烽县| 阳原县| 万载县| 陈巴尔虎旗| 息烽县| 深泽县| 临汾市| 苏尼特左旗| 阜宁县| 慈溪市| 陇西县| 滨州市| 微山县| 德安县| 麦盖提县| 襄城县| 樟树市| 安岳县| 嘉鱼县| 松滋市| 太和县| 普定县| 嘉义县| 芜湖市| 营山县| 凤凰县| 桦南县| 叶城县| 临江市| 双峰县| 潼关县| 原阳县| 荆门市| 安平县| 台山市| 闽清县| 丰城市| 乌拉特前旗|