在深入理解Linux內(nèi)核任務(wù)調(diào)度(中斷處理、softirg、tasklet、wq、內(nèi)核線程等)機(jī)制的基礎(chǔ)上,分析梳理send和recv過程中TCP/IP協(xié)議棧相關(guān)的運(yùn)行任務(wù)實(shí)體及相互協(xié)作的時(shí)序分析。
編譯、部署、運(yùn)行、測(cè)評(píng)、原理、源代碼分析、跟蹤調(diào)試等
應(yīng)該包括時(shí)序圖
在我們了解整個(gè)linux系統(tǒng)的網(wǎng)絡(luò)體系結(jié)構(gòu)之前,我們需要對(duì)整個(gè)網(wǎng)絡(luò)體系調(diào)用,初始化和交互的位置,同時(shí)也是Linux操作系統(tǒng)中最為關(guān)鍵的一部分代碼-------內(nèi)核,有一個(gè)初步的認(rèn)知。
首先,從功能上,我們將linux內(nèi)核劃分為五個(gè)不同的部分,分別是
(1)進(jìn)程管理:主要負(fù)載CPU的訪問控制,對(duì)CPU進(jìn)行調(diào)度管理;
(2)內(nèi)存管理:主要提供對(duì)內(nèi)存資源的訪問控制;
(3)文件系統(tǒng):將硬盤的扇區(qū)組織成文件系統(tǒng),實(shí)現(xiàn)文件讀寫等操作;
(4)設(shè)備管理:用于控制所有的外部設(shè)備及控制器;
(5)網(wǎng)洛:主要負(fù)責(zé)管理各種網(wǎng)絡(luò)設(shè)備,并實(shí)現(xiàn)各種網(wǎng)絡(luò)協(xié)議棧,最終實(shí)現(xiàn)通過網(wǎng)絡(luò)連接其它系統(tǒng)的功能;
每個(gè)部分分別處理一項(xiàng)明確的功能,又向其它各個(gè)部分提供自己所完成的功能,相互協(xié)調(diào),共同完成操作系統(tǒng)的任務(wù)。
Linux內(nèi)核架構(gòu)如下圖所示:
?
?
?圖1 Linux內(nèi)核架構(gòu)圖
內(nèi)核的基本架構(gòu)我們已經(jīng)了解清楚了,接下來我們重點(diǎn)關(guān)注到內(nèi)核中的網(wǎng)絡(luò)模塊,觀察在linux內(nèi)核中,我們是如何實(shí)現(xiàn)及運(yùn)用TCP/IP協(xié)議,并完成網(wǎng)絡(luò)的初始化及各個(gè)模塊調(diào)用調(diào)度。我們將內(nèi)核中的網(wǎng)絡(luò)部分抽出,通過對(duì)比TCP/IP分層協(xié)議,與Linux網(wǎng)絡(luò)實(shí)現(xiàn)體系相對(duì)比,深入的了解學(xué)習(xí)linux內(nèi)核是怎樣具體的實(shí)現(xiàn)TCP/IP協(xié)議棧的。
Linux網(wǎng)絡(luò)體系與TCP/IP協(xié)議棧如下圖所示。? ? ? ?
? ? ?
? ? ???
?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?圖2 linux網(wǎng)絡(luò)體系? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? 圖3 TCP/IP協(xié)議棧和OSI參考模型對(duì)應(yīng)關(guān)系
可以看到,在圖2中,linux為了抽象與實(shí)現(xiàn)相分離,將內(nèi)核中的網(wǎng)絡(luò)部分劃分為五層:
Linux網(wǎng)絡(luò)子系統(tǒng)通過這五層結(jié)構(gòu)的相互交互,共同完成TCP/IP協(xié)議棧的運(yùn)行。
Linux網(wǎng)絡(luò)協(xié)議棧的架構(gòu)如下圖所示。該圖展示了如何實(shí)現(xiàn)Internet模型,在最上面的是用戶空間中實(shí)現(xiàn)的應(yīng)用層,而中間為內(nèi)核空間中實(shí)現(xiàn)的網(wǎng)絡(luò)子系統(tǒng),底部為物理設(shè)備,提供了對(duì)網(wǎng)絡(luò)的連接能力。在網(wǎng)絡(luò)協(xié)議棧內(nèi)部流動(dòng)的是套接口緩沖區(qū)(SKB),用于在協(xié)議棧的底層、上層以及應(yīng)用層之間傳遞報(bào)文數(shù)據(jù)。
網(wǎng)絡(luò)協(xié)議棧頂部是系統(tǒng)調(diào)用接口,為用戶空間中的應(yīng)用程序提供一種訪問內(nèi)核網(wǎng)絡(luò)子系統(tǒng)的接口。下面是一個(gè)協(xié)議無關(guān)層,它提供了一種通用方法來使用傳輸層協(xié)議。然后是傳輸層的具體協(xié)議,包括TCP、UDP。在傳輸層下面是網(wǎng)絡(luò)層,之后是鄰居子系統(tǒng),再下面是網(wǎng)絡(luò)設(shè)備接口,提供了與各個(gè)設(shè)備驅(qū)動(dòng)通信的通用接口。最底層是設(shè)備驅(qū)動(dòng)程序。? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?
?
?
?圖4?Linux網(wǎng)絡(luò)協(xié)議棧的架構(gòu)
通過網(wǎng)絡(luò)協(xié)議棧通信需要對(duì)套接口進(jìn)行操作,套接口是一個(gè)與協(xié)議無關(guān)的接口,它提供了一組接口來支持各種協(xié)議,套接口層不但可以支持典型的TCP和UDP協(xié)議,還可以支持RAW套接口、RAW以太網(wǎng)以及其他傳輸協(xié)議。
Linux中使用socket結(jié)構(gòu)描述套接口,代表一條通信鏈路的一端,用來存儲(chǔ)與該鏈路有關(guān)的所有信息,這些信息中包括:
其示意圖如下所示:
圖5?使用socket結(jié)構(gòu)描述套接口
其中最關(guān)鍵的成員是sk和ops,sk指向與該套接口相關(guān)的傳輸控制塊,ops指向特定的傳輸協(xié)議的操作集。
下圖詳細(xì)展示了socket結(jié)構(gòu)體中的sk和ops字段,以TCP為例。
?
圖6 TCP的socket結(jié)構(gòu)體中的sk和ops字段
?
?
sk字段指向與該套接口相關(guān)的傳輸控制塊,傳輸層使用傳輸控制塊來存放套接口所需的信息,在上圖中即為TCP傳輸控制塊,即tcp_sock
結(jié)構(gòu)。
ops字段指向特定傳輸協(xié)議的操作集接口,proto_pos
結(jié)構(gòu)中定義的接口函數(shù)是從套接口系統(tǒng)調(diào)用到傳輸層調(diào)用的入口,因此其成員與socket系統(tǒng)調(diào)用基本上是一一對(duì)應(yīng)的。整個(gè)proto_ops
結(jié)構(gòu)就是一張?zhí)捉涌谙到y(tǒng)調(diào)用的跳轉(zhuǎn)表,TCP、UDP、RAW套接口的傳輸層操作集分別為inet_stream_ops
、inet_dgram_ops
、inet_sockraw_ops。
如下圖所示,網(wǎng)絡(luò)子系統(tǒng)中用來存儲(chǔ)數(shù)據(jù)的緩沖區(qū)叫做套接口緩存,簡(jiǎn)稱為SKB,該緩存區(qū)能夠處理可變長(zhǎng)數(shù)據(jù),即能夠很容易地在數(shù)據(jù)區(qū)頭尾部添加和移除數(shù)據(jù),且盡量避免數(shù)據(jù)的復(fù)制,通常每個(gè)報(bào)文使用一個(gè)SKB表示,各協(xié)議棧報(bào)文頭通過一組指針進(jìn)行定位,由于SKB是網(wǎng)絡(luò)子系統(tǒng)中數(shù)據(jù)管理的核心,因此有很多管理函數(shù)是對(duì)它進(jìn)行操作的。
SKB主要用于在網(wǎng)絡(luò)驅(qū)動(dòng)程序和應(yīng)用程序之間傳遞、復(fù)制數(shù)據(jù)包。當(dāng)應(yīng)用程序要發(fā)送一個(gè)數(shù)據(jù)包時(shí):
當(dāng)網(wǎng)絡(luò)設(shè)備接收到數(shù)據(jù)包也要分配一個(gè)SKB來對(duì)數(shù)據(jù)進(jìn)行存儲(chǔ),之后再向上傳遞,最終將數(shù)據(jù)復(fù)制到應(yīng)用程序后進(jìn)行釋放。
?
圖7 SKB原理
sk_buf是Linux網(wǎng)絡(luò)協(xié)議棧最重要的數(shù)據(jù)結(jié)構(gòu)之一,該數(shù)據(jù)結(jié)構(gòu)貫穿于整個(gè)數(shù)據(jù)包處理的流程。由于協(xié)議采用分層結(jié)構(gòu),上層向下層傳遞數(shù)據(jù)時(shí)需要增加包頭,下層向上層數(shù)據(jù)時(shí)又需要去掉包頭。sk_buff中保存了L2,L3,L4層的頭指針,這樣在層傳遞時(shí)只需要對(duì)數(shù)據(jù)緩沖區(qū)改變頭部信息,并調(diào)整sk_buff中的指針,而不需要拷貝數(shù)據(jù),這樣大大減少了內(nèi)存拷貝的需要。
sk_buf的示意圖如下:
?
圖8 sk_buf示意圖
各字段含義如下:
操作sk_buf的簡(jiǎn)單示意圖如下:
?
?
?
?圖9?sk_buf操作前后示意圖
在網(wǎng)絡(luò)適配器硬件和軟件協(xié)議棧之間需要一個(gè)接口,共同完成操作系統(tǒng)內(nèi)核中協(xié)議棧數(shù)據(jù)處理與異步收發(fā)的功能。在Linux網(wǎng)絡(luò)體系結(jié)構(gòu)中,這個(gè)接口要滿足以下要求:
?。?)抽象出網(wǎng)絡(luò)適配器的硬件特性。
?。?)為協(xié)議棧提供統(tǒng)一的調(diào)用接口。
以上兩個(gè)要求在Linux內(nèi)核的網(wǎng)絡(luò)體系結(jié)構(gòu)中分別由兩個(gè)軟件(設(shè)備獨(dú)立接口文件dev.c和網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序)和一個(gè)主要的數(shù)據(jù)結(jié)構(gòu)net_device實(shí)現(xiàn)。
設(shè)備獨(dú)立接口文件dev.c中實(shí)現(xiàn)了對(duì)上層協(xié)議的統(tǒng)一調(diào)用接口,dev.c文件中的函數(shù)實(shí)現(xiàn)了以下主要功能。
每一個(gè)網(wǎng)絡(luò)設(shè)備都必須有一個(gè)驅(qū)動(dòng)程序,并提供一個(gè)初始化函數(shù)供內(nèi)核啟動(dòng)時(shí)調(diào)用,或在裝載網(wǎng)絡(luò)驅(qū)動(dòng)程序模塊時(shí)調(diào)用。不管網(wǎng)絡(luò)設(shè)備內(nèi)部有什么不同,有一件事是所有網(wǎng)絡(luò)設(shè)備驅(qū)動(dòng)程序必須首先完成的任務(wù):初始化一個(gè)net_device數(shù)據(jù)結(jié)構(gòu)的實(shí)例作為網(wǎng)絡(luò)設(shè)備在內(nèi)核中的實(shí)體,并將net_device數(shù)據(jù)結(jié)構(gòu)實(shí)例的各數(shù)據(jù)域初始化為可工作的狀態(tài),然后將設(shè)備實(shí)例注冊(cè)到內(nèi)核中,為協(xié)議棧提供傳送服務(wù)。
net_device數(shù)據(jù)結(jié)構(gòu)從以下兩個(gè)方面描述了網(wǎng)絡(luò)設(shè)備的硬件特性在內(nèi)核中的表示。
net_device數(shù)據(jù)結(jié)構(gòu)實(shí)例是網(wǎng)絡(luò)設(shè)備在內(nèi)核中的表示,它是每個(gè)網(wǎng)絡(luò)設(shè)備在內(nèi)核中的基礎(chǔ)數(shù)據(jù)結(jié)構(gòu),它包含的信息不僅僅是網(wǎng)絡(luò)設(shè)備的硬件屬性(中斷、端口地址、驅(qū)動(dòng)程序函數(shù)等),還包括網(wǎng)絡(luò)中與設(shè)備有關(guān)的上層協(xié)議棧的配置信息(如IP地址、子網(wǎng)掩碼等)。它跟蹤連接到?TCP/IP協(xié)議棧上的所有設(shè)備的狀態(tài)信息。
net_device數(shù)據(jù)結(jié)構(gòu)代表了上層的網(wǎng)絡(luò)協(xié)議和硬件之間的一個(gè)通用接口,使我們可以將網(wǎng)絡(luò)協(xié)議層的實(shí)現(xiàn)從具體的網(wǎng)絡(luò)硬件部件中抽象出來,獨(dú)立于硬件設(shè)備。為了有效地實(shí)現(xiàn)這種抽象,net_device中使用了大量函數(shù)指針,這樣相對(duì)于上層的協(xié)議棧,它們?cè)谧鰯?shù)據(jù)收發(fā)操作時(shí)調(diào)用的函數(shù)的名字是相同的,但具體的函數(shù)實(shí)現(xiàn)細(xì)節(jié)可以根據(jù)不同的網(wǎng)絡(luò)適配器而不同,由設(shè)備驅(qū)動(dòng)程序提供,對(duì)網(wǎng)絡(luò)協(xié)議棧透明。
?
?
?圖10 網(wǎng)絡(luò)設(shè)備抽象數(shù)據(jù)結(jié)構(gòu)
內(nèi)核中的進(jìn)程可以通過socket結(jié)構(gòu)體來訪問linux內(nèi)核中的網(wǎng)絡(luò)系統(tǒng)中的傳輸層、網(wǎng)絡(luò)層以及數(shù)據(jù)鏈路層,也可以說socket是內(nèi)核中的進(jìn)程與內(nèi)核中的網(wǎng)絡(luò)系統(tǒng)的橋梁。
我們知道在TCP層中使用兩個(gè)協(xié)議:tcp協(xié)議和udp協(xié)議。而在將TCP層中的數(shù)據(jù)往下傳輸時(shí),要使用網(wǎng)絡(luò)層的協(xié)議,而網(wǎng)絡(luò)層的協(xié)議很多,不同的網(wǎng)絡(luò)使用不同的網(wǎng)絡(luò)層協(xié)議。我們常用的因特網(wǎng)中,網(wǎng)絡(luò)層使用的是IPV4和IPV6協(xié)議。所以在內(nèi)核中的進(jìn)程在使用struct socket提取內(nèi)核網(wǎng)絡(luò)系統(tǒng)中的數(shù)據(jù)時(shí),不光要指明struct socket的類型(用于說明是提取TCP層中tcp協(xié)議負(fù)載的數(shù)據(jù),還是udp層負(fù)載的數(shù)據(jù)),還要指明網(wǎng)絡(luò)層的協(xié)議類型(網(wǎng)絡(luò)層的協(xié)議用于負(fù)載TCP層中的數(shù)據(jù))。
linux內(nèi)核中的網(wǎng)絡(luò)系統(tǒng)中的網(wǎng)絡(luò)層的協(xié)議,在linux中被稱為address family(地址簇,通常以AF_XXX表示)或protocol family(協(xié)議簇,通常以PF_XXX表示)。
?
首先當(dāng)數(shù)據(jù)幀從網(wǎng)線到達(dá)網(wǎng)卡上的時(shí)候,第一站是網(wǎng)卡的接收隊(duì)列。網(wǎng)卡在分配給自己的RingBuffer中尋找可用的內(nèi)存位置,找到后DMA引擎會(huì)把數(shù)據(jù)DMA到網(wǎng)卡之前關(guān)聯(lián)的內(nèi)存里,這個(gè)時(shí)候CPU都是無感的。當(dāng)DMA操作完成以后,網(wǎng)卡會(huì)像CPU發(fā)起一個(gè)硬中斷,通知CPU有數(shù)據(jù)到達(dá)。
?
?
?圖11 網(wǎng)卡數(shù)據(jù)硬中斷處理過程
注意,當(dāng)RingBuffer滿的時(shí)候,新來的數(shù)據(jù)包將給丟棄。ifconfig查看網(wǎng)卡的時(shí)候,可以里面有個(gè)overruns,表示因?yàn)榄h(huán)形隊(duì)列滿被丟棄的包。如果發(fā)現(xiàn)有丟包,可能需要通過ethtool命令來加大環(huán)形隊(duì)列的長(zhǎng)度。
網(wǎng)卡的硬中斷注冊(cè)的處理函數(shù)是igb_msix_ring。
//file: drivers/net/ethernet/intel/igb/igb_main.cstatic irqreturn_t igb_msix_ring(int irq, void *data){ struct igb_q_vector *q_vector = data; /* Write the ITR value calculated from the previous interrupt. */ igb_write_itr(q_vector); napi_schedule(&q_vector->napi); return IRQ_HANDLED;}
igb_write_itr
只是記錄一下硬件中斷頻率(據(jù)說目的是在減少對(duì)CPU的中斷頻率時(shí)用到)。順著napi_schedule調(diào)用一路跟蹤下去,__napi_schedule
=>____napi_schedule。
/* Called with irq disabled */static inline void ____napi_schedule(struct softnet_data *sd, struct napi_struct *napi){ list_add_tail(&napi->poll_list, &sd->poll_list); __raise_softirq_irqoff(NET_RX_SOFTIRQ);}
這里我們看到,list_add_tail
修改了CPU變量softnet_data里的poll_list,將驅(qū)動(dòng)napi_struct傳過來的poll_list添加了進(jìn)來。 其中softnet_data中的poll_list是一個(gè)雙向列表,其中的設(shè)備都帶有輸入幀等著被處理。緊接著__raise_softirq_irqoff
觸發(fā)了一個(gè)軟中斷NET_RX_SOFTIRQ, 這個(gè)所謂的觸發(fā)過程只是對(duì)一個(gè)變量進(jìn)行了一次或運(yùn)算而已。
void __raise_softirq_irqoff(unsigned int nr){ trace_softirq_raise(nr); or_softirq_pending(1UL << nr);}//file: include/linux/irq_cpustat.h#define or_softirq_pending(x) (local_softirq_pending() |= (x))
Linux在硬中斷里只完成簡(jiǎn)單必要的工作,剩下的大部分的處理都是轉(zhuǎn)交給軟中斷的。通過上面代碼可以看到,硬中斷處理過程真的是非常短。只是記錄了一個(gè)寄存器,修改了一下下CPU的poll_list,然后發(fā)出個(gè)軟中斷。就這么簡(jiǎn)單,硬中斷工作就算是完成了。
?
圖12 ksoftirqd內(nèi)核線程
?
?ksoftirqd_should_run
代碼如下:
static int ksoftirqd_should_run(unsigned int cpu){ return local_softirq_pending();}#define local_softirq_pending() __IRQ_STAT(smp_processor_id(), __softirq_pending)
這里看到和硬中斷中調(diào)用了同一個(gè)函數(shù)local_softirq_pending
。使用方式不同的是硬中斷位置是為了寫入標(biāo)記,這里僅僅只是讀取。如果硬中斷中設(shè)置了NET_RX_SOFTIRQ
,這里自然能讀取的到。接下來會(huì)真正進(jìn)入線程函數(shù)中run_ksoftirqd
處理:
static void run_ksoftirqd(unsigned int cpu){ local_irq_disable(); if (local_softirq_pending()) { __do_softirq(); rcu_note_context_switch(cpu); local_irq_enable(); cond_resched(); return; } local_irq_enable();}
在__do_softirq
中,判斷根據(jù)當(dāng)前CPU的軟中斷類型,調(diào)用其注冊(cè)的action方法。
asmlinkage void __do_softirq(void){ do { if (pending & 1) { unsigned int vec_nr = h - softirq_vec; int prev_count = preempt_count(); ... trace_softirq_entry(vec_nr); h->action(h); trace_softirq_exit(vec_nr); ... } h ; pending >>= 1; } while (pending);}
在網(wǎng)絡(luò)子系統(tǒng)初始化小節(jié), 我們看到我們?yōu)镹ET_RX_SOFTIRQ注冊(cè)了處理函數(shù)net_rx_action。所以net_rx_action
函數(shù)就會(huì)被執(zhí)行到了。
這里需要注意一個(gè)細(xì)節(jié),硬中斷中設(shè)置軟中斷標(biāo)記,和ksoftirq的判斷是否有軟中斷到達(dá),都是基于smp_processor_id()的。這意味著只要硬中斷在哪個(gè)CPU上被響應(yīng),那么軟中斷也是在這個(gè)CPU上處理的。所以說,如果你發(fā)現(xiàn)你的Linux軟中斷CPU消耗都集中在一個(gè)核上的話,做法是要把調(diào)整硬中斷的CPU親和性,來將硬中斷打散到不通的CPU核上去。
我們?cè)賮戆丫械竭@個(gè)核心函數(shù)net_rx_action
上來。
static void net_rx_action(struct softirq_action *h){ struct softnet_data *sd = &__get_cpu_var(softnet_data); unsigned long time_limit = jiffies 2; int budget = netdev_budget; void *have; local_irq_disable(); while (!list_empty(&sd->poll_list)) { ...... n = list_first_entry(&sd->poll_list, struct napi_struct, poll_list); work = 0; if (test_bit(NAPI_STATE_SCHED, &n->state)) { work = n->poll(n, weight); trace_napi_poll(n); } budget -= work; }}
函數(shù)開頭的time_limit和budget是用來控制net_rx_action函數(shù)主動(dòng)退出的,目的是保證網(wǎng)絡(luò)包的接收不霸占CPU不放。 等下次網(wǎng)卡再有硬中斷過來的時(shí)候再處理剩下的接收數(shù)據(jù)包。其中budget可以通過內(nèi)核參數(shù)調(diào)整。 這個(gè)函數(shù)中剩下的核心邏輯是獲取到當(dāng)前CPU變量softnet_data,對(duì)其poll_list進(jìn)行遍歷, 然后執(zhí)行到網(wǎng)卡驅(qū)動(dòng)注冊(cè)到的poll函數(shù)。對(duì)于igb網(wǎng)卡來說,就是igb驅(qū)動(dòng)力的igb_poll
函數(shù)了。
/** * igb_poll - NAPI Rx polling callback * @napi: napi polling structure * @budget: count of how many packets we should handle **/static int igb_poll(struct napi_struct *napi, int budget){ ... if (q_vector->tx.ring) clean_complete = igb_clean_tx_irq(q_vector); if (q_vector->rx.ring) clean_complete &= igb_clean_rx_irq(q_vector, budget); ...}
在讀取操作中,igb_poll
的重點(diǎn)工作是對(duì)igb_clean_rx_irq
的調(diào)用。
static bool igb_clean_rx_irq(struct igb_q_vector *q_vector, const int budget){ ... do { /* retrieve a buffer from the ring */ skb = igb_fetch_rx_buffer(rx_ring, rx_desc, skb); /* fetch next buffer in frame if non-eop */ if (igb_is_non_eop(rx_ring, rx_desc)) continue; } /* verify the packet layout is correct */ if (igb_cleanup_headers(rx_ring, rx_desc, skb)) { skb = NULL; continue; } /* populate checksum, timestamp, VLAN, and protocol */ igb_process_skb_fields(rx_ring, rx_desc, skb); napi_gro_receive(&q_vector->napi, skb);}
igb_fetch_rx_buffer
和igb_is_non_eop
的作用就是把數(shù)據(jù)幀從RingBuffer上取下來。為什么需要兩個(gè)函數(shù)呢?因?yàn)橛锌赡軒级喽鄠€(gè)RingBuffer,所以是在一個(gè)循環(huán)中獲取的,直到幀尾部。獲取下來的一個(gè)數(shù)據(jù)幀用一個(gè)sk_buff來表示。收取完數(shù)據(jù)以后,對(duì)其進(jìn)行一些校驗(yàn),然后開始設(shè)置sbk變量的timestamp, VLAN id, protocol等字段。接下來進(jìn)入到napi_gro_receive中:
//file: net/core/dev.cgro_result_t napi_gro_receive(struct napi_struct *napi, struct sk_buff *skb){ skb_gro_reset_offset(skb); return napi_skb_finish(dev_gro_receive(napi, skb), skb);}
dev_gro_receive
這個(gè)函數(shù)代表的是網(wǎng)卡GRO特性,可以簡(jiǎn)單理解成把相關(guān)的小包合并成一個(gè)大包就行,目的是減少傳送給網(wǎng)絡(luò)棧的包數(shù),這有助于減少 CPU 的使用量。我們暫且忽略,直接看napi_skb_finish
, 這個(gè)函數(shù)主要就是調(diào)用了netif_receive_skb
。
//file: net/core/dev.cstatic gro_result_t napi_skb_finish(gro_result_t ret, struct sk_buff *skb){ switch (ret) { case GRO_NORMAL: if (netif_receive_skb(skb)) ret = GRO_DROP; break; ......}
在netif_receive_skb
中,數(shù)據(jù)包將被送到協(xié)議棧中。聲明,以下的3.3, 3.4, 3.5也都屬于軟中斷的處理過程,只不過由于篇幅太長(zhǎng),單獨(dú)拿出來成小節(jié)。
netif_receive_skb
函數(shù)會(huì)根據(jù)包的協(xié)議,假如是udp包,會(huì)將包依次送到ip_rcv(),udp_rcv()協(xié)議處理函數(shù)中進(jìn)行處理。
?
?
?圖13 網(wǎng)絡(luò)協(xié)議棧處理
//file: net/core/dev.cint netif_receive_skb(struct sk_buff *skb){ //RPS處理邏輯,先忽略 ...... return __netif_receive_skb(skb);}static int __netif_receive_skb(struct sk_buff *skb){ ...... ret = __netif_receive_skb_core(skb, false);}static int __netif_receive_skb_core(struct sk_buff *skb, bool pfmemalloc){ ...... //pcap邏輯,這里會(huì)將數(shù)據(jù)送入抓包點(diǎn)。tcpdump就是從這個(gè)入口獲取包的 list_for_each_entry_rcu(ptype, &ptype_all, list) { if (!ptype->dev || ptype->dev == skb->dev) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } } ...... list_for_each_entry_rcu(ptype, &ptype_base[ntohs(type) & PTYPE_HASH_MASK], list) { if (ptype->type == type && (ptype->dev == null_or_dev || ptype->dev == skb->dev || ptype->dev == orig_dev)) { if (pt_prev) ret = deliver_skb(skb, pt_prev, orig_dev); pt_prev = ptype; } }}
在__netif_receive_skb_core
中,我看著原來經(jīng)常使用的tcpdump的抓包點(diǎn),很是激動(dòng),看來讀一遍源代碼時(shí)間真的沒白浪費(fèi)。接著__netif_receive_skb_core
取出protocol,它會(huì)從數(shù)據(jù)包中取出協(xié)議信息,然后遍歷注冊(cè)在這個(gè)協(xié)議上的回調(diào)函數(shù)列表。ptype_base
?是一個(gè) hash table,在協(xié)議注冊(cè)小節(jié)我們提到過。ip_rcv 函數(shù)地址就是存在這個(gè) hash table中的。
//file: net/core/dev.cstatic inline int deliver_skb(struct sk_buff *skb, struct packet_type *pt_prev, struct net_device *orig_dev){ ...... return pt_prev->func(skb, skb->dev, pt_prev, orig_dev);}
pt_prev->func
這一行就調(diào)用到了協(xié)議層注冊(cè)的處理函數(shù)了。對(duì)于ip包來講,就會(huì)進(jìn)入到ip_rcv
(如果是arp包的話,會(huì)進(jìn)入到arp_rcv)。
我們?cè)賮泶笾驴匆幌耹inux在ip協(xié)議層都做了什么,包又是怎么樣進(jìn)一步被送到udp或tcp協(xié)議處理函數(shù)中的。
//file: net/ipv4/ip_input.cint ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev){ ...... return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, skb, dev, NULL, ip_rcv_finish);}
這里NF_HOOK
是一個(gè)鉤子函數(shù),當(dāng)執(zhí)行完注冊(cè)的鉤子后就會(huì)執(zhí)行到最后一個(gè)參數(shù)指向的函數(shù)ip_rcv_finish
。
static int ip_rcv_finish(struct sk_buff *skb){ ...... if (!skb_dst(skb)) { int err = ip_route_input_noref(skb, iph->daddr, iph->saddr, iph->tos, skb->dev); ... } ...... return dst_input(skb);}
跟蹤ip_route_input_noref
?后看到它又調(diào)用了?ip_route_input_mc
。 在ip_route_input_mc
中,函數(shù)ip_local_deliver
被賦值給了dst.input
, 如下:
//file: net/ipv4/route.cstatic int ip_route_input_mc(struct sk_buff *skb, __be32 daddr, __be32 saddr, u8 tos, struct net_device *dev, int our){ if (our) { rth->dst.input= ip_local_deliver; rth->rt_flags |= RTCF_LOCAL; }}
所以回到ip_rcv_finish
中的return dst_input(skb)
。
/* Input packet from network to transport. */static inline int dst_input(struct sk_buff *skb){ return skb_dst(skb)->input(skb);}
skb_dst(skb)->input
調(diào)用的input方法就是路由子系統(tǒng)賦的ip_local_deliver。
//file: net/ipv4/ip_input.cint ip_local_deliver(struct sk_buff *skb){ /* * Reassemble IP fragments. */ if (ip_is_fragment(ip_hdr(skb))) { if (ip_defrag(skb, IP_DEFRAG_LOCAL_DELIVER)) return 0; } return NF_HOOK(NFPROTO_IPV4, NF_INET_LOCAL_IN, skb, skb->dev, NULL, ip_local_deliver_finish);}static int ip_local_deliver_finish(struct sk_buff *skb){ ...... int protocol = ip_hdr(skb)->protocol; const struct net_protocol *ipprot; ipprot = rcu_dereference(inet_protos[protocol]); if (ipprot != NULL) { ret = ipprot->handler(skb); }}
如協(xié)議注冊(cè)小節(jié)看到inet_protos中保存著tcp_rcv()和udp_rcv()的函數(shù)地址。這里將會(huì)根據(jù)包中的協(xié)議類型選擇進(jìn)行分發(fā),在這里skb包將會(huì)進(jìn)一步被派送到更上層的協(xié)議中,udp和tcp。
udp協(xié)議的處理函數(shù)是udp_rcv
。
//file: net/ipv4/udp.cint udp_rcv(struct sk_buff *skb){ return __udp4_lib_rcv(skb, &udp_table, IPPROTO_UDP);}int __udp4_lib_rcv(struct sk_buff *skb, struct udp_table *udptable, int proto){ sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable); if (sk != NULL) { int ret = udp_queue_rcv_skb(sk, skb } icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);}
__udp4_lib_lookup_skb
是根據(jù)skb來尋找對(duì)應(yīng)的socket,當(dāng)找到以后將數(shù)據(jù)包放到socket的緩存隊(duì)列里。如果沒有找到,則發(fā)送一個(gè)目標(biāo)不可達(dá)的icmp包。
//file: net/ipv4/udp.cint udp_queue_rcv_skb(struct sock *sk, struct sk_buff *skb){ ...... if (sk_rcvqueues_full(sk, skb, sk->sk_rcvbuf)) goto drop; rc = 0; ipv4_pktinfo_prepare(skb); bh_lock_sock(sk); if (!sock_owned_by_user(sk)) rc = __udp_queue_rcv_skb(sk, skb); else if (sk_add_backlog(sk, skb, sk->sk_rcvbuf)) { bh_unlock_sock(sk); goto drop; } bh_unlock_sock(sk); return rc;}
sock_owned_by_user判斷的是用戶是不是正在這個(gè)socker上進(jìn)行系統(tǒng)調(diào)用(socket被占用),如果沒有,那就可以直接放到socket的接收隊(duì)列中。如果有,那就通過sk_add_backlog
把數(shù)據(jù)包添加到backlog隊(duì)列。 當(dāng)用戶釋放的socket的時(shí)候,內(nèi)核會(huì)檢查backlog隊(duì)列,如果有數(shù)據(jù)再移動(dòng)到接收隊(duì)列中。
sk_rcvqueues_full
接收隊(duì)列如果滿了的話,將直接把包丟棄。接收隊(duì)列大小受內(nèi)核參數(shù)net.core.rmem_max和net.core.rmem_default影響。
send的定義如下所示:
ssize_t send(int sockfd, const void *buf, size_t len, int flags)
當(dāng)在調(diào)用send
函數(shù)的時(shí)候,內(nèi)核封裝send()
為sendto()
,然后發(fā)起系統(tǒng)調(diào)用。其實(shí)也很好理解,send()
就是sendto()
的一種特殊情況,而sendto()
在內(nèi)核的系統(tǒng)調(diào)用服務(wù)程序?yàn)?code>sys_sendto,sys_sendto
的代碼如下所示:
int __sys_sendto(int fd, void __user *buff, size_t len, unsigned int flags, struct sockaddr __user *addr, int addr_len){ struct socket *sock; struct sockaddr_storage address; int err; struct msghdr msg; //用來表示要發(fā)送的數(shù)據(jù)的一些屬性 struct iovec iov; int fput_needed; err = import_single_range(WRITE, buff, len, &iov, &msg.msg_iter); if (unlikely(err)) return err; sock = sockfd_lookup_light(fd, &err, &fput_needed); if (!sock) goto out; msg.msg_name = NULL; msg.msg_control = NULL; msg.msg_controllen = 0; msg.msg_namelen = 0; if (addr) { err = move_addr_to_kernel(addr, addr_len, &address); if (err < 0) goto out_put; msg.msg_name = (struct sockaddr *)&address; msg.msg_namelen = addr_len; } if (sock->file->f_flags & O_NONBLOCK) flags |= MSG_DONTWAIT; msg.msg_flags = flags; err = sock_sendmsg(sock, &msg); //實(shí)際的發(fā)送函數(shù)out_put: fput_light(sock->file, fput_needed);out: return err;}
__sys_sendto
函數(shù)其實(shí)做了3件事:
struct socket
struct msghdr
sock_sendmsg
來執(zhí)行實(shí)際的發(fā)送繼續(xù)追蹤sock_sendmsg
,發(fā)現(xiàn)其最終調(diào)用的是sock->ops->sendmsg(sock, msg, msg_data_left(msg))
,即socet在初始化時(shí)賦值給結(jié)構(gòu)體struct proto tcp_prot
的函數(shù)tcp_sendmsg
,如下所示:
struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, .close = tcp_close, .pre_connect = tcp_v4_pre_connect, .connect = tcp_v4_connect, .disconnect = tcp_disconnect, .accept = inet_csk_accept, .ioctl = tcp_ioctl, .init = tcp_v4_init_sock, .destroy = tcp_v4_destroy_sock, .shutdown = tcp_shutdown, .setsockopt = tcp_setsockopt, .getsockopt = tcp_getsockopt, .keepalive = tcp_set_keepalive, .recvmsg = tcp_recvmsg, .sendmsg = tcp_sendmsg, ...
而tcp_send
函數(shù)實(shí)際調(diào)用的是tcp_sendmsg_locked
函數(shù),該函數(shù)的定義如下所示:
int tcp_sendmsg_locked(struct sock *sk, struct msghdr *msg, size_t size){ struct tcp_sock *tp = tcp_sk(sk);/*進(jìn)行了強(qiáng)制類型轉(zhuǎn)換*/ struct sk_buff *skb; flags = msg->msg_flags; ...... if (copied) tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH, size_goal);}
在tcp_sendmsg_locked
中,完成的是將所有的數(shù)據(jù)組織成發(fā)送隊(duì)列,這個(gè)發(fā)送隊(duì)列是struct sock
結(jié)構(gòu)中的一個(gè)域sk_write_queue
,這個(gè)隊(duì)列的每一個(gè)元素是一個(gè)skb
,里面存放的就是待發(fā)送的數(shù)據(jù)。在該函數(shù)中通過調(diào)用tcp_push()
函數(shù)將數(shù)據(jù)加入到發(fā)送隊(duì)列中。
sock
結(jié)構(gòu)體的部分代碼如下所示:
struct sock{ ... struct sk_buff_head sk_write_queue;/*指向skb隊(duì)列的第一個(gè)元素*/ ... struct sk_buff *sk_send_head;/*指向隊(duì)列第一個(gè)還沒有發(fā)送的元素*/}
tcp_push
的代碼如下所示:
static void tcp_push(struct sock *sk, int flags, int mss_now, int nonagle, int size_goal){ struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; skb = tcp_write_queue_tail(sk); if (!skb) return; if (!(flags & MSG_MORE) || forced_push(tp)) tcp_mark_push(tp, skb); tcp_mark_urg(tp, flags); if (tcp_should_autocork(sk, skb, size_goal)) { /* avoid atomic op if TSQ_THROTTLED bit is already set */ if (!test_bit(TSQ_THROTTLED, &sk->sk_tsq_flags)) { NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPAUTOCORKING); set_bit(TSQ_THROTTLED, &sk->sk_tsq_flags); } /* It is possible TX completion already happened * before we set TSQ_THROTTLED. */ if (refcount_read(&sk->sk_wmem_alloc) > skb->truesize) return; } if (flags & MSG_MORE) nonagle = TCP_NAGLE_CORK; __tcp_push_pending_frames(sk, mss_now, nonagle); //最終通過調(diào)用該函數(shù)發(fā)送數(shù)據(jù)}
在之后tcp_push調(diào)用了__tcp_push_pending_frames(sk, mss_now, nonagle);
來發(fā)送數(shù)據(jù)
__tcp_push_pending_frames
的代碼如下所示:
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle){ if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_mask(sk, GFP_ATOMIC))) //調(diào)用該函數(shù)發(fā)送數(shù)據(jù) tcp_check_probe_timer(sk);}
在__tcp_push_pending_frames
又調(diào)用了tcp_write_xmit
來發(fā)送數(shù)據(jù),代碼如下所示:
static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle, int push_one, gfp_t gfp){ struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; unsigned int tso_segs, sent_pkts; int cwnd_quota; int result; bool is_cwnd_limited = false, is_rwnd_limited = false; u32 max_segs; /*統(tǒng)計(jì)已發(fā)送的報(bào)文總數(shù)*/ sent_pkts = 0; ...... /*若發(fā)送隊(duì)列未滿,則準(zhǔn)備發(fā)送報(bào)文*/ while ((skb = tcp_send_head(sk))) { unsigned int limit; if (unlikely(tp->repair) && tp->repair_queue == TCP_SEND_QUEUE) { /* "skb_mstamp_ns" is used as a start point for the retransmit timer */ skb->skb_mstamp_ns = tp->tcp_wstamp_ns = tp->tcp_clock_cache; list_move_tail(&skb->tcp_tsorted_anchor, &tp->tsorted_sent_queue); tcp_init_tso_segs(skb, mss_now); goto repair; /* Skip network transmission */ } if (tcp_pacing_check(sk)) break; tso_segs = tcp_init_tso_segs(skb, mss_now); BUG_ON(!tso_segs); /*檢查發(fā)送窗口的大小*/ cwnd_quota = tcp_cwnd_test(tp, skb); if (!cwnd_quota) { if (push_one == 2) /* Force out a loss probe pkt. */ cwnd_quota = 1; else break; } if (unlikely(!tcp_snd_wnd_test(tp, skb, mss_now))) { is_rwnd_limited = true; break; ...... limit = mss_now; if (tso_segs > 1 && !tcp_urg_mode(tp)) limit = tcp_mss_split_point(sk, skb, mss_now, min_t(unsigned int, cwnd_quota, max_segs), nonagle); if (skb->len > limit && unlikely(tso_fragment(sk, TCP_FRAG_IN_WRITE_QUEUE, skb, limit, mss_now, gfp))) break; if (tcp_small_queue_check(sk, skb, 0)) break; if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp))) //調(diào)用該函數(shù)發(fā)送數(shù)據(jù) break; ......
tcp_write_xmit
位于tcpoutput.c中,它實(shí)現(xiàn)了tcp的擁塞控制,然后調(diào)用了tcp_transmit_skb(sk, skb, 1, gfp)
傳輸數(shù)據(jù),實(shí)際上調(diào)用的是__tcp_transmit_skb
。
__tcp_transmit_skb
的部分代碼如下所示:
static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb, int clone_it, gfp_t gfp_mask, u32 rcv_nxt){ skb_push(skb, tcp_header_size); skb_reset_transport_header(skb); ...... /* 構(gòu)建TCP頭部和校驗(yàn)和 */ th = (struct tcphdr *)skb->data; th->source = inet->inet_sport; th->dest = inet->inet_dport; th->seq = htonl(tcb->seq); th->ack_seq = htonl(rcv_nxt); tcp_options_write((__be32 *)(th 1), tp, &opts); skb_shinfo(skb)->gso_type = sk->sk_gso_type; if (likely(!(tcb->tcp_flags & TCPHDR_SYN))) { th->window = htons(tcp_select_window(sk)); tcp_ecn_send(sk, skb, th, tcp_header_size); } else { /* RFC1323: The window in SYN & SYN/ACK segments * is never scaled. */ th->window = htons(min(tp->rcv_wnd, 65535U)); } ...... icsk->icsk_af_ops->send_check(sk, skb); if (likely(tcb->tcp_flags & TCPHDR_ACK)) tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt); if (skb->len != tcp_header_size) { tcp_event_data_sent(tp, sk); tp->data_segs_out = tcp_skb_pcount(skb); tp->bytes_sent = skb->len - tcp_header_size; } if (after(tcb->end_seq, tp->snd_nxt) || tcb->seq == tcb->end_seq) TCP_ADD_STATS(sock_net(sk), TCP_MIB_OUTSEGS, tcp_skb_pcount(skb)); tp->segs_out = tcp_skb_pcount(skb); /* OK, its time to fill skb_shinfo(skb)->gso_{segs|size} */ skb_shinfo(skb)->gso_segs = tcp_skb_pcount(skb); skb_shinfo(skb)->gso_size = tcp_skb_mss(skb); /* Leave earliest departure time in skb->tstamp (skb->skb_mstamp_ns) */ /* Cleanup our debris for IP stacks */ memset(skb->cb, 0, max(sizeof(struct inet_skb_parm), sizeof(struct inet6_skb_parm))); err = icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl); //調(diào)用網(wǎng)絡(luò)層的發(fā)送接口 ......}
__tcp_transmit_skb
是位于傳輸層發(fā)送tcp數(shù)據(jù)的最后一步,這里首先對(duì)TCP數(shù)據(jù)段的頭部進(jìn)行了處理,然后調(diào)用了網(wǎng)絡(luò)層提供的發(fā)送接口:
icsk->icsk_af_ops->queue_xmit(sk, skb, &inet->cork.fl);
實(shí)現(xiàn)了數(shù)據(jù)的發(fā)送,自此,數(shù)據(jù)離開了傳輸層,傳輸層的任務(wù)也就結(jié)束了。
傳輸層時(shí)序圖如下圖所示:
?
圖14 傳輸層時(shí)序圖
?
?GDB調(diào)試如下所示。
?
?
圖15 GDB調(diào)試
將TCP傳輸過來的數(shù)據(jù)包打包成IP數(shù)據(jù)報(bào),將數(shù)據(jù)打包成IP數(shù)據(jù)包之后,通過調(diào)用ip_local_out
函數(shù),在該函數(shù)內(nèi)部調(diào)用了__ip_local_out
,該函數(shù)返回了一個(gè)nf_hook
函數(shù),在該函數(shù)內(nèi)部調(diào)用了dst_output
int ip_queue_xmit(struct sock *sk, struct sk_buff *skb, struct flowi *fl){ struct inet_sock *inet = inet_sk(sk); struct net *net = sock_net(sk); struct ip_options_rcu *inet_opt; struct flowi4 *fl4; struct rtable *rt; struct iphdr *iph; int res; /* Skip all of this if the packet is already routed, * f.e. by something like SCTP. */ rcu_read_lock(); /* * 如果待輸出的數(shù)據(jù)包已準(zhǔn)備好路由緩存, * 則無需再查找路由,直接跳轉(zhuǎn)到packet_routed * 處作處理。 */ inet_opt = rcu_dereference(inet->inet_opt); fl4 = &fl->u.ip4; rt = skb_rtable(skb); if (rt) goto packet_routed; /* Make sure we can route this packet. */ /* * 如果輸出該數(shù)據(jù)包的傳輸控制塊中 * 緩存了輸出路由緩存項(xiàng),則需檢測(cè) * 該路由緩存項(xiàng)是否過期。 * 如果過期,重新通過輸出網(wǎng)絡(luò)設(shè)備、 * 目的地址、源地址等信息查找輸出 * 路由緩存項(xiàng)。如果查找到對(duì)應(yīng)的路 * 由緩存項(xiàng),則將其緩存到傳輸控制 * 塊中,否則丟棄該數(shù)據(jù)包。 * 如果未過期,則直接使用緩存在 * 傳輸控制塊中的路由緩存項(xiàng)。 */ rt = (struct rtable *)__sk_dst_check(sk, 0); if (!rt) { /* 緩存過期 */ __be32 daddr; /* Use correct destination address if we have options. */ daddr = inet->inet_daddr; /* 目的地址 */ if (inet_opt && inet_opt->opt.srr) daddr = inet_opt->opt.faddr; /* 嚴(yán)格路由選項(xiàng) */ /* If this fails, retransmit mechanism of transport layer will * keep trying until route appears or the connection times * itself out. */ /* 查找路由緩存 */ rt = ip_route_output_ports(net, fl4, sk, daddr, inet->inet_saddr, inet->inet_dport, inet->inet_sport, sk->sk_protocol, RT_CONN_FLAGS(sk), sk->sk_bound_dev_if); if (IS_ERR(rt)) goto no_route; sk_setup_caps(sk, &rt->dst); /* 設(shè)置控制塊的路由緩存 */ } skb_dst_set_noref(skb, &rt->dst);/* 將路由設(shè)置到skb中 */packet_routed: if (inet_opt && inet_opt->opt.is_strictroute && rt->rt_uses_gateway) goto no_route; /* OK, we know where to send it, allocate and build IP header. */ /* * 設(shè)置IP首部中各字段的值。如果存在IP選項(xiàng), * 則在IP數(shù)據(jù)包首部中構(gòu)建IP選項(xiàng)。 */ skb_push(skb, sizeof(struct iphdr) (inet_opt ? inet_opt->opt.optlen : 0)); skb_reset_network_header(skb); iph = ip_hdr(skb);/* 構(gòu)造ip頭 */ *((__be16 *)iph) = htons((4 << 12) | (5 << 8) | (inet->tos & 0xff)); if (ip_dont_fragment(sk, &rt->dst) && !skb->ignore_df) iph->frag_off = htons(IP_DF); else iph->frag_off = 0; iph->ttl = ip_select_ttl(inet, &rt->dst); iph->protocol = sk->sk_protocol; ip_copy_addrs(iph, fl4); /* Transport layer set skb->h.foo itself. */ /* 構(gòu)造ip選項(xiàng) */ if (inet_opt && inet_opt->opt.optlen) { iph->ihl = inet_opt->opt.optlen >> 2; ip_options_build(skb, &inet_opt->opt, inet->inet_daddr, rt, 0); } ip_select_ident_segs(net, skb, sk, skb_shinfo(skb)->gso_segs ?: 1); /* TODO : should we use skb->sk here instead of sk ? */ /* * 設(shè)置輸出數(shù)據(jù)包的QoS類型。 */ skb->priority = sk->sk_priority; skb->mark = sk->sk_mark; res = ip_local_out(net, sk, skb); /* 輸出 */ rcu_read_unlock(); return res;no_route: rcu_read_unlock(); /* * 如果查找不到對(duì)應(yīng)的路由緩存項(xiàng), * 在此處理,將該數(shù)據(jù)包丟棄。 */ IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES); kfree_skb(skb); return -EHOSTUNREACH;}
dst_output()
實(shí)際調(diào)用skb_dst(skb)->output(skb)
,skb_dst(skb)
就是skb所對(duì)應(yīng)的路由項(xiàng)。skb_dst(skb)
指向的是路由項(xiàng)dst_entry,它的input在收到報(bào)文時(shí)賦值ip_local_deliver()
,而output在發(fā)送報(bào)文時(shí)賦值ip_output()
,該函數(shù)的作用是處理單播數(shù)據(jù)報(bào),設(shè)置數(shù)據(jù)報(bào)的輸出網(wǎng)絡(luò)設(shè)備以及網(wǎng)絡(luò)層協(xié)議類型參數(shù)。隨后調(diào)用ip_finish_output,觀察數(shù)據(jù)報(bào)長(zhǎng)度是否大于MTU,若大于,則調(diào)用ip_fragment分片,否則調(diào)用ip_finish_output2輸出。在ip_finish_output2函數(shù)中會(huì)檢測(cè)skb的前部空間是否還能存儲(chǔ)鏈路層首部。如果不夠,就會(huì)申請(qǐng)更大的存儲(chǔ)空間,最終會(huì)調(diào)用鄰居子系統(tǒng)的輸出函數(shù)neigh_output
進(jìn)行輸出,輸出分為有二層頭緩存和沒有兩種情況,有緩存時(shí)調(diào)用neigh_hh_output
進(jìn)行快速輸出,沒有緩存時(shí),則調(diào)用鄰居子系統(tǒng)的輸出回調(diào)函數(shù)進(jìn)行慢速輸出。
網(wǎng)絡(luò)層時(shí)序圖如下圖所示。
?
?
?圖16 網(wǎng)絡(luò)層時(shí)序圖
GDB調(diào)試結(jié)果如下。
?
?
圖17 GDB調(diào)試結(jié)果
網(wǎng)絡(luò)層最終會(huì)通過調(diào)用dev_queue_xmit
來發(fā)送報(bào)文,在該函數(shù)中調(diào)用的是__dev_queue_xmit(skb, NULL);
,如下所示:
int dev_queue_xmit(struct sk_buff *skb){ return __dev_queue_xmit(skb, NULL);}
直接調(diào)用__dev_queue_xmit
傳入的參數(shù)是一個(gè)skb 數(shù)據(jù)包
__dev_queue_xmit
函數(shù)會(huì)根據(jù)不同的情況會(huì)調(diào)用__dev_xmit_skb
或者sch_direct_xmit
函數(shù),最終會(huì)調(diào)用dev_hard_start_xmit
函數(shù),該函數(shù)最終會(huì)調(diào)用xmit_one
來發(fā)送一到多個(gè)數(shù)據(jù)包。
數(shù)據(jù)鏈路層時(shí)序圖如下所示。
?
?
圖18 數(shù)據(jù)鏈路層時(shí)序圖
GDB調(diào)試結(jié)果。
?
?
圖19 GDB調(diào)試結(jié)果
在數(shù)據(jù)鏈路層接受數(shù)據(jù)并傳遞給上層的步驟如下所示:
skb_buff
?數(shù)據(jù)結(jié)構(gòu),并將接收到的數(shù)據(jù)幀從網(wǎng)絡(luò)適配器I/O端口拷貝到skb_buff
?緩沖區(qū)中;從數(shù)據(jù)幀中提取出一些信息,并設(shè)置?skb_buff
相應(yīng)的參數(shù),這些參數(shù)將被上層的網(wǎng)絡(luò)協(xié)議使用,例如skb->protocol;
netif_rx;
(2)通過NAPI機(jī)制。該中斷處理程序調(diào)用 Network device的?netif_rx_schedule
函數(shù),進(jìn)入軟中斷處理流程,再調(diào)用net_rx_action
函數(shù)。netif _receive_skb
處理流程。netif_receive_skb
是鏈路層接收數(shù)據(jù)報(bào)的最后一站。它根據(jù)注冊(cè)在全局?jǐn)?shù)組 ptype_all 和 ptype_base 里的網(wǎng)絡(luò)層數(shù)據(jù)報(bào)類型,把數(shù)據(jù)報(bào)遞交給不同的網(wǎng)絡(luò)層協(xié)議的接收函數(shù)(INET域中主要是ip_rcv和arp_rcv)。該函數(shù)主要就是調(diào)用第三層協(xié)議的接收函數(shù)處理該skb包,進(jìn)入第三層網(wǎng)絡(luò)層處理。數(shù)據(jù)鏈路層的時(shí)序圖如下所示。
?
圖20 數(shù)據(jù)鏈路層時(shí)序圖
GDB調(diào)試如下。
?
?
?圖21 GDB調(diào)試
ip層的入口在ip_rcv
函數(shù),該函數(shù)首先會(huì)做包括 package checksum 在內(nèi)的各種檢查,如果需要的話會(huì)做 IP defragment(將多個(gè)分片合并),然后 packet 調(diào)用已經(jīng)注冊(cè)的 Pre-routing netfilter hook ,完成后最終到達(dá)ip_rcv_finish
函數(shù)。
int ip_rcv(struct sk_buff *skb, struct net_device *dev, struct packet_type *pt, struct net_device *orig_dev){ struct net *net = dev_net(dev); skb = ip_rcv_core(skb, net); if (skb == NULL) return NET_RX_DROP; return NF_HOOK(NFPROTO_IPV4, NF_INET_PRE_ROUTING, net, NULL, skb, dev, NULL, ip_rcv_finish);}
ip_rcv_finish
函數(shù)如下所示:
static int ip_rcv_finish(struct net *net, struct sock *sk, struct sk_buff *skb){ struct net_device *dev = skb->dev; int ret; /* if ingress device is enslaved to an L3 master device pass the * skb to its handler for processing */ skb = l3mdev_ip_rcv(skb); if (!skb) return NET_RX_SUCCESS; ret = ip_rcv_finish_core(net, sk, skb, dev, NULL); if (ret != NET_RX_DROP) ret = dst_input(skb); return ret;}
ip_rcv_finish 函數(shù)最終會(huì)調(diào)用ip_route_input
函數(shù),進(jìn)入路由處理環(huán)節(jié)。它首先會(huì)調(diào)用 ip_route_input 來更新路由,然后查找 route,決定該 package 將會(huì)被發(fā)到本機(jī)還是會(huì)被轉(zhuǎn)發(fā)還是丟棄:
ip_local_deliver
?函數(shù),可能會(huì)做 de-fragment(合并多個(gè) IP packet),然后調(diào)用ip_local_deliver
函數(shù)。該函數(shù)根據(jù) package 的下一個(gè)處理層的 protocal number,調(diào)用下一層接口,包括 tcp_v4_rcv (TCP), udp_rcv (UDP),icmp_rcv (ICMP),igmp_rcv(IGMP)。對(duì)于 TCP 來說,函數(shù)?tcp_v4_rcv
?函數(shù)會(huì)被調(diào)用,從而處理流程進(jìn)入 TCP 棧。dst_input
函數(shù)。該函數(shù)會(huì) (1)處理 Netfilter Hook (2)執(zhí)行 IP fragmentation (3)調(diào)用?dev_queue_xmit
,進(jìn)入鏈路層處理流程。網(wǎng)絡(luò)層時(shí)序圖如下圖所示。
?
?
圖22 網(wǎng)絡(luò)層時(shí)序圖
GDB調(diào)試如下圖所示。
?
?
圖23 GDB調(diào)試
對(duì)于recv
函數(shù),與send
函數(shù)類似,調(diào)用的系統(tǒng)調(diào)用是__sys_recvfrom
,其代碼如下所示:
int __sys_recvfrom(int fd, void __user *ubuf, size_t size, unsigned int flags, struct sockaddr __user *addr, int __user *addr_len){ ...... err = import_single_range(READ, ubuf, size, &iov, &msg.msg_iter); if (unlikely(err)) return err; sock = sockfd_lookup_light(fd, &err, &fput_needed); ..... msg.msg_control = NULL; msg.msg_controllen = 0; /* Save some cycles and don't copy the address if not needed */ msg.msg_name = addr ? (struct sockaddr *)&address : NULL; /* We assume all kernel code knows the size of sockaddr_storage */ msg.msg_namelen = 0; msg.msg_iocb = NULL; msg.msg_flags = 0; if (sock->file->f_flags & O_NONBLOCK) flags |= MSG_DONTWAIT; err = sock_recvmsg(sock, &msg, flags); //調(diào)用該函數(shù)接受數(shù)據(jù) if (err >= 0 && addr != NULL) { err2 = move_addr_to_user(&address, msg.msg_namelen, addr, addr_len); .....}
__sys_recvfrom
通過調(diào)用sock_recvmsg
來對(duì)數(shù)據(jù)進(jìn)行接收,該函數(shù)實(shí)際調(diào)用的是sock->ops->recvmsg(sock, msg, msg_data_left(msg), flags);
?,同樣類似send函數(shù)中,調(diào)用的實(shí)際上是socket
在初始化時(shí)賦值給結(jié)構(gòu)體struct proto tcp_prot
的函數(shù)tcp_rcvmsg
,如下所示:
struct proto tcp_prot = { .name = "TCP", .owner = THIS_MODULE, .close = tcp_close, .pre_connect = tcp_v4_pre_connect, .connect = tcp_v4_connect, .disconnect = tcp_disconnect, .accept = inet_csk_accept, .ioctl = tcp_ioctl, .init = tcp_v4_init_sock, .destroy = tcp_v4_destroy_sock, .shutdown = tcp_shutdown, .setsockopt = tcp_setsockopt, .getsockopt = tcp_getsockopt, .keepalive = tcp_set_keepalive, .recvmsg = tcp_recvmsg, .sendmsg = tcp_sendmsg, ...
tcp_rcvmsg
的代碼如下所示:
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock, int flags, int *addr_len){ ...... if (sk_can_busy_loop(sk) && skb_queue_empty(&sk->sk_receive_queue) && (sk->sk_state == TCP_ESTABLISHED)) sk_busy_loop(sk, nonblock); //如果接收隊(duì)列為空,則會(huì)在該函數(shù)內(nèi)循環(huán)等待 lock_sock(sk); ..... if (unlikely(tp->repair)) { err = -EPERM; if (!(flags & MSG_PEEK)) goto out; if (tp->repair_queue == TCP_SEND_QUEUE) goto recv_sndq; err = -EINVAL; if (tp->repair_queue == TCP_NO_QUEUE) goto out; ...... last = skb_peek_tail(&sk->sk_receive_queue); skb_queue_walk(&sk->sk_receive_queue, skb) { last = skb; ...... if (!(flags & MSG_TRUNC)) { err = skb_copy_datagram_msg(skb, offset, msg, used); //將接收到的數(shù)據(jù)拷貝到用戶態(tài) if (err) { /* Exception. Bailout! */ if (!copied) copied = -EFAULT; break; } } *seq = used; copied = used; len -= used; tcp_rcv_space_adjust(sk);
在連接建立后,若沒有數(shù)據(jù)到來,接收隊(duì)列為空,進(jìn)程會(huì)在sk_busy_loop
函數(shù)內(nèi)循環(huán)等待,知道接收隊(duì)列不為空,并調(diào)用函數(shù)數(shù)skb_copy_datagram_msg
將接收到的數(shù)據(jù)拷貝到用戶態(tài),該函數(shù)內(nèi)部實(shí)際調(diào)用的是__skb_datagram_iter
,其代碼如下所示:
int __skb_datagram_iter(const struct sk_buff *skb, int offset, struct iov_iter *to, int len, bool fault_short, size_t (*cb)(const void *, size_t, void *, struct iov_iter *), void *data){ int start = skb_headlen(skb); int i, copy = start - offset, start_off = offset, n; struct sk_buff *frag_iter; /* 拷貝tcp頭部 */ if (copy > 0) { if (copy > len) copy = len; n = cb(skb->data offset, copy, data, to); offset = n; if (n != copy) goto short_copy; if ((len -= copy) == 0) return 0; } /* 拷貝數(shù)據(jù)部分 */ for (i = 0; i < skb_shinfo(skb)->nr_frags; i ) { int end; const skb_frag_t *frag = &skb_shinfo(skb)->frags[i]; WARN_ON(start > offset len); end = start skb_frag_size(frag); if ((copy = end - offset) > 0) { struct page *page = skb_frag_page(frag); u8 *vaddr = kmap(page); if (copy > len) copy = len; n = cb(vaddr frag->page_offset offset - start, copy, data, to); kunmap(page); offset = n; if (n != copy) goto short_copy; if (!(len -= copy)) return 0; } start = end; }
傳輸層時(shí)序圖如下圖所示。
?
?圖24 傳輸層時(shí)序圖
?GDB調(diào)試如下圖所示。
?
?圖25 GDB調(diào)試
感謝孟寧老師的嚴(yán)謹(jǐn)授課,讓我受益匪淺!本文基于孟老師上課所講,從Linux操作系統(tǒng)實(shí)現(xiàn)入手,深入的分析了Linux操作系統(tǒng)對(duì)于TCP/IP棧的實(shí)現(xiàn)原理與具體過程,了解了Linux網(wǎng)絡(luò)子系統(tǒng)的具體構(gòu)成及流程,通過這次調(diào)研,使我對(duì)TCP/IP協(xié)議的原理及具體實(shí)現(xiàn)有了極其深入的理解。
int __skb_datagram_iter(const struct sk_buff *skb, int offset, struct iov_iter *to, int len, bool fault_short, size_t (*cb)(constvoid *, size_t, void *, struct iov_iter *), void *data) { int start = skb_headlen(skb); int i, copy = start - offset, start_off = offset, n; struct sk_buff *frag_iter;/* 拷貝tcp頭部 */if (copy > 0) { if (copy > len) copy = len; n = cb(skb->data offset, copy, data, to); offset = n; if (n != copy) goto short_copy; if ((len -= copy) == 0) return0; } /* 拷貝數(shù)據(jù)部分 */for (i = 0; i < skb_shinfo(skb)->nr_frags; i ) { int end; constskb_frag_t *frag = &skb_shinfo(skb)->frags[i]; WARN_ON(start > offset len); end = start skb_frag_size(frag); if ((copy = end - offset) > 0) { struct page *page = skb_frag_page(frag); u8 *vaddr = kmap(page); if (copy > len) copy = len; n = cb(vaddr frag->page_offset offset - start, copy, data, to); kunmap(page); offset = n; if (n != copy) goto short_copy; if (!(len -= copy)) return0; } start = end; }
來源:https://www.icode9.com/content-3-837801.html聯(lián)系客服