SKB_BUFF整理筆記

 

一. SKB_BUFF的基本概念
1. 一個完整的skb buff組成
(1) struct sk_buff--用於維護socket buffer狀態和描述信息
(2) header data--獨立於sk_buff結構體的數據緩衝區,用來存放報文分組,使各層協議的header存儲在連續的空間中,以方便協議棧對其操作
(3) struct skb_shared_info --作爲header data的補充,用於存儲ip分片,其中sk_buff *frag_list是一系列子skbuff鏈表,而frag[]是由一組單獨的page組成的數據緩衝區
skb buff結構圖如下:
 


struct skb_buff
表示接收或發送數據包的包頭信息,其成員變量在從一層向另一層傳遞時會發生修改。例如L3向L2傳遞前,會添加一個L3的頭部,所以在添加頭部前調用skb_reserve在緩衝區的頭部給協議頭預留一定的空間;L2向L3傳遞時候,L2的頭部只有在 網絡驅動處理L2的協議時有用,L3是不會關心它的信息的。但是,內核不會把L2的頭部從緩衝區中刪除,

sk_buff->h
sk_buff->nh
sk_buff->mac
指向TCP/IP各層協議頭的指針:h指向L4(傳輸層),nh指向L3(網絡層),mac指向L2(數據鏈路層)。每個指針的類型都是一個聯合, 包含多個數據結構,


sk_buff->head
sk_buff->data
sk_buff->tail
sk_buff->end
   表示緩衝區和數據部分的邊界。在每一層申請緩衝區時,它會分配比協議頭或協議數據大的空間。head和end指向緩衝區的頭部和尾部,而data和 tail指向實際數據的頭部和尾部。每一層會在head和data之間填充協議頭,或者在tail和end之間添加新的協議數據。數據部分會在尾部包含一 個附加的頭部。
下圖是TCP(L4)向下發送數據給鏈路層L2的過程。注意skb_buff->data在從L4向L2穿越過程中的變化
 


幾個len的區別?
(1)sk_buff->len:表示當前協議數據包的長度。它包括主緩衝區中的數據長度(data指針指向它)和分片中的數據長度。比如,處在網絡層,len指的是ip包的長度,如果包已經到了應用層,則len是應用層頭部和數據載荷的長度。

(2)sk_buff->data_len: data_len只計算分片中數據的長度,即skb_shared_info中有效數據總長度(包括frag_list,frags[]中的擴展數據),一般爲0

(3)sk_buff->truesize:這是緩衝區的總長度,包括sk_buff結構和數據部分。如果申請一個len字節的緩衝區,alloc_skb函數會把它初始化成len+sizeof(sk_buff)。當skb->len變化時,這個變量也會變化。

通常,Data Buffer 只是一個簡單的線性 buffer,這時候 len 就是線性 buffer 中的數據長度;
但在有 ‘paged data’ 情況下, Data Buffer 不僅包括第一個線性 buffer ,還包括多個 page buffer;這種情況下, ‘data_len’ 指的是 page buffer 中數據的長度,’len’ 指的是線性 buffer 加上 page buffer 的長度;len – data_len 就是線性 buffer 的長度。

 

 二. sk_buff結構操作函數
內核通過alloc_skb()和dev_alloc_skb()爲套接字緩存申請內存空間。這兩個函數的定義位於net/core/skbuff.c文件內。通過這alloc_skb()申請的內存空間有兩個,一個是存放實際報文數據的內存空間,通過kmalloc()函數申請;一個是sk_buff數據結構的內存空間,通過 kmem_cache_alloc()函數申請。dev_alloc_skb()的功能與alloc_skb()類似,它只被驅動程序的中斷所調用,與alloc_skb()比較只是申請的內存空間長度多了16個字節。

內核通過kfree_skb()和dev_kfree_skb()釋放爲套接字緩存申請的內存空間。dev_kfree_skb()被驅動程序使用,功能與kfree_skb()一樣。當skb->users爲1時kfree_skb()纔會執行釋放內存空間的動作,否則只會減少skb->users的值。skb->users爲1表示已沒有其他用戶使用該緩存了。

