Linux TCP/IP 協議棧的關鍵數據結構Socket Buffer(sk_buff )

 sk_buff結構可能是linux網絡代碼中最重要的數據結構,它表示接收或發送數據包的包頭信息。它在<include/linux/skbuff.h>中定義,幷包含很多成員變量供網絡代碼中的各子系統使用。

這個結構在linux內核的發展過程中改動過很多次,或者是增加新的選項,或者是重新組織已存在的成員變量以使得成員變量的佈局更加清晰。它的成員變量可以大致分爲以下幾類:

  • Layout 佈局

  • General 通用

  • Feature-specific功能相關

  • Management functions管理函數

這個結構被不同的網絡層(MAC或者其他二層鏈路協議,三層的IP,四層的TCP或UDP等)使用,並且其中的成員變量在結構從一層向另一層傳遞時改變。L4向L3傳遞前會添加一個L4的頭部,同樣,L3向L2傳遞前,會添加一個L3的頭部。添加頭部比在不同層之間拷貝數據的效率更高。由於在緩衝區的頭部添加數據意味着要修改指向緩衝區的指針,這是個複雜的操作,所以內核提供了一個函數skb_reserve(在後面的章節中描述)來完成這個功能。協議棧中的每一層在往下一層傳遞緩衝區前,第一件事就是調用skb_reserve在緩衝區的頭部給協議頭預留一定的空間。

skb_reserve同樣被設備驅動使用來對齊接收到包的包頭。如果緩衝區向上層協議傳遞,舊的協議層的頭部信息就沒什麼用了。例如,L2的頭部只有在網絡驅動處理L2的協議時有用,L3是不會關心它的信息的。但是,內核並沒有把L2的頭部從緩衝區中刪除,而是把有效荷載的指針指向L3的頭部,這樣做,可以節省CPU時間。

1. 網絡參數和內核數據結構

就像你在瀏覽TCP/IP規範或者配置內核時所看到的一樣,網絡代碼提供了很多有用的功能,但是這些功能並不是必須的,比如說,防火牆,多播,還有其他一些功能。大部分的功能都需要在內核數據結構中添加自己的成員變量。因此,sk_buff裏面包含了很多像#ifdef這樣的預編譯指令。例如,在sk_buff結構的最後,你可以找到:

struct sk_buff {
     ... ... ...
#ifdef CONFIG_NET_SCHED
     _ _u32     tc_index;
#ifdef CONFIG_NET_CLS_ACT
     _ _u32     tc_verd;
     _ _u32     tc_classid;
#endif
#endif
}

它表明,tc_index只有在編譯時定義了CONFIG_NET_SCHED符號纔有效。這個符號可以通過選擇特定的編譯選項來定義(例如:"Device Drivers Networking supportNetworking options QoS and/or fair queueing")。這些編譯選項可以由管理員通過make config來選擇,或者通過一些自動安裝工具來選擇。

前面的例子有兩個嵌套的選項:CONFIG_NET_CLS_ACT(包分類器)只有在選擇支持“QoS and/or fair queueing”時才能生效。

順便提一下,QoS選項不能被編譯成內核模塊。原因就是,內核編譯之後,由某個選項所控制的數據結構是不能動態變化的。一般來說,如果某個選項會修改內核數據結構(比如說,在sk_buff
裏面增加一個項tc_index),那麼,包含這個選項的組件就不能被編譯成內核模塊。

你可能經常需要查找是哪個make config編譯選項或者變種定義了某個#ifdef標記,以便理解內核中包含的某段代碼。在2.6內核中,最快的,查找它們之間關聯關係的方法, 就是查找分佈在內核源代碼樹中的kconfig文件中是否定義了相應的符號(每個目錄都有一個這樣的文件)。在 
2.4內核中,你需要查看Documentation/Configure.help文件。

2. Layout Fields

有些sk_buff成員變量的作用是方便查找或者是連接數據結構本身。內核可以把sk_buff組織成一個雙向鏈表。當然,這個鏈表的結構要比常見的雙向鏈表的結構複雜一點。

就像任何一個雙向鏈表一樣,sk_buff中有兩個指針next和prev,其中,next指向下一個節點,而 
prev指向上一個節點。但是,這個鏈表還有另一個需求:每個sk_buff結構都必須能夠很快找到鏈表頭節點。爲了滿足這個需求,在第一個節點前面會插入另一個結構sk_buff_head,這是一個輔助節點,它的定義如下:

 struct sk_buff_head {
   /* These two members must be first. */    struct sk_buff     * next;    
    struct sk_buff     * prev;       
    _ _u32         qlen;    
    spinlock_t     lock;    
 };

