skb_buff封裝

能夠說sk_buff結構體是Linux網絡協議棧的核心中的核心,幾乎全部的操做都是圍繞sk_buff這個結構體進行的,它的重要性和BSD的mbuf相似(看過《TCP/IP詳解 卷2》的都知道),那麼sk_buff是什麼呢?
       sk_buff就是網絡數據包自己以及針對它的操做元數據。
       想要理解sk_buff,最簡單的方式就是憑着本身對網絡協議棧的理解封裝一個直到以太層的數據幀而且成功發送出去,我的認爲這比看代碼/看文檔或者在網上搜資料強多了。固然,網上已經有了大量的這方面的文章,可是我認爲不少都太複雜了,它們都細化到了sk_buff結構體的每個指針字段,而且還都畫出了圖,但通常都逃不過《深刻理解Linux網絡技術內幕》這本書的圈子。試想,若是之後內核版本升級了,字段新增了或者名字變了,怎麼辦?這些文章包括那本經典的《ULN》還能有幫助嗎?
       所以,本文毫不深刻到sk_buff的細節,可是相信這種簡單的方式可讓本身在多年之後早已忘了什麼是Linux協議棧的狀況下,瞬間理解Linux是如何經過sk_buff封裝數據包的。咱們從網絡的分層模型開始。程序員

網絡分層模型

這是一切的本質。網絡被設計成分層的,因此網絡的操做就能夠稱做一個「棧」,這就是網絡協議棧的名稱的由來。在具體的操做上,數據包最終造成的過程就是一層一層封裝的過程,在棧上造成一段連續的數據,咱們能夠稱做是一層一層的push操做。一樣的,數據包的解封裝的過程,則能夠認爲是一層一層的pop操做。編程

sk_buff的操做

要想造成一個最終的數據包,即以太幀(不考慮其它的鏈路層)。要進行如下的操做:
1.分配一個skb結構體
2.分配數據包的數據區
3.在skb數據區定位應用層起始位置
4.拷貝數據到應用層(假設應用層協議沒有在socket接口之上被封裝)
5.在skb數據區定位傳輸層起始位置
6.設置傳輸層頭部字段
7.在skb數據區定位IP層起始位置
8.設置IP層頭部字段
9.在skb數據區定位以太層起始位置
10.設置以太頭部字段
能夠看出基本的模式,即「定位/設置」兩步驟操做,有點區別的是應用層操做,這是由於應用層的操做通常都是在socket接口之上完成的。可是既然本文講述的是skb的通用操做,就再也不區分這個了。網絡

skb的核心操做

在上面一小節,咱們展現了skb的封裝邏輯,可是具體到接口層面,就涉及到了skb的核心操做。app

1.分配skb

這個是由alloc_skb完成的,完成同一任務的接口造成一個接口族,可是alloc_skb是最基本的接口。socket

       該alloc_skb接口完成兩件事,即分配skb結構體以及skb數據包緩衝區,設置初始值。size參數表示skb的數據包緩衝區的大小,這個大小包括全部層的總和。若是該函數成功返回,那麼就至關於你已經有了一個大小爲size的空數據包緩衝區以及操做該數據包緩衝區的skb元數據。以下圖所示:函數

 

 

2.初始定位(skb_reserve)

skb的逐層封裝的關鍵在於寫指針的定位,即這一層從哪一個位置開始寫。從協議封裝的壓棧形象來看,這個定位應該是順序有規律的。初始定位十分重要,後面的定位就是例行公事了。初始定位固然是定位到應用層的末端,從這裏開始,逐層將協議頭push到skb的數據包緩衝區。初始定位圖示以下:編碼

 

 

3.拷貝應用層數據(skb_push/copy)

當skb分配好了以後,須要將協議「棧」的位置定位在數據包的「最低處」,這是初始定位,這樣才能夠把每一層的數據或者協議頭push到棧上,這個操做由skb_reserve來完成。應用層數據已經在socket之上封裝好了,那麼就把skb的數據包緩衝區寫指針定位到應用數據的開始處,此時的寫指針在應用層緩衝區的末尾,所以須要使用skb_push操做將寫指針定位到應用層開始處,這等於說壓入了應用層棧幀。
       skb_push接口是將一個協議棧幀壓入協議棧的接口,它返回一個position,該position就是skb數據包的寫指針,告訴調用者,這裏開始按照你的封裝邏輯封裝數據包,寫多少字節呢?由skb_push的參數n指示。應用層的壓棧操做以下圖所示:spa

 

 

將應用層棧幀壓入協議棧以後,就能夠在寫指針位置開始,日後連續寫n字節的應用層數據了,通常而言,這些數據來自socket。.net

4.設置傳輸層頭部