skb_reserve()函數爲skb_buff緩存結構預留足夠的空間來存放各層網絡協議的頭信息。該函數在在skb緩存申請成功後,加載報文數據前執行。在執行skb_reserve()函數前,skb->head,skb->data和skb->tail指針的位置的一樣的,都位於skb內存空間的開始位置。這部份空間叫做headroom。有效數據後的空間叫tailroom。skb_reserve的操作只是把skb->data和skb->tail指針向後移,但緩存總長不變。

運行skb_reserve()前sk_buff的結構
 
        sk_buff
 ----------------------   ---------->  skb->head,skb->data,skb->tail
|                      |
|                      |
|                      |
|                      |
|                      |
|                      |
|                      |
|                      |
|                      |
 ---------------------    ---------->  skb->end
 
運行skb_reserve()後sk_buff的結構
 
        sk_buff
 ----------------------   ---------->  skb->head
|                      |
|      headroom        |
|                      |
|--------------------- |  ---------->  skb->data,skb->tail
|                      |
|                      |
|                      |
|                      |
|                      |
 ---------------------    ---------->  skb->end
        
skb_put()向後擴大數據區空間,tailroom空間減少,skb->data指針不變,skb->tail指針下移。

skb_push()向前擴大數據區空間,headroom空間減少,skb->tail指針不變,skb->data指針上移

skb_pull()縮小數據區空間,headroom空間增大,skb->data指針下移,skb->tail指針不變。

skb_shared_info結構位於skb->end後,用skb_shinfo函數申請內存空間。該結構主要用以描述data內存空間的信息。

 ---------------------  ----------->  skb->head
|                     |
|                     |
|      sk_buff        |
|                     |
|                     |
|                     |
|---------------------| ----------->  skb->end
|                     |
|   skb_share_info    |
|                     |
 ---------------------
skb_clone和skb_copy可拷貝一個sk_buff結構,skb_clone方式是clone,只生成新的sk_buff內存區,不會生成新的data內存區,新sk_buff的skb->data指向舊data內存區。skb_copy方式是完全拷貝,生成新的sk_buff內存區和data內存區。。

 

三. skb 的分配細節
1. 關於 SKB 的分配細節.

LINUX 中 SKB 的分配最終是由函數 : struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,int fclone) 來完成.
SKB 可以分爲 SKB 描述符與 SKB 數據區兩個部分,其中描述符必須從 CACHE 中來分配 : 或者從skbuff_fclone_cache 中分配,或者從 skbuff_head_cache 中來分配.
如果從分配描述符失敗,則直接反悔 NULL,表示 SKB 分配失敗.

SKB 描述符分配成功後,即可分配數據區.
在具體分配數據區之前首先要對數據區的長度進行 ALIGN 操作, 通過宏 SKB_DATA_ALIGN 來重新確定 size 大小. 然後戲臺調用 kmalloc 函數分配數據區 :
data = kmalloc(size + sizeof(struct skb_shared_info), gfp_mask);
需要注意的是數據區的大小是 SIZE 的大小加上 skb_shared_info 結構的大小.

數據區分配成功後,便對 SKB 描述符進行與此數據區相關的賦值操作 :
    memset(skb, 0, offsetof(struct sk_buff, truesize));
    skb->truesize = size + sizeof(struct sk_buff);
    atomic_set(&skb->users, 1);
    skb->head = data;
    skb->data = data;
    skb->tail = data;
    skb->end  = data + size;
需要主意的是, SKB 的 truesize 的大小並不包含 skb_shared_info 結構的大小. 另外,skb 的 end 成員指針也就是skb_shared_info 結構的起始指針,系統用
一個宏 : skb_shinfo 來完成尋找 skb_shared_info 結構指針的操作.

最後,系統初始化 skb_shared_info 結構的成員變量 :
    atomic_set(&(skb_shinfo(skb)->dataref), 1);
    skb_shinfo(skb)->nr_frags  = 0;
    skb_shinfo(skb)->tso_size = 0;
    skb_shinfo(skb)->tso_segs = 0;
    skb_shinfo(skb)->frag_list = NULL;
    skb_shinfo(skb)->ufo_size = 0;
    skb_shinfo(skb)->ip6_frag_id = 0;

最後,返回 SKB 的指針.

2. SKB 的分配時機
SKB 的分配時機主要有兩種,最常見的一種是在網卡的中斷中,有數據包到達的時,系統分配 SKB 包進行包處理; 第二種情況是主動分配 SKB 包用於各種調試或者其他處理環境.