qlen代表鏈表元素的個數。lock用於防止對鏈表的併發訪問。

sk_buff和sk_buff_head的前兩個元素是一樣的:next和prev指針。這使得它們可以放到同一個鏈表中,儘管sk_buff_head要比sk_buff小得多。另外,相同的函數可以同樣應用於sk_buff和sk_buff_head。

爲了使這個數據結構更靈活,每個sk_buff結構都包含一個指向sk_buff_head的指針。這個指針的名字是list。圖1會幫助你理解它們之間的關係。

Figure 1. List of sk_buff elements

 

 

其他有趣的成員變量如下:

struct sock *sk
這是一個指向擁有這個sk_buff的sock結構的指針。這個指針在網絡包由本機發出或者由本機進程接收時有效,因爲插口相關的信息被L4(TCP或UDP)或者用戶空間程序使用。如果sk_buff只在轉發中使用(這意味着,源地址和目的地址都不是本機地址),這個指針是NULL。

unsigned int len
這是緩衝區中數據部分的長度。它包括主緩衝區中的數據長度(data指針指向它)和分片中的數據長度。它的值在緩衝區從一個層向另一個層傳遞時改變,因爲往上層傳遞,舊的頭部就沒有用了,而往下層傳遞,需要添加本層的頭部。len同樣包含了協議頭的長度。


unsigned int data_len
和len不同,data_len只計算分片中數據的長度。


unsigned int mac_len
這是mac頭的長度。

atomic_t users
這是一個引用計數,用於計算有多少實體引用了這個sk_buff緩衝區。它的主要用途是防止釋放sk_buff後,還有其他實體引用這個sk_buff。因此,每個引用這個緩衝區的實體都必須在適當的時候增加或減小這個變量。這個計數器只保護sk_buff結構本身,而緩衝區的數據部分由類似的計數器(dataref)來保護.
有時可以用atomic_inc和atomic_dec函數來直接增加或減小users,但是,通常還是使用函數skb_get和kfree_skb來操作這個變量。


unsigned int truesize
這是緩衝區的總長度,包括sk_buff結構和數據部分。如果申請一個len字節的緩衝區,alloc_skb函數會把它初始化成len+sizeof(sk_buff)。

struct sk_buff *alloc_skb(unsigned int size,int gfp_mask)
{
      ... ... ...
      skb->truesize = size + sizeof(struct sk_buff);
      ... ... ...
}
當skb->len變化時,這個變量也會變化。

unsigned char *head
unsigned char *end
unsigned char *data
unsigned char *tail
它們表示緩衝區和數據部分的邊界。在每一層申請緩衝區時,它會分配比協議頭或協議數據大的空間。head和end指向緩衝區的頭部和尾部,而data和tail指向實際數據的頭部和尾部,參見圖2。每一層會在head和data之間填充協議頭,或者在tail和end之間添加新的協議數據。圖2中右邊數據部分會在尾部包含一個附加的頭部。

 

Figure 2. head/end versus data/tail pointers

 


void (*destructor)(...)
這個函數指針可以初始化成一個在緩衝區釋放時完成某些動作的函數。如果緩衝區不屬於一個socket,這個函數指針通常是不會被賦值的。如果緩衝區屬於一個socket, 這個函數指針會被賦值爲sock_rfree或sock_wfree(分別由skb_set_owner_r或skb_set_owner_w函數初始化)。這兩個sock_xxx函數用於更新socket的隊列中的內存容量。

3. General Fields

本節描述sk_buff的主要成員變量,這些成員變量與特定的內核功能無關:

struct timeval stamp
這個變量只對接收到的包有意義。它代表包接收時的時間戳,或者有時代表包準備發出時的時間戳。它在netif_rx裏面由函數net_timestamp設置,而netif_rx是設備驅動收到一個包後調用的函數。

struct net_device *dev
這個變量的類型是net_device,net_device它代表一個網絡設備。dev的作用與這個包是準備發出的包還是剛接收的包有關。當收到一個包時,設備驅動會把sk_buff的dev指針指向收到這個包的設備的數據結構,就像下面的vortex_rx裏的一段代碼所做的一樣,這個函數屬於3c59x系列以太網卡驅動,用於接收一個幀。(drivers/net/3c59x.c):

