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

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

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

2022-07-05 20:02 作者:補(bǔ)給站Linux內(nèi)核  | 我要投稿
  • 因為要對百萬、千萬、甚至是過億的用戶提供各種網(wǎng)絡(luò)服務(wù),所以在一線互聯(lián)網(wǎng)企業(yè)里面試和晉升后端開發(fā)同學(xué)的其中一個重點要求就是要能支撐高并發(fā),要理解性能開銷,會進(jìn)行性能優(yōu)化。而很多時候,如果你對Linux底層的理解不深的話,遇到很多線上性能瓶頸你會覺得狗拿刺猬,無從下手。

  • 我們今天用圖解的方式,來深度理解一下在Linux下網(wǎng)絡(luò)包的接收過程。還是按照慣例來借用一段最簡單的代碼開始思考。為了簡單起見,我們用udp來舉例,如下:

  • 上面代碼是一段udp server接收收據(jù)的邏輯。當(dāng)在開發(fā)視角看的時候,只要客戶端有對應(yīng)的數(shù)據(jù)發(fā)送過來,服務(wù)器端執(zhí)行recv_from后就能收到它,并把它打印出來。我們現(xiàn)在想知道的是,當(dāng)網(wǎng)絡(luò)包達(dá)到網(wǎng)卡,直到我們的recvfrom收到數(shù)據(jù),這中間,究竟都發(fā)生過什么?

  • 通過本文,你將深入理解Linux網(wǎng)絡(luò)系統(tǒng)內(nèi)部是如何實現(xiàn)的,以及各個部分之間如何交互。相信這對你的工作將會有非常大的幫助。本文基于Linux 3.10,源代碼參見https://mirrors.edge.kernel.org/pub/linux/kernel/v3.x/,網(wǎng)卡驅(qū)動采用Intel的igb網(wǎng)卡舉例。

一、Linux網(wǎng)絡(luò)收包總覽

  • 在TCP/IP網(wǎng)絡(luò)分層模型里,整個協(xié)議棧被分成了物理層、鏈路層、網(wǎng)絡(luò)層,傳輸層和應(yīng)用層。物理層對應(yīng)的是網(wǎng)卡和網(wǎng)線,應(yīng)用層對應(yīng)的是我們常見的Nginx,F(xiàn)TP等等各種應(yīng)用。Linux實現(xiàn)的是鏈路層、網(wǎng)絡(luò)層和傳輸層這三層。

  • 在Linux內(nèi)核實現(xiàn)中,鏈路層協(xié)議靠網(wǎng)卡驅(qū)動來實現(xiàn),內(nèi)核協(xié)議棧來實現(xiàn)網(wǎng)絡(luò)層和傳輸層。內(nèi)核對更上層的應(yīng)用層提供socket接口來供用戶進(jìn)程訪問。我們用Linux的視角來看到的TCP/IP網(wǎng)絡(luò)分層模型應(yīng)該是下面這個樣子的。

  • 在Linux的源代碼中,網(wǎng)絡(luò)設(shè)備驅(qū)動對應(yīng)的邏輯位于driver/net/ethernet, 其中intel系列網(wǎng)卡的驅(qū)動在driver/net/ethernet/intel目錄下。協(xié)議棧模塊代碼位于kernel和net目錄。

  • 內(nèi)核和網(wǎng)絡(luò)設(shè)備驅(qū)動是通過中斷的方式來處理的。當(dāng)設(shè)備上有數(shù)據(jù)到達(dá)的時候,會給CPU的相關(guān)引腳上觸發(fā)一個電壓變化,以通知CPU來處理數(shù)據(jù)。對于網(wǎng)絡(luò)模塊來說,由于處理過程比較復(fù)雜和耗時,如果在中斷函數(shù)中完成所有的處理,將會導(dǎo)致中斷處理函數(shù)(優(yōu)先級過高)將過度占據(jù)CPU,將導(dǎo)致CPU無法響應(yīng)其它設(shè)備,例如鼠標(biāo)和鍵盤的消息。因此Linux中斷處理函數(shù)是分上半部和下半部的。上半部是只進(jìn)行最簡單的工作,快速處理然后釋放CPU,接著CPU就可以允許其它中斷進(jìn)來。剩下將絕大部分的工作都放到下半部中,可以慢慢從容處理。2.4以后的內(nèi)核版本采用的下半部實現(xiàn)方式是軟中斷,由ksoftirqd內(nèi)核線程全權(quán)處理。和硬中斷不同的是,硬中斷是通過給CPU物理引腳施加電壓變化,而軟中斷是通過給內(nèi)存中的一個變量的二進(jìn)制值以通知軟中斷處理程序。

  • 好了,大概了解了網(wǎng)卡驅(qū)動、硬中斷、軟中斷和ksoftirqd線程之后,我們在這幾個概念的基礎(chǔ)上給出一個內(nèi)核收包的路徑示意:

  • 當(dāng)網(wǎng)卡上收到數(shù)據(jù)以后,Linux中第一個工作的模塊是網(wǎng)絡(luò)驅(qū)動。網(wǎng)絡(luò)驅(qū)動會以DMA的方式把網(wǎng)卡上收到的幀寫到內(nèi)存里。再向CPU發(fā)起一個中斷,以通知CPU有數(shù)據(jù)到達(dá)。第二,當(dāng)CPU收到中斷請求后,會去調(diào)用網(wǎng)絡(luò)驅(qū)動注冊的中斷處理函數(shù)。網(wǎng)卡的中斷處理函數(shù)并不做過多工作,發(fā)出軟中斷請求,然后盡快釋放CPU。ksoftirqd檢測到有軟中斷請求到達(dá),調(diào)用poll開始輪詢收包,收到后交由各級協(xié)議棧處理。對于UDP包來說,會被放到用戶socket的接收隊列中。

  • 我們從上面這張圖中已經(jīng)從整體上把握到了Linux對數(shù)據(jù)包的處理過程。但是要想了解更多網(wǎng)絡(luò)模塊工作的細(xì)節(jié),我們還得往下看。

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


  • Linux驅(qū)動,內(nèi)核協(xié)議棧等等模塊在具備接收網(wǎng)卡數(shù)據(jù)包之前,要做很多的準(zhǔn)備工作才行。比如要提前創(chuàng)建好ksoftirqd內(nèi)核線程,要注冊好各個協(xié)議對應(yīng)的處理函數(shù),網(wǎng)絡(luò)設(shè)備子系統(tǒng)要提前初始化好,網(wǎng)卡要啟動好。只有這些都Ready之后,我們才能真正開始接收數(shù)據(jù)包。那么我們現(xiàn)在來看看這些準(zhǔn)備工作都是怎么做的。

