1. 程式人生 > >Linux 核心網路協議棧 ------sk_buff 結構體 以及 完全解釋 (2.6.16)

Linux 核心網路協議棧 ------sk_buff 結構體 以及 完全解釋 (2.6.16)


在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  我覺得差不多了~~~~~結束~~~~~~~~~~~~~

相關推薦

Linux 核心網路協議 ------sk_buff 結構 以及 完全解釋 2.6.16

在2.6.24之後這個結構體有了較大的變化,此處先說一說2.6.16版本的sk_buff,以及解釋一些問題。 一、 先直觀的看一下這個結構體~~~~~~~~~~~~~~~~~~~~~~在下面解釋每個欄位的意義~~~~~~~~~~~ struct sk_buff {

Linux 核心網路協議 -sk_buff結構

sk_buff結構體是linux網路程式碼中最重要的資料結構,是整個網路傳輸載體。所以sk_buff結構體裡面有很多關於其他功能的成員欄位,比如:防火牆,子路由系統,多播等。這些欄位並不是一定有的,只有在滿足特點條件才有的。  sk_buff結構體是雙鏈表結構,sk_buf

Linux 核心網路協議 中最重要的資料結構 SKB

在計算機的世界裡,備份是一種美德。-------------漫步雲端  作為核心網路協議部分最重要的資料結構SKB,有很多值得仔細推敲的問題。 SKB這種說法實際包含了兩部分,即skb描述符和skb資料。 Skb描述符即核心中的skbuff結構體,裡面含有大量的指標變數

Linux 核心網路協議 ------ 擁塞避免處理函式 tcp_reno_cong_avoid

慢啟動和快速重傳擁塞避免演算法,函式tcp_reno_cong_avoid 在“慢開始”階段,每收到一個ACK,cwnd++一次,那麼一個RTT之後,cwnd就會加倍 擁塞避免階段,其實就是在一個RTT時間內將cwnd++一次( 注意在不丟包的情況下 ) /* *

linux核心網路協議學習筆記6

本篇討論IP包的收發(暫不包括路由) 先來看inet_init, 首先是呼叫proto_register,註冊了tcp_prot, udp_prot, raw_prot,其中proto_register前半部分是初始化各種slab_cache,後半部分把這些struct

Linux 核心網路協議 ------ tcp重傳資料包 tcp_retransmit_skb 函式

/* This retransmits one SKB. Policy decisions and retransmit queue * state updates are done by the caller. Returns non-zero if an

Linux核心網路協議程式碼分析

^ |       sys_read                fs/read_write.c |       sock_read               net/socket.c |       sock_recvmsg            net/socket.c |       inet_re

Linux 核心網路協議 ----- Linux 核心路由機制 (2.6.25)

       核心的路由部分是是網路中重要部分,目前在Linux核心中預設的路由查詢演算法使用的是Hash查詢,所以你會看到很多的資料結構是XXX_hash什麼之類(例如fn_hash)。Linux核心從2.1開始就支援基於策略的路由,那麼什麼是基於策略的路由呢?我們一般

Linux 核心網路協議原始碼剖析】socket.c——BSD Socket層1

寫在前面:本系列文章先把各個層對應的檔案原始碼剖析一遍,最後再穿插起來,理清整個協議棧網路資料包的上下傳送通道,從整體實現上進行把握。         圖片來源於《Linux 核心網路棧原始碼情景分析》 更上層函式:tcp socket函式介紹。本篇則是介紹BSD Sock

Linux 核心網路協議 ------ tcp_ack 函式處理接收到的ACK包之後

注意 tcp_ack 是來處理接收到的ACK的,那麼到底怎麼去做呢?看下面: 先還上把tcp_sock的結構放在這裡,下面一些資料的分析需要用到: struct tcp_sock { /* inet_connection_sock has to be

Linux 核心網路協議原始碼剖析】bind 函式剖析

socket 函式並沒有為套接字繫結本地地址和埠號,對於伺服器端則必須顯性繫結地址和埠號。bind 函式主要是伺服器端使用,把一個本地協議地址賦予套接字。 1、應用層——bind 函式 #include <sys/socket.h> int bind(int

學習Linux-4.12核心網路協議1.5——協議的初始化(inet_init主要資料結構)

前面瞭解到網路初始化申請了兩塊skb快取記憶體和建立了一個/proc/net/protocols檔案,現在開始重頭戲,網路協議棧的初始化。這篇文章主要介紹網路棧中使用到的主要資料結構。 網路協議棧的核心實現和理論上的分層有些不一樣,在程式碼裡面的分層如下圖: 開始前,

學習Linux-4.12核心網路協議1.6——協議的初始化(inet_init實現過程)

這篇文章主要分析inet_init()函式的實現過程: 1796 static int __init inet_init(void) 1797 { 1798 struct inet_protosw *q; 1799 struct list_head *r;

學習Linux-4.12核心網路協議1.7——網路裝置的初始化struct net_device

在linux的網路裝置裡,其中一個最關鍵的結構體應該要算net_device了,它由對應的網路裝置驅動進行建立和初始化,服務於核心網路子系統。 1. struct net_device 註釋分析 struct net_device這個結構體比較大,在瞭解它之前,我們先看一下

學習Linux-4.12核心網路協議2.1——介面層加快傳輸速率的特性

前面花了好多篇幅,終於將網路協議棧的初始化相關的內容介紹完了,也就是說完成前面的那些步驟以後,網路協議棧具備了資料包的收發功能。在網路介面層,它只負責資料包的接收與傳送,而不關注資料包在網路層的型別是什麼,所以傳輸速率的快慢往往與介面層的處理有著密切的關係,下面我們來了解一下在提供傳輸速率上面,現在的核心都

學習Linux-4.12核心網路協議2.2——介面層資料包的接收上半部

前面寫了這麼多,終於可以開始分析資料報的傳輸過程了,那我們就愉快的開始吧! 我們知道,一箇中斷處理函式主要分兩個部分,上半部和下半部,這篇文章主要介紹上半部分。 當一個數據包到達的時候,網絡卡驅動會完成接收並且觸發中斷,我們就從這個中斷處理函式開始: 當一箇中斷產生併發送

學習Linux-4.12核心網路協議1.4——協議的初始化(proto_init)

這篇文章主要分析proto_init過程,它完成協議相關proc檔案的建立 3241 static __net_initdata struct pernet_operations proto_net_ops = { 3242 .init = proto_init

學習Linux-4.12核心網路協議3.1——網路層的收包處理

現在我們將進入傳輸層的分析: 在前面我們知道,資料包到達介面層的時候,它會根據ptype_base來查詢包的型別,並根據包的型別交給不同的網路層函式處理,比如ip_recv,icmp_recv等,現在我們就來看看網路層是怎麼處理不同型別的包的: 1. IP私有資訊控制塊

學習Linux-4.12核心網路協議1.8——網路裝置驅動模組的載入

1.瞭解PCI匯流排 說到網路裝置驅動,就不得不說PCI匯流排,但是這個話題可深可淺,而且網上的資料也是一大堆(比如),但是對於我們來說,目前並不需要掌握很深,下面是網上找的兩張最基本的PCI工作結構圖,雖然PCI總線上可以掛接不同種類的裝置,但我們這裡只要瞭解網路裝置就夠

Linux 網路協議開發程式碼分析篇之VLAN—— VLAN收發處理

        以一個例子來說明,當主機收到報文,交由vlan協議模組處理後(vlan_rcv),此時需要更換skb->dev所指向的裝置,以使上層協議認為報文是來自於虛擬網絡卡(比如eth1.1),而不知道網絡卡eth1的存在。更換裝置就需要知道skb->dev更換的目標。這由兩個因素決定:sk