ip 協議棧源碼剖析第三節

繼上一篇文章,我們繼續探討linux內核網絡子系統的源碼剖析,上一篇中我們講到了skb的聚合分散IO的知識點,現在繼續講解。

1. SKB之聚合分散IO

1.1 frag_list 成員變量

由上一篇文章的介紹,我們可以瞭解到,frag_list是skb_shared_info的成員變量,它的主要用途如下:

1) 用於IP報文的分片重組,將重組後的IP報文鏈接成一個鏈表,這個鏈表的組織就是靠frag_list這個成員。

2) 當輸出的UDP報文較大時,這個時候需要進行分片,所以將分片的報文鏈接到第一個的SKB。

3) 用來支持FRAGLIST類型的數據包,如果網絡設備支持該類型,就將skb掛到frag_list上。

1.2 數據存儲在SKB中的幾種方式

說起skb中存儲數據的方式,不得不說一個linux內核的函數,即skb_is_nonlinear,這個函數用來判斷skb中是否存在聚合分散IO的區域,實際上就是判斷skb中的成員變量data_len是否為0,如果不為0,則說明skb存在聚合分散的IO區域。

如果你不想處理skb的聚合分散IO區域,可以使用skb_linearize把聚合分散IO區域的數據線性化到線性區域,也就是data所指的數據區域。

圖1是一個數據全部存在了skb中的data區域,即線性區,從圖中我們可以看到數據的總長度是Y,即skb中data指針和tail指針的差值,我們還可觀察到,skb中的data_len為0,說明skb中不存在聚合分散IO的區域,可以觀察到,nr_frags和frag_list都為0,又再一次的說明了skb不存在聚合分散的IO區域的數據。

linux tcp/ip 協議棧源碼剖析第三節

圖1

圖2是skb中的包含聚合分散IO的區域,從圖2中我們可以看到數據的總長度是len為Y+S1+S2,其中Y是skb中data指針和tail指針的差值,S1和S2是兩個分片報文的長度,可知,兩個分片報文的長度data_len為S1+S2,我們可以看到,skb_shared_info結構中的nr_frags為2,frag_list為NULL,這再次說明這個報文包含了聚合分散的IO數據,且分片數是2,這兩個分片存在一個頁面內,偏移分別為0和S1。

linux tcp/ip 協議棧源碼剖析第三節

圖2

圖3是skb中的包含聚合分散IO的區域,從圖3中我們可以看到數據的總長度是len為Y+S1,其中Y是skb中data指針和tail指針的差值,S1是FRAGLIST類型的數據的長度,可知,這個分片報文的長度data_len為S1,我們可以看到,skb_shared_info結構中的nr_frags為0,frag_list不為NULL,這再次說明這個報文包含了聚合分散的IO數據,且分片數是1,這這個分片掛在了frag_list的鏈表上。

linux tcp/ip 協議棧源碼剖析第三節

圖3

1.3 skb相關的函數介紹

我們知道,程序在運行時,現申請內存會影響性能,所以,正常在寫程序時,如果碰到經常使用的結構,我們通常都會先申請一段內存池,當程序申請內存時,直接從內存池裡拿數據,這樣省去了現申請內存的消耗,那麼linux內核在處理skb時,也是提供了一個內存池。

1.3.1 skb內存池相關的函數

\t void __init skb_init(void)
{
skbuff_head_cache = kmem_cache_create("skbuff_head_cache",
sizeof(struct sk_buff),
0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC,
NULL);
skbuff_fclone_cache = kmem_cache_create("skbuff_fclone_cache",
(2*sizeof(struct sk_buff)) +
sizeof(atomic_t),
0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC,
NULL);
}

以上的函數,創建了兩個高速的緩存,skbuff_head_cache這個緩存主要用來分配一個skb時用的,一般情況下,都會用這個緩存,skbuff_fclone_cache這個緩存的用途是,當你要分配兩倍的skb時,就用這個緩存,以增加分配的效率,例如,想要克隆一個skb時,建議skb從skbuff_fclone_cache進行分配,這樣在克隆時,就不需要再次進行克隆了,而是直接使用後備的skb。

1.3.2 skb的分配函數