和應用層的操做相似,此次須要把傳輸層棧幀壓入協議棧中,以下圖所示:設計

 

 

接下來就能夠愉快地在skb_push返回的位置設置傳輸層頭部了,UDP,TCP,就看你對傳輸層的理解了。設置傳輸層頭部其實就是在skb_push返回的位置開始寫數據,寫入的長度由skb_push的參數指定,即n。

5.設置IP層頭部

和應用層以及傳輸層操做相似,此次須要把IP層的棧幀壓入協議棧中,以下圖所示:

 

 

接下來就能夠愉快地在skb_push返回的位置設置IP層頭部了,如何設置,就看你對IP層的理解了。因爲只是演示skb如何封裝,所以沒有涉及IP層至關重要的IP路由過程。

6.設置以太幀頭部

這個就不說了,和上述的相似...以下圖所示:

 

 

到此爲止,我封裝了一個完整的以太幀,能夠直接經過dev_queue_xmit發送的那種。一路下來,你會發現,skb數據包緩衝區以「壓棧(push)」的方式逐漸被填充,每一層,都是經過skb_push接口壓入一個棧幀,返回寫指針,而後按照該層的協議邏輯從寫指針開始寫入棧幀長度的數據。
       在skb_push返回的那一刻,一個棧幀被壓入了協議棧,而後該棧幀還仍未被寫入數據,也就是說尚未完成封裝過程,具體的封裝過程由調用者本身實現。
       skb_push致使了skb數據包緩衝區寫指針位置的前推,連帶的改變了好幾個變量,首先數據包的長度增長了n個字節,其次縮小了headroom的空間,而後經過reset_XXX_header的調用,skb記住了某層協議頭在數據包中的位置(這點特別重要!好比在TSO/UFO的狀況下,網卡驅動須要協議頭的位置信息,用以計算校驗值,因此雖然skb不記住協議頭的位置,一個數據包也能完成封裝,可是對於協議棧的完整實現而言,倒是不正確的作法,畢竟網卡計算校驗碼已經成了一種事實上的標準[即使它違背了嚴格的分層原則!])

7.在應用數據後面追加PADDING

目前爲止,從最後的圖示上能夠看到,在skb數據包緩衝區中,還有兩塊區域沒有使用,一個headroom,一個是tailroom,這些是幹什麼用的呢?做爲一個練習的例子,因爲存在某種對齊原則,在封裝完成後,我須要在數據包的最後追加一些填充,或者說我須要在最前面加一個前導碼,或者最多見的,我要在數據包的最後加一個糾錯碼,此時應該怎麼辦呢?

       這個時候就須要headroom或者tailroom了,以在數據包最後追加數據爲例,請看下圖:

 

 

實際上,skb_put的操做就是,在數據包的末尾追加數據。至於說headroom如何使用,我就很少說了,其實仍是skb_push,headroom有什麼用呢?前導碼,X over Y封裝,不一而足。

實際的例子

下面我給出一個實際的例子,封裝一個以太幀,而後發送出去:

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. skb = alloc_skb(1500, GFP_ATOMIC);  
  2. skb->dev = dev;  
  3. // 例行填充skb元數據  
  4.   
  5. /* 保留skb區域 */  
  6. skb_reserve (skb, 2 + sizeof(struct ethhdr) +  
  7.         sizeof(struct iphdr) +  
  8.         sizeof(struct udphdr) +  
  9.         sizeof(app_data));  
  10.   
  11. /* 構造數據區 */  
  12. p = skb_push(skb, sizeof(app_data));  
  13. memcpy(p, &app_data[0], sizeof(app_data));  
  14.   
  15. p = skb_push(skb, sizeof(struct udphdr));  
  16. udphdr = (struct udphdr *)p;    
  17. // 填充udphdr字段,略  
  18. skb_reset_transport_header(skb);  
  19.   
  20. /* 構造IP頭 */  
  21. p = skb_push(skb, sizeof(struct iphdr));  
  22. iphdr = (struct iphdr*)p;  
  23. // 填充iphdr字段,略  
  24. skb_reset_network_header(skb);  
  25.   
  26. /* 構造以太頭 */  
  27. p = skb_push(skb, sizeof(struct ethhdr));  
  28. ethhdr = (struct ethhdr*)p;  
  29. // 填充ethhdr字段,略  
  30. skb_reset_mac_header(skb);  
  31.   
  32. /* 發射 */  
  33. dev_queue_xmit(skb);  


解封裝的過程和封裝的過程相反,解封裝的過程是協議棧棧幀逐層pop的過程,可是Linux協議棧並無用棧的術語來定義接口名字,而是使用了push的反義詞,即pull來定義的,skb_pull就是核心接口,和skb_push嚴格相對。我就再也不一一畫圖了。

按照接口編碼而不是按照實現編碼