static int vortex_rx(struct net_device *dev)
{
            ... ... ...
         skb->dev = dev;
            ... ... ...
         skb->protocol = eth_type_trans(skb, dev);
         netif_rx(skb); /* Pass the packet to the higher layer */
            ... ... ...
} 

當一個包被髮送時,這個變量代表將要發送這個包的設備。在發送網絡包時設置這個值的代碼要比接收網絡包時設置這個值的代碼複雜。有些網絡功能可以把多個網絡設備組成一個虛擬的網絡設備(也就是說,這些設備沒有和物理設備直接關聯),並由一個虛擬網絡設備驅動管理。當虛擬設備被使用時,dev指針指向虛擬設備的net_device結構。而虛擬設備驅動會在一組設備中選擇一個設備並把dev指針修改爲這個設備的net_device結構。因此,在某些情況下, 指向傳輸設備的指針會在包處理過程中被改變。


struct net_device *input_dev
這是收到包的網絡設備的指針。如果包是本地生成的,這個值爲NULL。對以太網設備來說,這個值由eth_type_trans初始化,它主要被流量控制代碼使用。


struct net_device *real_dev
這個變量只對虛擬設備有意義,它代表與虛擬設備關聯的真實設備。例如,Bonding和VLAN設備都使用它來指向收到包的真實設備。

union {...} h
union {...} nh
union {...} mac

這些是指向TCP/IP各層協議頭的指針:h指向L4,nh指向L3,mac指向L2。每個指針的類型都是一個聯合,包含多個數據結構,每一個數據結構都表示內核在這一層可以解析的協議。例如,h是一個包含內核所能解析的L4協議的數據結構的聯合。每一個聯合都有一個raw變量用於初始化,後續的訪問都是通過協議相關的變量進行的。

當接收一個包時,處理n層協議頭的函數從n-1層收到一個緩衝區,它的skb->data指向n層協議的頭。處理n層協議的函數把本層的指針(例如,L3對應的是skb->nh指針)初始化爲skb->data,因爲這個指針的值會在處理下一層協議時改變(skb->data將被初始化成緩衝區裏的其他地址)。在處理n層協議的函數結束時,在把包傳遞給n+1層的處理函數前,它會把skb->data指針指向n層協議頭的末尾,這正好是n+1層協議的協議頭(參見圖3)。

發送包的過程與此相反,但是由於要爲每一層添加新的協議頭,這個過程要比接收包的過程複雜。

Figure 3. Header's pointer initializations while moving from layer two to layer three


 


struct dst_entry dst
這個變量在路由子系統中使用。

char cb[40]
這是一個“control buffer”,或者說是一個私有信息的存儲空間,由每一層自己維護並使用。它在分配sk_buff結構時分配(它目前的大小是40字節,已經足夠爲每一層存儲必要的私有信息了)。在每一層中,訪問這個變量的代碼通常用宏實現以增強代碼的可讀性。例如,TCP用這個變量存儲tcp_skb_cb結構,這個結構在include/net/tcp.h中定義:

struct tcp_skb_cb {
     ... ... ...
     _ _u32         seq;         /* Starting sequence number */
     _ _u32         end_seq;     /* SEQ + FIN + SYN + datalen*/
     _ _u32         when;        /* used to compute rtt's     */
     _ _u8          flags;       /* TCP header flags.         */
     ... ... ...
};

下面這個宏被TCP代碼用來訪問cb變量。在這個宏裏面,有一個簡單的類型轉換:

#define TCP_SKB_CB(_ _skb)     ((struct tcp_skb_cb *)&((_ _skb)->cb[0]))

下面的例子是TCP子系統在收到一個分段時填充相關數據結構的代碼:

int tcp_v4_rcv(struct sk_buff *skb)
{
         ... ... ...
         th = skb->h.th;
         TCP_SKB_CB(skb)->seq = ntohl(th->seq);
         TCP_SKB_CB(skb)->end_seq = (TCP_SKB_CB(skb)->seq + th->syn + th->fin +
                                     skb->len - th->doff * 4);
         TCP_SKB_CB(skb)->ack_seq = ntohl(th->ack_seq);
         TCP_SKB_CB(skb)->when = 0;
         TCP_SKB_CB(skb)->flags = skb->nh.iph->tos;
         TCP_SKB_CB(skb)->sacked = 0;
         ... ... ...
}

