這篇文章最初發布在RT-Thread官方論壇中,最近准備整理放到博客中來讓更多人一起探討學習。
2012年9月28日星期五
前言:
母語能力有限
概述:
這篇文字和大家分享一下今晚對RT-Thread的內存管理——小內存管理算法的理解。若有不對的地方請大家丟磚。
正文:
分析的源碼文件mem.c
主要的幾個函數:
1、rt_system_heap_init
2、rt_malloc
3、rt_free
4、plug_holes
armcc編譯器中的初始化內存的方式:
rt_system_heap_init((void*)&Image$$RW_IRAM1$$ZI$$Limit, (void*)STM32_SRAM_END);
接觸RTT半年以來,對這里的第一個參數是又愛又恨,愛它的神秘怪僻,恨它的怪僻神秘。
extern int Image$$RW_IRAM1$$ZI$$Limit;
從這里可以看得出它是火星人還是地球人。不錯這樣的聲明我們大概能知道的是僅僅只是一個變量而已。不過它的定義在何處?我糾結到昨天晚上才見到它真實的面貌(這還多虧aozima的指點)。這是一個鏈接器導出的符號,代表ZI段的結束(科普:假如芯片的RAM有32Kbyte,那么通常我們的程序沒有占用完全部的RAM,且ARMCC的鏈接器會將ZI段排在RAM已使用的RAM區域中的最后面。所以ZI段的后面就是程序未能使用到的RAM區域)。關於這種奇怪的符號都可以在MDK的幫助文檔中找到!
第二個參數就是整個RAM的結束地址。所以從這兩個參數上可以知道傳遞進去的是內存的管理區域。
rt_system_heap_init
這個函數是對堆進行初始的過程,堆的大小由傳進來的起始地址(begin_addr)和結束地址(end_addr)決定。當然如果我們還要考慮內存對齊,這樣一來,我們能使用的堆大小就不一定完全等於(end_addr - begin_addr)了。
rt_uint32_t begin_align = RT_ALIGN((rt_uint32_t)begin_addr, RT_ALIGN_SIZE);
rt_uint32_t end_align = RT_ALIGN_DOWN((rt_uint32_t)end_addr, RT_ALIGN_SIZE);
這兩句進一步對傳遞的起始地址后結束地址進行對齊操作了,有可能在起始地址上往后偏移幾個字節以保持在對齊的地址上,也有可能在結束地址的基礎上往前偏移幾個字節以保持在對齊地址上。所以這里就有可能被扣掉一點點的內存。但是這往往是很小的一點點,通常小到幾個字節,也許剛好是一個都沒有被扣掉。
if ((end_align > (2 * SIZEOF_STRUCT_MEM)) && ((end_align - 2 * SIZEOF_STRUCT_MEM) >= begin_align)) { /* calculate the aligned memory size */ mem_size_aligned = end_align - begin_align - 2 * SIZEOF_STRUCT_MEM; }
這部分是計算最大可分配的內存區域,?????,為什么說是最大‘可’分配的內存區域呢?因為還將被繼續扣除一部分內存,這部分被扣除的內存是用來放置內存管理算法所要用的數據結構。越精巧的算法當然是效率更高,浪費的也最少。
RTT扣掉了2個數據結構的尺寸,因為RTT有一個heap_ptr和一個heap_end這兩個標記,分別表示堆的起始和結束。mem_size_aligned就是整個可操作的區域尺寸。
RTT的內存管理用到的數據結構非常的精簡。
struct heap_mem { /* magic and used flag */ rt_uint16_t magic; rt_uint16_t used; rt_size_t next, prev; };
總共占用了2+2+2xcpu字長(8個字節)=12個字節。其中magic字段用以標記一個合法的內存塊,used字段標記這個堆內存塊是否被分配,next字段指向當前這個堆內存塊的末尾后1個字節(不說成是下一個可分配塊,是因為也許next指向了末尾),prev指向當前這個內存塊的前一個有效內存塊的數據結構起始處,否則指向自身(最前面那個內存塊)。所以可以看出這個設計思路是把整個內存塊用鏈表的形式組織在一起。當然我們使用的next和prev只是相對於起始地址heap_ptr的偏移量,而不是真正在通常列表中看到的指針。
接着標記了第一塊可分配的內存塊,這個內存塊把mem_size_aligned個字節大小划分出來,留下SIZEOF_STRUCT_MEM個字節的大小用來剛好放置heap_end,在前SIZEOF_STRUCT_MEM個字節中也就是最前面的SIZEOF_STRUCT_MEM個字節用來放置heap_ptr(堆內存的頭指針)。其中第一個可分配點就是從heap_ptr開始的,heap_prt的next指向了heap_end(也就是mem_size_aligned + SIZEOF_STRUCT_MEM),大致的初始時候的布局如下圖所示:
最后讓lfree指向當前活動的可分配的內存塊,這樣可以迅速找到最進被釋放的內存塊上。一般只要之前分配的內存塊被釋放后,lfree就盡量分配靠前的內存區域,也就是優先從前往后尋找可分配的內存塊。
rt_malloc
首先對申請的尺寸做對齊處理,但是這個對齊操作只會有可能比實際分配的尺寸要大一點。
for (ptr = (rt_uint8_t *)lfree - heap_ptr; ptr < mem_size_aligned - size; ptr = ((struct heap_mem *)&heap_ptr[ptr])->next)
循環查找從當前lfree所指向的區域開始,查找一塊能夠滿足區域的內存塊。
if ((!mem->used) && (mem->next - (ptr + SIZEOF_STRUCT_MEM)) >= size)
當這塊內存區域沒有被使用,且這塊內存區域扣掉頭部數據結構SIZEOF_STRUCT_MEM后的大小滿足所需分配的大小那么就可以使用這個內存塊來分配。
if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >= (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
如果這個即將被分配的內存塊除了能分配給當前尺寸后,還余下的有足夠的空間能夠組織成下一個可分配點的話,那么將對余下的部分組織成下一個可分配點。也就是將多余的空間划出來組織成一個新的可分配區域然后鏈接到鏈表中。
否則把當前這個內存塊標記為已分配狀態used=1。如果當前被分配的內存塊是lfree指向的內存塊,那么調整lfree,以讓其指向下一個可分配的點。
if (mem == lfree) { /* Find next free block after mem and update lowest free pointer */ while (lfree->used && lfree != heap_end) lfree = (struct heap_mem *)&heap_ptr[lfree->next]; RT_ASSERT(((lfree == heap_end) || (!lfree->used))); }
之后將得到的內存點返回,這個時候不是返回這個可分配點的其實地址,而是返回這個可分配點的起始地址偏移一個數據結構尺寸的地址,因為每個可分配的內存塊都在其前面有一個用於管理內存所需用到的一個數據結構(鏈表,魔數,used等信息)。
return (rt_uint8_t *)mem + SIZEOF_STRUCT_MEM;
如果當前這個循環從lfree開始查找,第一次沒有找到合適的內存塊,那么繼續往后找,循環的判斷條件是只要next小於mem_size_aligned - size就或許能找到一個合適尺寸的內存塊。否則將分配失敗,返回NULL。這里雖然分配的size小於可分配的區域,但是由於多次分配釋放等過程產生了內存碎片,真正連續可用的空間並非這么多。
rt_free
調用這個函數是釋放之前分配的堆內存。所以使用rt_malloc函數返回的內存塊地址如果需要釋放的時候,就需要調用rt_free。由於在rt_malloc和rt_free的外面使用者來說,是不知道這個內存地址的前面有一個管理數據結構的,所以在rt_free的時候需要往前偏移SIZEOF_STRUCT_MEM個字節,用以找到數據結構的起點。
mem = (struct heap_mem *)((rt_uint8_t *)rmem - SIZEOF_STRUCT_MEM);
接着將這個內存塊標記為未被分配的狀態。
mem->used = 0; mem->magic = 0;
如果釋放的內存地址在lfree的前面,那么將lfree指向當前釋放的內存塊上。
if (mem < lfree) { /* the newly freed struct is now the lowest */ lfree = mem; }
最后調用plug_holes函數進行一些附加的優化操作,這個優化操作是必不可少以及體現整個內存分配算法核心價值的地方(先賣個關子,等我慢慢道來)。
plug_holes
這個函數的作用就是合並當前這個內存點的前后緊接着的已經被釋放的內存塊。這樣一來就可以解決內存碎片問題。
nmem = (struct heap_mem *)&heap_ptr[mem->next];
if (mem != nmem && nmem->used == 0 && (rt_uint8_t *)nmem != (rt_uint8_t *)heap_end) { /* if mem->next is unused and not end of heap_ptr, combine mem and 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; }
這是看其后面的內存點是否可以被合並,合並其后的內存塊的時候只需要調整當前內存塊的next值。其次還需要調整當前內存塊后的內存塊的下一個內存塊的prev字段(這里有點繞口,其實就好比普通列表中的p->next->next->prev),使其指向當前本身的內存塊(好比p->next->next->prev = p)。
pmem = (struct heap_mem *)&heap_ptr[mem->prev];
if (pmem != mem && pmem->used == 0) { /* if mem->prev is unused, combine mem and 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; }
這是合並當前內存塊前面的已被釋放的內存塊,先取出前面的內存塊,然后調整前一個內存塊的next指向當前內存塊的next所指的地方,接着將當前內存塊的下一個內存塊的prev字段指向當前內存款的前一個內存塊(好比p->next->prev = p->prev)。
這樣兩大步就可以將當前釋放的內存塊的前后兩個已經被釋放的內存塊給合並成一個大的內存塊。從而避免了碎片問題,提高了內存分配的可靠性。
2012年9月28日3時16分46秒
感謝各位網友的支持,如果想得到最新的文章資訊請關注我的微信公眾號:鵬城碼夫 (微信號:rocotona)