前言:
前一章我們讨論了記憶體池的配置設定和回收的一些内幕,這一節我們将來讨論一下lwip的mem記憶體堆機制,那有的人就很好奇,既然有了記憶體池的管理機制了,為什麼還要多此一舉搞個記憶體堆管理呢?二者有什麼差別,又或者各有什麼優缺點呢? 這些疑惑将在這一節揭曉。
1、memp相關宏以及變量的解釋
【1】宏定義解釋
1、MEM_USE_POOLS //使用記憶體池配置設定記憶體堆
2、MEM_LIBC_MALLOC //使用标準c函數庫配置設定
3、MIN_SIZE //最小記憶體池大小
4、LWIP_RAM_HEAP_POINTER //定義的記憶體池的頭部
6、MEM_USE_POOLS_TRY_BIGGER_POOL //如果目前記憶體池枯竭,将嘗試其他大的記憶體池
【2】資料結構
struct memp_malloc_helper
{
memp_t poolnr;
}; //當使用MEM_USE_POOLS時,mem記憶體用于區分哪一類型記憶體池
struct mem {
mem_size_t next;
mem_size_t prev;
u8_t used;
}; //當不使用MEM_USE_POOLS是記憶體管理結構
以上就是記憶體堆管理最重要的兩個基本結構單元
【3】變量
u8_t ram_heap[MEM_SIZE_ALIGNED + (2*SIZEOF_STRUCT_MEM) + MEM_ALIGNMENT]; //實際定義的實體記憶體堆
static u8_t *ram; //始終指向記憶體堆的首部
static struct mem *ram_end; //始終指向記憶體堆的尾部
static struct mem *lfree; //始終指向記憶體堆中最低空閑記憶體塊的位址
有人就要問了,就上面幾個變量嗎?你可不要耍我啊,對你沒有看錯,在你眼中如此冗雜的記憶體管理僅僅使用上圖這麼幾個簡答的變量,由此可以看出其編碼的巧妙。這也告訴我們,看起來冗雜的東西不要害怕,maybe隻是虛胖,哈哈哈。。。
2、Mem的記憶體機制原理
這裡,為了讓大家更加直覺的認識,我覺得從上而下開始講解。其實MEM提供幾種配置設定機制。
1、使用使用LWIP自己的記憶體配置設定機制
2、使用系統的庫(malloc和free)來配置設定
而決定上面的配置設定方式是通過MEM_LIBC_MALLOC來實作的,當為1時采用c中的malloc和free來實作的。這裡我們着重分析LWIP自己的記憶體配置設定機制。好了,現在LWIP是通過自己的記憶體配置設定機制來實作記憶體堆了,那麼就結束了嗎??? 不不不,怎麼可能,LWIP在此基礎上又分了兩種構造機制。
- 使用MEMP機制來配置設定記憶體堆
- 使用自定義的實體記憶體來配置設定記憶體堆
由此,才将所謂的記憶體申請動作真正映射到實體記憶體,故一般我将第二個宏的作用定義為決定記憶體的來源。
本着有簡到難的觀點出發,我們首先來看一下USE POOLS。
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAzNfRHLGZkRGZkRfJ3bs92YsYTMfVmepNHL9MGRNlXQ65EeRRVT3V1MMBjVtJWd0ckW65UbM5WOHJWa5kHT20ESjBjUIF2X0hXZ0xCMx81dvRWYoNHLrdEZwZ1Rh5WNXp1bwNjW1ZUba9VZwlHdssmch1mclRXY39CXldWYtlWPzNXZj9mcw1ycz9WL49zZuBnL3IzN1EDOzEjMyEDNwkTMwIzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
上圖為使用記憶體池來配置設定記憶體堆的圖示,由圖我們可以看出,記憶體申請後,lwip在申請的記憶體前加了一個poolnr的結構,這個正是我們前面提到的struct memp_malloc_helper 結構,它專門用來管理使用記憶體池配置設定記憶體堆的。
有的人可能就要問了,為什麼要引入這樣的結構呢?按照我們以往的經驗來說,不應該是配置設定好記憶體,到釋放的時候直接使用這個位址來釋放就好了嗎? 是的,你說的沒錯,但是你忽略了一點,那就是不同的記憶體池的大小不一樣,也就是在釋放的時候必須還要指定具體是哪一個尺寸的記憶體池給你配置設定的記憶體,是以,這個結構主要就是記錄這個資訊。
有的人又要問,那我使用一個外部變量來管理不就可以了,确實如此,但是假象一下,當你申請了100個記憶體塊,此時如果你自己來管理将是多磨的冗雜,并且容易出錯,是以lwip為我們很好的解決了這個問題。
講完了mem使用記憶體池的申請,我們接下來看看記憶體堆自己對申請和釋放是如何管理的。首先來看一下記憶體堆初始的模型
由圖可以看出,在初始化完成,其實記憶體堆被分為了兩個塊,一塊是真正的記憶體卡,及ram_heap和ram_end之間的記憶體,另一個是ram_end之後的記憶體。至于為什麼其後面存在多餘的空閑的記憶體,主要是考慮的記憶體對齊的開銷,是以增加了适量的記憶體。
上圖所示為mem在初始化完成申請的第一個記憶體,可以看出Ifree會随着配置設定的進行指向最低空閑記憶體塊,同是由struct mem結構來連接配接新形成的記憶體塊,使其形成一個鍊式的結構,使用used域來辨別該記憶體塊是否使用。
上圖表示申請了n次後的一個記憶體狀況,可以看出,經過n次的記憶體申請和釋放後,記憶體趨于分散化,此時将形成記憶體碎片,而不是像memp一樣0碎片化。而對于如何優化和處理碎片也是衆多記憶體配置設定和釋放算法的一個重要的差別。對于這個小的碎片,我們需要在适時對其進行合并,以達到一些大的記憶體申請需要。Lwip也提供了這樣的機制,我們将在源碼分析中對其進行講解。我們可以通過下圖加以了解。
由于圖檔太大,貌似截圖都不太清晰,文章後邊将附上這些圖的下載下傳連結供讀者自行下載下傳。
3、Mem的源碼分析
【1】記憶體池的初始化
這裡需要注意一點,對于記憶體池配置設定方式而言,是不需要mem_init()的初始化的,原因在于mem_init主要是為記憶體堆準備起始環境,而如果使用記憶體池配置設定不需要該環境,而是需要初始化memp_init()記憶體池環境。
void
mem_init(void)
{
struct mem *mem;
LWIP_ASSERT("Sanity check alignment",
(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0);
ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);
mem = (struct mem *)(void *)ram;
mem->next = MEM_SIZE_ALIGNED;
mem->prev = 0;
mem->used = 0;
ram_end = (struct mem *)(void *)&ram[MEM_SIZE_ALIGNED];
ram_end->used = 1;
ram_end->next = MEM_SIZE_ALIGNED;
ram_end->prev = MEM_SIZE_ALIGNED;
lfree = (struct mem *)(void *)ram;
MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);
if(sys_mutex_new(&mem_mutex) != ERR_OK) {
LWIP_ASSERT("failed to create mem_mutex", 0);
}
}
由上面可以看出,記憶體堆初始化主要做了這幾件事。
1、初始化記憶體管理的基本結構,即mem結構。
2、初始化尾部ram_end及Ifree。
【2】記憶體池的配置設定
1、使用記憶體池配置設定
void *
mem_malloc(mem_size_t size)
{
void *ret;
struct memp_malloc_helper *element;
memp_t poolnr;
mem_size_t required_size = size + LWIP_MEM_ALIGN_SIZE(sizeof(struct memp_malloc_helper)); //計算實際需要的記憶體
for (poolnr = MEMP_POOL_FIRST; poolnr <= MEMP_POOL_LAST; poolnr = (memp_t)(poolnr + 1)) {
#if MEM_USE_POOLS_TRY_BIGGER_POOL
again:
#endif
if (required_size <= memp_sizes[poolnr]) {
break; //适配到需要的大小相當的記憶體池
}
}
if (poolnr > MEMP_POOL_LAST) {
LWIP_ASSERT("mem_malloc(): no pool is that big!", 0);
return NULL;
}
element = (struct memp_malloc_helper*)memp_malloc(poolnr); //從該記憶體池的連結清單上進行記憶體申請,但是此時不一樣連結清單上存在記憶體
if (element == NULL) {
#if MEM_USE_POOLS_TRY_BIGGER_POOL //如果定義了 可以查找其他的記憶體池鍊
if (poolnr < MEMP_POOL_LAST) { //周遊所有的記憶體池
poolnr++;
goto again;
}
#endif
return NULL; //記憶體枯竭 申請失敗
}
element->poolnr = poolnr;
ret = (u8_t*)element + LWIP_MEM_ALIGN_SIZE(sizeof(struct memp_malloc_helper));
return ret;
}
記憶體池的配置設定方式高效并且不會産生碎片,這在memp記憶體池這一章已經講過了,mem的主要優點在于可以對不同記憶體大小的配置設定更加的靈活。以上的配置設定的政策主要是:
- 查找目前記憶體是否存在大于需求的記憶體池。存在将傳回記憶體池的label
- 通過label查找該記憶體池的空閑塊看是否存在空閑記憶體,不存在将配置設定失敗。
當然,如果 使能了MEM_USE_POOLS_TRY_BIGGER_POOL宏的話,将再次搜尋下一個大于需求記憶體的label,直到找到或者全周遊。
2、使用記憶體堆配置設定
mem_malloc(mem_size_t size)
{
mem_size_t ptr, ptr2;
struct mem *mem, *mem2;
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
u8_t local_mem_free_count = 0;
#endif
LWIP_MEM_ALLOC_DECL_PROTECT();
if (size == 0) {
return NULL;
}
size = LWIP_MEM_ALIGN_SIZE(size);
if(size < MIN_SIZE_ALIGNED) {
size = MIN_SIZE_ALIGNED; //保證最小的記憶體空間,避免過小的記憶體碎片化
}
if (size > MEM_SIZE_ALIGNED) {
return NULL;
}
sys_mutex_lock(&mem_mutex);
LWIP_MEM_ALLOC_PROTECT();
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
do {
local_mem_free_count = 0;
#endif
//搜尋空閑記憶體
for (ptr = (mem_size_t)((u8_t *)lfree - ram); ptr < MEM_SIZE_ALIGNED - size; ptr = ((struct mem *)(void *)&ram[ptr])->next) {
mem = (struct mem *)(void *)&ram[ptr]; //擷取記憶體塊首位址
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_free_count = 0;
LWIP_MEM_ALLOC_UNPROTECT();
LWIP_MEM_ALLOC_PROTECT();
if (mem_free_count != 0) {
local_mem_free_count = 1;
break;
}
#endif
//查找空閑記憶體是否大于需求的記憶體
if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) {
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) { //檢視記憶體後邊的空閑記憶體是否可以繼續維持一個空閑記憶體塊
ptr2 = ptr + SIZEOF_STRUCT_MEM + size; //配置設定記憶體後邊的記憶體位址
mem2 = (struct mem *)(void *)&ram[ptr2]; //後邊記憶體形成一個新的記憶體塊
mem2->used = 0;
mem2->next = mem->next;
mem2->prev = ptr;
mem->next = ptr2;
mem->used = 1;
if (mem2->next != MEM_SIZE_ALIGNED) { //不是尾部結束塊需要将其指向前面的記憶體塊,形成雙向連結清單
((struct mem *)(void *)&ram[mem2->next])->prev = ptr2;
}
MEM_STATS_INC_USED(used, (size + SIZEOF_STRUCT_MEM));
}
else { //後邊記憶體過小,出現碎片化
mem->used = 1;
MEM_STATS_INC_USED(used, mem->next - (mem_size_t)((u8_t *)mem - ram));
}
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_malloc_adjust_lfree:
#endif
if (mem == lfree) { //Ifree更新到新的空閑記憶體位址
struct mem *cur = lfree;
while (cur->used && cur != ram_end) {
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_free_count = 0;
LWIP_MEM_ALLOC_UNPROTECT();
LWIP_MEM_ALLOC_PROTECT();
if (mem_free_count != 0) {
goto mem_malloc_adjust_lfree;
}
#endif
cur = (struct mem *)(void *)&ram[cur->next];
}
lfree = cur;
LWIP_ASSERT("mem_malloc: !lfree->used", ((lfree == ram_end) || (!lfree->used)));
}
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex);
LWIP_ASSERT("mem_malloc: allocated memory not above ram_end.",
(mem_ptr_t)mem + SIZEOF_STRUCT_MEM + size <= (mem_ptr_t)ram_end);
LWIP_ASSERT("mem_malloc: allocated memory properly aligned.",
((mem_ptr_t)mem + SIZEOF_STRUCT_MEM) % MEM_ALIGNMENT == 0);
LWIP_ASSERT("mem_malloc: sanity check alignment",
(((mem_ptr_t)mem) & (MEM_ALIGNMENT-1)) == 0);
return (u8_t *)mem + SIZEOF_STRUCT_MEM; //傳回申請的記憶體位址
}
}
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
} while(local_mem_free_count != 0);
#endif
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS, ("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
MEM_STATS_INC(err);
LWIP_MEM_ALLOC_UNPROTECT();
sys_mutex_unlock(&mem_mutex);
return NULL; //申請失敗
}
由上邊的源碼分析可知,記憶體堆的配置設定隻要是通過struct mem結構來形成一個雙向的連結清單進行管理的,而管理整個記憶體鍊的過程中,如何區分已使用的記憶體和空閑的記憶體塊呢? 于是引入used字段,是以,在這個記憶體鍊中,used字段将至關重要。還有一點大家需要明白,mem配置設定和memp配置設定有一個較大的差異,memp配置設定會将以使用的記憶體從空閑記憶體鍊中去除,而mem中,整個記憶體是一個完整的記憶體鍊,是以即使記憶體已經配置設定出去還是存在于系統的記憶體堆鍊中。
【3】記憶體的釋放
記憶體的申請存在兩種方式,那麼必然對應着兩種釋放方式。
1、記憶體池配置設定方式的釋放
void
mem_free(void *rmem)
{
struct memp_malloc_helper *hmem;
LWIP_ASSERT("rmem != NULL", (rmem != NULL));
LWIP_ASSERT("rmem == MEM_ALIGN(rmem)", (rmem == LWIP_MEM_ALIGN(rmem)));
hmem = (struct memp_malloc_helper*)(void*)((u8_t*)rmem - LWIP_MEM_ALIGN_SIZE(sizeof(struct memp_malloc_helper))); //找到配置設定的真正首位址
LWIP_ASSERT("hmem != NULL", (hmem != NULL));
LWIP_ASSERT("hmem == MEM_ALIGN(hmem)", (hmem == LWIP_MEM_ALIGN(hmem)));
LWIP_ASSERT("hmem->poolnr < MEMP_MAX", (hmem->poolnr < MEMP_MAX));
memp_free(hmem->poolnr, hmem); //通過struct memp_malloc_helper結構得到pool類型,hmem得到首位址,進而進行釋放
}
其實,對于記憶體池的申請比較簡單,這裡就不講了,讀者可以通過我們的上一篇關于memp的文章來學習,這裡帶過。
2、記憶體堆配置設定方式的釋放
void
mem_free(void *rmem)
{
struct mem *mem;
LWIP_MEM_FREE_DECL_PROTECT();
if (rmem == NULL) { //合法性檢查
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_TRACE | LWIP_DBG_LEVEL_SERIOUS, ("mem_free(p == NULL) was called.\n"));
return;
}
LWIP_ASSERT("mem_free: sanity check alignment", (((mem_ptr_t)rmem) & (MEM_ALIGNMENT-1)) == 0);
LWIP_ASSERT("mem_free: legal memory", (u8_t *)rmem >= (u8_t *)ram &&
(u8_t *)rmem < (u8_t *)ram_end);
//合法性檢查
if ((u8_t *)rmem < (u8_t *)ram || (u8_t *)rmem >= (u8_t *)ram_end) {
SYS_ARCH_DECL_PROTECT(lev);
LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE, ("mem_free: illegal memory\n"));
SYS_ARCH_PROTECT(lev);
MEM_STATS_INC(illegal);
SYS_ARCH_UNPROTECT(lev);
return;
}
LWIP_MEM_FREE_PROTECT();
mem = (struct mem *)(void *)((u8_t *)rmem - SIZEOF_STRUCT_MEM); //擷取真實的記憶體首位址
LWIP_ASSERT("mem_free: mem->used", mem->used);
mem->used = 0; //釋放記憶體
if (mem < lfree) { //ifree指針的更新
lfree = mem;
}
MEM_STATS_DEC_USED(used, mem->next - (mem_size_t)(((u8_t *)mem - ram)));
plug_holes(mem); //記憶體合并
#if LWIP_ALLOW_MEM_FREE_FROM_OTHER_CONTEXT
mem_free_count = 1;
#endif
LWIP_MEM_FREE_UNPROTECT();
}
由上可以,記憶體堆的釋放同樣比較簡單,主要是兩個工作。
- 釋放記憶體,及将used字段清0。
- 檢查釋放記憶體的前後,如果存在空閑記憶體将進行記憶體的合并。
當然,mem中還存在其他的一些功能函數,如void *mem_trim(void *rmem, mem_size_t newsize),進行記憶體的壓縮等等。讀者可根據興趣自行閱讀和分析。
最後在指出一點,就是當系統使用了預設的c配置設定函數,如malloc和free是,将存在如下的記憶體配置設定函數。
void *mem_calloc(mem_size_t count, mem_size_t size)
{
void *p;
p = mem_malloc(count * size);
if (p) {
memset(p, 0, count * size);
}
return p;
}
其同樣完成兩個工作。
- 記憶體的配置設定
- 對配置設定的記憶體進行清0操作,maybe這樣的清0操作将使得資料更安全。
#ifndef mem_free
#define mem_free free
#endif
#ifndef mem_malloc
#define mem_malloc malloc
#endif
#ifndef mem_calloc
#define mem_calloc calloc
#endif
這是使用c系統函數配置設定的聲明和定義,這裡我就不一一講解了,讀者可以自行在mem.h函數中查找,這也就是子產品化程式設計的最大優點,子產品間互相區裡,耦合性較低。
關于mem記憶體堆的相關問題的分析就到這裡