Linux 內核網絡協議棧 ------sk_buff 結構體 以及 完全解釋

鏈接:https://blog.csdn.net/shanshanpt/article/details/21024465
在2.6.24之後這個結構體有了較大的變化,此處先說一說2.6.16版本的sk_buff,以及解釋一些問題。

一、

先直觀的看一下這個結構體~~~~~~~~~~~~~~~~~~~~~~在下面解釋每個字段的意義~~~~~~~~~~~

struct sk_buff {
         /* These two members must be first. */
         struct sk_buff          *next;
         struct sk_buff          *prev;
 
         struct sock             *sk;
         struct skb_timeval      tstamp;
         struct net_device       *dev;
         struct net_device       *input_dev;
 
         union {
                 struct tcphdr   *th;
                 struct udphdr   *uh;
                 struct icmphdr  *icmph;
                 struct igmphdr  *igmph;
                 struct iphdr    *ipiph;
                 struct ipv6hdr  *ipv6h;
                 unsigned char   *raw;
         } h;
 
         union {
                 struct iphdr    *iph;
                 struct ipv6hdr  *ipv6h;
                 struct arphdr   *arph;
                 unsigned char   *raw;
         } nh;
 
         union {
                 unsigned char   *raw;
         } mac;
 
         struct  dst_entry       *dst;
         struct  sec_path        *sp;
 
         /*
          * This is the control buffer. It is free to use for every
          * layer. Please put your private variables there. If you
          * want to keep them across layers you have to do a skb_clone()
          * first. This is owned by whoever has the skb queued ATM.
          */
         char                    cb[48];
 
         unsigned int            len,
                                 data_len,
                                 mac_len,
                                 csum;
         __u32                   priority;
         __u8                    local_df:1,
                                 cloned:1,
                                 ip_summed:2,
                                 nohdr:1,
                                 nfctinfo:3;
         __u8                    pkt_type:3,
                                 fclone:2,
                                 ipvs_property:1;
         __be16                  protocol;
 
         void                    (*destructor)(struct sk_buff *skb);
#ifdef CONFIG_NETFILTER
         __u32                   nfmark;
         struct nf_conntrack     *nfct;
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
         struct sk_buff          *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
         struct nf_bridge_info   *nf_bridge;
#endif
#endif /* CONFIG_NETFILTER */
#ifdef CONFIG_NET_SCHED
         __u16                   tc_index;       /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
         __u16                   tc_verd;        /* traffic control verdict */
#endif
#endif
 
 
         /* These elements must be at the end, see alloc_skb() for details.  */
         unsigned int            truesize;
         atomic_t                users;
         unsigned char           *head,
                                 *data,
                                 *tail,
                                 *end;
};

> : next和prev,這兩個域是用來連接相關的skb的(例如如果有分片,將這些分片連接在一起可以)

> : sk,指向報文所屬的套接字指針

> : tstamp,記錄接收或者傳輸報文的時間戳

> : dev和input_dev,記錄接收或者發送的設備

>: union u,對於一個層次,例如tcp層,可能有很多不同的協議,他們的協議頭不一樣,那麼這個聯合體就是記錄這些協議頭的。

     此處u就是代表傳輸層

> : union nh,代表網絡層頭

> : union mac,代表鏈路層頭

> : dst,指向des_entry結構,記錄了到達目的地的路由信息,以及其他的一些網絡特徵信息。

> : sp:安全路徑,用於xfrm

> : cb[],保存與協議相關的控制信息,每個協議可能獨立使用這些信息。

> : 重要的字段 len 和 data_len:

      len代: 表整個數據區域的長度!這裏要提前解釋幾個定義,skb的組成是有sk_buff控制 + 線性數據 + 非線性數據 

      (skb_shared_info) 組成!

     後面會具體解釋是什麼意思!在sk_buff這個裏面沒有實際的數據,這裏僅僅是控制信息,數據是通過後面的data指針指向其他內

     存塊的!那個內存塊中是線性數據和

     非線性數據!那麼len就是length(線性數據) + length(非線性數據)!!!

     data_len: 指的是length(非線性數據)!!!那麼可以知道:length(線性數據) =  skb->len - skb->data_len

> : mac_len,指的是mac頭長度

> : csum,某時刻協議的校驗和

> : priority,報文排隊優先級,取決於ip中的tos域

> : local_df,允許在本地分配

> : cloned,保存當前的skb_buff是克隆的還是原始數據

> : ip_summed,是否計算ip校驗和

> : nohdr,僅僅引用數據區域