如果想要了解cb中的參數是如何被取出的,可以查看net/ipv4/tcp_output.c中的tcp_transmit_skb函數。這個函數被TCP用於向IP層發送一個分段。

unsigned int csum
unsigned char ip_summed
表示校驗和以及相關狀態標記。

unsigned char cloned
一個布爾標記,當被設置時,表示這個結構是另一個sk_buff的克隆。在“克隆和拷貝緩衝區”一節中有描述。

unsigned char pkt_type
這個變量表示幀的類型,分類是由L2的目的地址來決定的。可能的取值都在include/linux/if_packet.h中定義。對以太網設備來說,這個變量由eth_type_trans函數初始化。 
類型的可能取值如下:

PACKET_HOST
包的目的地址與收到它的網絡設備的L2地址相等。換句話說,這個包是發給本機的。

.PACKET_MULTICAST
包的目的地址是一個多播地址,而這個多播地址是收到這個包的網絡設備所註冊的多播地址。

PACKET_BROADCAST
包的目的地址是一個廣播地址,而這個廣播地址也是收到這個包的網絡設備的廣播地址。

PACKET_OTHERHOST
包的目的地址與收到它的網絡設備的地址完全不同(不管是單播,多播還是廣播),因此,如果本機的轉發功能沒有啓用,這個包會被丟棄。

PACKET_OUTGOING
這個包將被髮出。用到這個標記的功能包括Decnet協議,或者是爲每個網絡tap都複製一份發出包的函數。

PACKET_LOOPBACK
這個包發向loopback設備。由於有這個標記,在處理loopback設備時,內核可以跳過一些真實設備才需要的操作。

PACKET_FASTROUTE
這個包由快速路由代碼查找路由。快速路由功能在2.6內核中已經去掉了。

 

_ _u32 priority
這個變量描述發送或轉發包的QoS類別。如果包是本地生成的,socket層會設置priority變量。如果包是將要被轉發的,rt_tos2priority函數會根據ip頭中的Tos域來計算賦給這個變量的值。這個變量的值與DSCP(DiffServ CodePoint)沒有任何關係。

unsigned short protocol
這個變量是高層協議從二層設備的角度所看到的協議。典型的協議包括IP,IPV6和ARP。完整的列表在include/linux/if_ether.h中。由於每個協議都有自己的協議處理函數來處理接收到的包,因此,這個域被設備驅動用於通知上層調用哪個協議處理函數。每個網絡驅動都調用netif_rx來通知上層網絡協議的協議處理函數,因此protocol變量必須在這些協議處理函數調用之前初始化。

unsigned short security
這是包的安全級別。這個變量最初由IPSec子系統使用,但現在已經作廢了。

4. Feature-Specific Fields

linux內核是模塊化的,你可以選擇包含或者刪除某些功能。因此,sk_buff結構裏面的一些成員變量只有在內核選擇支持某些功能時纔有效,比如防火牆(netfilter)或者qos:

unsigned long nfmark
_ _u32 nfcache
_ _u32 nfctinfo
struct nf_conntrack *nfct
unsigned int nfdebug
struct nf_bridge_info *nf_bridge
這些變量被netfilter使用(防火牆代碼),內核編譯選項是“Device Drivers->Networking support-> Networking options-> Network packet filtering”和兩個子選項“Network packet filtering debugging”和“Bridged IP/ARP packets filtering”

union {...} private
這個聯合結構被高性能並行接口(HIPPI)使用。相應的內核編譯選項是“Device->Drivers ->Networking support ->Network device support ->HIPPI driver support”

_ _u32 tc_index
_ _u32 tc_verd
_ _u32 tc_classid
這些變量被流量控制代碼使用,內核編譯選項是“Device Drivers ->Networking->support ->Networking options ->QoS and/or fair queueing”和它的子選項“Packetclassifier API”

struct sec_path *sp
這個變量被IPSec協議用於跟蹤傳輸的信息。

5. Management Functions

有很多函數,通常都比較短小而且簡單,內核用這些函數操作sk_buff的成員變量或者sk_buff
鏈表。圖4會幫助我們理解其中幾個重要的函數。我們首先來看分配和釋放緩衝區的函數,然後是一些通過移動指針在緩衝區的頭部或尾部預留空間的函數。

