總的來說,LWIP的動態內存管理機制可以有三種:C運行時庫自帶的內存分配策略、動態內存堆(HEAP)分配策略和動態內存池(POOL)分配策略。
動態內存堆分配策略和C運行時庫自帶的內存分配策略具有很大的相似性,這是LWIP模擬運行時庫分配策略實現的。這兩種策略使用者只能從中選擇一種,這通過頭文件lwippools.h
中的宏定義MEM_LIBC_MALLOC
來實現的,當它被定義爲1時則使用標準C運行時庫自帶的內存分配策略,而爲0時則使用LWIP自身的動態內存堆分配策略。一般情況下,我們選擇使用LWIP自身的動態內存堆分配策略,這裏不對C運行時庫自帶的內存分配策略進行討論。
同時,動態內存堆分配策略可以有兩種實現方式,第一種就是如前所述的通過開闢一個內存堆,然後通過模擬C運行時庫的內存分配策略來實現。第二種就是通過動態內存池的方式來實現,也即動態內存堆分配函數通過簡單調用動態內存池(POOL)分配函數來完成其功能,在這種情況下,用戶需要在頭文件lwippools.h
中定義宏MEM_USE_POOLS
和MEM_USE_CUSTOM_POOLS
爲1,同時還要開闢一些額外的緩衝池區,如下:
LWIP_MALLOC_MEMPOOL_START
LWIP_MALLOC_MEMPOOL(20, 256)
LWIP_MALLOC_MEMPOOL(10, 512)
LWIP_MALLOC_MEMPOOL(5, 1512)
LWIP_MALLOC_MEMPOOL_END
這幾句摘自LWIP源碼註釋部分,表示爲動態內存堆相關功能函數分配20個256字節長度的內存塊,10個512字節的內存塊,5個1512字節的內存塊。內存池管理會根據以上的宏自動在內存中靜態定義一個大片內存用於內存池。在內存分配申請的時候,自動根據所請求的大小,選擇最適合他長度的池裏面去申請,如果啓用宏 MEM_USE_POOLS_TRY_BIGGER_POOL
,那麼,如果上述的最適合長度的池中沒有空間可以用了,分配器將從更大長度的池中去申請,不過這樣會浪費更多的內存。
動態內存堆分配策略原理就是在一個事先定義好大小的內存塊中進行管理,其內存分配的策略是採用最快合適(First Fit)方式,只要找到一個比所請求的內存大的空閒塊,就從中切割出合適的塊,並把剩餘的部分返回到動態內存堆中。分配的內存塊有個最小大小的限制,要求請求的分配大小不能小於MIN_SIZE
,否則請求會被分配到MIN_SIZE大小的內存空間。一般MIN_SIZE爲12字節,在這12個字節中前幾個字節會存放內存分配器管理用的私有數據,該數據區不能被用戶程序修改,否則導致致命問題。內存釋放的過程是相反的過程,但分配器會查看該節點前後相鄰的內存塊是否空閒,如果空閒則合併成一個大的內存空閒塊。採用這種分配策略,其優點就是內存浪費小,比較簡單,適合用於小內存的管理,其缺點就是如果頻繁的動態分配和釋放,可能會造成嚴重的內存碎片,如果在碎片情況嚴重的話,可能會導致內存分配不成功。對於動態內存的使用,比較推薦的方法就是分配->釋放->分配->釋放,這種使用方法能夠減少內存碎片。
下面具體來看看LWIP是怎麼來實現這些函數的:
mem_init( )
內存堆的初始化函數,主要是告知內存堆的起止地址,以及初始化空閒列表,由lwip初始化時自己調用,該接口爲內部私有接口,不對用戶層開放。
mem_malloc( )
申請分配內存。將總共需要的字節數作爲參數傳遞給該函數,返回值是指向最新分配的內存的指針,而如果內存沒有分配好,則返回值是NULL,分配的空間大小會收到內存對齊的影響,可能會比申請的略大。返回的內存是“沒有“初始化的。這塊內存可能包含任何隨機的垃圾,你可以馬上用有效數據或者至少是用零來初始化這塊內存。內存的分配和釋放,不能在中斷函數裏面進行。內存堆是全局變量,因此內存的申請、釋放操作做了線程安全保護,如果有多個線程在同時進行內存申請和釋放,那麼可能會因爲信號量的等待而導致申請耗時較長。
mem_calloc( )
是對mem_malloc( )函數的簡單包裝,他有兩個參數,分別爲元素的數目和每個元素的大小,這兩個參數的乘積就是要分配的內存空間的大小,與mem_malloc()不同的是它會把動態分配的內存清零。有經驗的程序員更喜歡使用mem_ calloc (),因爲這樣的話新分配內存的內容就不會有什麼問題,調用mem_ calloc ()肯定會清0,並且可以避免調用memset()。
動態內存池(POOL)分配策略可以說是一個比較笨的分配策略了,但其分配策略實現簡單,內存的分配、釋放效率高,可以有效防止內存碎片的產生。不過,他的缺點是會浪費部分內存。
爲什麼叫POOL?
這點很有趣,POOL有很多種,而這點依賴於用戶配置LWIP的方式。例如用戶在頭文件opt.h
文件中定義LWIP_UDP
爲1,則在編譯的時候與UDP類型內存池就會被建立;定義LWIP_TCP
爲1,則在編譯的時候與TCP類型內存池就會被建立。
另外,還有很多其他類型的內存池,如專門存放網絡包數據信息的PBUF_POOL
、還有上面講解動態內存堆分配策略時提到的CUSTOM_POOLS
等等等等。某種類型的POOL其單個大小是固定的,而分配該類POOL的個數是可以用戶配置的,用戶應該根據協議棧實際使用狀況進行配置。把協議棧中所有的POOL挨個放到一起,並把它們放在一片連續的內存區域,這呈現給用戶的就是一個大的緩衝池。所以,所謂的緩衝池的內部組織應該是這樣的:開始處放了A類型的POOL池a個,緊接着放上B類型的POOL池b個,再接着放上C類型的POOL池c個….直至最後N類型的POOL池n個。這一點很像UC/OSII中進程控制塊和事件控制塊,先開闢一堆各種類型的放那,你要用直接來取就是了。注意,這裏的分配必須是以單個緩衝池爲基本單位的,在這樣的情況下,可能導致內存浪費的情況。
下面我來看看在LWIP實現中是怎麼開闢出上面所論述的大大的緩衝池的。基本上絕大部分人看到這部分代碼都會被打得暈頭轉向,完全不曉得作者是在幹啥,但是仔細理解後,你不得不佩服作者超凡脫俗的代碼寫能力:
static u8_t memp_memory [ MEM_ALIGNMENT - 1
#define LWIP_MEMPOOL(name,num,size,desc) + ( (num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size) ) )
#include "lwip/memp_std.h"
];
上面的代碼定義了緩衝池所使用的內存緩衝區,很多人肯定會懷疑這到底是不是一個數組的定義。定義一個數組,裏面居然還有define
和include
關鍵字。解決問題的關鍵就在於頭文件memp_std.h
,它裏面的東西可以被簡化爲諸多條LWIP_MEMPOOL(name,num,size,desc)
。又由於用了define
關鍵字將LWIP_MEMPOOL (name,num,size,desc)
定義爲+((num) * (MEMP_SIZE + MEMP_ALIGN_SIZE(size)))
,所以,memp_std.h
被編譯後就爲一條一條的+(),+(),+(),+()….
所以最終的數組memp_memory
等價定義爲:
static u8_t memp_memory [ MEM_ALIGNMENT – 1
+()
+()….];
當然還有個小小的遺留問題,爲什麼數組要比實際需要的大MEM_ALIGNMENT – 1
?作者考慮的是編譯器的字對齊問題。
複製上面的數組建立的方法,協議棧還建立了一些與緩衝池管理的全局變量:
memp_num
:這個靜態數組用於保存各種類型緩衝池的成員數目
memp_sizes
:這個靜態數組用於保存各種類型緩衝池的結構大小
memp_tab
:這個指針數組用於指向各種類型緩衝池當前空閒節點
接下來就是理所當然的實現函數了:
memp_init()
:內存池的初始化,主要是爲每種內存池建立鏈表memp_tab
,其鏈表是逆序的,此外,如果有統計功能使能的話,也把記錄了各種內存池的數目。
memp_malloc()
:如果相應的memp_tab
鏈表還有空閒的節點,則從中切出一個節點返回,否則返回空。
memp_free()
:把釋放的節點添加到相應的鏈表memp_tab
頭上。
從上面的三個函數可以看出,動態內存池分配過程時相當的間接直觀啊。