這篇文章最初發布在RT-Thread官方論壇中,最近準備整理放到博客中來讓更多人一塊兒探討學習。算法
2012年9月28日星期五
前言:
母語能力有限微信
概述:
這篇文字和你們分享一下今晚對RT-Thread的內存管理——小內存管理算法的理解。如有不對的地方請你們丟磚。
正文:
分析的源碼文件mem.c
主要的幾個函數:
一、rt_system_heap_init
二、rt_malloc
三、rt_free
四、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);
這兩句進一步對傳遞的起始地址後結束地址進行對齊操做了,有可能在起始地址上日後偏移幾個字節以保持在對齊的地址上,也有可能在結束地址的基礎上往前偏移幾個字節以保持在對齊地址上。因此這裏就有可能被扣掉一點點的內存。可是這每每是很小的一點點,一般小到幾個字節,也許恰好是一個都沒有被扣掉。ui
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的內存管理用到的數據結構很是的精簡。spa
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)