如果你看過include/linux/skbuff.h和net/core/skbuff.c中的函數,你會發現,基本上每個函數都有兩個版本,名字分別是do_something和__do_something。通常第一種函數是一個包裝函數,它會在第二種函數的基礎上增加合法性檢查或者鎖。一般來說,類似__do_something的函數不能被直接調用(除非滿足特定的條件,比如說鎖)。那些違反這條規則而直接引用這些函數的不良代碼會最終被更正。

Figure 4. Before and after: (a)skb_put, (b)skb_push, (c)skb_pull, and (d)skb_reserve


 

 

5.1. Allocating memory: alloc_skb and dev_alloc_skb

alloc_skb是net/core/skbuff.c裏面定義的,用於分配緩衝區的函數。我們已經知道,數據緩衝區和緩衝區的描述結構(sk_buff結構)是兩種不同的實體,這就意味着,在分配一個緩衝區時,需要分配兩塊內存(一個是緩衝區,一個是緩衝區的描述結構sk_buff)。

alloc_skb調用函數kmem_cache_alloc從緩存中獲取一個sk_buff結構,並調用kmalloc分配緩衝區(如果有緩存的話,它同樣從緩存中獲取內存)。簡化後的代碼如下:

     skb = kmem_cache_alloc(skbuff_head_cache, gfp_mask & ~_ _GFP_DMA);
     ... ... ...
     size = SKB_DATA_ALIGN(size);
     data = kmalloc(size + sizeof(struct skb_shared_info), gfp_mask);

 

在調用kmalloc前,size參數通過SKB_DATA_ALIGN宏強制對齊。在函數返回前,它會初始化結構中的一些變量,最後的結構如圖5所示。在圖5右邊所示的內存塊的底部,你能看到對齊操作所帶來的填充區域。

 

Figure 5. alloc_skb function


 

 

dev_alloc_skb也是一個緩衝區分配函數,它主要被設備驅動使用,通常用在中斷上下文中。這是一個alloc_skb函數的包裝函數,它會在請求分配的大小上增加16字節的空間以優化緩衝區的讀寫效率,它的分配要求使用原子操作(GFP_ATOMIC),這是因爲它是在中斷處理函數中被調用的。

static inline struct sk_buff *dev_alloc_skb(unsigned int length)
{
     return _ _dev_alloc_skb(length, GFP_ATOMIC);
}

static inline
struct sk_buff *_ _dev_alloc_skb(unsigned int length, int gfp_mask)
{
     struct sk_buff *skb = alloc_skb(length + 16, gfp_mask);
     if (likely(skb))
             skb_reserve(skb, 16);
     return skb;
}

如果沒有體系架構相關的實現,缺省使用__dev_alloc_skb的實現。

5.2. Freeing memory: kfree_skb and dev_kfree_skb

這兩個函數釋放緩衝區,並把它返回給緩衝池(緩存)。kfree_skb可以直接調用,也可以通過包裝函數dev_kfree_skb調用。後面這個函數一般被設備驅動使用,與之功能相反的函數是dev_alloc_skb。dev_kfree_skb僅是一個簡單的宏,它什麼都不做,只簡單地調用kfree_skb。這些函數只有在skb->users爲1地情況下才釋放內存(沒有人引用這個結構)。否則,它只是簡單地減小 
skb->users。如果緩衝區有三個引用者,那麼只有第三次調用dev_kfree_skb或kfree_skb時才釋放內存。

圖6中的流程圖顯示了分配一個緩衝區所需要的步驟。當sk_buff釋放後,dst_release同樣會被調用以減小相關dst_entry數據結構的引用計數。

如果destructor被初始化過,相應的函數會在此時被調用.

在圖5中,我們看到,一個簡單的場景是:一個sk_buff結構與另一個內存塊相關,這個內存塊裏存儲的是真正的數據。當然,內存塊底部的skb_shared_info數據結構可以包含指向其他分片的指針(參見圖5)。如果存在分片,kfree_skb同樣會釋放這些分片所佔用的內存。最後,kfree_skb 把sk_buff結構返回給skbuff_head_cache緩存。

5.3. Data reservation and alignment: skb_reserve, skb_put, skb_push, and skb_pull

skb_reserve可以在緩衝區的頭部預留一定的空間,它通常被用來在緩衝區中插入協議頭或者在某個邊界上對齊。這個函數改變data和tail指針,而data和tail指針分別指向負載的開頭和結尾,圖4(d)展示了調用skb_reserve(skb,n)的結果。這個函數通常在分配緩衝區之後就調用,此時的 
data和tail指針還是指向同一個地方。