3. SKB 的 reserve 操作
SKB 在分配的過程中使用了一個小技巧 : 即在數據區中預留了 128 個字節大小的空間作爲協議頭使用, 通過移動 SKB 的 data 與 tail 指針的位置來實現這個功能.

4. SKB 的 put 操作
put 操作是 SKB 中一個非常頻繁也是非常重要的操作, 但是, skb_put()函數其實什麼也沒做!
它只是根據數據的長度移動了 tail 指針並改寫了 skb->len 的值,其他的什麼都沒做,然後就返回了 skb->data 指針(就是 tail 指針在移動之前的位置). 看上去此函數彷彿要拷貝數據到 skb 的數據區中,其實這事兒是 insl 這個函數乾的,跟 skb_put() 函數毫不相關,不過它仍然很重要.

5. 中斷環境下 SKB 的分配流程
當數據到達網卡後,會觸發網卡的中斷,從而進入 ISR 中,系統會在 ISR 中計算出此次接收到的數據的字節數 : pkt_len, 然後調用 SKB 分配函數來分配 SKB :
skb = dev_alloc_skb(pkt_len+5);
我們可以看到, 實際上傳入的數據區的長度還要比實際接收到的字節數多,這實際上是一種保護機制. 實際上,在 dev_alloc_skb 函數調用 __dev_alloc_skb 函數,而 __dev_alloc_skb 函數又調用 alloc_skb 函數時,其數據區的大小又增加了 128 字節, 這 128 字節就事前面我們所說的 reserve 機制預留的 header 空間.

 

 

四. 不同情況下構造skb數據包的實現

http://blog.csdn.net/efan_linux/archive/2009/09/23/4580024.aspx

 在我這個網絡接口的程序中(can0),其實難點就是怎樣組包。怎樣在原來數據包的基礎加上自己的數據,怎樣構造ip頭,怎樣構造udp頭。

調試了兩個星期,終於是調通了,在這個過程中,通過看內核源代碼和自己組包的嘗試,大概對組包的方法有了些瞭解,記錄在此,留做備忘,也希望能給需要這方面信息的朋友一點幫助吧。

1,正常網卡收到數據包後的情況:

她的工作就是剝離mac頭,然後給一些字段賦值,最後調用netif_rx將剝離mac頭後的數據報(比如ip數據包)發送到上層協議。由協議棧處理。在此以ldd3中的snull爲例,雖然snull跟硬件不相關,但這個過程都是類似的。

    struct sk_buff *skb;
    struct snull_priv *priv = netdev_priv(dev);


    skb = dev_alloc_skb(pkt->datalen + 2);
    if (!skb) {
        if (printk_ratelimit())
            printk(KERN_NOTICE "snull rx: low on mem - packet dropped/n");
        priv->stats.rx_dropped++;
        goto out;
    }
    skb_reserve(skb, 2); /* align IP on 16B boundary */
    memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);

    /* Write metadata, and then pass to the receive level */
    skb->dev = dev;
   skb->protocol = eth_type_trans(skb, dev);
    skb->ip_summed = CHECKSUM_UNNECESSARY; /* don't check it */
    priv->stats.rx_packets++;
    priv->stats.rx_bytes += pkt->datalen;
    netif_rx(skb);
 

注意:上面代碼中紅色放大的地方是重要的。

因爲此刻收到的數據包的格式如下:mac+ip+udp/udp+data

這時候的處理就是剝離mac頭,然後需要更新的一些域值。這些都是在函數eth_type_trans函數裏做的。需要注意的是,skb->dev = dev;這條語句是很重要的,如果沒有此語句,將會導致系統錯誤而死機(至少在我的板子上是這樣的)。

注意:eth_type_trans()函數主要賦值的是:

skb->mac.raw,skb->protocol和skb->pkt_type。見下面的代碼有無mac頭的情況。

2,完全從一個字符串開始構造一個新的skb數據包。

以前只是看過如何修改數據包,自己構造數據包,這還是頭一次,剛開始確實給我難住了,來來經過看內核代碼和自己摸索,我自己寫的代碼如下:

/*假設:data是一個指向字符串的指針,data_len是data的長度*/

struct ipv6hdr *ipv6h;
struct udphdr *udph;
struct sk__buff * new_skb;