> : pkt_type,報文類型,例如廣播,多播,迴環,本機,傳出...

> : fclone,skb_buff克隆狀態

> : ipvs_property,skb_buff是否屬於ipvs

> : protocal,協議信息

> : nfmark,用於鉤子之間通信

> : nfct_reasm,netfilter的跟蹤連接重新組裝指針

> : nf_bridge,保存橋接信息

> : tc_index: Traffic control index,tc_verd: traffic control verdict

> : truesize,該緩衝區分配的所有總的內存,包括:skb_buff + 所有數據大小

> : users,保存引用skb_buff的數量

> : 重要數據字段:head,data,tail,end!!!

    head:指向分配給的線性數據內存首地址( 建立起一個觀念:並不是分配這麼多內存,就都能被使用作爲數據存儲,可能沒這麼多

    數據也有可能!但是也不要認爲分配這麼多 就足夠了,也不一定(非線性數據就是例子) )

    data:指向保存數據內容的首地址!我們由head可以知道,head和data不一定就是指在同一個位置!!!

    tail:指向數據的結尾!

    end:指向分配的內存塊的結尾! ( 由上面我們知道數據結尾 != 分配的內存塊的結尾 )

    下面還會具體分析!!!!!!!!!!!

二、

我覺得需要先了解一些對於一個數據skb到底有什麼,或者說由哪些元素組成!這就需要知道所謂的 “線性數據” 和 “非線性數據”。

基本的組成如下:

> : sk_buff : 這是一個sk_buff的控制結構

> : 線性數據區域

> : 非線性數據區域( 由skb_shared_info結構體管理 )

那麼下面通過一個圖來看看這個skb結構到底是怎麼樣的!看(圖一)

                                                                                    (圖一)

藉助圖一,我們先來分析兩個重要字段:len和data_len!

之前說過len代表的是整個數據的長度,data_len代表的是非線性數據長度。我們由圖一可以看到線性數據長度爲l1,再看看非線性數據,其實就是看frags[]和frag_list

ok...那麼我們可以知道非線性數據長度爲( l2 + ... + ln ) + ( l(n+1) + ... + lm )

即:len = l1 + ( l2 + ... + ln ) + ( l(n+1) + ... + lm )

        data_len = ( l2 + ... + ln ) + ( l(n+1) + ... + lm )

ok...

現在從分配內存開始解釋這個圖的由來:

我們使用skb_alloc給skb分配空間,那麼剛剛分配結束返回時候,是什麼樣的情況呢?看下圖(圖二):

                                                

                                                                                     (圖二)

剛剛開始初始化的時候,預分配一個一塊線性數據區域,這個區域一般放入的是各個協議層次的不同的頭,還有一些實際數據,下面的非線性區域是爲了彌補當數據真的很多的時候,作爲數據區域的擴展!關於skb_shared_info具體意思下面會繼續說!注意在初始化的時候,head,data和tail都指向內存的開始位置,head在這個位置始終不變,它表示的是分配的內存的開始位置。end的位置也是不變的,表示的是分配的內存的結束位置。data和tail會隨着數據的加入和減少變化,總之表示的是放入數據的內存區域(由圖一)可知。

現在需要解釋一下skb_shared_info這個結構體,這個結構體真的是很很有特色!主要是其中的兩個字段frags和frag_list,下面繼續解釋:

struct skb_shared_info {
         atomic_t        dataref;        // 對象被引用次數
         unsigned short  nr_frags;       // 分頁段數目,即frags數組元素個數
         unsigned short  tso_size;       
         unsigned short  tso_segs;
         unsigned short  ufo_size;
         unsigned int    ip6_frag_id;
         struct sk_buff  *frag_list;    // 一般用於分段(還沒有非常清楚的理解)
         skb_frag_t      frags[MAX_SKB_FRAGS]; // 保存分頁數據(skb->data_len=所有的數組數據長度之和)
};

關於frags和frag_list沒有必然的聯繫!


> : 對於frags[]一般用在,當數據真的很多,而且在線性數據區域裝不下的時候,需要使用這個,skb_frag_t中是一頁一頁的數據,先看看結構體:

struct skb_frag_struct {
         struct page *page;    // 代表一頁數據
         __u16 page_offset;    // 代表相對開始位置的頁偏移量
         __u16 size;           // page中數據長度
};