如果你查看某個以太網設備驅動的收包函數(例如,drivers/net/3c59x.c中的vortex_rx), 你就會發現它在分配緩衝區之後,在向緩衝區中填充數據之前,會調用下面的函數:

skb_reserve(skb, 2);     /* Align IP on 16 byte boundaries */

 

Figure 6. kfree_skb function


 

 

由於以太網幀的頭部長度是14個八位組,這個函數把緩衝區的頭部指針向後移動了2個字節。這樣,緊跟在以太網頭部之後的IP頭部在緩衝區中存儲時就可以在16字節的邊界上對齊。如圖7所示。

Figure 7. (a) before skb_reserve, (b) after skb_reserve, and (c) after copying the frame on the buffer


 

 

圖8展示了一個在發送過程中使用skb_reserve的例子。

Figure 8. Buffer that is filled in while traversing the stack from the TCP layer down to the link layer


 

 

  1. 當TCP發送數據時,它根據一些條件分配一個緩衝區(比如,TCP的最大分段長度(mss),是否支持散讀散寫I/O等

  2. TCP在緩衝區的頭部預留足夠的空間(用skb_reserve)用於填充各層的頭部(如TCP,IP,鏈路層等)。MAX_TCP_HEADER參數是各層頭部長度的總和,它考慮了最壞的情況:由於tcp層不知道將要用哪個接口發送包,它爲每一層預留了最大的頭部長度。它甚至考慮了出現多個IP頭的可能性(如果內核編譯支持IP over IP, 我們就會遇到多個IP頭的情況)。

  3. 把TCP的負載拷貝到緩衝區。需要注意的是:圖8只是一個例子。TCP的負載可能會被組織成其他形式。例如它可以存儲到分片中。

  4. TCP層添加自己的頭部。

  5. TCP層把緩衝區傳遞給IP層,IP層同樣添加自己的頭部。

  6. IP層把緩衝區傳遞給鄰居層,鄰居層添加鏈路層頭部。

當緩衝區在協議棧中向下層傳遞時,每一層都把skb->data指針向下移動,然後拷貝自己的頭部,同時更新skb->len。這些操作都使用圖4中所展示的函數完成。

skb_reserve函數並沒有把數據移出或移入緩衝區,它只是簡單地更新了緩衝區的兩個指針,這兩個指針在圖4(d)中有描述。

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

skb_push在緩衝區的開頭加入一塊數據,而skb_put在緩衝區的末尾加入一塊數據。與skb_reserve
類似,這些函數都不會真的往緩衝區中添加數據,相反,它們只是移動緩衝區的頭指針和尾指針。數據由其他函數拷貝到緩衝區中。skb_pull通過把head指針往前移來在緩衝區的頭部刪除一塊數據。圖4展示了這些函數是如何工作的。

2.1.5.4. The skb_shared_info structure and the skb_shinfo function

如圖5所示,在緩衝區數據的末尾,有一個數據結構skb_shared_info,它保存了數據塊的附加信息。這個數據結構緊跟在end指針所指的地址之後(end指針指示數據的末尾)。下面是這個結構的定義:

struct skb_shared_info {
     atomic_t         dataref;
     unsigned int     nr_frags;
     unsigned short   tso_size;
     unsigned short   tso_seqs;
     struct sk_buff   *frag_list;
     skb_frag_t       frags[MAX_SKB_FRAGS];
};

dataref表示數據塊的“用戶”數,這個值在下一節(克隆和拷貝緩衝區)中有描述。nf_frags,frag_list和frags用於存儲IP分片。skb_is_nonlinear函數用於測試一個緩衝區是否是分片的,而skb_linearize可以把分片組合成一個單一的緩衝區。組合分片涉及到數據拷貝,它將嚴重影響系統性能。

有些網卡硬件可以完成一些傳統上由CPU完成的任務。最常見的例子就是計算L3和L4校驗和。有些網卡甚至可以維護L4協議的狀態機。在下面的例子中,我們主要討論TCP段卸載TCP segmentation offload,這些網卡實現了TCP層的一部分功能。tso_size和tso_seqs就在這種情況下使用。

需要注意的是:sk_buff中沒有指向skb_shared_info結構的指針。如果要訪問這個結構, 就需要使用skb_info宏,這個宏簡單地返回end指針:

#define skb_shinfo(SKB)     ((struct skb_shared_info *)((SKB)->end))

下面的語句展示瞭如何使用這個宏來增加結構中的某個成員變量的值:

skb_shinfo(skb)->dataref++;

2.1.5.5. Cloning and copying buffers

如果一個緩衝區需要被不同的用戶獨立地操作,而這些用戶可能會修改sk_buff中某些變量的值(比如h和nh值),內核沒有必要爲每個用戶複製一份完整的sk_buff以及相應的緩衝區。相反,爲提高性能,內核克隆一個緩衝區。克隆過程只複製sk_buff結構, 同時修改緩衝區的引用計數以避免共享的數據被提前釋放。克隆緩衝區使用skb_clone函數。

一個使用包克隆的場景是:一個接收包的過程需要把這個包傳遞給多個接收者,例如包處理函數或者一個或多個網絡模塊。

被克隆的sk_buff不會放在任何鏈表中,同時也不會有到socket的引用。原始的和克隆的sk_buff中的skb->cloned值都被置爲1。克隆包的skb->users值被置爲1,這樣,在釋放時,可以先釋放sk_buff結構。同時,緩衝區的引用計數(dataref)增加1(因爲有多個sk_buff結構指向它)。圖9展示了克隆緩衝區的例子。

Figure 9. skb_clone function


 

 

skb_cloned函數可以用來測試skb的克隆狀態。

圖9展示了一個分片緩衝區的例子,這個緩衝區的一些數據保存在分片結構數組frags中。

skb_share_check用於檢查引用計數skb->users,如果users變量表明skb是被共享的, 則克隆一個新的sk_buff。

如果一個緩衝區被克隆了,這個緩衝區的內容就不能被修改。這就意味着,訪問數據的函數沒有必要加鎖。因此,當一個函數不僅要修改sk_buff,而且要修改緩衝區內容時, 就需要同時複製緩衝區。在這種情況下,程序員有兩個選擇。如果他知道所修改的數據在skb->start和skb->end
之間,他可以使用pskb_copy來複制這部分數據。如果他同時需要修改分片中的數據,他就必須使用skb_copy。圖10展示了pskb_copy和skb_copy的結果。skb_shared_info結構也可以包含一個 
sk_buff的鏈表(鏈表名稱是frag_list)。這個鏈表在pskb_copy和skb_copy中的處理方式與frags數組的處理方式相同(圖10忽略了frag_list)。

 

Figure 10. (a) pskb_copy function and (b) skb_copy function

 

 

 

在決定克隆或複製一個緩衝區時,子系統的程序員不能預測其他內核組件(其他子系統)是否需要使用緩衝區裏的原始數據。內核是模塊化的,它的狀態變化是不可預測的,因此,每個子系統都不知道其他子系統是如何操作緩衝區的。因此,內核程序員需要記錄它們對緩衝區的修改,並且在修改緩衝區前,複製一個新的緩衝區,以避免其他子系統在使用緩衝區的原始數據時出現錯誤。

2.1.5.6. List management functions

這些函數管理sk_buff的鏈表(也被稱作隊列)。在<include/linux/skbuff.h>和<net/core/skbuff.c>中有函數完整列表。以下是一些經常使用的函數:

skb_queue_head_init
初始化sk_buff_head結構,創建一個空隊列。

skb_queue_headskb_queue_tail
把一個緩衝區加入隊列的頭部或尾部。

skb_dequeueskb_dequeue_tail
從隊列的頭部或尾部取下一個緩衝區。第一個函數的名字應該是skb_dequeue_head,以保持和其他函數的名字風格一致。

skb_queue_purge
清空一個隊列。

skb_queue_walk
Runs a loop on each element of a queue in turn.

這些函數都是原子操作,它們必須先獲取sk_buff_head中的自旋鎖,然後才能訪問隊列中的元素。否則,它們有可能被其他異步的添加或刪除操作打斷,比如在定時器中調用的函數,這將導致鏈表出現錯誤而使得系統崩潰。

因此,每個函數的實現都採用下面這種形式:

static inline function_name ( parameter_list )
{
         unsigned long flags;

         spin_lock_irqsave(...);
         _ _ _function_name ( parameter_list )
         spin_unlock_irqrestore(...);
}

這些函數先獲取鎖,然後調用一個以兩個下劃線開頭的同名函數(這個函數做具體的操作,而且不需要鎖),然後釋放鎖。

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