2.1 創(chuàng)建ksoftirqd內(nèi)核線程

  • Linux的軟中斷都是在專門的內(nèi)核線程(ksoftirqd)中進(jìn)行的,因此我們非常有必要看一下這些進(jìn)程是怎么初始化的,這樣我們才能在后面更準(zhǔn)確地了解收包過程。該進(jìn)程數(shù)量不是1個,而是N個,其中N等于你的機(jī)器的核數(shù)。

  • 系統(tǒng)初始化的時候在kernel/smpboot.c中調(diào)用了smpboot_register_percpu_thread, 該函數(shù)進(jìn)一步會執(zhí)行到spawn_ksoftirqd(位于kernel/softirq.c)來創(chuàng)建出softirqd進(jìn)程。


相關(guān)代碼如下:

當(dāng)ksoftirqd被創(chuàng)建出來以后,它就會進(jìn)入自己的線程循環(huán)函數(shù)ksoftirqd_should_run和run_ksoftirqd了。不停地判斷有沒有軟中斷需要被處理。這里需要注意的一點是,軟中斷不僅僅只有網(wǎng)絡(luò)軟中斷,還有其它類型。

2.2 網(wǎng)絡(luò)子系統(tǒng)初始化

linux內(nèi)核通過調(diào)用subsys_initcall來初始化各個子系統(tǒng),在源代碼目錄里你可以grep出許多對這個函數(shù)的調(diào)用。這里我們要說的是網(wǎng)絡(luò)子系統(tǒng)的初始化,會執(zhí)行到net_dev_init函數(shù)。

在這個函數(shù)里,會為每個CPU都申請一個softnet_data數(shù)據(jù)結(jié)構(gòu),在這個數(shù)據(jù)結(jié)構(gòu)里的poll_list是等待驅(qū)動程序?qū)⑵鋚oll函數(shù)注冊進(jìn)來,稍后網(wǎng)卡驅(qū)動初始化的時候我們可以看到這一過程。

