1. 程式人生 > >sk_buff整理筆記(三、記憶體申請和釋放)

sk_buff整理筆記(三、記憶體申請和釋放)

        承接上一篇blog--sk_buff整理筆記(二、操作函式),這篇是要來講解下sk_buff結構的記憶體申請和釋放函式。因為sk_buff結構是比較複雜的(並不是其本身結構複雜,而是其所指的資料區以及分片結構等,合在一起就變複雜了),所以在記憶體申請和釋放時,就要搞清楚什麼函式對應的申請分配或釋放什麼結構記憶體。這裡不提倡自己用kmalloc()和kfree()函式來為sk_buff相關結構體申請記憶體及銷燬,而要用核心提供好的一些函式去為這些結構體申請記憶體。核心開發的原則:儘量用核心定義好的資料和函式以及操作巨集,不到迫不得已不要自行定義任何東西。這樣做是為了介面統一,移植方便,容易維護。

        第一、sk_buff結構的記憶體申請:

static inline struct sk_buff *alloc_skb(unsigned int size,
					gfp_t priority)
{
	return __alloc_skb(size, priority, 0, -1);
}

static inline struct sk_buff *alloc_skb_fclone(unsigned int size,
					       gfp_t priority)
{
	return __alloc_skb(size, priority, 1, -1);
}
struct sk_buff *dev_alloc_skb(unsigned int length)
{
	/*
	 * There is more code here than it seems:
	 * __dev_alloc_skb is an inline
	 */
	return __dev_alloc_skb(length, GFP_ATOMIC);
}
static inline struct sk_buff *__dev_alloc_skb(unsigned int length,
					      gfp_t gfp_mask)
{
	struct sk_buff *skb = alloc_skb(length + NET_SKB_PAD, gfp_mask);
	if (likely(skb))
		skb_reserve(skb, NET_SKB_PAD);
	return skb;
}

        這幾個函式都是在linux-2.6.32.63\include\linux\sk_buff.h檔案中的,其實就函式也可以看得出這是行內函數。不記得在前面講過沒,對於簡潔常用的函式一般都定義為行內函數,這樣做是為了提高CPU工作效率和記憶體利用率。一般的函式呼叫要儲存現場(在堆疊中儲存呼叫函式時現場狀態,包括地址,執行狀態等);當呼叫函式完後,又要恢復現場狀態(把開始儲存的狀態資料從堆疊中讀取出來)。在呼叫函式和返回時,浪費了CPU很多時間,再個在儲存時也會出現些碎片。所以再使用簡潔函式時,一般定義為行內函數,在函式前面加個inline關鍵字。

        講了行內函數細節後,來看下上面這幾個記憶體申請函式。發現這幾個函式其實本質上都是封裝了 __alloc_skb();函式,那麼首先就要來分析下__alloc_skb()函式,然後再去分析下這幾個函式的異同點,以方便在申請sk_buff結構時,該用哪個函式去申請。

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;
}
        首先來看下函式引數:第一個引數 unsigned int size,資料區的大小;第二個引數 gfp_t gfp_mask,有些blog說這是優先順序,個人覺得是不對的。我們都知道核心動態分配函式kmalloc()裡面要有兩個引數,第一個是要分配的空間大小,第二個是記憶體分配方式(這裡我們一般用GFP_KERNEL)。所以這裡的gfp_t gfp_mask應該是核心動態分配方式的一個掩碼(就是各種申請方式的集合);第三個引數 int fclone,表示在哪塊分配器上分配;第四個引數 int node,用來表示用哪種區域去分配空間。

        第9行程式碼:cache = fclone ? skbuff_fclone_cache : skbuff_head_cache;由傳入的引數來決定用哪個緩衝區中的記憶體來分配(一般是用skbuff_head_cache快取池來分配)。說到這裡就插入的講下快取池的概念。

        核心對於sk_buff結構的記憶體分配不是和一般的結構動態記憶體申請一樣:只分配指定大小的記憶體空間。而是在開始的時候,在初始化函式skb_init()中就分配了兩段記憶體(skbuff_head_cache和skbuff_fclone_cache )來供sk_buff後期申請時用,所以後期要為sk_buff結構動態申請記憶體時,都會從這兩段記憶體中來申請(其實這不叫申請了,因為這兩段記憶體開始就申請好了的,只是根據你要的記憶體大小從某個你選定的記憶體段中還回個指標給你罷了)。如果在這個記憶體段中申請失敗,則再用核心中用最低層,最基本的kmalloc()來申請記憶體了(這才是真正的申請)。釋放時也一樣,並不會真正的釋放,只是把資料清零,然後放回記憶體段中,以供下次sk_buff結構的申請。這是核心動態申請的一種策略,專門為那些經常要申請和釋放的結構設計的,這種策略不僅可以提高申請和釋放時的效率,而且還可以減少記憶體碎片的。(注意:上面提到的記憶體段中的段不是指記憶體管理中的段、頁的段,而是表示塊,就是兩塊比較大的記憶體)

        第13行程式碼:skb = kmem_cache_alloc_node(cache, gfp_mask & ~__GFP_DMA, node);從指定段中為skb分配記憶體,分配的方式是去除在DMA記憶體中分配,因為DMA記憶體比較小,且有特定的作用,一般不用來分配skb。

        第17行程式碼是調整sk_buff結構指向的資料區的大小。

        第19行程式碼:data = kmalloc_node_track_caller(size + sizeof(struct skb_shared_info),  gfp_mask, node);這也是關鍵性程式碼之一(這裡和上面分配skb的分配方式不一樣,這裡允許有些資料可以用DMA記憶體來分配)。這也是從特殊的快取池中分配記憶體的,如果看函式裡面的引數不難發現,要分配的空間大小為:size + sizeof(struct skb_shared_info),前面的size是指skb結構體指向的資料區大小,而sizeof(struct skb_shared_info)則是為分片資料分配空間。因為分片結構就是在skb結構指向的資料區的下面,就是end指標的下一個位元組,所以就一起分配。

        第29行到42行程式碼則是為sk_buff的資料區初始化,第44行到第53行則是為分片結構進行初始化。

        第55行開始就是另外個知識點了,和第9行程式碼有點關係。skbuff_fclone_cache和skbuff_head_cache兩個塊記憶體快取池是不一樣的。我們一般是在skbuff_head_cache這塊快取池中來申請記憶體的,但是如果你開始就知道這個skb將很有可能被克隆(至於克隆和複製將在下一篇bolg講),那麼你最好還是選擇在skbuff_fclone_cache快取池中申請。因為在這塊快取池中申請的話,將會返回2個skb的記憶體空間,第二個skb則是用來作為克隆時使用。(其實從函式名字就應該知道是為克隆準備的,fclone嘛)雖然是分配了兩個sk_buff結構記憶體,但是資料區卻是隻有一個的,所以是兩個sk_buff結構中的指標都是指向這一個資料區的。也正因為如此,所以分配sk_buff結構時也順便分配了個引用計數器,就是來表示有多少個sk_buff結構在指向資料區(引用同一個資料區),這是為了防止還有sk_buff結構在指向資料區時,而銷燬掉這個資料區。有了這個引用計數,一般在銷燬時,先檢視這個引用計數是否為0,如果不是為0,就讓引用計數將去1;如果是0,才真正銷燬掉這個資料區。

        第56行程式碼就好理解了:用child結構體變數來指向第二sk_buff結構體記憶體地址。第57行程式碼就是獲取到引用計數器,因為引用計數器是在第二個sk_buff結構體記憶體下一個位元組空間開始的,所以用(child + 1)來獲取到引用計數器的開始地址。後面的程式碼就比較好理解了,無非就是些設定性引數了。

        好了,基本函式__alloc_skb()已經分析過了,那麼現在來看下開始的那幾個函式的異同點吧。

        alloc_skb():是用來分配單純的sk_buff結構記憶體的,一般都是使用這個;alloc_skb_fclone():這是用來分配克隆sk_buff結構的,因為這個分配函式會分配一個子skb用來後期克隆使用,所以如果能預見要克隆skb_buff結構,則使用這種方法會方便些。dev_alloc_skb():其實這個函式實質上是呼叫了alloc_skb()函式單純分配了一個sk_buff記憶體。但是通常來說這是在驅動程式中申請sk_buff結構時,用到的申請函式,和一般的申請記憶體函式有點不一樣,它是用GFP_ATOMIC的記憶體分配方式來申請的(一般我們用GFP_KERNEL),這是個原子操作,表示申請時不能被中斷。其實還有個申請函式:netdev_alloc_skb(),這個沒怎麼研究,估計是專門為網路裝置中使用sk_buff時,用來記憶體分配的吧。

        第二、sk_buff結構的記憶體釋放:

void kfree_skb(struct sk_buff *skb)
{
	if (unlikely(!skb))
		return;
	if (likely(atomic_read(&skb->users) == 1))
		smp_rmb();
	else if (likely(!atomic_dec_and_test(&skb->users)))
		return;
	trace_kfree_skb(skb, __builtin_return_address(0));
	__kfree_skb(skb);
}
void __kfree_skb(struct sk_buff *skb)
{
	skb_release_all(skb);
	kfree_skbmem(skb);
}
static void skb_release_all(struct sk_buff *skb)
{
	skb_release_head_state(skb);
	skb_release_data(skb);
}
        首先還是來說下dev_kfree_skb()函式吧,這個函式和dev_alloc_skb()相對應的,一般也是在驅動程式中使用,其實現也是對kfree_skb()進行封裝的,所以重點還是kfree_skb()函式。

        kfree_skb()函式首先是獲取到skb->users成員欄位,這是個引用計數器,當只有skb->users == 1是才能真正釋放空間記憶體(也不是釋放,而是放回到快取池中)。如果不為1的話,那麼kfree_skb()函式只是簡單的skb->users減去個1而已。skb->users表示有多少個人正在引用這個結構體,如果不為1表示還有其他人在引用他,不能釋放掉這

個結構體,否則會讓引用者出現野指標、非法操作記憶體等錯誤。這種情況下只需要skb->users減去個1即可,表明我不再引用這個結構體了。如果skb->users == 1,則表明是最後一個引用該結構體的,所以可以呼叫_kfree_skb()函式直接釋放掉了。當skb釋放掉後,dst_release同樣會被呼叫以減小相關dst_entry資料結構的引用計數。如果destructor(skb的解構函式)被初始化過,相應的函式會在此時被呼叫。還有分片結構體(skb_shared_info)也會相應的被釋放掉,然後把所有記憶體空間全部返還到skbuff_head_cache快取池中,這些操作都是由kfree_skbmem()函式來完成的。這裡分片的釋放涉及到了克隆問題:如果skb沒有被克隆,資料區也沒有其他skb引用,則直接釋放即可;如果是克隆了skb結構,則當克隆數計數為1時,才能釋放skb結構體;如果分片結構被克隆了,那麼也要等到分片克隆計數為1時,才能釋放掉分片資料結構。如果skb是從skbuff_fclone_cache快取池中申請的記憶體時,則要仔細銷燬過程了,因為從這個快取池中申請的記憶體,會返還2個skb結構體和一個引用計數器。所以銷燬時不僅要考慮克隆問題還要考慮2個skb的釋放順序。銷燬過程見下圖(原圖來自《深入理解linux網路技術內幕》):