sk_buff
目錄
1 sk_buff介紹
2 sk_buff組成
3 struct sk_buff 結構體
4 sk_buff成員變量
4.1 Layout布局
4.2 General通用
4.3 Feature-specific功能相關
5 sk_buff管理和操作函數
5.1緩沖區操作函數skb_reserve skb_put skb_push skb_pull
5.2發送tcp報文示例
5.3 緩沖區分配、克隆和釋放函數alloc_skb skb_clone pskb_copy skb_copy kfree_skb
1 sk_buff介紹
sk_buff(socket buffer)結構是linux網絡代碼中重要的數據結構,它管理和控制接收或發送數據包的信息。
2 sk_buff組成
Packet data:通過網卡收發的報文,包括鏈路層、網絡層、傳輸層的協議頭和攜帶的應用數據,包括head room,data,tail room三部分。
skb_shared_info 作為packet data的補充,用於存儲ip分片,其中sk_buff *frag_list是一系列子skbuff鏈表,而frag[]是由一組單獨的page組成的數據緩沖區。
Data buffer:用於存儲packet data的緩沖區,分為以上兩部分。
Sk_buff:緩沖區控制結構sk_buff。
整個sk_buff結構圖如圖1。
圖1 sk_buff結構圖
3 struct sk_buff 結構體
/* struct sk_buff - socket buffer */
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next;
struct sk_buff *prev;
struct sock *sk;
struct skb_timeval tstamp; /* Time we arrived,記錄接收或發送報文的時間戳*/
struct net_device *dev; /*通過該設備接收或發送,記錄網絡接口的信息和完成操作
struct net_device *input_dev; /*接收數據的網絡設備
struct net_device *curlayer_input_dev;
struct net_device *l2tp_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; //鏈路層報頭
.
.
.
unsigned int len, //len緩沖區中數據部分的長度。
data_len, //data_len只計算分片中數據的長度
mac_len, //mac頭的長度
csum; //校驗和
__u32 priority;
__u8 local_df:1,
cloned:1, //表示該結構是另一個sk_buff克隆的
ip_summed:2,
nohdr:1,
nfctinfo:3;
__u8 pkt_type:3,
fclone:2,
ipvs_property:1;
__be16 protocol;
__u32 flag; /*packet flags*/
.
.
.
/* These elements must be at the end, see alloc_skb() for details. */
unsigned int truesize; //這是緩沖區的總長度,包括sk_buff結構和數據部分
atomic_t users;
unsigned char *head, //指向緩沖區的頭部
*data,// 指向實際數據的頭部
*tail, //指向實際數據的尾部
*end;//指向緩沖區的尾部
};
4 sk_buff成員變量
Sk_buff成員變量主要包括以下3類
1 Layout布局
2 General通用
3 Feature-specific功能相關
4.1 Layout布局
1 struct sk_buff *next, struct sk_buff *prev
有些sk_buff成員變量的作用是方便查找,或者是連接數據結構本身. 內核可以把sk_buff組織成一個雙向鏈表。當然,這個鏈表的結構要比常見的雙向鏈表的結構復雜一點。就像任何一個雙向鏈表一樣,sk_buff中有兩個指針next和prev,其中,next指向下一個節點,而prev指向上一個節點。但是,這個鏈表還有另一個需求:每個sk_buff結構都必須能夠很快找到鏈表頭節點。為了滿足這個需求,在第一個節點前面會插入另一個結構sk_buff_head,這是一個輔助節點,它的定義如下
sk_buff和sk_buff_head的前兩個元素是一樣的:next和prev指針。這使得它們可以放到同一個鏈表中,盡管sk_buff_head要比sk_buff小得多。另外,相同的函數可以同樣應用於sk_buff和sk_buff_head。
圖2
2 struct sock *sk
這是一個指向擁有這個sk_buff的sock結構的指針。這個指針在網絡包由本機發出或者由本機進程接收時有效,因為插口相關的信息被L4(TCP或UDP)或者用戶空間程序使用。如果sk_buff只在轉發中使用(這意味着,源地址和目的地址都不是本機地址),這個指針是NULL。
3 unsigned int len
這是緩沖區中數據部分的長度。它包括主緩沖區中的數據長度(data指針指向它)和分片中的數據長度。它的值在緩沖區從一個層向另一個層傳遞時改變,因為往上層傳遞,舊的頭部就沒有用了,而往下層傳遞,需要添加本層的頭部。len同樣包含了協議頭的長度。
4 unsigned int data_len
和len不同,data_len只計算分片中數據的長度。
5 unsigned int mac_len
這是mac頭的長度。
6 atomic_t users
這是一個引用計數,用於計算有多少實體引用了這個sk_buff緩沖區。它的主要用途是防止釋放sk_buff后,還有其他實體引用這個sk_buff。因此,每個引用這個緩沖區的實體都必須在適當的時候增加或減小這個變量。這個計數器只保護sk_buff結構本身,而緩沖區的數據部分由類似的計數器(dataref)來保護。有時可以用atomic_inc和atomic_dec函數來直接增加或減小users,但是,通常還是使用函數skb_get和kfree_skb來操作這個變量。
7 unsigned int truesize
這是緩沖區的總長度,包括sk_buff結構和數據部分。如果申請一個len字節的緩沖區,alloc_skb函數會把它初始化成len+sizeof(sk_buff)。
8 unsigned char *head ,*end, *data, *tail
它們表示緩沖區和數據部分的邊界。在每一層申請緩沖區時,它會分配比協議頭或協議數據大的空間。head和end指向緩沖區的頭部和尾部,而data和tail指向實際數據的頭部和尾部,參見圖3。每一層會在head和data之間填充協議頭,或者在tail和end之間添加新的協議數據。圖3中右邊數據部分會在尾部包含一個附加的頭部。
圖3
9 void (*destructor)(...)
這個函數指針可以初始化成一個在緩沖區釋放時完成某些動作的函數。如果緩沖區不屬於一個socket,這個函數指針通常是不會被賦值的。如果緩沖區屬於一個socket,這個函數指針會被賦值為sock_rfree或sock_wfree(分別由skb_set_owner_r或skb_set_owner_w函數初始化)。這兩個sock_xxx函數用於更新socket隊列中的內存容量。
4.2 General通用
本節描述sk_buff的主要成員變量,這些成員變量與特定的內核功能無關。
1struct timeval tstamp
這個變量只對接收到的包有意義。它代表包接收時的時間戳,或者有時代表包准備發出時的時間戳。它在netif_rx里面由函數net_timestamp設置,而netif_rx是設備驅動收到一個包后調用的函數。
2 struct net_device *dev
這個變量的類型是net_device,net_device它代表一個網絡設備。dev的作用與這個包是准備發出的包,還是剛接收的包有關。當收到一個包時,設備驅動會把sk_buff的dev指針指向收到這個包的設備的數據結構,就像下面的vortex_rx里的一段代碼所做的一樣,這個函數屬於3c59x系列以太網卡驅動,用於接收一個幀。(drivers/net/3c59x.c):
當一個包被發送時,這個變量代表將要發送這個包的設備。在發送網絡包時設置這個值的代碼要比接收網絡包時設置這個值的代碼復雜。有些網絡功能可以把多個網絡設備組成一個虛擬的網絡設備(也就是說,這些設備沒有和物理設備直接關聯),並由一個虛擬網絡設備驅動管理。當虛擬設備被使用時,dev指針指向虛擬設備的net_device結構。而虛擬設備驅動會在一組設備中選擇一個設備並把dev指針修改為這個設備的net_device結構。因此,在某些情況下,指向傳輸設備的指針會在包處理過程中被改變。
3 struct net_device *input_dev
這是收到包的網絡設備的指針。如果包是本地生成的,這個值為NULL。對以太網設備來說,這個值由eth_type_trans初始化,它主要被流量控制代碼使用。
4 struct net_device *real_dev
這個變量只對虛擬設備有意義,它代表與虛擬設備關聯的真實設備。例如,Bonding和VLAN設備都使用它來指向收到包的真實設備。
5 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層協議的協議頭(參見圖4)。
發送包的過程與此相反,但是由於要為每一層添加新的協議頭,這個過程要比接收包的過程復雜。
圖4
6 struct dst_entry dst 這個變量在路由子系統中使用。
7 char cb[40]
這是一個控制緩存,或者說是一個私有信息的存儲空間,由每一層自己維護並使用。它在分配sk_buff結構時分配(它目前的大小是40字節,已經足夠為每一層存儲必要的私有信息了)。在每一層中,訪問這個變量的代碼通常用宏實現,以增強代碼的可讀性。例如,TCP用這個變量存儲tcp_skb_cb結構,這個結構在include/net/tcp.h中定義:
下面這個宏被TCP代碼用來訪問cb變量。在這個宏里面,有一個簡單的類型轉換:
#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))
下面的例子是TCP子系統在收到一個分段時填充相關數據結構的代碼:
int tcp_v4_rcv(struct sk_buff *skb)
8 unsigned int csum unsigned char ip_summed
表示校驗和以及相關狀態標記。
unsigned char cloned
一個布爾標記,當被設置時,表示這個結構是另一個sk_buff的克隆。
9 unsigned char pkt_type
這個變量表示幀的類型,分類是由L2的目的地址來決定的。可能的取值都在include/linux/if_packet.h中定義。對以太網設備來說,這個變量由eth_type_trans函數初始化。
10 __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.3 Feature-specific功能相關
linux內核是模塊化的,你可以選擇包含或者刪除某些功能。因此,sk_buff結構里面的一些成員變量只有在內核選擇支持某些功能時才有效,比如防火牆(netfilter)或者qos:
1 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"
2 union {...} private
這個聯合結構被高性能並行接口(HIPPI)使用。相應的內核編譯選項是"Device->Drivers ->Networking support ->Network device support ->HIPPI driver support"
3 __u32 tc_index __u32 tc_verd __u32 tc_classid
這些變量被流量控制代碼使用,內核編譯選項是"Device Drivers ->Networking->support ->Networking options ->QoS and/or fair queueing"和它的子選項"Packetclassifier API"
4 struct sec_path *sp
這個變量被IPSec協議用於跟蹤傳輸的信息。
5 sk_buff管理和操作函數
5.1緩沖區操作函數
有很多函數,通常都比較短小而且簡單,內核用這些函數操作sk_buff的成員變量或者sk_buff鏈表。首先來看分配和釋放緩沖區的函數,然后是一些通過移動指針在緩沖區的頭部或尾部預留空間的函數。如果你看過include/linux/skbuff.h和net/core/skbuff.c中的函數,你會發現,基本上每個函數都有兩個版本,名字分別是do_something和__do_something。通常第一種函數是一個包裝函數,它會在第二種函數的基礎上增加合法性檢查或者鎖。一般來說,類似__do_something的函數不能被直接調用(除非滿足特定的條件,比如說鎖)。那些違反這條規則而直接引用這些函數的不良代碼會最終被更正。
各操作函數緩沖區與移動指針變化如圖5所示。
圖5 操作前與操作后指針變化圖: (a)skb_put, (b)skb_push, (c)skb_pull, and (d)skb_reserve
1 unsigned char *skb_put(struct sk_buff *skb, unsigned int len)
在緩沖區的尾部空間擴充len字節數據區l,將tail指針下移,並增加skb的len值。data和tail之間的空間就是可以存放網絡報文的空間。這個操作增加了可以存儲網絡報文的空間,但是增加不能使 tail的值大於end的值,skb的len值大於truesize 的值。
2 unsigned char *skb_push(struct sk_buff *skb, unsigned int len)
在緩沖區的頭部空間擴充len字節的數據區。將data指針上移,並增加skb的len值。這個操作在存儲空間的頭部增加了一段可以存儲網絡報文的空間,但是增加不能使data的值小於 head的值,skb的len值大於truesize的值。
3 unsigned char * skb_pull(struct sk_buff *skb, unsigned int len)
從緩沖區的數據區刪除len字節,把騰出的內存歸還給頭部空間。將data指針下移,並減小skb的len值。這個操作使data指針指向下一層網絡報文的頭部。
4 void skb_reserve(struct sk_buff *skb, unsigned int len)
從空白緩沖區中分配len字節的數據區,通過減少尾部空間,增加一個空&sk_buff的首部空間,將data指針和tail指針同時下移。這個操作在存儲空間的頭部預留len長度的空隙。
如果查看某個以太網設備驅動的收包函數(例如,drivers/net/3c59x.c中的vortex_rx),你就會發現它在分配緩沖區之后,在向緩沖區中填充數據之前,會調用下面的函數:
由於以太網幀的頭部長度是14個八位組,這個函數把緩沖區的頭部指針向后移動了2個字節。這樣,緊跟在以太網頭部之后的IP頭部在緩沖區中存儲時就可以在16字節的邊界上對齊。如圖6所示。
圖6 (a) skb_reserve開始前, (b) skb_reserve后(c) 復制數據到緩沖區
5.2發送tcp報文示例
發送報文時,在不同協議層處理數據時,該數據要添加相應的協議頭。因此,最高層添加數據和自身的協議頭。alloc_skb用來申請一個sk_buff。skb_reserve用來創建頭空間。skb_put用來創建用戶數據空間,用戶數據復制到sk->data指向的數據區。接下來使用skb_push是在用戶數據的前面加上各層協議頭。
圖7是發送tcp報文的整個過程示意圖。
1)當TCP發送數據時,它根據一些條件分配一個緩沖區(比如,TCP的最大分段長度(mss),是否支持散讀散寫I/O等
2)TCP在緩沖區的頭部預留足夠的空間(用skb_reserve)用於填充各層的頭部(如TCP,IP,鏈路層等)。MAX_TCP_HEADER參數是各層頭部長度的總和,它考慮了最壞的情況:由於tcp層不知道將要用哪個接口發送包,它為每一層預留了最大的頭部長度。它甚至考慮了出現多個IP頭的可能性(如果內核編譯支持IP over IP,我們就會遇到多個IP頭的情況)。
3)把TCP的負載拷貝到緩沖區(用skb_put,復制數據)。需要注意的是:圖7只是一個例子。TCP的負載可能會被組織成其他形式。例如它可以存儲到分片中。
4)TCP層添加自己的頭部(用skb_push)。
5)TCP層把緩沖區傳遞給IP層,IP層同樣添加自己的頭部(用skb_push)。
6)IP層把緩沖區傳遞給鄰居層,鄰居層添加鏈路層頭部(用skb_push)。
Tcp報文發送過程如圖6所示。
接收報文時:當緩沖區在協議棧中向下層傳遞時,每一層都把skb->data指針向下移動,然后拷貝自己的頭部,同時更新skb->len。
圖7 tcp報文發送過程
5.3 緩沖區分配、克隆和釋放函數分析
1 alloc_skb
alloc_skb是net/core/skbuff.c里面定義的,用於分配緩沖區的函數。我們已經知道,數據緩沖區和緩沖區的描述結構(sk_buff結構)是兩種不同的實體,這就意味着,在分配一個緩沖區時,需要分配兩塊內存(一個是緩沖區,一個是緩沖區的描述結構sk_buff)。
alloc_skb函數起始可以看作三部分,第一部分是從cache中分配內存,第二部分是初始化分配的skb的相關域。第三部分是處理fclone。
1)分配內存
首先調用函數kmem_cache_alloc從緩存中獲取一個sk_buff結構,然后調用kmalloc_cachhe_alloc_node分配緩沖區(如果有緩存的話,它同樣從緩存中獲取內存)。
2)初始化
在調用kmalloc前,size參數通過SKB_DATA_ALIGN宏強制對齊。在函數返回前,它會初始化結構中的一些變量。
3)處理fclone
每次skb_clone一個skb的時候,都是要調用kmem_cache_alloc從cache中alloc一塊新的內存。而現在當我們擁有了fastclone之后,通過調用alloc_skb_fclone函數來分配一塊大於sizeof(structsk_buff)的內存,也就是在這次請求的skb的下方多申請了一些內存,然后返回的時候設置返回的skb的fclone標記為SKB_FCLONE_ORIG,而多申請的那塊內存的sk_buff的fclone為SKB_FCLONE_UNAVAILABLE,這樣當我們調用skb_clone克隆這個skb的時候看到fclone的標記就可以直接將skb的指針+1,而不需要從cache中取了。這樣的話節省了一次內存存取,提高了clone的效率,不過調用flcone一般都是我們確定接下來這個skb會被clone很多次。
4)skb指針狀態
alloc_skb之后的skb的指針的狀態如圖8所示。在圖7右邊所示的內存塊的底部,可以能看到對齊操作所帶來的填充區域(padding)。
圖8 skb指針狀態
2 克隆skb_clone
如果一個緩沖區需要被不同的用戶獨立地操作,而這些用戶可能會修改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。
圖9
clone的意思就是只復制skb而不復制data域。
1它會先判斷將要被clone的skb的fclone段,以便與決定是否重新分配一塊內存來保存skb。2調用__skb_clone來初始化相關的域。
3 pskb_copy, skb_copy
當一個skb被clone之后,這個skb的數據區是不能被修改的,這就意為着,我們存取數據不需要任何鎖。可是有時我們需要修改數據區,這個時候會有兩個選擇,一個是我們只修改linear段,也就是head和end之間的段使用,可以使用pskb_copy來復制這部分數據,一種是還要修改切片數據,也就是skb_shared_info,就必須使用skb_copy。
這樣就有兩個函數供我們選擇,第一個是pskb_copy,第二個是skb_copy.
pskb_copy,函數
先alloc一個新的skb,然后調用skb_copy_from_linear_data來復制線性區的數據,並更新相關域,最后復制切片數據的指針。
skb_copy函數
先alloc一個新的skb,然后復制skb的所有數據段,包括切片數據。
pskb_copy: alloc一個新的skb, 復制數據,更新相關域
復制切片數據指針
skb_copy: alloc一個新的skb, 復制所有數據
psb_copy和skb_copy調用后的內存模型如圖9,其中a是pskb_copy,b是skb_copy:
圖9
4 釋放緩沖區kfree_skb
kfree_skb函數釋放緩沖區,並把它返回給緩沖池(緩存)。只有在skb->users為1的情況下才釋放內存(沒有人引用這個結構)。否則,它只是簡單地減小skb->users。如果緩沖區有三個引用者,那么只有第三次調用kfree_skb時才釋放內存。
kfree_skb(skb)__kfree_skb(skb)kfree_skbmem(skb)kmem_cache_free(skbuff_head_cache, skb)
最終kfree_skb把sk_buff結構返回給skbuff_head_cache緩存。