另外open_softirq注冊了每一種軟中斷都注冊一個處理函數(shù)。NET_TX_SOFTIRQ的處理函數(shù)為net_tx_action,NET_RX_SOFTIRQ的為net_rx_action。繼續(xù)跟蹤open_softirq后發(fā)現(xiàn)這個注冊的方式是記錄在softirq_vec變量里的。后面ksoftirqd線程收到軟中斷的時候,也會使用這個變量來找到每一種軟中斷對應(yīng)的處理函數(shù)。

2.3 協(xié)議棧注冊

內(nèi)核實現(xiàn)了網(wǎng)絡(luò)層的ip協(xié)議,也實現(xiàn)了傳輸層的tcp協(xié)議和udp協(xié)議。這些協(xié)議對應(yīng)的實現(xiàn)函數(shù)分別是ip_rcv(),tcp_v4_rcv()和udp_rcv()。和我們平時寫代碼的方式不一樣的是,內(nèi)核是通過注冊的方式來實現(xiàn)的。Linux內(nèi)核中的fs_initcall和subsys_initcall類似,也是初始化模塊的入口。fs_initcall調(diào)用inet_init后開始網(wǎng)絡(luò)協(xié)議棧注冊。通過inet_init,將這些函數(shù)注冊到了inet_protos和ptype_base數(shù)據(jù)結(jié)構(gòu)中了。如下圖:

相關(guān)代碼如下

上面的代碼中我們可以看到,udp_protocol結(jié)構(gòu)體中的handler是udp_rcv,tcp_protocol結(jié)構(gòu)體中的handler是tcp_v4_rcv,通過inet_add_protocol被初始化了進(jìn)來。

inet_add_protocol函數(shù)將tcp和udp對應(yīng)的處理函數(shù)都注冊到了inet_protos數(shù)組中了。再看dev_add_pack(&ip_packet_type);這一行,ip_packet_type結(jié)構(gòu)體中的type是協(xié)議名,func是ip_rcv函數(shù),在dev_add_pack中會被注冊到ptype_base哈希表中。

這里我們需要記住inet_protos記錄著udp,tcp的處理函數(shù)地址,ptype_base存儲著ip_rcv()函數(shù)的處理地址。后面我們會看到軟中斷中會通過ptype_base找到ip_rcv函數(shù)地址,進(jìn)而將ip包正確地送到ip_rcv()中執(zhí)行。在ip_rcv中將會通過inet_protos找到tcp或者udp的處理函數(shù),再而把包轉(zhuǎn)發(fā)給udp_rcv()或tcp_v4_rcv()函數(shù)。

擴(kuò)展一下,如果看一下ip_rcv和udp_rcv等函數(shù)的代碼能看到很多協(xié)議的處理過程。例如,ip_rcv中會處理netfilter和iptable過濾,如果你有很多或者很復(fù)雜的 netfilter 或 iptables 規(guī)則,這些規(guī)則都是在軟中斷的上下文中執(zhí)行的,會加大網(wǎng)絡(luò)延遲。再例如,udp_rcv中會判斷socket接收隊列是否滿了。對應(yīng)的相關(guān)內(nèi)核參數(shù)是net.core.rmem_max和net.core.rmem_default。如果有興趣,建議大家好好讀一下inet_init這個函數(shù)的代碼。

2.4 網(wǎng)卡驅(qū)動初始化

每一個驅(qū)動程序(不僅僅只是網(wǎng)卡驅(qū)動)會使用 module_init 向內(nèi)核注冊一個初始化函數(shù),當(dāng)驅(qū)動被加載時,內(nèi)核會調(diào)用這個函數(shù)。比如igb網(wǎng)卡驅(qū)動的代碼位于drivers/net/ethernet/intel/igb/igb_main.c

驅(qū)動的pci_register_driver調(diào)用完成后,Linux內(nèi)核就知道了該驅(qū)動的相關(guān)信息,比如igb網(wǎng)卡驅(qū)動的igb_driver_name和igb_probe函數(shù)地址等等。當(dāng)網(wǎng)卡設(shè)備被識別以后,內(nèi)核會調(diào)用其驅(qū)動的probe方法(igb_driver的probe方法是igb_probe)。驅(qū)動probe方法執(zhí)行的目的就是讓設(shè)備ready,對于igb網(wǎng)卡,其igb_probe位于drivers/net/ethernet/intel/igb/igb_main.c下。主要執(zhí)行的操作如下:

