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

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

圖解Linux網(wǎng)絡(luò)包接收過程(下)

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

接上篇圖解Linux網(wǎng)絡(luò)包接收過程(上)

三、迎接數(shù)據(jù)的到來

3.1 硬中斷處理

首先當(dāng)數(shù)據(jù)幀從網(wǎng)線到達網(wǎng)卡上的時候,第一站是網(wǎng)卡的接收隊列。網(wǎng)卡在分配給自己的RingBuffer中尋找可用的內(nèi)存位置,找到后DMA引擎會把數(shù)據(jù)DMA到網(wǎng)卡之前關(guān)聯(lián)的內(nèi)存里,這個時候CPU都是無感的。當(dāng)DMA操作完成以后,網(wǎng)卡會像CPU發(fā)起一個硬中斷,通知CPU有數(shù)據(jù)到達。


注意:當(dāng)RingBuffer滿的時候,新來的數(shù)據(jù)包將給丟棄。ifconfig查看網(wǎng)卡的時候,可以里面有個overruns,表示因為環(huán)形隊列滿被丟棄的包。如果發(fā)現(xiàn)有丟包,可能需要通過ethtool命令來加大環(huán)形隊列的長度。

在啟動網(wǎng)卡一節(jié),我們說到了網(wǎng)卡的硬中斷注冊的處理函數(shù)是igb_msix_ring。

igb_write_itr只是記錄一下硬件中斷頻率(據(jù)說目的是在減少對CPU的中斷頻率時用到)。順著napi_schedule調(diào)用一路跟蹤下去,__napi_schedule=>____napi_schedule

這里我們看到,list_add_tail修改了CPU變量softnet_data里的poll_list,將驅(qū)動napi_struct傳過來的poll_list添加了進來。其中softnet_data中的poll_list是一個雙向列表,其中的設(shè)備都帶有輸入幀等著被處理。緊接著__raise_softirq_irqoff觸發(fā)了一個軟中斷NET_RX_SOFTIRQ, 這個所謂的觸發(fā)過程只是對一個變量進行了一次或運算而已。

我們說過,Linux在硬中斷里只完成簡單必要的工作,剩下的大部分的處理都是轉(zhuǎn)交給軟中斷的。通過上面代碼可以看到,硬中斷處理過程真的是非常短。只是記錄了一個寄存器,修改了一下下CPU的poll_list,然后發(fā)出個軟中斷。就這么簡單,硬中斷工作就算是完成了。


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

3.2 ksoftirqd內(nèi)核線程處理軟中斷


內(nèi)核線程初始化的時候,我們介紹了ksoftirqd中兩個線程函數(shù)ksoftirqd_should_run和run_ksoftirqd。其中ksoftirqd_should_run代碼如下:


這里看到和硬中斷中調(diào)用了同一個函數(shù)local_softirq_pending。使用方式不同的是硬中斷位置是為了寫入標(biāo)記,這里僅僅只是讀取。如果硬中斷中設(shè)置了NET_RX_SOFTIRQ,這里自然能讀取的到。接下來會真正進入線程函數(shù)中run_ksoftirqd處理:

在__do_softirq中,判斷根據(jù)當(dāng)前CPU的軟中斷類型,調(diào)用其注冊的action方法。

在網(wǎng)絡(luò)子系統(tǒng)初始化小節(jié), 我們看到我們?yōu)镹ET_RX_SOFTIRQ注冊了處理函數(shù)net_rx_action。所以net_rx_action函數(shù)就會被執(zhí)行到了。

這里需要注意一個細節(jié),硬中斷中設(shè)置軟中斷標(biāo)記,和ksoftirq的判斷是否有軟中斷到達,都是基于smp_processor_id()的。這意味著只要硬中斷在哪個CPU上被響應(yīng),那么軟中斷也是在這個CPU上處理的。所以說,如果你發(fā)現(xiàn)你的Linux軟中斷CPU消耗都集中在一個核上的話,做法是要把調(diào)整硬中斷的CPU親和性,來將硬中斷打散到不同的CPU核上去。

我們再來把精力集中到這個核心函數(shù)net_rx_action上來。