int length = data_len + sizeof(struct ipv6hdr) + sizeof(udphdr);
new_skb = dev_alloc_skb(length);
if(!new_skb)
    {
        printk("low memory.../n"):
        return -1;
    }

skb_reserve(new_skb,length);

memcpy(skb_push(new_skb,data_len),data,data_len);

new_skb->h.uh = udph = (struct udphdr *)skb_push(new_skb,sizeof(struct udphdr));
memcpy(udph,&udph_tmp,sizeof(struct udphdr)); //注意,此刻我的udph_tmp是在另一個過程中截獲的數據包的udp頭,如果完全是自己構造數據包,則需要自己填充udp數據頭中的字段。


udph->len = .............. ; //此處需要給udph->len賦值。注意udph->len是__u16的。存儲時是高低位互換的,所以你應該先將你要更新的數字編成16進制的數,然後高低位互換,在賦值給udh->len。

udplen = new_skb->len;

new_skb->nh.ipv6h = ipv6h = (struct ipv6hdr *)skb_push(new_skb,sizeof(struct ipv6hdr));
memcpy(ipv6h,&ipv6h_tmp,sizeof(struct ipv6hdr)); //同udp頭註釋。

ipb6h->payload_len = ..........; //此處同udph->len.需要注意的是,此處所指的長度並不包括ipv6頭的長度,而是去掉ipv6頭後的長度。

udph->check = 0;
udph->check = csum_ipv6_magic(&ipv6h->saddr, &ipv6h->daddr, udplen, IPPROTO_UDP, csum_partial((char *)udph, udplen, 0));

///////////////注意,如果是ipv4,則還需要計算ip校驗和,但此處是ipv6,不用計算ip檢驗和,所以此處沒有ipv6頭的校驗。//////////////////////////

new_skb->mac.raw = new_skb->data; //因爲無mac頭
new_skb->protocol = htons(ETH_P_IPV6); //表明包是ipv6數據包
new_skb->pkt_type = PACKET_HOST; //表明是發往本機的包

new_skb->dev = &can_control; //此處很重要,如果沒有這條語句,則內核跑死。至少在我板子上是這樣的。can_control是我的net_device結構體變量。

netif_rx(new_skb);
 

3,當需要改變原有skb的數據域的情況。

此時,有兩種辦法:
可以先判斷skb的tailroom,如果空間夠大,則我們可以把需要添加的數據放在skb的tailroom裏。如果tailroom不夠大,則需要調用skb_copy_expand函數來擴充tailroom或者headroom。
 
例如我們需要在skb的後面加上一個16個字節的字符串,則代碼類似如下:
if(skb_tailroom(skb) < 16)
{
    nskb = skb_copy_expand(skb, skb_headroom(skb), skb_tailroom(skb)+16,GFP_ATOMIC);
    if(!nskb)
    {
        printk("low memory..../n");       
        dev_kfree_skb(skb);
        return -1;
    }
   
    else
    {
        kfree_skb(skb); // 注意,如果此時是鉤子函數鉤出來的,則skb不能在這裏釋放,否則會造成死機。

        skb = nskb;
    }
   
    memcpy(skb_put(skb,16),ipbuf,16); //ipbuf爲要加到skb後面的字符串

   
    udplen = skb->len - sizeof(struct ipv6hdr);
   
    udph->len += 0x1000; //換成十進制爲 + 16

    ipv6h->payload_len += 0x1000;
   
    udph->check = 0;
    udph->check = csum_ipv6_magic(&ipv6h->saddr, &ipv6h->daddr, udplen, IPPROTO_UDP, csum_partial((char *)udph,udplen,0));   
   
    skb->mac.raw = new_skb->data; //因爲無mac頭

    skb->protocol = htons(ETH_P_IPV6); //表明包是ipv6數據包

    skb->pkt_type = PACKET_HOST; //表明是發往本機的包


    skb->dev = &can_control; //此處很重要,如果沒有這條語句,則內核跑死。至少在我板子上是這樣的。can_control是我的net_device結構體變量。


  netif_rx(skb);
}
 

注意:當調用skb_copy_expand或者修改了skb的數據域後,一定要更新udph->len和ipv6h->payload_len。否則上層應用(比如udp套接字)收到的數據包還是原來的數據包而不是修改後的數據包,因爲udph->len的原因。

 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/efan_linux/archive/2009/09/23/4580024.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章