這個標題起得比較糾結,之前熟知的PPPOE是作為PPP協議的底層載體,而實際上它也是一個完整的協議,不過它的實現比較簡單,由它出發,可以很容易理清楚Linux網絡棧的實現方式。
1.總述
Linux中用戶空間的網絡編程,是以socket為接口,一般創建一個sockfd = socket(family,type,protocol),之后以該sockfd為參數,進行各種系統調用來實現網絡通信功能。其中family指明使用哪種協議域(如INET、UNIX等),protocol指明該協議域中具體哪種協議(如INET中的TCP、UDP等),type表明該接口的類型(如STREAM、DGRAM等),一般設protocol=0,那么就會用該family中該type類型的默認協議(如INET中的STREAM默認就是TCP協議)。
Linux中利用module機制,層次分明地實現了這套協議體系,并具有很好的擴展性,其基本模塊構成如下:
先看右邊,頂層的socket模塊提供一個sock_register()函數,供各個協議域模塊使用,在全局的net_family[]數組中增加一項;各個協議域模塊也提供一個類似的register_xx_proto()函數,供各個具體的協議使用,在該協議域私有的xx_proto[]數組中增加一項。這兩個數組中的存放的都是指針,指向的數據結構如下圖所示:
很明顯它們是用來創建不同類型的socket接口的,且是一種分層次的創建過程,可想而知,頂層socket_create()完成一些共有的操作,如分配內存等,然后調用下一層create;協議域內的create()完成一些該協議域內共有的初始化工作;最后具體協議中的create()完成協議特有的初始化。具體的下一節講。
再來看上圖右邊的,也是頂層socket模塊提供的4個函數,前兩個一般由具體協議模塊調用,由于協議棧與應用層的交互,具體的后面會講到。后兩個一般有協議域模塊調用,用于底層設備與協議棧間的交互。但這也不絕對,如在PPPOE協議中,這4個函數都由具體協議模塊調用,這是因為PPPOX協議域內的共有部分不多,各個協議間幾乎獨立。這4個函數的功能及所用到的數據結構,在后面具體用到時會詳細說明。
2.socket插口創建
首先來看一下最終創建好的socket插口由哪些部分組成,該結構是相當龐大的,這里只給出框架:
基本屬性有state(listen、accept等),flags標志(blocked等),type類型,這里family和protocol都沒有了,因為它們再創建時使用過了,已經被融入到socket結構中。
File指針指向一個file結構,在Linux中一個socket也被抽象為一個文件,所以在應用層一般通過標準的文件操作來操作它。
Ops指向一個struct proto_ops結構,它是每種協議特有的,應用層的系統調用,最終映射到網絡棧中具體協議的操作方法。
Sk指向一個struct sock結構,而該結構在分配空間時,多分配了一點以作為該協議的私有部分,這里包含了該協議的具體信息,內容相當多。首先是一個struct sock_common結構,包含了協議的基本信息;然后是一個sk_prot_create指針,指向一個struct proto結構體,該結構體就是第一節中所述的,用proto_regsiter()注冊到內核中的,它包含應用層到協議棧的交互操作和信息(也可以說成是Appàtransport layer的交互信息);然后還有一個sk_backlog_rcv函數指針,所指函數在協議棧處理完接收到的包之后調用,一般僅是把數據包放到該socket的接收隊列中,等待APP讀取;最后協議的私有部分里存放該協議的私有信息,如pppoe的sessionID、daddr,tcp的連接4元組等,這些信息很重要,利用它們來區分同一個協議中的多個socket。
創建的總體過程,第一節已講過了,下面以pppoe為例,描述一個socket插口的具體創建過程:
之前所述的關鍵點這里幾乎都涉及到了,要注意的是這里的struct proto結構非常簡單,因為PPPOE協議幾乎沒有傳輸層,所以不需要有太多的中間操作,僅需要一個obj_size來指明struct sock結構后需分配的私有結構大小,關于私有結構的內容,一般在connect操作時才能初始化。
創建好socket之后,其中的fops,proto_ops,sk_backlog_rcv等操作是如何作用,來實現網絡通信的功能?這是后面要講述的內容。
3.主動過程
主動過程即在應用層中通過系統調用,觸發socket完成某種動作,有些系統調用和標準的文件操作類似,因此可以直接用sockfd的fops來描述,如read、write、ioctl等,有些則是socket接口特有的,需重新定義系統調用接口,Linux中用SYSCALL_DEFINEn()宏來定義系統調用接口,如bind、accept等。這些系統調用一般都很簡單,最終都會去調用socket內部proto_ops中的接口函數。
如下圖所示,在socket層,并不是所有的文件操作都適用于socket,因此其特有的socket_file_ops中只指定了部分函數;另外還封裝了幾個系統調用,是我們熟悉的bind、listen、connect、accept。這些系統調用接口都是靜態的,它們一般經過簡單的處理,就調用具體socket中的proto_ops操作。
在協議棧中,主要是socket特有的proto_ops操作,但對于一些復雜的協議,如TCP,還需要其它一些操作來支持,這些接口都放在struct sock中的struct proto中。PPPOE協議比較簡單,不需要struct proto的操作來支持,但其中的obj_size仍然重要,如前所述。
如上圖所示,PPPOE協議中,并不是所有協議操作都需要,如bind、accept等,下面選幾個來詳細看一下socket的主動過程的工作。
Ioctl系統調用:ioctl是通過標準的文件操作來調用的,具體如下圖所示:
其中頂層sock_ioctl中,對于一些特殊情況,如VLAN、BRIDGE等,它們并不是要對socket插口本身操作,而是要調用VLAN、BRIDGE模塊中的創建函數,這看起來有點格格不入,但為了操作方便,且保證網絡相關的操作都封裝在socket中,這么做也是不得已。
在pppoe_ioctl中,根據cmd進行相應操作,其中有一個值得注意的,就是PPPIOCGCHAN選項,它使得該pppoe_socket成為一個特殊的channel,這主要是pppoe為了給ppp協議提供服務而特有的,與網絡協議棧關系不大,以后會具體看。
Read系統調用:read也是標準的文件操作,但要注意,在網絡棧中,read并不是接收過程,而僅是從該sock的接收隊列中取出skb,提交給應用層,如下圖所示。而這些skb是如何獲得的,那是一個復雜的被動過程,下面再講。
Connect系統調用:connect是socket.c中封裝的一個系統特用,其代碼也很簡單,最終調用協議棧中的pppoe_connect接口,該接口函數是pppoe協議中一個非常重要的操作,具體如下圖所示:
首先先一下通配地址的問題,這是network programming中一個基本問題,因為各個協議用到的地址結構不同,在應用層,為了方便可讀性,可以用協議特有的地址結構,只要符標準的模式即可(即第一個元素為family),然后強制轉換成sockaddr*類型,傳遞給通用的系統調用接口。在最終調用協議模塊中的接口函數時,再轉換回來。
再看pppoe_connect中,首先由sock結構指針得到pn指針,它們是分配在一起的(如前所述),這很容易得到,同時還得到pppoe_net結構的指針(它是該協議中全局共有的)。然后把用戶傳遞進來的addr的數據放到socket中來,并且執行一個set_item函數,該函數主要根據addr信息,把該socket指針放到協議全局的pppoe_net結構中(這一步對接收過程很重要,后面會細講)。最后初始化了該socket中特有的chan結構,并調用ppp_register_net_channel(),這主要為ppp服務,以后再看。
4.發送流程
這也是一個主動過程,在協議體系中,它是一個比較重要的過程,所以單獨列出來。Socket框架中,發送過程是通過標準的文件操作write完成的, socket的write操作為sock_aio_write(),最終會調用proto_ops->sendmsg()函數,即pppoe模塊中的pppoe_sendmsg(),如下圖所示:
首先從sock中獲得相關信息,最重要的當然是dev設備,因為pppoe的設備是選定的(由useraddr提供),而有些協議如IP,則會根據協議地址,有協議棧自動選擇dev。然后分配skb,并準備其中的package,這是每個協議的關鍵,由于pppoe協議很簡單,只需要設置好一個pppoe header即可。最后直接調用dev_queue_xmit(skb),通過設備將該package發送出去。
5.網絡協議棧結構小結
這里想講一下的是,pppoe到底是什么層的協議,鏈路層。而通過上面的描述,更準確的說法應該是,pppoe是一個完整的協議,是從應用層到設備之間的協議模塊,從這個意義上來講,它和INET域中的協議是等價的。如下圖所示:
這里講的協議是從應用層往下直到物理設備的完整過程,有些協議具有一定的相似性,(如TCP、UDP,還包括裸IP等都以IP協議為基礎),則把它們歸為一個協議域內。至于協議分層,則是概念上的,如PPPOE協議的主要功能體現在鏈路層,則一般稱它為鏈路層協議, 而狹義上稱TCP、UDP為傳輸層協議(而前面講的廣義上的TCP、UDP則是包括傳輸層、以IP為基礎的網絡層、鏈路層的完整協議)。
有點饒人,不過沒關系,只要理解協議棧的功能就是從socket接口得到數據,封裝成一定的包結構,最終由物理設備發送出去(接收過程反過來)。至于具體的實現,則是由具體協議的特點決定的,對于一些復雜協議,分層方式則是一種比較好的選擇。
而其中有些協議會比較特殊,如之前講的VLAN,它甚至從來都不會進入到協議棧,僅在設備驅動層,就被轉化成以太網協議,協議棧中根本不需要為它準備處理接口。再如比較典型的ICMP協議,它既可以是一個完整的協議,被應用層調用(如典型的Ping程序),也可以只作為TCP的附屬協議(只被TCP處理,對應用層不可見)。這里的PPPOE與此很類似,本文講述了其作為完整協議的工作方式,另外它也可以作為PPP協議的底層基礎,在下一篇中會講述其具體的實現方法。
6.被動過程-接收流程
接收過程是一個被動過程,在屋里設備層,它往往是由中斷觸發,其實現的復雜度也較發送過程高很多。在協議棧中,其實現也同樣與發送過程很不對稱。因為發送時,本身主機擁有控制權,而接收時,是一個數據包對多個接收模塊(一對多),只能從數據包中的信息中一點一點分析,并去尋找接收模塊。
先給出接收流程的框架,再逐步去分析其實現。如下圖所示:
先不看橙色部分,一個接收流程由物理設備的中斷觸發,設備驅動程序進行相應處理,得到協議棧中標準的數據結構sk_buff(簡稱skb),并根據一個特殊的全局數據結構packet_type,將數據交給相應的協議;協議根據自身設計特點對skb數據進行處理,并通過全局變量xx_net_id和各個協議私有的特殊數據結構xx_net,尋找到該數據包對應的應用層socket插口,并將其放在該socket插口的接收隊列中;最后應用層在某個時刻會通過read系統調用讀取該數據(如第3節所講)。
6.1設備驅動層的處理
設備驅動層的接收過程在之前的篇章中已經講過了,一般是由硬件中斷觸發,然后或是采用中斷模式、或是采用NAPI模式,總之其根本任務就是:根據設備的特點(先驗知識,如以太網設備驅動事先就是知道以太網幀的基本結構的),將接收到的裸數據轉換成協議棧所認識的標準結構skb(從而實現底層設備對上層的透明性),然后提交給相應的協議。很明顯問題有兩個,skb是什么樣的,要準備什么?怎么知道提交給誰?
準備skb結構。首先來看一下sk_buff的構成,如下圖所示。Skb只是一個控制結構,實際的數據放在一個data_buf中,并由skb中一些列參數索引,具體見下圖右所示,這之中有些參數是在分配data_buf、copy數據時就決定的,如head、end、data、tail等;有些則要經過一定的識別才能得到,如mac_header一般在設備驅動中得到,而network_header、transport_header則要到協議棧中才知道,且各個協議的處理各不相同,如PPPOE協議根本不需要只需要指明network_header,而TCP協議則有復雜的頭部信息。最終由skb->data指針和頭部長可得到app_data的位置,因此應用層可以只讀取應用數據即可。
Skb中另外一些參數也相當重要,如vlan_tci用于指明vlan的id,其用法在前面已講過。dev參數則是要貫穿整個流程的,因為該龐大結構中的多個信息會在整個網絡系統中用到,要注意的是該參數由設備驅動程序決定,一般就是接收的物理設備,但在Linux中,網絡設備是由net_device結構指示的,一個物理設備可有多個協議設備,這在VLAN、BRIDGE中很明顯,其實在PPP協議中,這也是一個關鍵點,后面會講到。Sk參數指示了該數據包屬于哪個應用層socket插口,它由具體協議根據特定方法得到,后面會講到。Protocol參數是本節的重點,它由mac_header中的字節決定。
設備驅動程序只關心mac_header,即數據包最初始的部分。前面也提到了,這需要一定的先驗知識,如ethernet設備驅動,它先驗的指導以太網頭部由DMAC、SMAC和兩字節的協議構成,下面是一個RTL8012驅動的接收片段(~/dev/net/Ethernet/realtek/apt.c):
提交協議棧。主要就是根據skb->protocol參數,當然還需要另一個重要的數據結構packet_type。
設備驅動中最后提交過程有netif_skb_receive()函數完成,它會遍歷系統中所有的packet_type,找到protocol和dev(這個是啥意思)都相同,就調用該ptype中的func函數,如ip_packet中的func為ip_rcv()函數,這樣skb就到了協議棧中。
系統中所有全局的packet_type構成一個list,并由全局變量ptype_all索引,另外還提供ptype_base[]全局數組,將type相同的packet_type單獨成鏈,為遍歷提供方便。
這些全局的packet_type結構是從哪來的,這就要看第一節圖中,左邊4個函數中的一個dev_add_packet(struct packet_type*)。協議模塊在加載時,調用該函數,將自己特有的packet_type結構注冊進內核中,而其中的(*func)則有協議自己定義。
最后要注意的是,打開if_ether.h文件,可以看到現在已定義的協議protocol有_P_IP、_P_ARP、_P_8021Q、_P_PPP_SES、_P_PPP_DIS等,如果根據傳統的分層協議來看它們,會覺得很亂,有網絡層的、鏈路層的、甚至同一種協議還有兩個,但如果用第5節的概念來看,則很容易理解。再看由什么模塊注冊,TCP、UDP都是以IP協議為基礎,只要有INET協議域模塊注冊一個即可,而ARP雖然也屬于INET域,但它卻必須自己有一個packet_type,PPPOE協議雖然只是一個協議,但卻有兩個階段,所以它有兩個不同的packet_tpye。可見這種實現是很靈活的,根據具體協議的特點決定。
6.2協議棧接收處理
協議棧的處理由各協議決定,如TCP協議的處理過程是相當復雜的,而這里的pppoe的處理卻非常簡單,但由它卻可以避開細節,更清楚地看到流程的梗概,如下圖所示:
可以看到pppoe協議的處理過程幾乎沒有,僅是設置了skb的network_header, transport_header,然后就利用get_item()函數找到它所屬的socket插口,直接把它提交給上層。如上圖右所示,是典型的TCP接收流程,是相當復雜的,其中TCP與IP的接頭處還需用到額外的私有數據結構。
提交函數sk_backlog_rcv,即這里的pppoe_rcv_core(sk,skb)函數,首先判斷是否為ppp通道的數據,若是則提交給ppp協議。一般正常情況下,直接用sock_queue_rcv_skb(sk,skb)函數將它放在socket的接收隊列中。
匹配應用層接口:協議棧在對數據包進行處理后,需要確定該包屬于哪個socket插口,這個過程在內核中有一套完整的機制來完成,其框架如下圖所示:
首先內核有個全局結構net_generic,其中一個最重要的元素是指針數組。然后每個協議module加載時,會調用register_pernet_device(struct pernet_operations*)(見第一節圖),pernet_operations結構中最關鍵的兩個參數,一個是size,它指示內核為該模塊分配一個私有數據結構(如pppoe即為struct pppoe_net),另一個是xx_net_id,它指示由net_generic.ptr[xx_net_id]來指向該數據結構,這樣每個協議模塊中,就可以根據自己的xx_net_id很容易尋找到內核分配給自己的私有結構。最后協議的私有模塊中一般也有一個指針數組,用以索引屬于它的各個socket。
工作流程就很清楚了,具體的工作方式還要看兩個函數,
就不看細節了,僅看兩個函數的原型就能明白,其中pn參數就是上面所述的用全局結構net_generic和各協議私有xx_net_id獲得的。Set_item()函數在connect時調用(參見第3節),它根據pppox_sock中的sessionID、remoteMAC(這兩個參數由*useraddr傳入,詳見下一篇協議分析),根據一個hash算法得到一個hashInt值,然后用pn->hash_ptr[hashInt]指向該socket結構。那反過來,接收時由這兩個參數(由數據包的協議頭中獲得)得到hashInt,便能很容易找到對應的socket了。
各個協議使用的方法及參數都不同,但思路都一樣,就是依據協議本身特有的參數(如TCP中的連接4元組),在socket創建、或連接的時候(接收數據之前),根據一定的算法,將它的指針放在該協議私有的xx_net中,這樣接收時就可以由數據報的協議參數找到它了。
6.3命名空間namespace
上述的socket索引方法有個繞彎的地方:就是每個協議私有的xx_net結構可以直接由協議模塊本身分配,索引起來也方便,不要用到全局的net_generic。而目前內核所用的方法,其實是為了另外的目的,那就是命名空間namespace。也就是虛擬多用戶的一套機制,具體的也沒細看,好像目前內核整個namespace還沒有全部完成。
network的命名空間問題主要在于,每個協議模塊的xx_net私有結構不僅是一個,而是由內核全局決定的,即每注冊一個新的用戶(有點像虛擬機機制),就分配一個新的xx_net結構,這樣多用戶間可以用參數相同的socket連接,但卻指向不同的socket。
可以看到前面所述的很多內容中,都會有個net參數,就是為了這個作用,主要實現函數在namespace.c中。
7.總結
主要結合pppoe協議,學習了Linux中網絡棧的實現。由于pppoe協議本身很簡單,代碼量少,更容易抓住協議實現的梗概。Linux網絡棧,繼承Unix,采用socket插口作為主線,主要包括創建、協議連接、主動過程、匹配機制、被動過程等內容。
要注意的是,實際應用中,很少有直接利用pppoe協議通信的,而是把它作為ppp協議的底層基礎來用,而這需要協議實現中的一些技巧來支持。
編輯:hfy
-
Linux
+關注
關注
87文章
11342瀏覽量
210140 -
Socket
+關注
關注
0文章
212瀏覽量
34812 -
PPPoE
+關注
關注
0文章
24瀏覽量
12174 -
TCP協議
+關注
關注
1文章
101瀏覽量
12105
發布評論請先 登錄
相關推薦
評論