第5步中我們看到,網(wǎng)卡驅(qū)動實現(xiàn)了ethtool所需要的接口,也在這里注冊完成函數(shù)地址的注冊。當(dāng) ethtool 發(fā)起一個系統(tǒng)調(diào)用之后,內(nèi)核會找到對應(yīng)操作的回調(diào)函數(shù)。對于igb網(wǎng)卡來說,其實現(xiàn)函數(shù)都在drivers/net/ethernet/intel/igb/igb_ethtool.c下。相信你這次能徹底理解ethtool的工作原理了吧?這個命令之所以能查看網(wǎng)卡收發(fā)包統(tǒng)計、能修改網(wǎng)卡自適應(yīng)模式、能調(diào)整RX 隊列的數(shù)量和大小,是因為ethtool命令最終調(diào)用到了網(wǎng)卡驅(qū)動的相應(yīng)方法,而不是ethtool本身有這個超能力。

第6步注冊的igb_netdev_ops中包含的是igb_open等函數(shù),該函數(shù)在網(wǎng)卡被啟動的時候會被調(diào)用。

第7步中,在igb_probe初始化過程中,還調(diào)用到了igb_alloc_q_vector。他注冊了一個NAPI機(jī)制所必須的poll函數(shù),對于igb網(wǎng)卡驅(qū)動來說,這個函數(shù)就是igb_poll,如下代碼所示。

2.5 啟動網(wǎng)卡

當(dāng)上面的初始化都完成以后,就可以啟動網(wǎng)卡了?;貞浨懊婢W(wǎng)卡驅(qū)動初始化時,我們提到了驅(qū)動向內(nèi)核注冊了 structure net_device_ops 變量,它包含著網(wǎng)卡啟用、發(fā)包、設(shè)置mac 地址等回調(diào)函數(shù)(函數(shù)指針)。當(dāng)啟用一個網(wǎng)卡時(例如,通過 ifconfig eth0 up),net_device_ops 中的 igb_open方法會被調(diào)用。它通常會做以下事情:

在上面__igb_open函數(shù)調(diào)用了igb_setup_all_tx_resources,和igb_setup_all_rx_resources。在igb_setup_all_rx_resources這一步操作中,分配了RingBuffer,并建立內(nèi)存和Rx隊列的映射關(guān)系。(Rx Tx 隊列的數(shù)量和大小可以通過 ethtool 進(jìn)行配置)。我們再接著看中斷函數(shù)注冊igb_request_irq:

在上面的代碼中跟蹤函數(shù)調(diào)用, __igb_open => igb_request_irq => igb_request_msix, 在igb_request_msix中我們看到了,對于多隊列的網(wǎng)卡,為每一個隊列都注冊了中斷,其對應(yīng)的中斷處理函數(shù)是igb_msix_ring(該函數(shù)也在drivers/net/ethernet/intel/igb/igb_main.c下)。我們也可以看到,msix方式下,每個 RX 隊列有獨立的MSI-X 中斷,從網(wǎng)卡硬件中斷的層面就可以設(shè)置讓收到的包被不同的 CPU處理。(可以通過 irqbalance ,或者修改 /proc/irq/IRQ_NUMBER/smp_affinity能夠修改和CPU的綁定行為)。

當(dāng)做好以上準(zhǔn)備工作以后,就可以開門迎客(數(shù)據(jù)包)了!

由于篇幅有限,后面內(nèi)容下次分享




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

分享到微博請遵守國家法律
伊吾县| 兰溪市| 邓州市| 禹城市| 通城县| 西畴县| 沛县| 双峰县| 平果县| 中牟县| 龙陵县| 昭平县| 汤阴县| 石嘴山市| 宁强县| 巩义市| 彭水| 绵竹市| 凌源市| 获嘉县| 城口县| 平果县| 五华县| 芮城县| 新密市| 长岛县| 徐州市| 麟游县| 同仁县| 孝义市| 竹山县| 普陀区| 芦溪县| 临海市| 都匀市| 黔西| 卓资县| 三亚市| 临湘市| 伊春市| 翁源县|