[導讀] 前文描述了棧的基本概念,本文來聊聊堆是怎麼會事兒。RT-Thread 在社區廣受歡迎,閱讀了其內核代碼,實現了堆的管理,代碼設計很清晰,可讀性很好。故一方面瞭解RT-Thread內核實現,一方面能夠弄清楚其堆的內部實現。將學習體會記錄分享,但願對於堆的理解及實現有一個更深刻的認知。程序員
注,文中代碼分析基於rt-thread-v4.0.2 版本。算法
C語言堆是由malloc(),calloc(),realloc()等函數動態獲取內存的一種機制。使用完成後,由程序員調用free()等函數進行釋放。使用時,須要包含stdlib.h頭文件。編程
C++預言的堆管理則是使用new操做符向堆管理器申請動態內存分配,使用delete操做符將使用完畢內存的釋放給堆管理器。windows
注:本文只描述C的堆管理器實現相關內容。數組
以C語言爲例,將上面的描述,翻譯成一個圖:微信
要動態管理一片內存,且須要動態分配釋放,這樣一個需求。很顯然C語言須要將動態內存區抽象描述起來並實現動態管理。事實上,C語言中堆管理器其本質是利用數據結構將堆區抽象描述,所須要描述的方面:數據結構
再利用相應算法對於這類數據結構對象進行動態管理而實現的堆管理器。架構
常常看到各類算法書不少只講算法原理,而不講應用實例,每每體會不深。私覺得能夠作些改善。學而不能致用,何須費力去學。因此不是晦澀難懂的算法無用,而是沒有去真正結合應用。能夠再進一步想,若是算法沒有應用場景,也必定會在技術發展的歷程中逐漸被世人遺忘。因此建議學習閱讀算法書籍時,找些實例來看看,必定會加深對算法的理解領悟。這是比較重要的題外話,送給你們以共勉。編程語言
因此從本質上講,堆管理器就是數據結構+算法實現的動態內存管理器,管理內存的動態分配以及釋放。函數
C編程語言對內存管理方式有靜態,自動或動態三種方式。 靜態內存分配的變量一般與程序的可執行代碼一塊兒分配在主存儲器中,並在程序的整個生命週期內有效。 自動分配內存的變量在棧上分配,並隨着函數的調用和返回而申請或釋放。 對於靜態分配內存和自動分配內存的生命週期,分配的大小必須是編譯時常量(可變長度自動數組[5]除外)。 若是所需的內存大小直到運行時才知道(例如,若是要從用戶或磁盤文件中讀取任意大小的數據),則使用固定大小的數據對象則知足不了要求了。試想,即使假定都知道要多大內存,如在windows/Linux下有那麼多應用程序,每一個應用程序加載時都將運行中所需的內存採樣靜態分配策略,則如多個程序運行內存將很快耗盡。
分配的內存的生命週期也可能引發關注。 靜態或自動分配都不能知足全部狀況。 自動分配內存不能在多個函數調用之間保留,而靜態數據在程序的整個生命週期中必然保留,不管是否真正須要(因此都採用這樣的策略必然形成浪費)。 在許多狀況下,程序員在管理分配的內存的生命週期具備更多的靈活性。
經過使用動態內存分配則避免了這些限制/缺點,在動態內存分配中,更明確(但更靈活)地管理內存,一般是經過從免費存儲區(非正式地稱爲「堆」)中分配內存(爲此目的而構造的內存區域)進行分配的。 在C語言中,庫函數malloc用於在堆上分配一個內存塊。 程序經過malloc返回的指針訪問該內存塊。 當再也不須要內存時,會將指針傳遞給free,從而釋放內存,以即可以將其用於其餘目的。
若是一問道這個問題,立刻會說C編譯器。不錯C編譯器實現了堆管理器,而事實上並不是編譯器在編譯的過程當中實現動態內存管理器,而是C編譯器所實現的C庫實現了堆管理器,好比ANSI C,VC, IAR C編譯器,GNU C等其實都須要一些C庫的支持,那麼這些庫的內部就隱藏了這麼一個堆管理器。眼見爲實吧,仍是以IAR ARM 8.40.1 爲例,其堆管理器就實如今:
.\IAR Systems\Embedded Workbench 8.3\arm\src\lib\dlib\heap
一看有這麼多的源碼,那麼對於應用開發而言,有哪些選項須要進行配置呢?
支持四個選項:
可是若是認爲僅僅標準C庫負責實現堆管理器,則這種理解並不全面。回到事物的本質,堆管理器是利用數據結構及算法動態管理一片內存的分配與釋放。那麼有這樣需求的地方,均可能須要實現一個堆管理器。
堆管理器的實現很大程度取決於操做系統以及硬件體系架構。大致上須要實現堆內存管理器的有兩大類:
對於RT-Thread的內核而言,也實現了一個內核堆管理器,這裏就來梳理一下RT-Thread內核版本的小堆管理器的實現,同時來了解一下鏈表數據結構及算法操做的實例應用。
其堆管理器實現位於.\rt-thread-v4.0.2\rt-thread\src下mem.c,memheap.c以及mempool.c。
其堆管理器主要的數據結構爲heap_mem。
堆管理器的初始化入口在mem.c,函數爲:
void rt_system_heap_init(void *begin_addr, void *end_addr) { struct heap_mem *mem; /*按4字節對齊轉換地址*/ /*如0x2000 0001~0x2000 0003,轉後爲0x2000 0004*/ rt_ubase_t begin_align = RT_ALIGN((rt_ubase_t)begin_addr, RT_ALIGN_SIZE); /*如0x3000 0001~0x3000 0003,轉後爲0x3000 0000*/ rt_ubase_t end_align = RT_ALIGN_DOWN((rt_ubase_t)end_addr, RT_ALIGN_SIZE); /*調試信息,函數不可用於中斷內部*/ RT_DEBUG_NOT_IN_INTERRUPT; /* 分配地址範圍至少能存儲兩個heap_mem */ if ((end_align > (2 * SIZEOF_STRUCT_MEM)) && ((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align)) { /* 計算可用堆區,4字節對齊 */ mem_size_aligned = end_align - begin_align - 2 * SIZEOF_STRUCT_MEM; } else { rt_kprintf("mem init, error begin address 0x%x, and end address 0x%x\n", (rt_ubase_t)begin_addr, (rt_ubase_t)end_addr); return; } /* heap_ptr指向堆區起始地址 */ heap_ptr = (rt_uint8_t *)begin_align; RT_DEBUG_LOG(RT_DEBUG_MEM, ("mem init, heap begin address 0x%x, size %d\n", (rt_ubase_t)heap_ptr, mem_size_aligned)); /* 初始化堆起始描述符 */ mem = (struct heap_mem *)heap_ptr; mem->magic = HEAP_MAGIC; mem->next = mem_size_aligned + SIZEOF_STRUCT_MEM; mem->prev = 0; mem->used = 0; #ifdef RT_USING_MEMTRACE rt_mem_setname(mem, "INIT"); #endif /* 初始化堆結束描述符 */ heap_end = (struct heap_mem *)&heap_ptr[mem->next]; heap_end->magic = HEAP_MAGIC; heap_end->used = 1; heap_end->next = mem_size_aligned + SIZEOF_STRUCT_MEM; heap_end->prev = mem_size_aligned + SIZEOF_STRUCT_MEM; #ifdef RT_USING_MEMTRACE rt_mem_setname(heap_end, "INIT"); #endif rt_sem_init(&heap_sem, "heap", 1, RT_IPC_FLAG_FIFO); /* 初始化釋放指針指向堆的開始 */ lfree = (struct heap_mem *)heap_ptr; }
傳入連接堆區的內存起始地址,以及結束地址。以STM32爲例,傳入0x20000000--0x20018000,96k字節
上述rt_system_heap_init( 0x20000000,0x20018000),主要作了下圖這麼一件事情。
將堆管理頭尾描述符進行了初始化,並指向對應的內存地址。用圖翻譯一下:
技巧點:
mem = (struct heap_mem *)heap_ptr; heap_end = (struct heap_mem *)&heap_ptr[mem->next];
用戶調用rt_malloc 用於申請分配動態內存。
void *rt_malloc(rt_size_t size) { rt_size_t ptr, ptr2; struct heap_mem *mem, *mem2; if (size == 0) return RT_NULL; RT_DEBUG_NOT_IN_INTERRUPT; /*按四字節對齊申請,如申請5字節,則實際按8字節申請*/ if (size != RT_ALIGN(size, RT_ALIGN_SIZE)) RT_DEBUG_LOG(RT_DEBUG_MEM, ("malloc size %d, but align to %d\n", size, RT_ALIGN(size, RT_ALIGN_SIZE))); else RT_DEBUG_LOG(RT_DEBUG_MEM, ("malloc size %d\n", size)); /* 按四字節對齊申請,如申請5字節,則實際按8字節申請 */ size = RT_ALIGN(size, RT_ALIGN_SIZE); if (size > mem_size_aligned) { RT_DEBUG_LOG(RT_DEBUG_MEM, ("no memory\n")); return RT_NULL; } /* 每塊的長度必須至少爲MIN_SIZE_ALIGNED=12 STM32*/ if (size < MIN_SIZE_ALIGNED) size = MIN_SIZE_ALIGNED; /* 獲取堆保護信號量 */ rt_sem_take(&heap_sem, RT_WAITING_FOREVER); for (ptr = (rt_uint8_t *)lfree - heap_ptr; ptr < mem_size_aligned - size; ptr = ((struct heap_mem *)&heap_ptr[ptr])->next) { mem = (struct heap_mem *)&heap_ptr[ptr]; /*若是該塊未使用,且知足大小要求*/ if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size) { /* mem沒有被使用,至少完美的配合是可能的: * mem->next - (ptr + SIZEOF_STRUCT_MEM) 計算出mem的「用戶數據大小」 */ if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED)) { /* (除了上面的,咱們測試另外一個結構heap_mem (SIZEOF_STRUCT_MEM) * 是否包含至少MIN_SIZE_ALIGNED的數據也適合'mem'的'用戶數據空間') * -> 分割大的塊,建立空的餘數, * 餘數必須足夠大,以包含MIN_SIZE_ALIGNED大小數據: * 若是mem->next - (ptr + (2*SIZEOF_STRUCT_MEM)) == size, * struct heap_mem 會適合,在mem2及mem2->next沒有使用 */ ptr2 = ptr + SIZEOF_STRUCT_MEM + size; /* create mem2 struct */ mem2 = (struct heap_mem *)&heap_ptr[ptr2]; mem2->magic = HEAP_MAGIC; mem2->used = 0; mem2->next = mem->next; mem2->prev = ptr; #ifdef RT_USING_MEMTRACE rt_mem_setname(mem2, " "); #endif /*將ptr2插入mem及mem->next之間 */ mem->next = ptr2; mem->used = 1; if (mem2->next != mem_size_aligned + SIZEOF_STRUCT_MEM) { ((struct heap_mem *)&heap_ptr[mem2->next])->prev = ptr2; } #ifdef RT_MEM_STATS used_mem += (size + SIZEOF_STRUCT_MEM); if (max_mem < used_mem) max_mem = used_mem; #endif } else { mem->used = 1; #ifdef RT_MEM_STATS used_mem += mem->next - ((rt_uint8_t *)mem - heap_ptr); if (max_mem < used_mem) max_mem = used_mem; #endif } /* 設置塊幻數 */ mem->magic = HEAP_MAGIC; #ifdef RT_USING_MEMTRACE if (rt_thread_self()) rt_mem_setname(mem, rt_thread_self()->name); else rt_mem_setname(mem, "NONE"); #endif if (mem == lfree) { /* 尋找下一個空閒塊並更新lfree指針*/ while (lfree->used && lfree != heap_end) lfree = (struct heap_mem *)&heap_ptr[lfree->next]; RT_ASSERT(((lfree == heap_end) || (!lfree->used))); } rt_sem_release(&heap_sem); RT_ASSERT((rt_ubase_t)mem + SIZEOF_STRUCT_MEM + size <= (rt_ubase_t)heap_end); RT_ASSERT((rt_ubase_t)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM) % RT_ALIGN_SIZE == 0); RT_ASSERT((((rt_ubase_t)mem) & (RT_ALIGN_SIZE - 1)) == 0); RT_DEBUG_LOG(RT_DEBUG_MEM, ("allocate memory at 0x%x, size: %d\n", (rt_ubase_t)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM), (rt_ubase_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr)))); RT_OBJECT_HOOK_CALL(rt_malloc_hook, (((void *)((rt_uint8_t *)mem + SIZEOF_STRUCT_MEM)), size)); /* 返回除mem結構以外的內存地址 */ return (rt_uint8_t *)mem + SIZEOF_STRUCT_MEM; } } /* 釋放堆保護信號量 */ rt_sem_release(&heap_sem); return RT_NULL; }
其基本思路,從空閒塊鏈表開始檢索內存塊,如檢索到某塊空閒且知足申請大小且其剩餘空間至少能存儲描述符,則知足了申請要求,則將後續內存頭部生成描述,更新先後指針,標記幻數以及塊已被使用標記,將該塊插入鏈表。返回申請成功的內存地址。若是檢索不到,則返回空指針,表示申請失敗,堆目前沒有知足要求的內存可供使用。實際上,上述代碼在運行時將堆內存區按照下述示意圖進行動態維護。
歸納一下:
釋放內存由rt_free實現:
void rt_free(void *rmem) { struct heap_mem *mem; if (rmem == RT_NULL) return; RT_DEBUG_NOT_IN_INTERRUPT; RT_ASSERT((((rt_ubase_t)rmem) & (RT_ALIGN_SIZE - 1)) == 0); RT_ASSERT((rt_uint8_t *)rmem >= (rt_uint8_t *)heap_ptr && (rt_uint8_t *)rmem < (rt_uint8_t *)heap_end); RT_OBJECT_HOOK_CALL(rt_free_hook, (rmem)); /* 申請釋放地址不在堆區 */ if ((rt_uint8_t *)rmem < (rt_uint8_t *)heap_ptr || (rt_uint8_t *)rmem >= (rt_uint8_t *)heap_end) { RT_DEBUG_LOG(RT_DEBUG_MEM, ("illegal memory\n")); return; } /* 獲取塊描述符 */ mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM); RT_DEBUG_LOG(RT_DEBUG_MEM, ("release memory 0x%x, size: %d\n", (rt_ubase_t)rmem, (rt_ubase_t)(mem->next - ((rt_uint8_t *)mem - heap_ptr)))); /* 獲取堆保護信號量 */ rt_sem_take(&heap_sem, RT_WAITING_FOREVER); /* 待釋放的內存,其塊描述符需是使用狀態 */ if (!mem->used || mem->magic != HEAP_MAGIC) { rt_kprintf("to free a bad data block:\n"); rt_kprintf("mem: 0x%08x, used flag: %d, magic code: 0x%04x\n", mem, mem->used, mem->magic); } RT_ASSERT(mem->used); RT_ASSERT(mem->magic == HEAP_MAGIC); /* 清除使用標誌 */ mem->used = 0; mem->magic = HEAP_MAGIC; #ifdef RT_USING_MEMTRACE rt_mem_setname(mem, " "); #endif if (mem < lfree) { /* 更新空閒塊lfree指針 */ lfree = mem; } #ifdef RT_MEM_STATS used_mem -= (mem->next - ((rt_uint8_t *)mem - heap_ptr)); #endif /* 如臨近塊也處於空閒態,則合併整理成一個更大的塊 */ plug_holes(mem); rt_sem_release(&heap_sem); } RTM_EXPORT(rt_free);
合併空閒塊plug_holes
static void plug_holes(struct heap_mem *mem) { struct heap_mem *nmem; struct heap_mem *pmem; RT_ASSERT((rt_uint8_t *)mem >= heap_ptr); RT_ASSERT((rt_uint8_t *)mem < (rt_uint8_t *)heap_end); RT_ASSERT(mem->used == 0); /* 前向整理 */ nmem = (struct heap_mem *)&heap_ptr[mem->next]; if (mem != nmem && nmem->used == 0 && (rt_uint8_t *)nmem != (rt_uint8_t *)heap_end) { /*若是mem->next是空閒,且非尾節點,則合併*/ if (lfree == nmem) { lfree = mem; } mem->next = nmem->next; ((struct heap_mem *)&heap_ptr[nmem->next])->prev = (rt_uint8_t *)mem - heap_ptr; } /* 後向整理 */ pmem = (struct heap_mem *)&heap_ptr[mem->prev]; if (pmem != mem && pmem->used == 0) { /* 如mem->prev空閒,將mem與mem->prev合併 */ if (lfree == mem) { lfree = pmem; } pmem->next = mem->next; ((struct heap_mem *)&heap_ptr[mem->next])->prev = (rt_uint8_t *)pmem - heap_ptr; } }
動態內存的釋放相對比較簡單,其思路主要是判斷傳入地址是否在堆區,如是堆內存,則判斷其塊信息是否合法。若是合法,則將使用標誌清除。同時若是臨近塊若是是空閒態,則利用plug_holes將空閒塊進行合併,合併成一個大的空閒塊。
使用free釋放內存失敗會致使不可重用內存的累積,程序再也不使用這些內存。這將浪費內存資源,並可能在耗盡這些資源時致使分配失敗。
對於STM32而言,位於board.h
/ * 配置堆區大小,可根據實際使用進行修改 */ #define HEAP_BEGIN STM32_SRAM1_START #define HEAP_END STM32_SRAM1_END /* 用於板級初始化堆區 */ void rt_system_heap_init(void *begin_addr, void *end_addr)
用於動態申請內存 void *rt_malloc(rt_size_t size) /*追加申請內存,此函數將更改先前分配的內存塊。*/ void *rt_realloc(void *rmem, rt_size_t newsize) /* 申請的內存被初始化爲0 */ void *rt_calloc(rt_size_t count, rt_size_t size)
內存分配不能保證成功,而是可能返回一個空指針。使用返回的值,而不檢查分配是否成功,將調用未定義的行爲。這一般會致使崩潰,但不能保證會發生崩潰,所以依賴於它也會致使問題。
對於申請的內存,使用前必須進行返回值判斷,不然申請失敗,且任繼續使用。將會出現意想不到的錯誤!!
經過對RT-Thread的小堆管理器實現的梳理,層層遞進更深刻理解如下一些要點:
文章出自微信公衆號:嵌入式客棧,更多內容,請關注本人公衆號,嚴禁商業使用,違法必究