sk_buff就是網絡資料包本身以及針對它的操作中繼資料。
是以,本文絕不深入到sk_buff的細節,但是相信這種簡單的方式可以讓自己在多年以後早已忘了什麼是Linux協定棧的情況下,瞬間了解Linux是如何通過sk_buff封裝資料包的。我們從網絡的分層模型開始。
這 是一切的本質。網絡被設計成分層的,是以網絡的操作就可以稱作一個“棧”,這就是網絡協定棧的名稱的由來。在具體的操作上,資料包最終形成的過程就是一層 一層封裝的過程,在棧上形成一段連續的資料,我們可以稱作是一層一層的push操作。同樣的,資料包的解封裝的過程,則可以認為是一層一層的pop操作。
要想形成一個最終的資料包,即以太幀(不考慮其它的鍊路層)。要進行以下的操作:
1.配置設定一個skb結構體
2.配置設定資料包的資料區
3.在skb資料區定位應用層起始位置
4.拷貝資料到應用層(假設應用層協定沒有在socket接口之上被封裝)
5.在skb資料區定位傳輸層起始位置
6.設定傳輸層頭部字段
7.在skb資料區定位IP層起始位置
8.設定IP層頭部字段
9.在skb資料區定位以太層起始位置
10.設定以太頭部字段
可以看出基本的模式,即“定位/設定”兩步驟操作,有點差別的是應用層操作,這是因為應用層的操作一般都是在socket接口之上完成的。但是既然本文講述的是skb的通用操作,就不再區分這個了。
在上面一小節,我們展示了skb的封裝邏輯,但是具體到接口層面,就涉及到了skb的核心操作。
這個是由alloc_skb完成的,完成同一任務的接口形成一個接口族,但是alloc_skb是最基本的接口。
該alloc_skb接口完成兩件事,即配置設定skb結構體以及skb資料包緩沖區,設定初始值。size參數表示skb的資料包緩沖區的大小,這個大小包 括所有層的總和。如果該函數成功傳回,那麼就相當于你已經有了一個大小為size的空資料包緩沖區以及操作該資料包緩沖區的skb中繼資料。如下圖所示:
<a href="http://s3.51cto.com/wyfs02/M01/59/89/wKiom1TXCajiZtUqAAC4azU1xQw990.jpg" target="_blank"></a>
skb 的逐層封裝的關鍵在于寫指針的定位,即這一層從哪個位置開始寫。從協定封裝的壓棧形象來看,這個定位應該是順序有規律的。初始定位十分重要,後面的定位就 是例行公事了。初始定位當然是定位到應用層的末端,從這裡開始,逐層将協定頭push到skb的資料包緩沖區。初始定位圖示如下:
<a href="http://s3.51cto.com/wyfs02/M02/59/86/wKioL1TXCrKhD_ByAADSHm3lldM817.jpg" target="_blank"></a>
當 skb配置設定好了之後,需要将協定“棧”的位置定位在資料包的“最低處”,這是初始定位,這樣才可以把每一層的資料或者協定頭push到棧上,這個操作由 skb_reserve來完成。應用層資料已經在socket之上封裝好了,那麼就把skb的資料包緩沖區寫指針定位到應用資料的開始處,此時的寫指針在 應用層緩沖區的末尾,是以需要使用skb_push操作将寫指針定位到應用層開始處,這等于說壓入了應用層棧幀。
skb_push接口是将一個協定棧幀壓入協定棧的接口,它傳回一個position,該position就是skb資料包的寫指針,告訴調用者,這裡開 始按照你的封裝邏輯封裝資料包,寫多少位元組呢?由skb_push的參數n訓示。應用層的壓棧操作如下圖所示:
<a href="http://s3.51cto.com/wyfs02/M02/59/89/wKiom1TXCdayKytDAAC-dvyaJFY337.jpg" target="_blank"></a>
将應用層棧幀壓入協定棧之後,就可以在寫指針位置開始,往後連續寫n位元組的應用層資料了,一般而言,這些資料來自socket。
和應用層的操作類似,這次需要把傳輸層棧幀壓入協定棧中,如下圖所示:
<a href="http://s3.51cto.com/wyfs02/M00/59/89/wKiom1TXCeXg84xcAADJhGC632c723.jpg" target="_blank"></a>
接下來就可以愉快地在skb_push傳回的位置設定傳輸層頭部了,UDP,TCP,就看你對傳輸層的了解了。設定傳輸層頭部其實就是在skb_push傳回的位置開始寫資料,寫入的長度由skb_push的參數指定,即n。
和應用層以及傳輸層操作類似,這次需要把IP層的棧幀壓入協定棧中,如下圖所示:
<a href="http://s3.51cto.com/wyfs02/M01/59/86/wKioL1TXCuXBKhDxAADOm6rB5a8619.jpg" target="_blank"></a>
接下來就可以愉快地在skb_push傳回的位置設定IP層頭部了,如何設定,就看你對IP層的了解了。由于隻是示範skb如何封裝,是以沒有涉及IP層相當重要的IP路由過程。
這個就不說了,和上述的類似...如下圖所示:
<a href="http://s3.51cto.com/wyfs02/M01/59/89/wKiom1TXCgfAWwIlAADnL28ZAD0542.jpg" target="_blank"></a>
到 此為止,我封裝了一個完整的以太幀,可以直接通過dev_queue_xmit發送的那種。一路下來,你會發現,skb資料包緩沖區以“壓棧 (push)”的方式逐漸被填充,每一層,都是通過skb_push接口壓入一個棧幀,傳回寫指針,然後按照該層的協定邏輯從寫指針開始寫入棧幀長度的數 據。
在skb_push傳回的那一刻,一個棧幀被壓入了協定棧,然後該棧幀還仍未被寫入資料,也就是說還沒有完成封裝過程,具體的封裝過程由調用者自己實作。
skb_push導緻了skb資料包緩沖區寫指針位置的前推,連帶的改變了好幾個變量,首先資料包的長度增加了n個位元組,其次縮小了headroom的空 間,然後通過reset_XXX_header的調用,skb記住了某層協定頭在資料包中的位置(這點特别重要!比如在TSO/UFO的情況下,網卡驅動 需要協定頭的位置資訊,用以計算校驗值,是以雖然skb不記住協定頭的位置,一個資料包也能完成封裝,但是對于協定棧的完整實作而言,卻是不正确的做法, 畢竟網卡計算校驗碼已經成了一種事實上的标準[即便它違背了嚴格的分層原則!])
目前為 止,從最後的圖示上可以看到,在skb資料包緩沖區中,還有兩塊區域沒有使用,一個headroom,一個是tailroom,這些是幹什麼用的呢?作為 一個練習的例子,由于存在某種對齊原則,在封裝完成後,我需要在資料包的最後追加一些填充,或者說我需要在最前面加一個前導碼,或者最常見的,我要在資料 包的最後加一個糾錯碼,此時應該怎麼辦呢?
這個時候就需要headroom或者tailroom了,以在資料包最後追加資料為例,請看下圖:
<a href="http://s3.51cto.com/wyfs02/M02/59/86/wKioL1TXCwzBddarAADywBV_aoM676.jpg" target="_blank"></a>
實際上,skb_put的操作就是,在資料包的末尾追加資料。至于說headroom如何使用,我就不多說了,其實還是skb_push,headroom有什麼用呢?前導碼,X over Y封裝,不一而足。
下面我給出一個實際的例子,封裝一個以太幀,然後發送出去:
解封裝的過程和封裝的過程相反,解封裝的過程是協定棧棧幀逐層pop的過程,但是Linux協定棧并沒有用棧的術語來定義接口名字,而是使用了push的反義詞,即pull來定義的,skb_pull就是核心接口,和skb_push嚴格相對。我就不再一一畫圖了。
這好像是Effective C++裡面的一條,同樣也适合于skb的操作場景。典型的就是“如何讓skb記住IP層協定頭,傳輸層協定頭,mac頭的位置”,接口是:
調用時機為skb_push傳回的當時。曾幾何時,我按照下面的方式設定了協定頭的位置:
有錯嗎?咋一看是沒錯的,但是卻報錯了:
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,該類型在編譯期确定:
節約空間之外,對于和大小相關的操作,接口實作也更加統一。這就是細節,而這些細節并不是玩網絡協定棧的人所要關注的,不是嗎?這完全是系統實作的層面,和業務邏輯是無關的。
本文講述到此為止。事實上,sk_buff還有更多的,相當多的細節,但是不能再一一描述了,因為那樣就違背了本文一開始的初衷,即用最簡單的方式揭露本質,如果一一描述了,那麼本文将成為一個文檔而非一篇感悟,時隔多年以後,相信自己也不會看下去的。
關于sk_buff還有超級多的内容,僅僅結構體裡面豐富字段的含義就夠折騰好久的了,加上它如何配合Linux各層協定的實作,内容就更加豐富了。不過 最基本的,就是本文講述的,你得知道資料是怎樣塞到一個skb并封裝成一個可以被網卡實際發送的資料包的。好了,基本就是這些。最後我來總結一下本文提到 的幾個接口:
alloc_skb:配置設定一個skb;
skb_reserver:寫指針向後移動到一個位置p,确定為資料包尾部,自始,寫指針開始從該位置前移封裝資料包;
skb_push:寫指針前移n,更新資料包長度,從它傳回的位置可以寫n個位元組資料-即封裝n位元組的協定;
skb_put:寫指針移動到資料包尾部,傳回尾部指針,可以從此位置寫n位元組資料,同時更新尾指針和資料包長度;
...
本文轉自 dog250 51CTO部落格,原文連結:http://blog.51cto.com/dog250/1612791