函數(shù)開頭的time_limit和budget是用來控制net_rx_action函數(shù)主動退出的,目的是保證網(wǎng)絡(luò)包的接收不霸占CPU不放。等下次網(wǎng)卡再有硬中斷過來的時候再處理剩下的接收數(shù)據(jù)包。其中budget可以通過內(nèi)核參數(shù)調(diào)整。這個函數(shù)中剩下的核心邏輯是獲取到當(dāng)前CPU變量softnet_data,對其poll_list進行遍歷, 然后執(zhí)行到網(wǎng)卡驅(qū)動注冊到的poll函數(shù)。對于igb網(wǎng)卡來說,就是igb驅(qū)動力的igb_poll函數(shù)了。

在讀取操作中,igb_poll的重點工作是對igb_clean_rx_irq的調(diào)用。

igb_fetch_rx_buffer和igb_is_non_eop的作用就是把數(shù)據(jù)幀從RingBuffer上取下來。為什么需要兩個函數(shù)呢?因為有可能幀要占多多個RingBuffer,所以是在一個循環(huán)中獲取的,直到幀尾部。獲取下來的一個數(shù)據(jù)幀用一個sk_buff來表示。收取完數(shù)據(jù)以后,對其進行一些校驗,然后開始設(shè)置sbk變量的timestamp, VLAN id, protocol等字段。接下來進入到napi_gro_receive中:

dev_gro_receive這個函數(shù)代表的是網(wǎng)卡GRO特性,可以簡單理解成把相關(guān)的小包合并成一個大包就行,目的是減少傳送給網(wǎng)絡(luò)棧的包數(shù),這有助于減少 CPU 的使用量。我們暫且忽略,直接看napi_skb_finish, 這個函數(shù)主要就是調(diào)用了netif_receive_skb。

在netif_receive_skb中,數(shù)據(jù)包將被送到協(xié)議棧中。聲明,以下的3.3, 3.4, 3.5也都屬于軟中斷的處理過程,只不過由于篇幅太長,單獨拿出來成小節(jié)。

3.3 網(wǎng)絡(luò)協(xié)議棧處理

netif_receive_skb函數(shù)會根據(jù)包的協(xié)議,假如是udp包,會將包依次送到ip_rcv(),udp_rcv()協(xié)議處理函數(shù)中進行處理。

在__netif_receive_skb_core中,我看著原來經(jīng)常使用的tcpdump的抓包點,很是激動,看來讀一遍源代碼時間真的沒白浪費。接著__netif_receive_skb_core取出protocol,它會從數(shù)據(jù)包中取出協(xié)議信息,然后遍歷注冊在這個協(xié)議上的回調(diào)函數(shù)列表。ptype_base 是一個 hash table,在協(xié)議注冊小節(jié)我們提到過。ip_rcv 函數(shù)地址就是存在這個 hash table中的。

pt_prev->func這一行就調(diào)用到了協(xié)議層注冊的處理函數(shù)了。對于ip包來講,就會進入到ip_rcv(如果是arp包的話,會進入到arp_rcv)。

3.4 IP協(xié)議層處理

我們再來大致看一下linux在ip協(xié)議層都做了什么,包又是怎么樣進一步被送到udp或tcp協(xié)議處理函數(shù)中的。

這里NF_HOOK是一個鉤子函數(shù),當(dāng)執(zhí)行完注冊的鉤子后就會執(zhí)行到最后一個參數(shù)指向的函數(shù)ip_rcv_finish。

跟蹤ip_route_input_noref 后看到它又調(diào)用了 ip_route_input_mc。在ip_route_input_mc中,函數(shù)ip_local_deliver被賦值給了dst.input, 如下:

所以回到ip_rcv_finish中的return dst_input(skb);。

skb_dst(skb)->input調(diào)用的input方法就是路由子系統(tǒng)賦的ip_local_deliver。

如協(xié)議注冊小節(jié)看到inet_protos中保存著tcp_rcv()和udp_rcv()的函數(shù)地址。這里將會根據(jù)包中的協(xié)議類型選擇進行分發(fā),在這里skb包將會進一步被派送到更上層的協(xié)議中,udp和tcp。

3.5 UDP協(xié)議層處理

在協(xié)議注冊小節(jié)的時候我們說過,udp協(xié)議的處理函數(shù)是udp_rcv。

