skb詳細解析【轉】


 
摘自:http://blog.chinaunix.net/uid-30035229-id-4883992.html
 
 
在自己的模塊發送函數中,需要對skb進行重新構造和別的一些操作。在網上看到一個寫的還可以的,粘過來,就不自己寫了,估計這個哥們也是看<Understanding Linux Network Internals>翻譯或者總結的。
 
------------------------------------------------

1.   定義

Packet:       通過網卡收發的報文,包括鏈路層、網絡層、傳輸層的協議頭和攜帶的數據
Data Buffer:用於存儲 packet 的內存空間
SKB:           struct sk_buffer 的簡寫
 
 

2.   概述

Struct sk_buffer 是 linux TCP/IP stack 中,用於管理Data Buffer的結構。Sk_buffer 在數據包的發送和接收中起着重要的作用。
為了提高網絡處理的性能,應盡量避免數據包的拷貝。Linux 內核開發者們在設計 sk_buffer 結構的時候,充分考慮到這一點。目前 Linux 協議棧在接收數據的時候,需要拷貝兩次:數據包進入網卡驅動后拷貝一次,從內核空間遞交給用戶空間的應用時再拷貝一次。
Sk_buffer結構隨着內核版本的升級,也一直在改進。
學習和理解 sk_buffer 結構,不僅有助於更好的理解內核代碼,而且也可以從中學到一些設計技巧。
 
 

3.   Sk_buffer 定義

struct sk_buff {
                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;
                char                                       cb[40];
 
                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;
                __be16                                  protocol;
                void                                        (*destructor)(struct sk_buff *skb);
 
                /* These elements must be at the end, see alloc_skb() for details.  */
                unsigned int                         truesize;
                atomic_t                               users;
                unsigned char                     *head,
                                                *data,
                                                *tail,
                                                *end;
};
 
 

4.   成員變量

 
·                struct skb_timeval    tstamp;
此變量用於記錄 packet 的到達時間或發送時間。由於計算時間有一定開銷,因此只在必要時才使用此變量。需要記錄時間時,調用net_enable_timestamp(),不需要時,調用net_disable_timestamp() 。
tstamp 主要用於包過濾,也用於實現一些特定的 socket 選項,一些 netfilter 的模塊也要用到這個域。
·                struct net_device      *dev;
·                struct net_device      *input_dev;
 
這幾個變量都用於跟蹤與 packet 相關的 device。由於 packet 在接收的過程中,可能會經過多個 virtual driver 處理,因此需要幾個變量。
接收數據包的時候, dev 和 input_dev 都指向最初的 interface,此后,如果需要被 virtual driver 處理,那么 dev 會發生變化,而 input_dev 始終不變。
 
 
(These three members help keep track of the devices assosciated with a packet. The reason we have three different device pointers is that the main 'skb->dev' member can change as we encapsulate and decapsulate via a virtual device.
So if we are receiving a packet from a device which is part of a bonding device instance, initially 'skb->dev' will be set to point the real underlying bonding slave. When the packet enters the networking (via 'netif_receive_skb()') we save 'skb->dev' away in 'skb->real_dev' and update 'skb->dev' to point to the bonding device.
Likewise, the physical device receiving a packet always records itself in 'skb->input_dev'. In this way, no matter how many layers of virtual devices end up being decapsulated, 'skb->input_dev' can always be used to find the top-level device that actually received this packet from the network. )
 
·                char                               cb[40];
此數組作為 SKB 的控制塊,具體的協議可用它來做一些私有用途,例如 TCP 用這個控制塊保存序列號和重傳狀態。
 
 
·                unsigned int               len,
·                                                     data_len,
·                                                     mac_len,
·                                                     csum;
 
‘len’ 表示此 SKB 管理的 Data Buffer 中數據的總長度;
通常,Data Buffer 只是一個簡單的線性 buffer,這時候 len 就是線性 buffer 中的數據長度;
但在有 ‘paged data’ 情況下, Data Buffer 不僅包括第一個線性 buffer ,還包括多個 page buffer;這種情況下, ‘data_len’ 指的是 page buffer 中數據的長度,’len’ 指的是線性 buffer 加上 page buffer 的長度;len – data_len 就是線性 buffer 的長度。
 
‘mac_len’ 指 MAC 頭的長度。目前,它只在 IPSec 解封裝的時候被使用。將來可能從 SKB 結構中
去掉。
‘csum’ 保存 packet 的校驗和。
(Finally, 'csum' holds the checksum of the packet. When building send packets, we copy the data in from userspace and calculate the 16-bit two's complement sum in parallel for performance. This sum is accumulated in 'skb->csum'. This helps us compute the final checksum stored in the protocol packet header checksum field. This field can end up being ignored if, for example, the device will checksum the packet for us.
On input, the 'csum' field can be used to store a checksum calculated by the device. If the device indicates 'CHECKSUM_HW' in the SKB 'ip_summed' field, this means that 'csum' is the two's complement checksum of the entire packet data area starting at 'skb->data'. This is generic enough such that both IPV4 and IPV6 checksum offloading can be supported. )
 
·                __u32                            priority;
 
“priority”用於實現 QoS,它的值可能取之於 IPv4 頭中的 TOS 域。Traffic Control 模塊需要根據這個域來對 packet 進行分類,以決定調度策略。
 
 
·                __u8                              local_df:1,
·                                                     cloned:1,
·                                                     ip_summed:2,
·                                                     nohdr:1,
·                                                     nfctinfo:3;
 
 
為了能迅速的引用一個 SKB 的數據,
當 clone 一個已存在的 SKB 時,會產生一個新的 SKB,但是這個 SKB 會共享已有 SKB 的數據區。
當一個 SKB 被 clone 后,原來的 SKB 和新的 SKB 結構中,”cloned” 都要被設置為1。
 
(The 'local_df' field is used by the IPV4 protocol, and when set allows us to locally fragment frames which have already been fragmented. This situation can arise, for example, with IPSEC.
The 'nohdr' field is used in the support of TCP Segmentation Offload ('TSO' for short). Most devices supporting this feature need to make some minor modifications to the TCP and IP headers of an outgoing packet to get it in the right form for the hardware to process. We do not want these modifications to be seen by packet sniffers and the like. So we use this 'nohdr' field and a special bit in the data area reference count to keep track of whether the device needs to replace the data area before making the packet header modifications.
The type of the packet (basically, who is it for), is stored in the 'pkt_type' field. It takes on one of the 'PACKET_*' values defined in the 'linux/if_packet.h' header file. For example, when an incoming ethernet frame is to a destination MAC address matching the MAC address of the ethernet device it arrived on, this field will be set to 'PACKET_HOST'. When a broadcast frame is received, it will be set to 'PACKET_BROADCAST'. And likewise when a multicast packet is received it will be set to 'PACKET_MULTICAST'.
The 'ip_summed' field describes what kind of checksumming assistence the card has provided for a receive packet. It takes on one of three values: 'CHECKSUM_NONE' if the card provided no checksum assistence, 'CHECKSUM_HW' if the two's complement checksum over the entire packet has been provides in 'skb->csum', and 'CHECKSUM_UNNECESSARY' if it is not necessary to verify the checksum of this packet. The latter usually occurs when the packet is received over the loopback device. 'CHECKSUM_UNNECESSARY' can also be used when the device only provides a 'checksum OK' indication for receive packet checksum offload. )
 
·                void                               (*destructor)(struct sk_buff *skb);
·                unsigned int               truesize;
 
一個 SKB 所消耗的內存包括 SKB 本身和 data buffer。
truesize 就是 data buffer 的空間加上 SKB 的大小。
struct sock 結構中,有兩個域,用於統計用於發送的內存空間和用於接收的內存空間,它們是:
rmem_alloc
wmem_alloc
 
另外兩個域則統計接收到的數據包的總大小和發送的數據包的總大小。
rcvbuf
sndbuf
 
rmem_alloc 和 rcvbuf,wmem_alloc 和sndbuf 用於不同的目的。
 
當我們收到一個數據包后,需要統計這個 socket 總共消耗的內存,這是通過skb_set_owner_r() 來做的。
 
static inline void skb_set_owner_r(struct sk_buff *skb, struct sock *sk)
{
        skb->sk = sk;
        skb->destructor = sock_rfree;
        atomic_add(skb->truesize, &sk->sk_rmem_alloc);
}
 最后,當釋放一個 SKB 后,需要調用 skb->destruction() 來減少rmem_alloc 的值。
 
同樣,在發送一個 SKB 的時候,需要調用skb_set_owner_w() ,
 
static inline void skb_set_owner_w(struct sk_buff *skb, struct sock *sk)
{
        sock_hold(sk);
        skb->sk = sk;
        skb->destructor = sock_wfree;
        atomic_add(skb->truesize, &sk->sk_wmem_alloc);
}
 在釋放這樣的一個 SKB 的時候,需要 調用 sock_free() 
 
void sock_wfree(struct sk_buff *skb)
{
        struct sock *sk = skb->sk;
 
        /* In case it might be waiting for more memory. */
        atomic_sub(skb->truesize, &sk->sk_wmem_alloc);
        if (!sock_flag(sk, SOCK_USE_WRITE_QUEUE))
               sk->sk_write_space(sk);
        sock_put(sk);
}
 
 
(Another subtle issue is worth pointing out here. For receive buffer accounting, we do not grab a reference to the socket (via 'sock_hold()'), because the socket handling code will always make sure to free up any packets in it's receive queue before allowing the socket to be destroyed. Whereas for send packets, we have to do proper accounting with 'sock_hold()' and 'sock_put()'. Send packets can be freed asynchronously at any point in time. For example, a packet could sit in a devices transmit queue for a long time under certain conditions. If, meanwhile, the socket is closed, we have to keep the socket reference around until SKBs referencing that socket are liberated. )
 
·                unsigned char                        *head,
·                                                     *data,
·                                                     *tail,
·                                                     *end;
 SKB 對 Data Buffer 的巧妙管理,就是靠這四個指針實現的。
 
下圖展示了這四個指針是如何管理數據 buffer 的:
Head 指向 buffer 的開始,end 指向 buffer 結束。 Data 指向實際數據的開始,tail 指向實際數據的結束。這四個指針將整個 buffer 分成三個區:
 
Packet data:這個空間保存的是真正的數據
Head room:處於 packet data 之上的空間,是一個空閑區域
Tail room:處於 packet data 之下的空間,也是空閑區域。
 
由於 TCP/IP 協議族是一種分層的協議,傳輸層、網絡層、鏈路層,都有自己的協議頭,因此 TCP/IP 協議棧對於數據包的處理是比較復雜的。為了提高處理效率,避免數據移動、拷貝,sk_buffer 在對數據 buffer 管理的時候,在 packet data 之上和之下,都預留了空間。如果需要增加協議頭,只需要從 head room 中拿出一塊空間即可,而如果需要增加數據,則可以從 tail room 中獲得空間。這樣,整個內存只分配一次空間,此后 協議的處理,只需要挪動指針。

5.   Sk_buffer 對內存的管理

我們以構造一個用於發送的數據包的過程,來理解 sk_buffer 是如何管理內存的。
 

5.1.                    構造Skb_buffer

 
alloc_skb() 用於構造 skb_buffer,它需要一個參數,指定了存放 packet 的空間的大小。
構造時,不僅需要創建 skb_buffer 結構本身,還需要分配空間用於保存 packet。
 
skb = alloc_skb(len, GFP_KERNEL);
  
上圖是在調用完 alloc_skb() 后的情況:
 
head, data, tail 指向 buffer 開始,end 指向 buffer 結束,整個 buffer 都被當作 tail room。
Sk_buffer 當前的數據長度是0。
 
 

5.2.                     protocol header 留出空間

 
通常,當構造一個用於發送的數據包時,需要留出足夠的空間給協議頭,包括 TCP/UDP header, IP header 和鏈路層頭。
對 IPv4 數據包,可以從 sk->sk_prot->max_header 知道協議頭的最大長度。
 
skb_reserve(skb, header_len);
 
 
  
圖是調用 skb_reserver() 后的情況
 
 

5.3.                    將用戶空間數據拷貝到 buffer 

 
首先通過 skb_put(skb, user_data_len) ,從 tail room 中留出用於保存數據的空間
然后通過csum_and_copy_from_user() 將數據從用戶空間拷貝到這個空間中。
 
 

5.4.                    構造UDP協議頭

 
通過 skb_push() ,向 head room 中要一塊空間
然后在此空間中構造 UDP 頭。
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

5.5.                    構造 IP 

 
通過 skb_push() ,向 head room 中要一塊空間
然后在此空間中構造 IP 頭。

 

 
6.   Sk_buffer 的秘密
當調用 alloc_skb() 構造 SKB 和 data buffer時,需要的 buffer 大小是這樣計算的:
 
data = kmalloc(size + sizeof(struct skb_shared_info), gfp_mask);
 
除了指定的 size 以外,還包括一個 struct skb_shared_info 結構的空間大小。也就是說,當調用 alloc_skb(size) 要求分配 size 大小的 buffer 的時候,同時還創建了一個 skb_shared_info 。
 
這個結構定義如下:
 
struct skb_shared_info {
            atomic_t            dataref;
            unsigned int       nr_frags;
            unsigned short   tso_size;
            unsigned short   tso_segs;
            struct sk_buff     *frag_list;
            skb_frag_t         frags[MAX_SKB_FRAGS];
};
 
 
 
我們只要把 end 從 char* 轉換成skb_shared_info* ,就能訪問到這個結構
Linux 提供一個宏來做這種轉換:
 
#define skb_shinfo(SKB)             ((struct skb_shared_info *)((SKB)->end))
 
 
 
那么,這個隱藏的結構用意何在?
它至少有兩個目的:
1、   用於管理 paged data
2、   用於管理分片
 
接下來分別研究 sk_buffer 對paged data 和分片的處理。
 

7.    paged data 的處理

 
某些情況下,希望能將保存在文件中的數據,通過 socket 直接發送出去,這樣,避免了把數據先從文件拷貝到緩沖區,從而提高了效率。
Linux 采用一種 “paged data” 的技術,來提供這種支持。這種技術將文件中的數據直接被映射為多個 page。
 
Linux 用 struct skb_frag_strut 來管理這種 page:
 
typedef struct skb_frag_struct skb_frag_t;
 
struct skb_frag_struct {
        struct page *page;
        __u16 page_offset;
        __u16 size;
};
 
並在shared info 中,用數組 frags[] 來管理這些結構。
 
 
如此一來,sk_buffer 就不僅管理着一個 buffer 空間的數據了,它還可能通過 share info 結構管理一組保存在 page 中的數據。
 
在采用 “paged data” 時,data_len 成員派上了用場,它表示有多少數據在 page 中。因此,
如果 data_len 非0,這個 sk_buffer 管理的數據就是“非線性”的。
Skb->len – skb->data_len 就是非 paged 數據的長度。
 
在有 “paged data” 情況下, skb_put()就無法使用了,必須使用 pskb_put() 。。。
 
 
 
 
 
 
 
 
 
 
 
 

8.   對分片的處理

 

9.   SKB 的管理函數

 

9.1.                    Data Buffer 的基本管理函數

 
·                unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
 
“推”入數據
在 buffer 的結束位置,增加數據,len是要增加的長度。
這個函數有兩個限制,需要調用者自己注意,否則后果由調用者負責
1)、不能用於 “paged data” 的情況
這要求調用者自己判斷是否為 “paged data” 情況
2)、增加新數據后,長度不能超過 buffer 的實際大小。
這要求調用者自己計算能增加的數據大小
 
·                unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
“壓”入數據
從 buffer 起始位置,增加數據,len 是要增加的長度。
實際就是將新的數據“壓”入到 head room 中
 
·                unsigned char *skb_pull(struct sk_buff *skb, unsigned int len)
“拉”走數據
從 buffer 起始位置,去除數據, len 是要去除的長度。
如果 len 大於 skb->len,那么,什么也不做。
在處理接收到的 packet 過程中,通常要通過 skb_pull() 將最外層的協議頭去掉;例如當網絡層處理完畢后,就需要將網絡層的 header 去掉,進一步交給傳輸層處理。
 
 
·                void skb_trim(struct sk_buff *skb, unsigned int len)
調整 buffer 的大小,len 是調整后的大小。
如果 len 比 buffer 小,則不做調整。
因此,實際是將 buffer 底部的數據去掉。
對於沒有 paged data 的情況,很好處理;
但是有 paged data 情況下,則需要調用 __pskb_trim() 來進行處理。
 
 
 

9.2.                    “Paged data”  分片的管理函數

 
·                char *pskb_pull(struct sk_buff *skb, unsigned int len)
 
“拉“走數據
如果 len 大於線性 buffer 中的數據長度,則調用__pskb_pull_tail()  進行處理。
(Q:最后, return skb->data += len;  是否會導致 skb->data 超出了鏈頭范圍?)
 
·                int pskb_may_pull(struct sk_buff *skb, unsigned int len)
在調用 skb_pull() 去掉外層協議頭之前,通常先調用此函數判斷一下是否有足夠的數據用於“pull”。
如果線性 buffer足夠 pull,則返回1;
如果需要 pull 的數據超過 skb->len,則返回0;
最后,調用__pskb_pull_tail() 來檢查 page buffer 有沒有足夠的數據用於 pull。
 
 
·                int pskb_trim(struct sk_buff *skb, unsigned int len)
將 Data Buffer 的數據長度調整為 len
在沒有 page buffer 情況下,等同於 skb_trim();
在有 page buffer 情況下,需要調用___pskb_trim() 進一步處理。
 
·                int skb_linearize(struct sk_buff *skb, gfp_t gfp)
 
 
·                struct sk_buff *skb_clone(struct sk_buff *skb, gfp_t gfp_mask)
‘clone’ 一個新的 SKB。新的 SKB 和原有的 SKB 結構基本一樣,區別在於:
1)、它們共享同一個 Data Buffer
2)、它們的 cloned 標志都設為1
3)、新的 SKB 的 sk 設置為空
(Q:在什么情況下用到克隆技術?)
 
·                struct sk_buff *skb_copy(const struct sk_buff *skb, gfp_t gfp_mask)
 
·                struct sk_buff *pskb_copy(struct sk_buff *skb, gfp_t gfp_mask)
 
·                struct sk_buff *skb_pad(struct sk_buff *skb, int pad)
 
·                void skb_clone_fraglist(struct sk_buff *skb)
 
·                void skb_drop_fraglist(struct sk_buff *skb)
 
·                void copy_skb_header(struct sk_buff *new, const struct sk_buff *old)
 
·                pskb_expand_head(struct sk_buff *skb, int nhead, int ntail, gfp_t gfp_mask)
 
·                int skb_copy_bits(const struct sk_buff *skb, int offset, void *to, int len)
 
·                int skb_store_bits(const struct sk_buff *skb, int offset, void *from, int len)
 
·                struct sk_buff *skb_dequeue(struct sk_buff_head *list)
 
·                struct sk_buff *skb_dequeue(struct sk_buff_head *list)
 
·                void skb_queue_purge(struct sk_buff_head *list)
 
·                void skb_queue_purge(struct sk_buff_head *list)
 
·                void skb_queue_tail(struct sk_buff_head *list, struct sk_buff *newsk)
 
·                void skb_unlink(struct sk_buff *skb, struct sk_buff_head *list)
 
·                void skb_append(struct sk_buff *old, struct sk_buff *newsk, struct sk_buff_head *list)
 
·                void skb_insert(struct sk_buff *old, struct sk_buff *newsk, struct sk_buff_head *list)
 
·                int skb_add_data(struct sk_buff *skb, char __user *from, int copy)
 
·                struct sk_buff *skb_padto(struct sk_buff *skb, unsigned int len)
 
·                int skb_cow(struct sk_buff *skb, unsigned int headroom)
 
這個函數要對 SKB 的 header room 調整,調整后的 header room 大小是 headroom.
如果 headroom 長度超過當前header room 的大小,或者 SKB 被 clone 過,那么需要調整,方法是:
分配一塊新的 data buffer 空間,SKB 使用新的 data buffer 空間,而原有空間的引用計數減1。在沒有其它使用者的情況下,原有空間被釋放。
 
 
 
·                struct sk_buff *dev_alloc_skb(unsigned int length)
 
·                void skb_orphan(struct sk_buff *skb)
 
·                void skb_reserve(struct sk_buff *skb, unsigned int len)
 
·                int skb_tailroom(const struct sk_buff *skb)
 
·                int skb_headroom(const struct sk_buff *skb)
 
·                int skb_pagelen(const struct sk_buff *skb)
 
·                int skb_headlen(const struct sk_buff *skb)
 
·                int skb_is_nonlinear(const struct sk_buff *skb)
 
·                struct sk_buff *skb_share_check(struct sk_buff *skb, gfp_t pri)
 
如果skb 只有一個引用者,直接返回 skb
否則 clone 一個 SKB,將原來的 skb->users 減1,返回新的 SKB
 
 
需要特別留意 pskb_pull() 和 pskb_may_pull() 是如何被使用的:
 
1)、在接收數據的時候,大量使用 pskb_may_pull(),其主要目的是判斷 SKB 中有沒有足夠的數據,例如在 ip_rcv() 中:
 
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
                        goto inhdr_error;
 
iph = skb->nh.iph;
 
它的目的是拿到 IP header,但取之前,先通過 pskb_may_pull() 判斷一下有沒有足夠一個 IP header 的數據。
 
 
2)、當我們構造 IP 分組的時候,對於數據部分,通過 put向下擴展空間(如果一個sk_buffer 不夠用怎么分片?);對於 傳輸層、網絡層、鏈路層的頭,通過 push 向上擴展空間;
 
3)、當我們解析 IP 分組的時候,通過 pull(),從頭開始,向下壓縮空間。
 
因此,put 和 push 主要用在發送數據包的時候;而pull 主要用在接收數據包的時候。
 
 
 
 
 
 

10.                     各種 header

 
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;
 
--------------------------------------------
 
其中的第五部分正是我需要的,在此向上面朋友的辛苦表示感謝。


免責聲明!

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



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