struct sk_buff {
/* These two members must be first. */
struct sk_buff *next; //這兩個變量讓sk_buff
struct sk_buff *prev; //構成雙向的鏈表
struct sk_buff_head *list; //指向鏈表的頭
struct sock *sk; //指向創建這個sk_buff的socket
struct timeval stamp; //該packet到達的時間(以jiffies爲單位)
struct net_device *dev; //incoming/outcoming 的設備
struct net_device *input_dev; //到達的設備
struct net_device *real_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; //目的表項(指向一個路由cache,在後面文章詳解)
struct sec_path *sp; //只被xfrm使用(這個討論基本上沒用到)
/*
* 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[40];
unsigned int len, //真實數據區的長度
data_len, //數據區的長度
mac_len, //mac長度
csum; //checknum
__u32 priority;
//以下都是作爲flag用,只用了一個byte的5個bit
__u8 local_df:1, //允許local分片標誌
cloned:1, //允許clone操作標誌
ip_summed:2,
nohdr:1;
/* 3 bits spare */
__u8 pkt_type; //packet的類型,在if_packet.h中定義取值
__be16 protocol;
void (*destructor)(struct sk_buff *skb);
............
............
/* These elements must be at the end, see alloc_skb() for details. */
unsigned int truesize; //buffer的大小(後面詳解)
atomic_t users; //user計數,atomic_t指明該變量只能"原子"操作
//在後面詳細介紹
unsigned char *head,
*data,
*tail,
*end;
};
struct sk_buff可能是linux網絡代碼中最重要的數據結構,它表示接收或發送數據包的包頭信息,幷包含很多成員變量供網絡代碼中的各子系統使用。
這個結構被網絡的不同層(MAC或者其他二層鏈路協議,三層的IP,四層的TCP或UDP等)使用,並且其中的成員變量在結構從一層向另一層傳遞時改變。 L4向L3傳遞前會添加一個L4的頭部,同樣,L3向L2傳遞前,會添加一個L3的頭部。添加頭部比在不同層之間拷貝數據的效率更高。由於在緩衝區的頭部 添加數據意味着要修改指向緩衝區的指針,這是個複雜的操作,所以內核提供了一個函數skb_reserve來完成這個功能。協議棧中的每一層在往下一層傳 遞緩衝區前,第一件事就是調用skb_reserve在緩衝區的頭部給協議頭預留一定的空間。
skb_reserve同樣被設備驅動使用來對齊接收到包的包頭。如果緩衝區向上層協議傳遞,舊的協議層的頭部信息就沒什麼用了。例如,L2的頭部只有在 網絡驅動處理L2的協議時有用,L3是不會關心它的信息的。但是,內核並沒有把L2的頭部從緩衝區中刪除,而是把有效荷載的指針指向L3的頭部,這樣做, 可以節省CPU時間。
有些sk_buff成員變量的作用是方便查找或者是連接數據結構本身。內核可以把sk_buff組織成一個雙向鏈表。當然,這個鏈表的結構要比常見的雙向 鏈表的結構複雜一點。就像任何一個雙向鏈表一樣,sk_buff中有兩個指針next和prev,其中,next指向下一個節點,而prev指向上一個節 點。在第一個節點前面會插入另一個結構sk_buff_head,這是一個輔助節點(作爲sk_buff雙向鏈表的頭),它的定義如下:
struct sk_buff_head {
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
這是一個指向擁有這個sk_buff的sock結構的指針。這個指針在網絡包由本機發出或者由本機進程接收時有效,因爲插口相關的信息被L4(TCP或 UDP)或者用戶空間程序使用。如果sk_buff只在轉發中使用(這意味着,源地址和目的地址都不是本機地址),這個指針是NULL
sk_buff->len
表示當前協議數據包的長度。它包括主緩衝區中的數據長度(data指針指向它)和分片中的數據長度。
sk_buff->data_len
和len不同,data_len只計算分片中數據的長度
sk_buff->mac_len
這是mac頭的長度
sk_buff->users
這是一個引用計數,用於計算有多少實體引用了這個sk_buff緩衝區。它的主要用途是防止釋放sk_buff後,還有其他實體引用這個sk_buff。 因此,每個引用這個緩衝區的實體都必須在適當的時候增加或減小這個變量。這個計數器只保護sk_buff結構本身,而緩衝區的數據部分由類似的計數器 (dataref)來保護.有時可以用atomic_inc和atomic_dec函數來直接增加或減小users,但是,通常還是使用函數 skb_get和kfree_skb來操作這個變量。
sk_buff->truesize
這是緩衝區的總長度,包括sk_buff結構和數據部分。如果申請一個len字節的緩衝區,alloc_skb函數會把它初始化成len+sizeof(sk_buff)。當skb->len變化時,這個變量也會變化。
sk_buff->head
sk_buff->data
sk_buff->tail
sk_buff->end
它們表示緩衝區和數據部分的邊界。在每一層申請緩衝區時,它會分配比協議頭或協議數據大的空間。head和end指向緩衝區的頭部和尾部,而data和 tail指向實際數據的頭部和尾部。每一層會在head和data之間填充協議頭,或者在tail和end之間添加新的協議數據。數據部分會在尾部包含一 個附加的頭部。
struct sk_buff
|----------|
| | -+---->|--------|
| | -| | | head
| | -| | |
|----------| -| -+->|--------|
| head |--+ -| -| |
|----------| | -| |
| data |-----+ -| | data
|----------| | |
| tail |-----+ -| |
|----------| | -| |
| end |--+ -| -| |
|----------| -| -+->|--------|
| | | tail
| | |
+---->|--------|
void (*destructor)(struct sk_buff *skb)
這個函數指針可以初始化成一個在緩衝區釋放時完成某些動作的函數。如果緩衝區不屬於一個socket,這個函數指針通常是不會被賦值的。如果緩衝區屬於一 個socket,這個函數指針會被賦值爲sock_rfree或sock_wfree(分別由skb_set_owner_r或 skb_set_owner_w函數初始化)。這兩個sock_xxx函數用於更新socket的隊列中的內存容量。
sk_buff->tstamp
這個變量只對接收到的包有意義。它代表包接收時的時間戳,或者有時代表包準備發出時的時間戳。它在netif_rx裏面由函數net_timestamp設置,而netif_rx是設備驅動收到一個包後調用的函數。
sk_buff->dev
這個變量的類型是net_device,net_device它代表一個網絡設備。dev的作用與這個包是準備發出的包還是剛接收的包有關。當收到一個包時,設備驅動會把sk_buff的dev指針指向收到這個包的網絡設備;當一個包被髮送時,這個變量代表將要發送這個包的設備。在發送網絡包時設置這個值的代碼要比接收網絡包時設置這個值的代碼複雜。有些網絡功能可以把多個網 絡設備組成一個虛擬的網絡設備(也就是說,這些設備沒有和物理設備直接關聯),並由一個虛擬網絡設備驅動管理。當虛擬設備被使用時,dev指針指向虛擬設 備的net_device結構。而虛擬設備驅動會在一組設備中選擇一個設備並把dev指針修改爲這個設備的net_device結構。因此,在某些情況 下,指向傳輸設備的指針會在包處理過程中被改變。
sk_buff->input_dev
這是收到包的網絡設備的指針。如果包是本地生成的,這個值爲NULL。對以太網設備來說,這個值由eth_type_trans初始化,它主要被流量控制代碼使用。
sk_buff->h
sk_buff->nh
sk_buff->mac
這些是指向TCP/IP各層協議頭的指針:h指向L4(傳輸層),nh指向L3(網絡層),mac指向L2(數據鏈路層)。每個指針的類型都是一個聯合, 包含多個數據結構,每一個數據結構都表示內核在這一層可以解析的協議。例如,h是一個包含內核所能解析的L4協議的數據結構的聯合。每一個聯合都有一個 raw變量用於初始化,後續的訪問都是通過協議相關的變量進行的。
當接收一個包時,處理n層協議頭的函數從其下層(n-1層)收到一個緩衝區,它的skb->data指向n層協議的頭。處理n層協議的函數把本層的 指針(例如,L3對應的是skb->nh指針)初始化爲skb->data,因爲這個指針(data指針)的值會在處理下一層協議時改變 (skb->data將被初始化成緩衝區裏的其他地址)。在處理n層協議的函數結束時,在把包傳遞給n+1層的處理函數前,它會把skb-> data指針指向n層協議頭的末尾,這正好是n+1層協議的協議頭。
當網卡驅動程序收到一個UDP數據報後,它創建一個結構體struct sk_buff,確保sk_buff->data成員指向的空間足夠存放收到的數據(對於數據報分片的情況,因爲比較複雜,我們暫時忽略,我們假設 一次收到的是一個完整的UDP數據報)。把收到的數據全部拷貝到sk_buff->data指向的空間,然後,把skb->mac.raw指 向data,此時,數據報的開始位置是一個以太網頭,所以skb->mac.raw指向鏈路層的以太網頭。然後通過調用skb_pull剝掉以太網 頭,所謂剝掉以太網頭,只是把data加上sizeof(struct ethhdr),同時len減去這個值,這樣,在邏輯上,skb已經不包含以太網頭了,但通過skb->mac.raw還能找到它。這就是我們通常 所說的,IP數據報被收到後,在鏈路層被剝去以太網頭。
sk_buff->dst
這個變量在路由子系統中使用
sk_buff->sp
這個變量被IPSec協議用於跟蹤傳輸的信息
sk_buff->cb[48]
這是一個“control buffer”,或者說是一個私有信息的存儲空間,由每一層自己維護並使用。它在分配sk_buff結構時分配(它目前的大小是48字節,已經足夠爲每一 層存儲必要的私有信息了)。在每一層中,訪問這個變量的代碼通常用宏實現以增強代碼的可讀性。例如,TCP用這個變量存儲tcp_skb_cb結構。
下面這個宏被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層發送一個分段。
sk_buff->csum
sk_buff->ip_summed
表示校驗和以及相關狀態標記
sk_buff->cloned
一個布爾標記,當被設置時,表示這個結構是另一個sk_buff的克隆
sk_buff->pkt_type
這個變量表示幀的類型,分類是由L2的目的地址來決定的。這個值在網卡驅動程序中由函數eth_type_trans通過判斷目的以太網地址來確定。如果 目的地址是FF:FF:FF:FF:FF:FF,則爲廣播地址,pkt_type = PACKET_BROADCAST;如果最高位爲1,則爲組播地址,pkt_type = PACKET_MULTICAST;如果目的mac地址跟本機mac地址不相等,則不是發給本機的數據報,pkt_type = PACKET_OTHERHOST;否則就是缺省值PACKET_HOST。
/* Packet types */
#define PACKET_HOST 0 /* To us */
#define PACKET_BROADCAST 1 /* To all */
#define PACKET_MULTICAST 2 /* To group */
#define PACKET_OTHERHOST 3 /* To someone else */
#define PACKET_OUTGOING 4 /* Outgoing of any type */
sk_buff->priority
這個變量描述發送或轉發包的QoS類別。如果包是本地生成的,socket層會設置priority變量。如果包是將要被轉發的, rt_tos2priority函數會根據ip頭中的Tos域來計算賦給這個變量的值。這個變量的值與DSCP(DiffServ CodePoint)沒有任何關係。
sk_buff->protocol
這個變量是高層協議從二層設備的角度所看到的協議。典型的協議包括IP,IPV6和ARP。完整的列表在 include/linux/if_ether.h中。由於每個協議都有自己的協議處理函數來處理接收到的包,因此,這個域被設備驅動用於通知上層調用哪 個協議處理函數。每個網絡驅動都調用netif_rx來通知上層網絡協議的協議處理函數,因此protocol變量必須在這些協議處理函數調用之前初始 化。
----------------------------------------
linux內核是模塊化的,你可以選擇包含或者刪除某些功能。因此,sk_buff結構裏面的一些成員變量只有在內核選擇支持某些功能時纔有效,比如防火牆(netfilter)或者qos:
__u32 nfctinfo
...
#ifdef CONFIG_NETFILTER
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
__u32 nfmark;
#endif /* CONFIG_NETFILTER */
這些變量被netfilter使用(防火牆代碼),內核編譯選項是“Device Drivers->Networking support-> Networking options-> Network packet filtering”和兩個子選項“Network packet filtering debugging”和“Bridged IP/ARP packets filtering”
#ifdef CONFIG_NET_SCHED
__u16 tc_index;
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd;
#endif
#endif
這兩個變量被流量控制代碼使用。tc_index只有在編譯時定義了CONFIG_NET_SCHED符號纔有效;tc_verd只有在編譯時定義了CONFIG_NET_CLS_ACT符號纔有效。這兩個符號可以通過選擇特定的編譯選項來定義:
Networking --->
Networking options --->
QoS and/or fair queueing --->
[*] QoS and/or fair queueing
[*] Actions
QoS選項不能被編譯成內核模塊。原因就是,內核編譯之後,由某個選項所控制的數據結構是不能動態變化的。一般來說,如果某個選項會修改內核數據結構(比如說,在sk_buff裏面增加一個項tc_index),那麼,包含這個選項的組件就不能被編譯成內核模塊。
你可能經常需要查找是哪個make menuconfig編譯選項或者變種定義了某個#ifdef標記,以便理解內核中包含的某段代碼。在2.6內核中,最快的,查找它們之間關聯關係的方 法,就是查找分佈在內核源代碼樹中的kconfig文件中是否定義了相應的符號(每個目錄都有一個這樣的文件)。在2.4內核中,你需要查看 Documentation/Configure.help文件。
Quality of Service(QoS)服務質量
struct sk_buff * next;
struct sk_buff * prev;
struct sk_buff_head * list;
struct sock *sk;
struct timeval stamp;
struct net_device *dev;
struct net_device *real_dev;
union
{
struct tcphdr *th;
struct udphdr *uh;
struct icmphdr *icmph;
struct igmphdr *igmph;
struct iphdr *ipiph;
struct spxhdr *spxh;
unsigned char *raw;
} h;
union
{
struct iphdr *iph;
struct ipv6hdr *ipv6h;
struct arphdr *arph;
struct ipxhdr *ipxh;
unsigned char *raw;
} nh;
union
{
struct ethhdr *ethernet;
unsigned char *raw;
} mac;
struct dst_entry *dst;
char cb[48];
unsigned int len;
unsigned int data_len;
unsigned int csum;
unsigned char __unused,
cloned, pkt_type,
ip_summed; __u32 priority; atomic_t users;
unsigned short protocol;
unsigned short security;
unsigned int truesize;
unsigned char *head;
unsigned char *data;
unsigned char *tail;
unsigned char *end;
void (*destructor)(struct sk_buff *);
#ifdef CONFIG_NETFILTER
unsigned long nfmark;
__u32 nfcache;
struct nf_ct_info *nfct;
#ifdef CONFIG_NETFILTER_DEBUG
unsigned int nf_debug;
#endif
#endif
#if defined(CONFIG_HIPPI)
union{
__u32 ifield;
} private;
#endif
#ifdef CONFIG_NET_SCHED
__u32 tc_index;
#endif
};
該結構維護一個收到的或者要發送的網絡包。但其本身並不包含存放網絡包的數據的存儲區。存儲區是另外單獨分配的內存空間,但該結構說明了如何訪問存儲區空間,如何維護多個存儲區空間以及存儲網絡包解析的成果。
所有的sk_buff是通過一個雙向鏈表進行維護的。需要說明的是該雙向鏈表中的一個元素是struct sk_buff_head類型。它相當於該雙向鏈表的表頭,其中有鎖,鏈表元素個數等維護鏈表的相關信息。鏈表中其它的元素都是sk_buff類型。
鏈表結構圖如下:
而對於每個sk_buff而言,如前所述,其本身並不維護網絡包的存儲區。該結構跟存儲區的關係如下圖所示:
圖的左邊表示sk_buff結構,右側表示網絡包的存儲區。可以看到,sk_buff中有四個指針指向存儲區。其中head一定指向存儲區的開頭,end一定指向存儲區的結尾。data指向實際內容的開頭,tail指向實際內容的結尾。這樣做有兩個原因,一是在分配空間的時候我們尚不知道具體需要多大的空間,只能按照最大可能空間來分配;二是爲了滿足字節對齊的需要。
圖中Data部分的內容包括網絡包的所有內容。對於輸入包而言,其就是從當前層向上的所有層的頭和最後的負載,每解析掉一層的頭,該協議的協議頭對應的數據就不再繼續處理,所以data指針在每層會逐漸增大。對於輸出包而言,其每向下傳輸一層,都會添加一層的頭,所以sk_buff的data指針也會不斷減小。
對於輸入包而言,存儲區的內容是不變的。但我們在分析包時,在每層都會剝除該層的頭。爲了保留我們的剝除結果,結構中會有三個指針分別指向以太,網絡和傳輸三層的協議頭。這三個指針依次分別是union {…} mac,union {…} nh和union{…} h。三個成員都是聯合,其中的內容是該層可能的協議類型的指針。
還有一組成員表示存儲區中數據的長度。len表示存儲區的數據長度和分片長度之和。data_len表示分片長度。mac_len表示mac頭的長度。truesize表示存儲區總長度(即end-head)和sk_buff本身長度之和。
下面的內容是關於如何維護存儲區。
alloc_skb和dev_alloc_skb用來分配sk_buff和存儲區,kfree_skb和dev_kfree_skb則釋放sk_buff和其對應的存儲區。剛初始化完的存儲區,head,data,tail都指向存儲區的開頭,end指向結尾。下面這些函數是修改data和tail指針的指向。
skb_reserve是在存儲區的開頭保留一段空間。其有兩個作用,對於接收到的報文而言,可以保持字節對齊;對於發送的報文而言,可以保留空間用於存放各層的協議頭。畢竟我們在高層協議就會分配該存儲區,所以需要預留下層的協議頭的空間。該函數的作用會導致data和tail兩個指針都下移,在head和它們之間留出空間。
skb_put則是將tail下移,即增加了真正空間的長度。
skb_push是將data上移,也增加了真正空間的長度。
skb_pull將data下移,減少了真正空間的長度。
下面是sk_buff的其它成員:
struct sock *sk:表示該網絡包所屬的socket。對於該主機發送的和接收的網絡包,自然對應着一個socket套接字。對於轉發的網絡包,則該成員爲NULL。
atomic_t users:表示該sk_buff結構實例被引用的計數。這個是結構本身的引用計數,而不是其對應的存儲區的引用計數。
void (*destructor)():析構函數。
struct timeval stamp:對於接受包,表示該包接收到的時間。
struct net_device *dev:接收或者發送包的網卡接口。
struct net_device *input_dev:接收到包的網卡接口。
struct net_device *real_dev:在某些機器中,多個物理網卡接口可以被看作一個虛擬設備。這中情況下real_dev表示物理結構。
char cb[40]:一個用於對該網絡包進行處理的通用緩衝區。可以存放一些在不同層之間傳輸的數據。
unsigned int csum,ipsum:校驗和。
unsigned char cloned:
unsigned char pkt_type:包類型,可以是單發給該主機的包,廣播包,多播包,轉發包等。
unsigned short protocol:表示以太層的上層協議,可以是IP,IPv6和ARP。
下面是關於skb_shared_info結構的。當我們使用alloc_skb分配空間時,會在存儲區的後面緊接着一個該結構的實例。如下圖所示:
其中存儲了關於網絡塊的附加信息。不知爲何不也放在sk_buff中。其中維護了存儲塊的引用計數,IP分片數據的信息。
對於sk_buff和存儲塊,有時是需要拷貝的。分爲三種情況,一是隻拷貝sk_buff,存儲塊複用,此時使用skb_clone函數;二是拷貝sk_buff和存儲塊的主體,但存儲塊後的skb_shared_info中frags數組指向的分片數據不拷貝,這使用pskb_copy函數;三是上述的全都拷貝,使用skb_copy函數。在不同情況下不同使用。