需要注意的是:只有在DMA支持物理分散頁的Scatter/Gather(SG,分散/聚集)操作時候纔可以使用frags[]來保存剩下的數據,否則,只能擴展線性數據區域進行保存!!!
這些頁其實是其實就是虛擬頁映射到物理頁的結構,看下圖(圖三):

                                                                                         (圖三)

> : 對於frag_list來說,一般我們在分片的時候裏面裝入每個片的信息,注意,每個片最終也都是被封裝成一個小的skb,這個必須

     的!

     注意:具體怎麼分片的看上一篇博文:數據分片 (  看其中的ip_fragment函數  )

     那麼看一下其基本結構如圖四:

                                                         

                                                                                         (圖四)

三、

最重要的是需要理解對於這個skb是怎麼操作的,在操作的過程中,每一塊的內存分配是怎麼變化的,這才更重要!

看下面的函數們:

> : alloc_skb()函數

static inline struct sk_buff *alloc_skb(unsigned int size,
                                         gfp_t priority)
{
         return __alloc_skb(size, priority, 0);
}


其實看__alloc_skb函數:
struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
                             int fclone)
{
         kmem_cache_t *cache;
         struct skb_shared_info *shinfo;
         struct sk_buff *skb;
         u8 *data;
 
         cache = fclone ? skbuff_fclone_cache : skbuff_head_cache;    // 根據克隆狀態來判斷在哪一個緩衝區進行分配cache
 
         /* Get the HEAD */
         skb = kmem_cache_alloc(cache, gfp_mask & ~__GFP_DMA);        // 得到skb,注意這裏沒有包含數據,僅僅是skb_buff這個結構體
         if (!skb)
                 goto out;
 
         /* Get the DATA. Size must match skb_add_mtu(). */
         size = SKB_DATA_ALIGN(size);                                     // 獲得線性數據分片長度(注意對齊)
         data = kmalloc(size + sizeof(struct skb_shared_info), gfp_mask); // 注意分配的是什麼,是size + skb_shared_info!!!!!
         if (!data)
                 goto nodata;
 
         memset(skb, 0, offsetof(struct sk_buff, truesize));          // 初始化
         skb->truesize = size + sizeof(struct sk_buff);               // 實際大小等於sk_buff + size,剛剛開始還沒有非線性數據
         atomic_set(&skb->users, 1);                                  
         skb->head = data;                                            // 注意指針,這個結合上面的圖一清二楚
         skb->data = data;
         skb->tail = data;
         skb->end  = data + size;
         /* make sure we initialize shinfo sequentially */
         shinfo = skb_shinfo(skb);
         atomic_set(&shinfo->dataref, 1);
         shinfo->nr_frags  = 0;
         shinfo->tso_size = 0;
         shinfo->tso_segs = 0;
         shinfo->ufo_size = 0;
         shinfo->ip6_frag_id = 0;
         shinfo->frag_list = NULL;
 
         if (fclone) {
                 struct sk_buff *child = skb + 1;
                 atomic_t *fclone_ref = (atomic_t *) (child + 1);
 
                 skb->fclone = SKB_FCLONE_ORIG;
                 atomic_set(fclone_ref, 1);
 
                 child->fclone = SKB_FCLONE_UNAVAILABLE;
         }
out:
         return skb;
nodata:
         kmem_cache_free(cache, skb);
         skb = NULL;
         goto out;
}


那麼alloc之後的圖就是(圖五):

                                               
                                                                                          (圖五)

其實和圖二是一樣的!我們可以看到,現在僅僅是分配了線束數據區域,但是現在還沒有數據!一定要注意!所以前面三個指針指在一起!因爲沒有數據,那麼len和data_len的值就是0 !

> : skb_reserve函數

static inline void skb_reserve(struct sk_buff *skb, int len)
{
         skb->data += len;
         skb->tail += len;
 }


代碼其實很easy、就是移動兩個指針而已~


這個函數很重要,是爲“協議頭”預留空間!而且是盡最大的空間預留,因爲很多頭都會有可選項,那麼我們不知道可選項是多大,所以只能是按照最大的分配,那麼也說明了一點,預留的空間headroom也就是不一定都能使用完的!可能還有剩餘的,由上面的圖也可以看出來!這也是爲什麼需要這麼多指針的問題!那麼這個函數直接導致head指針和tail、data指針分離,入下面圖六所示:

                                           

                                                                                           (圖六)

注意headroom就是用來存儲各個協議頭的足夠大的空間,tailroom就可以認爲是存儲其他線性數據的空間。( 這裏不要曲解協議頭不是線性數據,其實協議頭也是!!!所以當增加頭的時候,data指針向上移動,當增加其他數據的時候,tail指針向下移動 )。現在data和tail指向一起,那麼還是說明數據沒有!!!