\t struct sk_buff *__alloc_skb(unsigned int size, gfp_t gfp_mask,
int fclone, int node)
{
struct kmem_cache *cache;
struct skb_shared_info *shinfo;
struct sk_buff *skb;
u8 *data;

cache = fclone ? skbuff_fclone_cache : skbuff_head_cache;

/* Get the HEAD */
skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);
if (!skb)
goto out;

size = SKB_DATA_ALIGN(size);
data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info),
gfp_mask, node);
if (!data)
goto nodata;

/*
* Only clear those fields we need to clear, not those that we will
* actually initialise below. Hence, don't put any more fields after
* the tail pointer in struct sk_buff!
*/
memset(skb, 0, offsetof(struct sk_buff, tail));
skb->truesize = size + sizeof(struct sk_buff);
atomic_set(&skb->users, 1);
skb->head = data;
skb->data = data;
skb_reset_tail_pointer(skb);
skb->end = skb->tail + size;
kmemcheck_annotate_bitfield(skb, flags1);
kmemcheck_annotate_bitfield(skb, flags2);
#ifdef NET_SKBUFF_DATA_USES_OFFSET
skb->mac_header = ~0U;
#endif

/* make sure we initialize shinfo sequentially */
shinfo = skb_shinfo(skb);
atomic_set(&shinfo->dataref, 1);
shinfo->nr_frags = 0;
shinfo->gso_size = 0;
shinfo->gso_segs = 0;

shinfo->gso_type = 0;
shinfo->ip6_frag_id = 0;
shinfo->tx_flags.flags = 0;
skb_frag_list_init(skb);
memset(&shinfo->hwtstamps, 0, sizeof(shinfo->hwtstamps));

if (fclone) {
struct sk_buff *child = skb + 1;
atomic_t *fclone_ref = (atomic_t *) (child + 1);

kmemcheck_annotate_bitfield(child, flags1);
kmemcheck_annotate_bitfield(child, flags2);
skb->fclone = SKB_FCLONE_ORIG;
atomic_set(fclone_ref, 1);

child->fclone = SKB_FCLONE_UNAVAILABLE;
}
out:
return skb;
nodata:
kmem_cache_free(cache, skb);
skb = NULL;
goto out;
}

1: 根據參數fclone的值,來確定從哪個高速緩存分配skb。

2: 調用kmem_cache_alloc_node從高速緩存中分配內存,大家可以看到,去掉了__GFP_DMA標誌,目的是不從DMA區域分配內存,因為DMA區域比較小,主要用於特殊的用途,因此skb描述符的分配不從DMA區域分配。

3: 對size進行對齊。

4: 調用kmalloc_node_track_caller函數,對skb分配其數據的緩存區域,我們可以看到長度是size和sizeof(struct skb_shared_info)之和,因為在其末尾要留有skb_shared_info這麼大的數據大小的區間,為聚合分散做準備。

5: 對skb的成員進行初始化。

6: 如果fclone置為1,則表示要申請兩倍的skb,因此要將父skb的fclone置為SKB_FCLONE_ORIG,表示可以被克隆,child的skb被置為SKB_FCLONE_UNAVAILABLE,表示child還沒有被利用。

最後創建的skb結構,如下圖所示:

linux tcp/ip 協議棧源碼剖析第三節

skb結構示意圖

1.3.3 skb的空間預留

skb_reserve專門在數據區的前面預留一定的空間,主要的作用有兩個,第一是,進行內存上的對齊,第二,需要在數據區的前面預留一定的空間,來插入協議的首部的部分。

例如:當skb從上層向下層傳遞時,會更新skb->data的指針,使得data指針向上移動,來添加協議的首部,如下圖所示:

linux tcp/ip 協議棧源碼剖析第三節

skb_reserve 示意圖

1.3.4 skb_push 函數的作用

其實這個函數的作用就是在data區域加入各層報文的首部,例如,當要發送TCP的數據時,通常都會對data區域預留一個空間,預留空間的目的就是添加各層協議的首部,通常TCP預留的首部的大小是MAX_TCP_HEADER。

執行的流程如下圖所示:

linux tcp/ip 協議棧源碼剖析第三節

skb push 示意圖1

未完待續


分享到:


相關文章: