sk_buff封裝和解封裝網絡數據包的過程詳解


轉自:http://www.2cto.com/os/201502/376226.html

 

可以說sk_buff結構體是Linux網絡協議棧的核心中的核心,幾乎所有的操作都是圍繞sk_buff這個結構體進行的,它的重要性和BSD的mbuf類似(看過《TCP/IP詳解 卷2》的都知道),那么sk_buff是什么呢?
網絡分層模型這是一切的本質。網絡被設計成分層的,所以網絡的操作就可以稱作一個“棧”,這就是網絡協議棧的名稱的由來。在具體的操作上,數據包最終形成的過程就是一層一層封裝的過程,在棧上形成一段連續的數據,我們可以稱作是一層一層的push操作。同樣的,數據包的解封裝的過程,則可以認為是一層一層的pop操作。
sk_buff的操作要想形成一個最終的數據包,即以太幀(不考慮其它的鏈路層)。要進行以下的操作:
1.分配一個skb結構體
可以看出基本的模式,即“定位/設置”兩步驟操作,有點區別的是應用層操作,這是因為應用層的操作一般都是在socket接口之上完成的。但是既然本文講述的是skb的通用操作,就不再區分這個了。
skb的核心操作在上面一小節,我們展示了skb的封裝邏輯,但是具體到接口層面,就涉及到了skb的核心操作。
1.分配skb這個是由alloc_skb完成的,完成同一任務的接口形成一個接口族,但是alloc_skb是最基本的接口。
該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操作將寫指針定位到應用層開始處,這等於說壓入了應用層棧幀。


將應用層棧幀壓入協議棧之后,就可以在寫指針位置開始,往后連續寫n字節的應用層數據了,一般而言,這些數據來自socket。
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接口壓入一個棧幀,返回寫指針,然后按照該層的協議邏輯從寫指針開始寫入棧幀長度的數據。
7.在應用數據后面追加PADDING目前為止,從最后的圖示上可以看到,在skb數據包緩沖區中,還有兩塊區域沒有使用,一個headroom,一個是tailroom,這些是干什么用的呢?作為一個練習的例子,由於存在某種對齊原則,在封裝完成后,我需要在數據包的最后追加一些填充,或者說我需要在最前面加一個前導碼,或者最常見的,我要在數據包的最后加一個糾錯碼,此時應該怎么辦呢?
這個時候就需要headroom或者tailroom了,以在數據包最后追加數據為例,請看下圖:


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

實際的例子

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

    skb = alloc_skb(1500, GFP_ATOMIC);
    skb->dev = dev;
    // 例行填充skb元數據

    /* 保留skb區域 */
    skb_reserve (skb, 2 + sizeof(struct ethhdr) +
            sizeof(struct iphdr) +
            sizeof(struct iphdr) +
            sizeof(app_data));

    /* 構造數據區 */
    p = skb_push(skb, sizeof(app_data));
    memcpy(p, &app_data[0], sizeof(app_data));

    p = skb_push(skb, sizeof(struct udphdr));
    udphdr = (struct udphdr *)p;  
    // 填充udphdr字段,略
    skb_reset_transport_header(skb);

    /* 構造IP頭 */
    p = skb_push(skb, sizeof(struct iphdr));
    iphdr = (struct iphdr*)p;
    // 填充iphdr字段,略
    skb_reset_network_header(skb);

    /* 構造以太頭 */
    p = skb_push(skb, sizeof(struct ethhdr));
    ethhdr = (struct ethhdr*)p;
    // 填充ethhdr字段,略
    skb_reset_mac_header(skb);

    /* 發射 */
    dev_queue_xmit(skb);


按照接口編碼而不是按照實現編碼這好像是Effective C++里面的一條,同樣也適合於skb的操作場景。典型的就是“如何讓skb記住IP層協議頭,傳輸層協議頭,mac頭的位置”,接口是:
skb_reset_mac_header skb_reset_network_header skb_reset_transport_header 調用時機為skb_push返回的當時。曾幾何時,我按照下面的方式設置了協議頭的位置:
/* 構造IP頭 */ p = skb_push(skb, sizeof(struct iphdr)); iphdr = (struct iphdr*)p; // 填充iphdr字段,略 //skb_reset_network_header(skb); skb->network_header = p; 有錯嗎?咋一看是沒錯的,但是卻報錯了:
protocol 0008 is buggy, dev eth2
#if BITS_PER_LONG > 32 #define NET_SKBUFF_DATA_USES_OFFSET 1 #endif #ifdef NET_SKBUFF_DATA_USES_OFFSET typedef unsigned int sk_buff_data_t; #else typedef unsigned char *sk_buff_data_t; #endif 節約空間之外,對於和大小相關的操作,接口實現也更加統一。這就是細節,而這些細節並不是玩網絡協議棧的人所要關注的,不是嗎?這完全是系統實現的層面,和業務邏輯是無關的。
為何未竟全功本文講述到此為止。事實上,sk_buff還有更多的,相當多的細節,但是不能再一一描述了,因為那樣就違背了本文一開始的初衷,即用最簡單的方式揭露本質,如果一一描述了,那么本文將成為一個文檔而非一篇感悟,時隔多年以后,相信自己也不會看下去的。
alloc_skb:分配一個skb;
skb_reserver:寫指針向后移動到一個位置p,確定為數據包尾部,自始,寫指針開始從該位置前移封裝數據包;
skb_push:寫指針前移n,更新數據包長度,從它返回的位置可以寫n個字節數據-即封裝n字節的協議;
skb_put:寫指針移動到數據包尾部,返回尾部指針,可以從此位置寫n字節數據,同時更新尾指針和數據包長度;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM