1.前言
在互聯網技術裏,有兩件事最爲重要,一個是 TCP/IP 協議,它是萬物互聯的事實標準;另一個是 Linux 操作系統,它是推動互聯網技術走向繁榮的基石。在 Linux內核的協議棧中的實現中,數據結構skb_buff是最關鍵和最核心的數據,它表示接收或發送數據包的包頭信息,幷包含很多成員變量供網絡代碼中的各子系統使用。本文以及後續關於skb_buff的介紹,均來源於經典著作《深入理解linux網絡技術內幕》和《linux內核源碼剖析:TCP/IP實現》。
2.skb_buff基本原理
內核中sk_buff結構體在各層協議之間傳輸不是用拷貝sk_buff結構體,而是通過增加協議頭和移動指針來操作的。如果是從L4傳輸到L2,則是通過往sk_buff結構體中增加該層協議頭來操作;如果是從L4到L2,則是通過移動sk_buff結構體中的data指針來實現,不會刪除各層協議頭,這樣方式極大的提高CPU工作效率。
sk_buff結構體是linux網絡代碼中最重要的數據結構,是整個網絡傳輸載體。所以sk_buff結構體裏面有很多關於其他功能的成員字段,比如:防火牆,子路由系統,多播等。這些字段並不是一定有的,只有在滿足特點條件纔有的。所以可以在需要時候再去關心這些成員字段,現在我們只來講解主要的成員字段。
3.skb_buff主要字段
爲了好理解結構中的一些成員字段,先把後面要講的內容提前說下。sk_buff結構體關聯多個其他結構體,主要可以分爲:
第一是數據區:由sk_buff中head和end指向的數據塊,用來存儲sk_buff結構的數據也即是存儲數據包的內容和各層協議頭。
第二是分片結構:用來表示IP分片的一個結構體,實則上是和sk_buff結構的數據區相連的,即是end指針的下一個字節開始就是分片結構。正因此,分片結構和sk_buff數據區內存分配及銷燬時都是一起的。
第三個是分片結構指向的數據區,即是IP分片內容。
struct sk_buff {
/* These two members must be first. */
struct sk_buff *next; // 因爲sk_buff結構體是雙鏈表,所以有前驅後繼。這是個指向後面的sk_buff結構體指針
struct sk_buff *prev; // 這是指向前一個sk_buff結構體指針
//老版本(2.6以前)應該還有個字段: sk_buff_head *list //即每個sk_buff結構都有個指針指向頭節點
struct sock *sk; // 指向擁有此緩衝的套接字sock結構體,即:宿主傳輸控制模塊
ktime_t tstamp; // 時間戳,表示這個skb的接收到的時間,一般是在包從驅動中往二層發送的接口函數中設置
struct net_device *dev; // 表示一個網絡設備,當skb爲輸出/輸入時,dev表示要輸出/輸入到的設備
unsigned long _skb_dst; // 主要用於路由子系統,保存路由有關的東西
char cb[48]; // 保存每層的控制信息,每一層的私有信息
unsigned int len, // 表示數據區的長度(tail - data)與分片結構體數據區的長度之和。其實這個len中數據區長度是個有效長度,
// 因爲不刪除協議頭,所以只計算有效協議頭和包內容。如:當在L3時,不會計算L2的協議頭長度。
data_len; // 只表示分片結構體數據區的長度,所以len = (tail - data) + data_len;
__u16 mac_len, // mac報頭的長度
hdr_len; // 用於clone時,表示clone的skb的頭長度
// 接下來是校驗相關域,這裏就不詳細講了。
__u32 priority; // 優先級,主要用於QOS
kmemcheck_bitfield_begin(flags1);
__u8 local_df:1, // 是否可以本地切片的標誌
cloned:1, // 爲1表示該結構被克隆,或者自己是個克隆的結構體;同理被克隆時,自身skb和克隆skb的cloned都要置1
ip_summed:2,
nohdr:1, // nohdr標識payload是否被單獨引用,不存在協議首部。 // 如果被引用,則決不能再修改協議首部,也不能通過skb->data來訪問協議首部。</span></span>
nfctinfo:3;
__u8 pkt_type:3, // 標記幀的類型
fclone:2, // 這個成員字段是克隆時使用,表示克隆狀態
ipvs_property:1,
peeked:1,
nf_trace:1;
__be16 protocol:16; // 這是包的協議類型,標識是IP包還是ARP包或者其他數據包。
kmemcheck_bitfield_end(flags1);
void (*destructor)(struct sk_buff *skb); // 這是析構函數,後期在skb內存銷燬時會用到
#if defined(CONFIG_NF_CONNTRACK) || defined(CONFIG_NF_CONNTRACK_MODULE)
struct nf_conntrack *nfct;
struct sk_buff *nfct_reasm;
#endif
#ifdef CONFIG_BRIDGE_NETFILTER
struct nf_bridge_info *nf_bridge;
#endif
int iif; // 接受設備的index
#ifdef CONFIG_NET_SCHED
__u16 tc_index; /* traffic control index */
#ifdef CONFIG_NET_CLS_ACT
__u16 tc_verd; /* traffic control verdict */
#endif
#endif
kmemcheck_bitfield_begin(flags2);
__u16 queue_mapping:16;
#ifdef CONFIG_IPV6_NDISC_NODETYPE
__u8 ndisc_nodetype:2;
#endif
kmemcheck_bitfield_end(flags2);
/* 0/14 bit hole */
#ifdef CONFIG_NET_DMA
dma_cookie_t dma_cookie;
#endif
#ifdef CONFIG_NETWORK_SECMARK
__u32 secmark;
#endif
__u32 mark;
__u16 vlan_tci;
sk_buff_data_t transport_header; // 指向四層幀頭結構體指針
sk_buff_data_t network_header; // 指向三層IP頭結構體指針
sk_buff_data_t mac_header; // 指向二層mac頭的頭
/* These elements must be at the end, see alloc_skb() for details. */
sk_buff_data_t tail; // 指向數據區中實際數據結束的位置
sk_buff_data_t end; // 指向數據區中結束的位置(非實際數據區域結束位置)
unsigned char *head, // 指向數據區中開始的位置(非實際數據區域開始位置)
*data; // 指向數據區中實際數據開始的位置
unsigned int truesize; // 表示總長度,包括sk_buff自身長度和數據區以及分片結構體的數據區長度
atomic_t users; // skb被克隆引用的次數,在內存申請和克隆時會用到
}; //end sk_buff
char cb[48];這個字段是skb信息控制塊,也就是存儲每層的一些協議信息,當數據包在哪一層時,存儲的就是哪一層協議信息。這個字段由數據包所在層使用和維護,如果要訪問本層協議信息,可以通過用一些宏來操作這個成員字段。如:#define TCP_SKB_CB(__skb) ((struct tcp_skb_cb *)&((__skb)->cb[0]))
_u8 fclone:2;這是個克隆狀態標誌,到sk_buff結構內存申請時會使用到。這裏提前講下:若fclone = SKB_FCLONE_UNAVAILABLE,則表明SKB未被克隆;若fclone = SKB_FCLONE_ORIG,則表明是從skbuff_fclone_cache緩存池(這個緩存池上分配內存時,每次都分配一對skb內存)中分配的父skb,可以被克隆;若fclone = SKB_FCLONE_CLONE,則表明是在skbuff_fclone_cache分配的子SKB,從父SKB克隆得到的;
atomic_t users;這是個引用計數,表明了有多少實體引用了這個skb。其作用就是在銷燬skb結構體時,先查看下users是否爲零,若不爲零,則調用函數遞減下引用計數users即可;當某一次銷燬時,users爲零才真正釋放內存空間。有兩個操作函數:atomic_inc()引用計數增加1;atomic_dec()引用計數減去1;
sk_buff->data_len:只計算分片中數據的長度,即是分片結構體中page指向的數據區長度。這個在分片結構體中會再詳細講解下。
sk_buff->len:表示當前緩衝區中數據塊的大小的總長度。它包括主緩衝中(即是sk_buff結構中指針data指向)的數據區的實際長度(data-tail)和分片中的數據長度。這個長度在數據包在各層間傳輸時會改變,因爲分片數據長度不變,從L2到L4時,則len要減去幀頭大小和網絡頭大小;從L4到L2則相反,要加上幀頭和網絡頭大小。所以:len = (data - tail) + data_len;
sk_buff->truesize:這是緩衝區的總長度,包括sk_buff結構和數據部分。如果申請一個len字節的緩衝區,alloc_skb函數會把它初始化成len+sizeof(sk_buff)。當skb->len變化時,這個變量也會變化。所以:truesize = len + sizeof(sk_buff) = (data - tail) + data_len + sizeof(sk_buff);