LwIP 之五 详解动态内存管理 内存堆(mem.c/h)
寫在前面
??目前網上有很多介紹LwIP內存的文章,但是絕大多數都不夠詳細,甚至很多介紹都是錯誤的!無論是代碼的說明還是給出的圖例,都欠佳!下面就從源代碼,到圖例詳細進行說明。
??目前,網絡上多數文章所使用的LwIP版本為1.4.1。最新版本為2.0.3。從1.4.1到2.0.3(貌似從2.0.0開始),LwIP的源碼有了一定的變化,甚至于源碼的文件結構也不一樣,內部的一些實現源文件也被更新和替換了。
簡介
??對于嵌入式開發來說,內存管理及使用是至關重要的,內存的使用多少、內存泄漏等時刻需要注意!合理的內存管理策略將從根本上決定內存分配和回收效率,最終決定系統的整體性能。LwIP為了能夠靈活的使用內存,為使用者提供兩種簡單卻又高效的動態內存管理機制:***動態內存堆管理(heap)、動態內存池管理(pool)***。這兩中內存管理策略的實現分別對應著源碼文件mem.c/h和memp.c/h。
??其中,***動態內存池管理(heap)***又可以分為兩種: C 運行時庫自帶的內存分配策略、LwIP自己實現的內存堆分配策略。這兩者的選擇需要通過宏值MEM_LIBC_MALLOC來選擇,且二者只能選擇其一。
??其次,LwIP在自己的內存堆和內存池具體實現上也比較靈活。內存池可有由內存堆實現,反之,內存堆也可以有內存池實現。通過宏值MEM_USE_POOLS和MEMP_MEM_MALLOC來選擇,且二者只能選擇其一。
內存堆和內存池
??動態內存堆分配策略原理就是在一個事先定義好大小的內存塊中進行管理,其內存分配的策略是采用最快合適( First Fit)方式,只要找到一個比所請求的內存大的空閑塊,就從中切割出合適的塊,并把剩余的部分返回到動態內存堆中。內存的釋放時,重新將申請到的內存返回堆中。
??其優點就是內存浪費小,比較簡單,適合用于小內存的管理,其缺點就是如果頻繁的動態分配和釋放,可能會造成嚴重的內存碎片,如果在碎片情況嚴重的話,可能會導致內存分配不成功。
這其中也有個問題,就是內存合并問題。因為內存堆的管理通常為鏈表的形式進行管理。可選擇將小的鏈表節點(較小的內存)進行合并。
??內存池的特點是預先開辟多組固定大小的內存塊組織成鏈表,實現簡單,分配和回收速度快,不會產生內存碎片,但是大小固定,并且需要預估算準確。
內存對齊
??一般來說,每一種處理器都會有自己的內存對齊要求,這樣做的目的很大程度上是為了處理器讀取內存數據的效率,且與對應硬件上的設計也有很大的關系。LwIP中,對于內存的操作函數都用到了內存對齊。
??在LwIP中,用戶需要處理一個重要的部分sys_arch。具體可以參見另一片博文。整個arch框架中,就包含了內存對其這一塊的配置:
/** Allocates a memory buffer of specified size that is of sufficient size to align* its start address using LWIP_MEM_ALIGN.* You can declare your own version here e.g. to enforce alignment without adding* trailing padding bytes (see LWIP_MEM_ALIGN_BUFFER) or your own section placement* requirements.\n* e.g. if you use gcc and need 32 bit alignment:\n* \#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[size] \_\_attribute\_\_((aligned(4)))\n* or more portable:\n* \#define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u32_t variable_name[(size + sizeof(u32_t) - 1) / sizeof(u32_t)]*//* 定義全局數組作為內存堆的內存,LwIP就是實現的如何管理這塊內存的。這塊內存時經過對其操作的 */ #ifndef LWIP_DECLARE_MEMORY_ALIGNED #define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)] #endif/** Calculate memory size for an aligned buffer - returns the next highest* multiple of MEM_ALIGNMENT (e.g. LWIP_MEM_ALIGN_SIZE(3) and* LWIP_MEM_ALIGN_SIZE(4) will both yield 4 for MEM_ALIGNMENT == 4).*//* 數據占用空間大小對齊計算 */ #ifndef LWIP_MEM_ALIGN_SIZE #define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U)) #endif/** Calculate safe memory size for an aligned buffer when using an unaligned* type as storage. This includes a safety-margin on (MEM_ALIGNMENT - 1) at the* start (e.g. if buffer is u8_t[] and actual data will be u32_t*)*/ #ifndef LWIP_MEM_ALIGN_BUFFER #define LWIP_MEM_ALIGN_BUFFER(size) (((size) + MEM_ALIGNMENT - 1U)) #endif/** Align a memory pointer to the alignment defined by MEM_ALIGNMENT* so that ADDR % MEM_ALIGNMENT == 0*//* 數據起始地址對齊 */ #ifndef LWIP_MEM_ALIGN #define LWIP_MEM_ALIGN(addr) ((void *)(((mem_ptr_t)(addr) + MEM_ALIGNMENT - 1) & ~(mem_ptr_t)(MEM_ALIGNMENT-1))) #endif為什么要對齊
??這個其實和各種處理器的硬件設計息息相關,具體可以參見該博文《為什么要內存對齊》。
對齊的本質
??首先,這里所說的對其是指 2k2^k2k 字節的對其(其中k取0,1,2,3…正整數)。如果你糾結于什么3字節的對齊等。這里不適用! 在計算機中,所有的數據都是二進制!所謂對齊,就是將一串二進制的最后N位抹成 0。具體幾位呢?這就是根據自己要對齊的字節數來定了。1字節對齊時N == 0;2 字節對齊時N == 1,4 字節對齊時N == 2;以此類推。對齊字節數是根據硬件平臺來的,一般不會隨便來。
??再一點就是,對齊的抹 0,是需要向上取整的。為什么要向上取整呢?如果網下取整,那么,實際返回的大小就比用戶實際需要的要小了。這可就麻煩了,甚至由于不滿足自己需要的大小,申請的這塊內存都沒法使用!
關于任意自己對齊,可以參考一下文章:
??1. 實現任意字節對齊的內存分配和釋放
??2. 任意字節對齊的內存分配和釋放
LWIP_MEM_ALIGN_SIZE(size)
??這個宏的作用就是將指定的大小處理成對其后大小,對其的大小由用戶提供宏值MEM_ALIGNMENT來決定。其中size為想要分配的大小。下面來看看這個宏:
- ~(MEM_ALIGNMENT-1U): 這一步就是按照對應的對齊字節數據,將二進制的最后最后幾位置為 0。例如,MEM_ALIGNMENT為4 ,則該步就將后2位置為了 0。自己可以轉為二進制計算一下!
- ((size) + MEM_ALIGNMENT - 1U): 這里其實就是為了在處理時,能夠向上取整。
??下面是針對其中不同值得計算結果(MEM_ALIGNMENT表示要對其的字節,size 為想要的大小,ALIG表示對齊后實際的大小)
MEM_ALIGNMENT = 1 MEM_ALIGNMENT = 2 MEM_ALIGNMENT = 4 MEM_ALIGNMENT = 8 size = 1 ALIG = 1 size = 1 ALIG = 2 size = 1 ALIG = 4 size = 1 ALIG = 8 size = 2 ALIG = 2 size = 2 ALIG = 2 size = 2 ALIG = 4 size = 2 ALIG = 8 size = 3 ALIG = 3 size = 3 ALIG = 4 size = 3 ALIG = 4 size = 3 ALIG = 8 size = 4 ALIG = 4 size = 4 ALIG = 4 size = 4 ALIG = 4 size = 4 ALIG = 8 size = 5 ALIG = 5 size = 5 ALIG = 6 size = 5 ALIG = 8 size = 5 ALIG = 8 size = 6 ALIG = 6 size = 6 ALIG = 6 size = 6 ALIG = 8 size = 6 ALIG = 8 size = 7 ALIG = 7 size = 7 ALIG = 8 size = 7 ALIG = 8 size = 7 ALIG = 8 size = 8 ALIG = 8 size = 8 ALIG = 8 size = 8 ALIG = 8 size = 8 ALIG = 8 size = 9 ALIG = 9 size = 9 ALIG = 10 size = 9 ALIG = 12 size = 9 ALIG = 16 size = 10 ALIG = 10 size = 10 ALIG = 10 size = 10 ALIG = 12 size = 10 ALIG = 16 size = 11 ALIG = 11 size = 11 ALIG = 12 size = 11 ALIG = 12 size = 11 ALIG = 16 size = 12 ALIG = 12 size = 12 ALIG = 12 size = 12 ALIG = 12 size = 12 ALIG = 16 size = 13 ALIG = 13 size = 13 ALIG = 14 size = 13 ALIG = 16 size = 13 ALIG = 16 size = 14 ALIG = 14 size = 14 ALIG = 14 size = 14 ALIG = 16 size = 14 ALIG = 16 size = 15 ALIG = 15 size = 15 ALIG = 16 size = 15 ALIG = 16 size = 15 ALIG = 16 size = 16 ALIG = 16 size = 16 ALIG = 16 size = 16 ALIG = 16 size = 16 ALIG = 16 size = 17 ALIG = 17 size = 17 ALIG = 18 size = 17 ALIG = 20 size = 17 ALIG = 24 size = 18 ALIG = 18 size = 18 ALIG = 18 size = 18 ALIG = 20 size = 18 ALIG = 24 size = 19 ALIG = 19 size = 19 ALIG = 20 size = 19 ALIG = 20 size = 19 ALIG = 24 size = 20 ALIG = 20 size = 20 ALIG = 20 size = 20 ALIG = 20 size = 20 ALIG = 24LWIP_MEM_ALIGN(addr)
??這個宏用來處理數據起始地址對齊。其處理方式與上面的數據大小的處理沒有任何區別。關于數據類型在arch.h的開頭部分有定義,這里有個文件stdint.h需要注意一下!在某寫平臺,可能沒有該文件,需要用戶自己來添加。
#if !LWIP_NO_STDINT_H #include <stdint.h> typedef uint8_t u8_t; typedef int8_t s8_t; typedef uint16_t u16_t; typedef int16_t s16_t; typedef uint32_t u32_t; typedef int32_t s32_t; typedef uintptr_t mem_ptr_t; /* 這個通常為一個 unsigned int 類型 */ #endifLwIP中宏配置及使用
??LwIP中,內存的選擇是通過以下這幾個宏值來決定的,根據用戶對宏值的定義值來判斷使用那種內存管理策略,具體如下:
- MEM_LIBC_MALLOC: 該宏值定義是否使用C 運行時庫自帶的內存分配策略。該值默認情況下為0,表示不使用C 運行時庫自帶的內存分配策略。即默認使用LwIP提供的內存堆分配策略。
??如果要使用C運行時庫自帶的分配策略,則需要把該值定義為 1。此時,宏值MEM_USE_POOLS必然不能為 1。 - MEMP_MEM_MALLOC: 該宏值定義是否使用lwip內存堆分配策略實現內存池分配(即:要從內存池中獲取內存時,實際是從內存堆中分配)。默認情況下為 0,表示不從內存堆中分配,內存池為獨立一塊內存實現。與MEM_USE_POOLS只能選擇其一。
- MEM_USE_POOLS:該宏值定時是否使用lwip內存池分配策略實現內存堆的分配(即:要從內存堆中獲取內存時,實際是從內存池中分配)。默認情況下為 0,表示不使用。與MEMP_MEM_MALLOC只能選擇其一。
??要使用內存池的方式,則需要將該宏值定義為 1,且MEMP_MEM_MALLOC必須為 0,除此之外還需要做一下處理:
??根據宏值的不同,mem.c/h會部分有效,下面簡單看看mem.c文件的結構:
#if MEM_LIBC_MALLOC /* 這里表示使用C庫時的部分 */ #elif MEM_USE_POOLS /* 這里表示使用內存池方式實現的內存堆函數 */ #else /* 使用內存堆實現分配函數 */ #endif而對于memp.c/h來說,就沒這么復雜了。因為該文件就是獨立實現內存池的管理策略的,無論宏值怎么配置,里面的函數就是那些。唯一有影響的宏值是MEMP_MEM_MALLOC后面會說明。
??總結來說,無論宏值怎么配置,LwIP都有兩種內存管理策略:內存堆和內存池:
- 內存堆: 可以來自C庫,也可以使用LwIP自己實現的。
- 內存池: 可以單獨實現,也可以從內存堆中分配實現。
??上面說了如何進行配置,那么在LwIP內部是如何做到兼容以上的靈活配置的呢?其實很簡單,LwIP內部全部使用統一的內存分配函數,只是在不同模式下,對相應的函數進行了重新定義,具體函數如下:
- void mem_init(void):內存堆初始化函數,主要設置內存堆的起始地址,以及初始化空閑列表,lwip初始化時調用,內部接口。配置不同時,其實現也不同(可能為空)
- void *mem_trim(void *mem, mem_size_t size): 減小mem_malloc所分配的內存。mem為申請到的內存的地址,size為新的大小。
與標準C函數realloc不同,該函數只能縮減分配內存
- void *mem_malloc(mem_size_t size):申請分配內存,size為需要申請的內存字節數,返回值為最新分配的內存塊的數據地址。
- void *mem_calloc(mem_size_t count, mem_size_t size):是對mem_malloc()函數的簡單包裝,兩個入口參數,count為元素的總個數,size為每個元素大小,兩個參數的乘積就是實際要分配的內存空間的大小。與mem_malloc()不同的是它會把動態分配的內存清零。
- void mem_free(void *mem):內存釋放函數,mem前面申請到的內存時分配得地址。
這樣,無論用戶選擇了什么配置方式,對于LwIP內部來說,函數全都是一個樣的!下面,詳細說明每種方式下,以上函數是怎么實現的。
C庫分配方式
??上面已經說了要使用該方式如何進行配置,下面結合源碼看看,如果用戶選擇了該方式,LwIP內部是如何處理的。該部分對應的源碼文件為mem.c。
??首先,如果用戶選擇了該方式,那么內存處理函數void mem_init(void)和void* mem_trim(void *mem, mem_size_t size)將沒有實際的實現內容,既然選擇了C庫策略,此時也必然沒法實現。
??接下來我們將看到如下代碼:
這里將C庫的標準內存處理函數進行了重定義,沒啥太大作用,就是為了后面使用的方便。下面以void * mem_malloc(mem_size_t size)為例看看以上宏值的用法(其他函數類似,注意:有幾個函數在文件的最末尾統一處理了):
void * mem_malloc(mem_size_t size) {/* 這里就是調用的malloc有木有! */void* ret = mem_clib_malloc(size + MEM_LIBC_STATSHELPER_SIZE);if (ret == NULL) { /* 分配失敗 */MEM_STATS_INC(err);} else { /* 需要重點注意的就是,內存的對其問題,下面就是處理對其的。然后直接返回申請到的內存地址。 */LWIP_ASSERT("malloc() must return aligned memory", LWIP_MEM_ALIGN(ret) == ret); #if LWIP_STATS && MEM_STATS*(mem_size_t*)ret = size;ret = (u8_t*)ret + MEM_LIBC_STATSHELPER_SIZE;MEM_STATS_INC_USED(used, size); #endif}return ret; }??最后,使用C庫內存處理策略時,LwIP的處理就是這么簡單,也沒啥需要特殊注意的地方!如果說非得注意點啥,就是一旦選擇了該種模式,內存池的配置問題。
LwIP內存堆
??上面已經說了要使用該方式如何進行配置,下面結合源碼看看,如果用戶選擇了該方式,LwIP內部是如何處理的。該部分對應的源碼文件為mem.c。
用內存池實現
??首先,LwIP會判斷用戶是否定義了宏值MEM_USE_POOLS,如果定義了該宏值,表示內存堆的內存來自于內存池,需要調用內存池的相關函數來處理內存。
??既然是調用內存堆中的各函數,那么這邊的實現也相對簡單。這與上面的調用C庫的各函數沒啥太大區別,至少在處理邏輯上是一致的,當然,由于內存池的實現上的原因,該部分看著還是挺復雜的!下面仍舊以函數void *mem_malloc(mem_size_t size)來看看LwIP是如何處理的。
??該部分許多地方需要看看后面的內存池。再次不做過多深入說明。
獨立實現內存堆
??如果沒有定義宏值MEM_USE_POOLS,那么LwIP提供了一套獨立實現的內存堆處理接口。
??內存堆的本質是對一個事先定義好的內存塊進行合理有效的組織和管理。主要用于任意大小的內存分配,實現較復雜,分配需要查找,回收需要合并,容易產生內存碎片,需要合理估算內存堆的總大小。LwIP使用類似于鏈表的結構來組織管理堆內存。節點的結構如下(在該部分的源碼實現中,第一部分的代碼便是以下結構體):
??LwIP的內存堆實現中,分配的內存塊有個最小大小的限制,要求請求的分配大小不能小于 MIN_SIZE,默認 MIN_SIZE為 12 字節。所以在該部分實現中,我看到的第二部分便是如下結構(第一部分是鏈表結構體):
/** All allocated blocks will be MIN_SIZE bytes big, at least!* MIN_SIZE can be overridden to suit your needs. Smaller values save space,* larger values could prevent too small blocks to fragment the RAM too much. */ #ifndef MIN_SIZE #define MIN_SIZE 12 #endif /* MIN_SIZE */ /* some alignment macros: we define them here for better source code layout */ #define MIN_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MIN_SIZE) /* 最小大小做對齊處理,后面均用對齊后的該宏值 */ #define SIZEOF_STRUCT_MEM LWIP_MEM_ALIGN_SIZE(sizeof(struct mem)) /* 內存塊頭大小做對齊處理,后面均用對齊后的該宏值 */ #define MEM_SIZE_ALIGNED LWIP_MEM_ALIGN_SIZE(MEM_SIZE) /* 用戶定義的堆大小做對齊處理,后面均用對齊后的該宏值 */這就有一個問題,為什么默認值是12呢?因為這正好是2個sizeof(struct mem)的長度。為什么是2個sizeof(struct mem)。后面再說內存堆的定義及初始化時,我們會詳細說明。
??接下來就是使用宏值LWIP_DECLARE_MEMORY_ALIGNED定義內存堆所使用的內存空間的了,這個宏值在上面一節由介紹,具體如下。從下面的定義中,我們可以看到***內存堆空間就是定義的一個名為ram_heap,大小為MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM)的數組***。內存堆管理的具體實現,就是實現如何管理使用這個數組!
注意:這里的大小多加了兩個SIZEOF_STRUCT_MEM
??在定義完內存空間后,接下來就是一些為了管理內存堆,而定義的結構性的全局變量的定義。再后面是一些內存保護相關的宏,這里不具體說明。下面看看,為了管理內存堆,LwIP都定義了那些變量。
/** pointer to the heap (ram_heap): for alignment, ram is now a pointer instead of an array */ static u8_t *ram; /* 指向對齊后的內存堆的地址。由于對齊問題,不能直接使用數組名上面的數組名。*/ /** the last entry, always unused! */ static struct mem *ram_end; /* 指向對齊后的內存堆的最后一個內存塊。*/ /** pointer to the lowest free block, this is used for faster search */ static struct mem *lfree; /* 指向已被釋放的索引號最小的內存塊(內存堆最前面的已被釋放的)。*/下面,就結合各接口函數來說說LwIP對內存堆的管理是如何實現的。先說第一個函數void mem_init(void)。先好看看源碼,具體說明見其中的注釋部分:
void mem_init(void) {struct mem *mem;LWIP_ASSERT("Sanity check alignment",(SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT-1)) == 0);/* align the heap 對內存堆的地址(全局變量的名)進行對齊。*/ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);/* initialize the start of the heap 建立第一個內存塊,內存塊由內存塊頭+空間組成。 */mem = (struct mem *)(void *)ram;mem->next = MEM_SIZE_ALIGNED; /* 下一個內存塊不存在,因此指向內存堆的結束 */mem->prev = 0; /* 前一個內存塊就是它自己,因為這是第一個內存塊 */mem->used = 0; /* 第一個內存塊沒有被使用 *//* initialize the end of the heap 建立最后一個內存塊 */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;/* 前一個不存在,因此指向內存堆的結束 *//* initialize the lowest-free pointer to the start of the heap */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);} }??經過初始化,LwIP將內存堆劃分為了如下格式:
上圖中已經非常詳細的給出了從定義到初始化后,詳細的內存堆的結構!在整個內存堆的開頭和結尾,各有一個內存塊頭。開頭的內存塊頭用來定義第一個內存塊(也就是整個內存堆只有一個內存塊),最后一個內存塊頭用來標識內存塊的結尾。 看圖的時候,結合源碼和上一節的內存對齊部分。因為在上圖中,到處都是對齊!
??接下來,在看看函數void *mem_malloc(mem_size_t size)。仍舊是先看源代碼,具體語句在源碼中都有注釋,對于源碼中,保護部分暫不作說明!
??分配內存的過程,就是從初始化后的內存塊中,劃分新內存塊的過程。新內存塊劃分完成后,將其標記為已使用,并且新建的空內存塊。在劃分過程中,需要處理已經被釋放的內存塊的問題。劃分后如下圖所示:
??首先需要重點注意的就是遍歷查找新內存塊時的起始位置:ptr = (mem_size_t)((u8_t *)lfree - ram)。前面說過,變量lfree永遠指向內存堆中最靠前的那個已經釋放的內存塊。這也就是意味值,每次總是從整個內存堆的開始的第一個空閑內存塊開始找合適的內存塊的,即便后面可能有更大的空間!
??這其中有個需要注意的地方:if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))。這個地方具體是干嘛的呢?看下圖(圖例不考慮對齊問題):
??接下來一個注意的地方就是if (mem2->next != MEM_SIZE_ALIGNED) { 省略...。這個地方又是干什么用的呢?具體看下圖(圖例不考慮對齊問題)
??其實上面兩個問題,就是針對中間已經釋放過的內存塊由重新劃分時的處理。一旦從釋放過的內存塊中劃分出新的內存后,則可能會導致剩余的內存塊有可能充足,也可能不充足,這就是針對以上兩個情況!
??最后再看看內存的釋放函數,和上面相同,仍舊從源代碼說起,具體見注釋。
釋放過程非常簡單,就是將指定的內存塊標記為未使用,其他不做任何處理。唯一處理的一個地方就就是全局變量lfree,必須檢查是否為最前面的內存塊,釋放后如下圖:
內存塊釋放后,其原先建立的連接關系是不會改變的。
??這部分中還有一個函數:static void plug_holes(struct mem *mem),用來對相鄰且未用的內存塊進行合并。該函數暫時未使用,這里就不過多說明了!
LwIP內存池
見后文 LwIP 之 詳解動態內存管理 內存池(memp.c/h)
總結
以上是生活随笔為你收集整理的LwIP 之五 详解动态内存管理 内存堆(mem.c/h)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LwIP 之四 超时处理/定时器(tim
- 下一篇: SourceInsight 4.0 之一