__udp4_lib_lookup_skb是根據(jù)skb來尋找對應(yīng)的socket,當(dāng)找到以后將數(shù)據(jù)包放到socket的緩存隊列里。如果沒有找到,則發(fā)送一個目標(biāo)不可達的icmp包。

sock_owned_by_user判斷的是用戶是不是正在這個socker上進行系統(tǒng)調(diào)用(socket被占用),如果沒有,那就可以直接放到socket的接收隊列中。如果有,那就通過sk_add_backlog把數(shù)據(jù)包添加到backlog隊列。當(dāng)用戶釋放的socket的時候,內(nèi)核會檢查backlog隊列,如果有數(shù)據(jù)再移動到接收隊列中。

sk_rcvqueues_full接收隊列如果滿了的話,將直接把包丟棄。接收隊列大小受內(nèi)核參數(shù)net.core.rmem_max和net.core.rmem_default影響。

四、recvfrom系統(tǒng)調(diào)用

花開兩朵,各表一枝。上面我們說完了整個Linux內(nèi)核對數(shù)據(jù)包的接收和處理過程,最后把數(shù)據(jù)包放到socket的接收隊列中了。那么我們再回頭看用戶進程調(diào)用recvfrom后是發(fā)生了什么。我們在代碼里調(diào)用的recvfrom是一個glibc的庫函數(shù),該函數(shù)在執(zhí)行后會將用戶進行陷入到內(nèi)核態(tài),進入到Linux實現(xiàn)的系統(tǒng)調(diào)用sys_recvfrom。在理解Linux對sys_revvfrom之前,我們先來簡單看一下socket這個核心數(shù)據(jù)結(jié)構(gòu)。這個數(shù)據(jù)結(jié)構(gòu)太大了,我們只把對和我們今天主題相關(guān)的內(nèi)容畫出來,如下:



socket數(shù)據(jù)結(jié)構(gòu)中的const struct proto_ops對應(yīng)的是協(xié)議的方法集合。每個協(xié)議都會實現(xiàn)不同的方法集,對于IPv4 Internet協(xié)議族來說,每種協(xié)議都有對應(yīng)的處理方法,如下。對于udp來說,是通過inet_dgram_ops來定義的,其中注冊了inet_recvmsg方法。


socket數(shù)據(jù)結(jié)構(gòu)中的另一個數(shù)據(jù)結(jié)構(gòu)struct sock *sk是一個非常大,非常重要的子結(jié)構(gòu)體。其中的sk_prot又定義了二級處理函數(shù)。對于UDP協(xié)議來說,會被設(shè)置成UDP協(xié)議實現(xiàn)的方法集udp_prot。

看完了socket變量之后,我們再來看sys_revvfrom的實現(xiàn)過程。


在inet_recvmsg調(diào)用了sk->sk_prot->recvmsg。


上面我們說過這個對于udp協(xié)議的socket來說,這個sk_prot就是net/ipv4/udp.c下的struct proto udp_prot。由此我們找到了udp_recvmsg方法。

終于我們找到了我們想要看的重點,在上面我們看到了所謂的讀取過程,就是訪問sk->sk_receive_queue。如果沒有數(shù)據(jù),且用戶也允許等待,則將調(diào)用wait_for_more_packets()執(zhí)行等待操作,它加入會讓用戶進程進入睡眠狀態(tài)。

五、總結(jié)

網(wǎng)絡(luò)模塊是Linux內(nèi)核中最復(fù)雜的模塊了,看起來一個簡簡單單的收包過程就涉及到許多內(nèi)核組件之間的交互,如網(wǎng)卡驅(qū)動、協(xié)議棧,內(nèi)核ksoftirqd線程等??雌饋砗軓?fù)雜,本文想通過圖示的方式,盡量以容易理解的方式來將內(nèi)核收包過程講清楚?,F(xiàn)在讓我們再串一串整個收包過程。

當(dāng)用戶執(zhí)行完recvfrom調(diào)用后,用戶進程就通過系統(tǒng)調(diào)用進行到內(nèi)核態(tài)工作了。如果接收隊列沒有數(shù)據(jù),進程就進入睡眠狀態(tài)被操作系統(tǒng)掛起。這塊相對比較簡單,剩下大部分的戲份都是由Linux內(nèi)核其它模塊來表演了。