這好像是Effective C++裏面的一條,一樣也適合於skb的操做場景。典型的就是「如何讓skb記住IP層協議頭,傳輸層協議頭,mac頭的位置」,接口是:

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. skb_reset_mac_header  
  2. skb_reset_network_header  
  3. skb_reset_transport_header  

調用時機爲skb_push返回的當時。曾幾什麼時候,我按照下面的方式設置了協議頭的位置:

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. /* 構造IP頭 */  
  2. p = skb_push(skb, sizeof(struct iphdr));  
  3. iphdr = (struct iphdr*)p;  
  4. // 填充iphdr字段,略  
  5. //skb_reset_network_header(skb);  
  6. skb->network_header = p;  

有錯嗎?咋一看是沒錯的,可是卻報錯了:
protocol 0008 is buggy, dev eth2
這是怎麼回事?緣由就在於skb紀錄的協議頭位置是錯誤的!難道以上的設置skb的network_header字段的方式有何不妥嗎?固然不妥!這就是沒有按照接口編碼的惡果。
       緣由在於,系統設置skb的network_header字段的方式有兩種,經過一個宏來識別:NET_SKBUFF_DATA_USES_OFFSET。也就是說,能夠經過相對於skb的head指針的偏移來定位協議頭的位置,也能夠經過絕對地址來定位,具體使用哪種取決於系統有沒有定義NET_SKBUFF_DATA_USES_OFFSET宏,以上的skb->network_header = p明顯是經過絕對地址來定位的,一旦系統定義了NET_SKBUFF_DATA_USES_OFFSET宏,確定就不對了。既然宏定義在編譯期肯定,那麼經過定義接口就能夠在編譯期惟一肯定一種實現,程序員沒必要在意是否認義了NET_SKBUFF_DATA_USES_OFFSET宏,這就是經過接口編程的益處。若是基於skb的實現來編程,你不得不針對全部的狀況編寫好幾套實現,而以上錯誤的實現只是其中一種,並且還用錯了場景!這是多麼痛的領悟!
       NET_SKBUFF_DATA_USES_OFFSET宏是一個細節問題,若是使用接口編程便沒必要關注這個細節,不然你就必須搞清楚系統爲什麼這麼設計,即使這並非你所關注的!爲什麼呢?
       因爲指針的長度大小在32位系統和64位系統中是不同的,因此按理說skb中的指針型的元數據大小也會不一樣,且64位系統的將會是32位系統的兩倍,爲了平滑掉這個差異,使元數據大小一致,就必須讓64位系統的對應指針類型變爲4個字節,而這是不可能的。所以在64位系統中,使用偏移來定位元數據,而偏移的類型爲固定不變的unsigned int,即4個字節。爲了支持上述說法,skb中加入了一個新的層次,即定義了一種新的數據類型sk_buff_data_t,該類型在編譯期肯定:

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到個人代碼片
  1. #if BITS_PER_LONG > 32  
  2. #define NET_SKBUFF_DATA_USES_OFFSET 1  
  3. #endif  
  4.   
  5. #ifdef NET_SKBUFF_DATA_USES_OFFSET  
  6. typedef unsigned int sk_buff_data_t;  
  7. #else  
  8. typedef unsigned char *sk_buff_data_t;  
  9. #endif  

節約空間以外,對於和大小相關的操做,接口實現也更加統一。這就是細節,而這些細節並非玩網絡協議棧的人所要關注的,不是嗎?這徹底是系統實現的層面,和業務邏輯是無關的。

爲什麼未竟全功

本文講述到此爲止。事實上,sk_buff還有更多的,至關多的細節,可是不能再一一描述了,由於那樣就違背了本文一開始的初衷,即用最簡單的方式揭露本質,若是一一描述了,那麼本文將成爲一個文檔而非一篇感悟,時隔多年之後,相信本身也不會看下去的。
       關於sk_buff還有超級多的內容,僅僅結構體裏面豐富字段的含義就夠折騰很久的了,加上它如何配合Linux各層協議的實現,內容就更加豐富了。不過最基本的,就是本文講述的,你得知道數據是怎樣塞到一個skb並封裝成一個能夠被網卡實際發送的數據包的。好了,基本就是這些。最後我來總結一下本文提到的幾個接口:
alloc_skb:分配一個skb;
skb_reserver:寫指針向後移動到一個位置p,肯定爲數據包尾部,自始,寫指針開始從該位置前移封裝數據包;
skb_push:寫指針前移n,更新數據包長度,從它返回的位置能夠寫n個字節數據-即封裝n字節的協議;
skb_put:寫指針移動到數據包尾部,返回尾部指針,能夠今後位置寫n字節數據,同時更新尾指針和數據包長度;...

相關文章
相關標籤/搜索