> : skb_put函數 ----> 用於操作線性數據區域(tailroom區域)的用戶數據

static inline unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
{
         unsigned char *tmp = skb->tail;
         SKB_LINEAR_ASSERT(skb);          
         skb->tail += len;                 // 移動指針
         skb->len  += len;                 // 數據空間增大len
         if (unlikely(skb->tail>skb->end)) // 如果tail指針超過end指針了,那麼處理錯誤~
                 skb_over_panic(skb, len, current_text_addr());
         return tmp;
}


這函數其實就是從tailroom預留空間,相當於是移動tail指針,這樣如果從上圖(圖六)開始看,也就是tail開始向下移動,和data分離了。。。一般來說,這樣做都是爲了用戶數據再次處理,或者說爲TCP/IP的負載預留空間!

看圖七,當使用skb_put時候,由圖六---->圖七

                                                  

                                                                                          (圖七)

我們可以看到指針的移動data還是在headroom的下面,中間的是用戶數據預留的部分,由skb_put得到,tail表示數據結尾!再看一下sk_buff中的len,變成了數據長度ld!!

> : skb_push函數:----------> 用於操作headroom區域的協議頭

static inline unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
{
         skb->data -= len;      // 向上移動指針
         skb->len  += len;      // 數據長度增加
         if (unlikely(skb->data<skb->head))  // data指針超過head那麼就是處理錯誤~
                 skb_under_panic(skb, len, current_text_addr());
         return skb->data;
}

和skb_put對應,上面試操作用戶數據的,這裏是操作協議頭的!其實就是data指針向上移動而已~注意len增大了哦~前面說了協議頭也是屬於數據!

如下面圖所示,由圖七---->圖八

                                           

                                                                                        (圖八)

我們可以知道,data向上移動了,同時注意len變成ld+lp了,其中lp是這個增加的協議頭的長度!

> : skb_pull函數:-----------> 其實這個函數纔是與skb_push函數對應的函數!因爲這是去頭函數,而skb_push是增頭函數!所以這個函數一般用在解包的時候!

static inline unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)
{
         return unlikely(len > skb->len) ? NULL : __skb_pull(skb, len);
}
 
 
static inline unsigned char *__pskb_pull(struct sk_buff *skb, unsigned int len)
{
         if (len > skb_headlen(skb) &&
             !__pskb_pull_tail(skb, len-skb_headlen(skb)))
                 return NULL;
         skb->len -= len;                              // 長度減小
         return skb->data += len;                      // 移動指針
}

其實就是data指針向下移動,當前一個協議頭被去掉,headroom剩餘的空間增大了!看下圖:

由圖八---->圖九:

                                                     

                                                                                      (圖九)

虛線是data之前的指針位置,現在移動到下面實線!!需注意:len的長度減小,減小的大小是剝去的頭的大小!!

四、

最後我們從兩條線整體分析一下:

1:從應用層用戶數據開始,直到物理層發送出去

      > 初始化的什麼就不多說了,和前面的差不多,現在也加入用戶數據已經在了,如圖七所示一樣!那麼到了TCP層,需要增加

         TCP層的頭:

         如圖10所示:

                                                   

                                                                                                  (圖10)

            需要注意的是這裏是傳輸層,那麼傳輸層的結構u中的th代表的是tcp的頭,那麼tcp指向tcp頭OK!同時注意 len長度+=l1 哦~~~

        > 再看到了IP層:如圖11

                                                   

                                                                                                 (圖11)

                至於需要解釋什麼就沒什麼了,都是一樣的~

             > 到鏈路層了:如圖12

                                                     

                                                                                             (圖12)

  OK!

2:第二個過程其實是第一個逆過程,都差不多,所以不多說了~

五、

最後看一下操作skb的兩個函數pskb_copy和skb_copy

前者僅僅是將sk_buff的結構體和線性數據copy過來,對於非線性數據,是引用原始的skb的數據的!而後者是不僅將sk_buff和線性數據拷貝,同時將非線性數據也copy了一份,看下面就明白了!這就在效率上就差了很多!所以如果不想修改數據,那麼還是使用pskb_copy更好!

對於pskb_copy:

對於skb_copy:

OK  我覺得差不多了~~~~~結束~~~~~~~~~~~~~


————————————————
版權聲明:本文爲CSDN博主「小刀刀」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/shanshanpt/article/details/21024465

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