首先在開始收包之前,Linux要做許多的準(zhǔn)備工作:

  • 1. 創(chuàng)建ksoftirqd線程,為它設(shè)置好它自己的線程函數(shù),后面指望著它來處理軟中斷呢

  • 2. 協(xié)議棧注冊,linux要實現(xiàn)許多協(xié)議,比如arp,icmp,ip,udp,tcp,每一個協(xié)議都會將自己的處理函數(shù)注冊一下,方便包來了迅速找到對應(yīng)的處理函數(shù)

  • 3. 網(wǎng)卡驅(qū)動初始化,每個驅(qū)動都有一個初始化函數(shù),內(nèi)核會讓驅(qū)動也初始化一下。在這個初始化過程中,把自己的DMA準(zhǔn)備好,把NAPI的poll函數(shù)地址告訴內(nèi)核

  • 4. 啟動網(wǎng)卡,分配RX,TX隊列,注冊中斷對應(yīng)的處理函數(shù)

以上是內(nèi)核準(zhǔn)備收包之前的重要工作,當(dāng)上面都ready之后,就可以打開硬中斷,等待數(shù)據(jù)包的到來了。

當(dāng)數(shù)據(jù)到來了以后,第一個迎接它的是網(wǎng)卡(我去,這不是廢話么):

  • 1. 網(wǎng)卡將數(shù)據(jù)幀DMA到內(nèi)存的RingBuffer中,然后向CPU發(fā)起中斷通知

  • 2. CPU響應(yīng)中斷請求,調(diào)用網(wǎng)卡啟動時注冊的中斷處理函數(shù)

  • 3. 中斷處理函數(shù)幾乎沒干啥,就發(fā)起了軟中斷請求

  • 4. 內(nèi)核線程ksoftirqd線程發(fā)現(xiàn)有軟中斷請求到來,先關(guān)閉硬中斷

  • 5. ksoftirqd線程開始調(diào)用驅(qū)動的poll函數(shù)收包

  • 6. poll函數(shù)將收到的包送到協(xié)議棧注冊的ip_rcv函數(shù)中

  • 7. ip_rcv函數(shù)再講包送到udp_rcv函數(shù)中(對于tcp包就送到tcp_rcv)

現(xiàn)在我們可以回到開篇的問題了,我們在用戶層看到的簡單一行recvfrom,Linux內(nèi)核要替我們做如此之多的工作,才能讓我們順利收到數(shù)據(jù)。這還是簡簡單單的UDP,如果是TCP,內(nèi)核要做的工作更多,不由得感嘆內(nèi)核的開發(fā)者們真的是用心良苦。

理解了整個收包過程以后,我們就能明確知道Linux收一個包的CPU開銷了。首先第一塊是用戶進程調(diào)用系統(tǒng)調(diào)用陷入內(nèi)核態(tài)的開銷。第二塊是CPU響應(yīng)包的硬中斷的CPU開銷。第三塊是ksoftirqd內(nèi)核線程的軟中斷上下文花費的。后面我們再專門發(fā)一篇文章實際觀察一下這些開銷。

另外網(wǎng)絡(luò)收發(fā)中有很多末支細節(jié)咱們并沒有展開了說,比如說no NAPI, GRO,RPS等。因為我覺得說的太對了反而會影響大家對整個流程的把握,所以盡量只保留主框架了,少即是多!



圖解Linux網(wǎng)絡(luò)包接收過程(下)的評論 (共 條)

分享到微博請遵守國家法律
内江市| 罗城| 齐齐哈尔市| 新化县| 桦甸市| 宜君县| 吴旗县| 江北区| 顺平县| 长丰县| 新安县| 贡嘎县| 砚山县| 湟中县| 合作市| 东平县| 辽中县| 出国| 应城市| 巴青县| 庆安县| 清水河县| 北碚区| 东阿县| 乌拉特前旗| 神木县| 太康县| 托克逊县| 安吉县| 奈曼旗| 涞源县| 罗定市| 嘉定区| 平谷区| 霍州市| 汉中市| 凤山市| 松桃| 乌兰县| 普陀区| 太仆寺旗|