LWIP的內存管理主要三種:內存池Pool,內存堆,和C庫方式。三種方式中C庫因為是直接從系統堆中分配內存空間且易產生碎片因此,基本不會使用,其他兩種是LWIP默認全部采用的方式,也是綜合效率和空間的一種實現方法,接下來將根據源碼看看具體的內存管理方案的實現,其中內存池用的一些技巧,曾經讓我一頭霧水source insight都無法定位一些變量的聲明,不過看明白才明白LWIP作者的厲害之處,接下來先說內存堆的實現。
內存堆
內存堆常見的實現方式是,通過申請一個大的內存空間,作為內存分配釋放的總內存。LWIP也是這樣實現的先定義的一個數組,然后將這塊內存進行管理,看代碼
//實際的內存堆數據聲明 #ifndef LWIP_RAM_HEAP_POINTER LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM)); #define LWIP_RAM_HEAP_POINTER ram_heap #endif //中間的宏的定義 #ifndef LWIP_DECLARE_MEMORY_ALIGNED #define LWIP_DECLARE_MEMORY_ALIGNED(variable_name, size) u8_t variable_name[LWIP_MEM_ALIGN_BUFFER(size)] #endif
其中 MEM_SIZE_ALIGNE SIZEOF_STRUCT_MEM 的定義在mem.c文件中是這樣定義的
/** 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)
這里需要提一下,LWIP中常用的 LWIP_MEM_ALIGN_SIZE 這個宏函數,這個宏定義如下
#ifndef LWIP_MEM_ALIGN_SIZE #define LWIP_MEM_ALIGN_SIZE(size) (((size) + MEM_ALIGNMENT - 1U) & ~(MEM_ALIGNMENT-1U)) #endif
其中MEM_ALIGNMENT就是在配置文件中定義的對齊字節,這里先 加上MEM_ALIGNMENT - 1U,這個操作是為了避免,對齊后地址空間小於size值。
MEM_SIZE_ALIGNED:這個宏是將用戶配置的內存堆總大小進行對齊后的大小 一般 MEM_SIZE_ALIGNED >= MEM_SIZE ,MEM_SIZE一般由用戶配置。
SIZEOF_STRUCT_MEM:這個宏的大小內存控制塊的占用空間的對齊后size大小。
內存控制塊結結構體定義:
struct mem { /** index (-> ram[next]) of the next struct */ mem_size_t next; /** index (-> ram[prev]) of the previous struct */ mem_size_t prev; /** 1: this area is used; 0: this area is unused */ u8_t used; #if MEM_OVERFLOW_CHECK /** this keeps track of the user allocation size for guard checks */ mem_size_t user_size; #endif };
next 保存的並不是地址,而是內存堆對應大數組的下標索引,prev同上,我猜想作者這樣設計應該是為了內存釋放時合並方便。通過最上面的代碼可以看出來定義了一個ram_heap[ MEM_SIZE_ALIGNED + (2U*SIZEOF_STRUCT_MEM)]數組,其中多加了兩個內存塊的空間,一個是為了數組開頭內存控制塊的占用,另一個是為了后面取內存堆對齊地址會丟棄的部分增加的從而保證內存堆的總大小不小於用戶配置的MEM_SIZE。
然后是內存堆的初始化
void mem_init(void) { struct mem *mem; /* 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; if (sys_mutex_new(&mem_mutex) != ERR_OK) { LWIP_ASSERT("failed to create mem_mutex", 0); } }
初始化過的內存堆結構示意圖如下,就是將整個內存堆分成一塊MEM_SIZE_ALIGNED的空閑塊,和永久占用的結束塊。
當進行內存申請時,LWIP采用最先匹配原則,並由lfree記着地址最低的空閑塊地址,其中全局變量ram和ram_end分別記錄着對齊后的堆起始和結束地址。內存分配函數如下,總的來說就是,將申請size進行對齊和合法檢查后,從lfree指定的地方遍歷,如果空閑且夠size和內存控制塊的總占用空間的話就開始處理,這里的處理是因為LWIP采用的是最先匹配原則,所有有可能當前內存比要需要的空間大的多,比如初始化后第一個申請的塊,就會匹配到整個堆大小的塊,因此不能全部拿走使用,LWIP采用的方式是,如果當前塊的大小除去size和控制塊的占用后剩余的部分不小於內存堆申請的最小size就要將多余的部分切除,返還給堆,否則返回一個比目標申請空間大的內存塊,從而避免過小的內存塊。源碼簡化注釋后如下:
void * mem_malloc(mem_size_t size) { mem_size_t ptr, ptr2; struct mem *mem, *mem2; if (size == 0) { return NULL; } /* 對齊操作調整size值,到對齊的倍數 */ size = LWIP_MEM_ALIGN_SIZE(size); if (size < MIN_SIZE_ALIGNED) { /*太小設置size為允許最小值 */ size = MIN_SIZE_ALIGNED; } if (size > MEM_SIZE_ALIGNED) { /*超出總大小,失敗返回空地址*/ return NULL; } /* 互斥 */ sys_mutex_lock(&mem_mutex); /*根據最低空閑內存塊地址,計算空閑地址起始索引值ptr。*/ 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]; /*取ptr地址檢查ptr地址處內存塊是否夠size加上控制塊的總大小。不夠:ptr=ram[ptr]->next返回第四步起始。*/ 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; } } else { /*是,則直接將本內存塊不切割*/ mem->used = 1; } /*檢查分配走的塊的內存地址是否低於當前最低空閑地址lfree*/ if (mem == lfree) { struct mem *cur = lfree; /*遍歷,更新最低空閑塊地址lfree*/ while (cur->used && cur != ram_end) { cur = (struct mem *)(void *)&ram[cur->next]; } lfree = cur; } sys_mutex_unlock(&mem_mutex); /*將申請到的內存偏移后返回,防止內存控制塊被用戶修改*/ return (u8_t *)mem + SIZEOF_STRUCT_MEM; } } sys_mutex_unlock(&mem_mutex); return NULL; }
其中還考慮到有操作系統時的,內存分配函數的原子操作保護。每分配一塊內存,就將內存鏈表重新連起來,方便后續的釋放合並。
內存釋放
這里就可以看出來,內存控制塊next和prev中保存內存塊的數組索引而不是真實地址的用意了,因為通過這樣組織內存,內存塊之間的物理順序就能對應上,從而方便釋放,釋放內存函數很簡單,需要注意的是內存分配函數返回的內存地址不是內存的真實其實地址,而是分配到的內存塊向后偏移了一個內存控制塊的地址,從而避免用戶不小心改動內存控制塊,從而破壞內存控制塊。其中內存合並過程最為巧妙。
void mem_free(void *rmem) { struct mem *mem; if (rmem == NULL) { return; } if ((u8_t *)rmem < (u8_t *)ram || (u8_t *)rmem >= (u8_t *)ram_end) { /*地址溢出,錯誤返回*/ return; } LWIP_MEM_FREE_PROTECT(); /*將地址偏移到內存控制塊起始地址*/ mem = (struct mem *)(void *)((u8_t *)rmem - SIZEOF_STRUCT_MEM); /*標記內存塊空閑*/ mem->used = 0; if (mem < lfree) { /* 如果釋放的內存塊地址,低於lfree更新lfree */ lfree = mem; } /*內存塊合並*/ plug_holes(mem); LWIP_MEM_FREE_UNPROTECT(); } static void plug_holes(struct mem *mem) { struct mem *nmem; struct mem *pmem; /*找出當前塊的下一個塊的地址*/ nmem = (struct mem *)(void *)&ram[mem->next]; if (mem != nmem && nmem->used == 0 && (u8_t *)nmem != (u8_t *)ram_end) { /*下一個塊不是自己,下一個塊空閑,並且不是最后一個塊*/ if (lfree == nmem) { /* 可能需要更新lfree,存疑應該是防止,分系統移植下中斷和后台程序同時執行free函數的情況出現 */ lfree = mem; } /*新釋放的塊的結束索引向后合並*/ mem->next = nmem->next; /*新空閑塊后面空閑塊的后面的空閑塊的前一個塊的索引更新為新塊的起始索引*/ ((struct mem *)(void *)&ram[nmem->next])->prev = (mem_size_t)((u8_t *)mem - ram); } /* 取釋放塊的前一個塊地址pmem */ pmem = (struct mem *)(void *)&ram[mem->prev]; /* pmem不是自己的地址,同時也是空閑的,需要合並 */ if (pmem != mem && pmem->used == 0) { /* 可能需要更新lfree,存疑 */ if (lfree == mem) { lfree = pmem; } /*將前一個塊的next索引更新到,新釋放塊后面的塊的結束索引上,合並三個塊*/ pmem->next = mem->next; /*將釋放塊的后面空閑塊的下一個塊的前一個塊的索引值更新為釋放塊前一個塊的起始地址索引*/ ((struct mem *)(void *)&ram[mem->next])->prev = (mem_size_t)((u8_t *)pmem - ram); } }
內存合並函數,會查看前后塊是否空閑,如果空閑就合並成一個更大的內存塊,因為每次釋放都會進行內存合並,因此不存在釋放塊前兩個塊都空閑的情況,因此只用處理相鄰的塊的合並,不過我看下了覺得內存塊合並中的兩處暫時無法理解,就是更新lfree指針的地方我也注釋了存疑,如果有人明白還望指點下啊。下一篇再探內存池的實現。
2019-06-16 13:19:42