Pooled Allocation(池式分配)例項——Keil 記憶體管理

weixin_33941350發表於2013-09-11
  • 引言:說到動態申請(Dynamic Allocation)記憶體的好處,學過C/C++的人可能都有體會。執行時的靈活申請自然要比編碼時的猜測好的多。而在記憶體受限情況下這種靈活性又有特別的好處——能讓我們把有限的記憶體用的更充分。所以Keil給我們實現了一個簡捷的版本,也就是這裡所記錄的內容。

    最近翻看Kei安裝目錄,無意中發現C51\LIB下的幾個.C檔案:

    CALLOC.C
    FREE.C
    INIT_MEM.C
    MALLOC.C
    REALLOC.C

    看到 MALLOC.C 和 FREE.C 想到可能和“記憶體管理”有關。花了半個上午把這個幾個檔案看完,感覺程式碼雖然短,確有幾個巧妙之處。看的時候也有幾處疑問,看完之後豁然開朗。

    1) CALLOC.C

    我首先點開的是calloc.c(因為calloc()平時沒怎麼用過,最為好奇),看到了這樣的程式碼: 

    這個函式很簡單,它並沒有直接獲取記憶體,而是呼叫了malloc;看到這樣的程式碼很容易想到——這是一個用來分配動態陣列的函式。size是元素大小,len是陣列長度。應該是這樣用的:

    // ...
    pBase = (int*)calloc(sizeof(int), 10); // 10個整數
    // ...
    

     

      在calloc裡看的了 _MALLOC_MEM_ 讓人不解,順著CALLOC.C的#include找上去,看到了:

    原來是這個… …(如果有同學不知道xdata是什麼,可以簡單的理解為“堆”。管它呢!)。

    2) MALLOC.C

    繼續點開MALLOC.C(這份程式碼不短),一看到頭部,猜到它可能是個連結串列:

    很明顯,這裡的next用作連結;但是,len的作用暫時還不能確定(猜測:標識空閒塊的長度,註釋說的)。它們是這樣:


    接下來是型別、常量定義定義:

     

    typedef struct __mem__         __memt__;
    typedef __memt__ _MALLOC_MEM_ *__memp__;
     
    #define    HLEN    (sizeof(__memt__))
    
    extern __memt__ _MALLOC_MEM_ __mem_avail__ [];
    
    #define AVAIL    (__mem_avail__[0])
    
    #define MIN_BLOCK    (HLEN * 4)

     

    看到這些typedef,#define也不能確定各自是做什麼用的。但是有個extern宣告的陣列!應該在別的地方有定義。(關於宣告和定義不多說了)

    然後就是完整的malloc()了(部分註釋已被刪除):

     

    void _MALLOC_MEM_ *malloc( unsigned int size)
    {
       __memp__ q;            /* ptr to free block */
       __memp__ p;            /* q->next */
       unsigned int k;        /* space remaining in the allocated block */
      
       q = &AVAIL;
      
    while (1)
    {
        if ((p = q->next) == NULL)
        {
            return (NULL);                /* FAILURE */
        }
    
        if (p->len >= size)
          break;
    
       q = p;
    }
    
     k = p->len - size;        /* calc. remaining bytes in block */
    
     if (k < MIN_BLOCK)        /* rem. bytes too small for new block */
       {
       q->next = p->next;
     return (&p[1]);                /* SUCCESS */
       }
    
     k -= HLEN;
     p->len = k;
    
     q = (__memp__ ) (((char _MALLOC_MEM_ *) (&p [1])) + k);
     q->len = size;
    
     return (&q[1]);                    /* SUCCESS */
     }

     

    稍加分析可知,while(1)是迴圈遍歷連結串列的(迴圈內的p=q->next和q=p這兩句)。所以q剛開始指向的應該是連結串列的頭結點,AVAIL即__mem_avail__[0]裡存放著連結串列的頭結點。
    由16行 if(p->len > size) break; 可知,len的作用確實是用來標識空閒塊的長度;
    所以整個連結串列應該是這樣的(綠色部分為空閒記憶體,白色是連結串列節點):
     
    由此可知,註釋裡的free block指的是一個“白色+綠色”。

    注意,一旦滿足條件(找到一個足夠大的空閒塊),跳出迴圈時,p指向這個“夠用”的塊,q指向p的前驅(與連結串列方向相反的一塊)(如上圖p,q);

    往下,k很明確,計算空閒塊中剩下的位元組數;
    如果剩下的太小(<MIN_BLOCK),直接拋棄之,即將p指向的節點刪除,即26行q->next = p->next;並返回空閒記憶體的地址&p[1](即綠色的開頭處);
    繼續往下(夠大≥MIN_BLOCK),這四句結合起來才能看得懂:

     

      k-=HLEN;  // 空閒塊內也要建立一個節點
      p->len=k;  // 此時的可用空間已經少size+sizeof(__mem__)
      q = (__memp__ ) (((char _MALLOC_MEM_ *) (&p [1])) + k); // !切下的是空閒塊的後部
      q->len = size; // 這個新的節點僅用來記錄分割了多少位元組(便於free時回收),
      // 並沒有連結為連結串列,next欄位也就沒有賦值

    最終情形是這樣的: 


    其中,ret表示返回值,藍色為呼叫malloc所返回的記憶體(稱這段“白色+藍色”的為Allocated block)。
    所以p->len(當前)變成了p->len(初始的)-size-sizeof(__mem__)。

    至此,malloc完成,切割後部的一大好處是,對於原來的連結串列,你只需要修改p->len即可;試想,如果切割前半部分,那麼,空閒塊內新建立的節點(上圖藍色左邊)要插入到原來的空閒連結串列上,而且被切下的記憶體塊前的節點(上圖綠色左邊)要從原來的空閒連結串列上刪除,操作相對較麻煩。(嗯,你可以想象從一個掛滿臘肉的肉架上切肉,“切下一塊直接拿走”總是要比“把大塊臘肉拿下,從穿孔的那頭切下一塊,再將剩下的那塊穿上孔掛上架子”要來的簡單。)

    小結

    malloc如此組織記憶體:用__mem_avail__[0]為連結串列頭結點(因為malloc原始碼中只用了它的next欄位,而沒有用到它的len欄位)的單連結串列(稱其為free list)連線所有free block,而每個free block的結構如我上圖所畫,其中包含一個節點struct __mem__,之後是一段長度為len的可用記憶體。
    每次呼叫malloc(size)時從連結串列的第一個節點(__mem_avail__[0]->next)開始找,直到找到一個“足夠大”(len欄位比size大)的free block。如果len比size多出的位元組數不多,就直接將這個節點從free list上移除,並直接返回當前的可用記憶體地址(綠色的開頭);
    否則,將該free block切為兩段,並將後一段交給malloc返回;實際切下的大小要比size多出一個連結串列節點的大小,而這多出的一個節點,僅用了len欄位,用於記錄當前malloc的長度,以便free之時準確將其回收到free list之上。(注:這裡有點浪費)

    3) FREE.C

    有了這一番分析,也能猜得出free是如何做到“記憶體回收”的。
    前面的型別定義完全一樣,這裡略去(應該定義到一個.h裡,再各自inlcude)。

    直接上free的程式碼,free的註釋較為準確:

     

      void free (
      void _MALLOC_MEM_ *memp)
      {
      /*-----------------------------------------------
      FREE attempts to organize Q, P0, and P so that
      Q < P0 < P.  Then, P0 is inserted into the free
      list so that the list is maintained in address
      order.
      
     FREE also attempts to consolidate small blocks
     into the largest block possible.  So, after
     allocating all memory and freeing all memory,
     you will have a single block that is the size
     of the memory pool.  The overhead for the merge
     is very minimal.
     -----------------------------------------------*/
     __memp__ q;        /* ptr to free block */
     __memp__ p;        /* q->next */
     __memp__ p0;        /* block to free */
    
     /*-----------------------------------------------
     If the user tried to free NULL, get out now.
     Otherwise, get the address of the header of the
    memp block (P0).  Then, try to locate Q and P
    such that Q < P0 < P.
     -----------------------------------------------*/
     if ((memp == NULL) || (AVAIL.len == 0))
     return;
    
     p0 = memp;
     p0 = &p0 [-1];        /* get address of header */
    
     /*-----------------------------------------------
     Initialize.
     Q = Location of first available block.
     -----------------------------------------------*/
     q = &AVAIL;
    
     /*-----------------------------------------------
     B2. Advance P.
     Hop through the list until we find a free block
     that is located in memory AFTER the block we're
     trying to free.
     -----------------------------------------------*/
     while (1)
       {
       p = q->next;
    
     if ((p == NULL) || (p > memp))
       break;
    
       q = p;
      }
    
     /*-----------------------------------------------
     B3. Check upper bound.
     If P0 and P are contiguous, merge block P into
     block P0.
     -----------------------------------------------*/
     if ((p != NULL) && ((((char _MALLOC_MEM_ *)memp) + p0->len) == p))
       {
       p0->len += p->len + HLEN;
       p0->next = p->next;
       }
     else
       {
       p0->next = p;
       }
    
     /*-----------------------------------------------
     B4. Check lower bound.
     If Q and P0 are contiguous, merge P0 into Q.
     -----------------------------------------------*/
     if ((((char _MALLOC_MEM_ *)q) + q->len + HLEN) == p0)
       {
       q->len += p0->len + HLEN;
       q->next = p0->next;
       }
     else
       {
       q->next = p0;
       }
     }

     

    30~31行,求得當前malloc所得block的節點結構。
    45~53行的while(1)仍然是遍歷連結串列,但退出條件已經不一樣了,
    變成了:if ((p == NULL) || (p > memp)),退出時p指向的free block在memp之後,q在memp之前。
    後面的兩個if做檢查,如果memp所在的block和p,q某一或兩個相鄰都將被合併為一個free block,否則只將他們所在的free block節點連結起來。如下,memp所在free block和q所指向的free block相鄰的情形:
     
    其中藍色(memp指向的)為要free的記憶體,p0所指block與p所指block相鄰,所以會發生合併(修改前一個的len值),合併後情形如下:
     
    兩個block合併成功!

    4) INIT_MEM.C

    MALLOC.C和FREE.C中都沒有看到陣列__mem_avail__的真身(僅用extern做了宣告,不會取得記憶體實體),原來它藏在了INTI_MEM.C裡:

     

      __memt__ _MALLOC_MEM_ __mem_avail__ [2] =
        {
          { NULL, 0 },    /* HEAD for the available block list */
          { NULL, 0 }, /* UNUSED but necessary so free doesn't join HEAD or ROVER with the pool */
        };

     

    INIT_MEM.C還定義了一個重要的函式:

     

    void init_mempool ( 
      void _MALLOC_MEM_ *pool, // address of the memory pool 
      unsigned int size);             // size of the pool in bytes

     

    其原始碼如下:

     

     void init_mempool (
      void _MALLOC_MEM_ *pool,
      unsigned int size)
      {
       /*-----------------------------------------------
      If the pool points to the beginning of a memory
      area (NULL), change it to point to 1 and decrease
      the pool size by 1 byte.
     -----------------------------------------------*/
     if (pool == NULL)   {
         pool = 1;
         size--;
       }
    
     /*-----------------------------------------------
     Set the AVAIL header to point to the beginning
     of the pool and set the pool size.
     -----------------------------------------------*/
       AVAIL.next = pool;
       AVAIL.len  = size;
    
     /*-----------------------------------------------
     Set the link of the block in the pool to NULL
     (since it's the only block) and initialize the
     size of its data area.
     -----------------------------------------------*/
       (AVAIL.next)->next = NULL;
       (AVAIL.next)->len  = size - HLEN;
    
     }

     

    由這段程式碼印證了malloc原始碼中AVAIL為頭結點的猜想,16~19行的註釋可以看到,AVIL.len記錄的是記憶體池的大小,而非一般節點的空閒記憶體的位元組數。
    這裡的過程是這樣的:
    首先,將頭結點指向記憶體(block)塊的首地址pool,再將len修改為size(記憶體塊的長度)。

    然後,在這個記憶體塊(block)內部建立一個節點:

    5) REALLOC.C

    有了malloc和free想要實現realloc當然簡單,realloc的原始碼如下:

     

      void _MALLOC_MEM_ *realloc (
       void _MALLOC_MEM_ *oldp,
       unsigned int size)
       {
       __memp__ p0;
       void _MALLOC_MEM_ *newp;
      
       if ((oldp == NULL) || (AVAIL.len == 0))
       return (NULL);
    
     p0 = oldp;
     p0 = &p0 [-1];        /* get address of header */
    
     if ((newp = malloc (size)) == NULL)
       {
     return (NULL);
       }
    
     if (size > p0->len)
       size = p0->len;
    
     memcpy (newp, oldp, size);
     free (oldp);
    
     return (newp);
     }

     

    注:realloc可以理解為具有“延長”動態陣列能力的一個函式,在你一次malloc的記憶體不夠長時可以呼叫它;當然,你也可以直接呼叫它,但那麼做是不安全的。

    因果

    可能你會有疑問:為什麼在Keil中會有init_mempool?為什麼Keil的malloc,free這麼複雜(VC的malloc,free就很簡單)?
    用過Keil的朋友都知道,Keil是用來開發嵌入式軟體的,它編譯出來的可執行檔案不是windows的PE格式也不是Linux的ELF格式,而是HEX-80。
    有必要提一下VC中malloc的實現,VC中malloc呼叫了HeapAlloc,HeapAlloc是Windows API,實現從堆中申請記憶體的功能,除此之外,Windows API還提供了功能和realloc相似的HeapReAlloc,以及功能和相似的HeapFree。所以VC中malloc,free的實現要比Keil中簡單。

    VC的編譯的目標程式是在Windows上執行的,而windows系統本身已經提供了一套記憶體管理的功能(API就是使用這些功能的一種方式),所以其上的應用程式不需要寫太多的記憶體管理的程式碼(Windows已經為你做好了)。VC編譯出來的程式呼叫malloc,malloc呼叫HeapAlloc,而HeapAlloc的原型是:

     

       LPVOID WINAPI HeapAlloc(
         _In_  HANDLE hHeap,
         _In_  DWORD dwFlags,
         _In_  SIZE_T dwBytes
       );

     

    傳入的hHeap引數必須是一個可用的“堆”(通常用HeapCreate),就和init_mempool一樣,HeapAlloc呼叫前也需要先呼叫HeapCreate,以及其他環境的初始化操作,只是這些都是執行庫(Runtime Library)做的事。Windows程式執行在作業系統之上,作業系統和執行庫會為你準備好一切;而這些我們是看不到的,所以看到這裡的init_mempool可能會感到有點奇怪。

    而Keil是編譯的程式往往是在裸機(沒有作業系統)上執行的,所以你要想有“記憶體管理”的功能,就要你自己實現,而Keil的開發商早已想到了這點,所以他們幫你你實現了一個版本(即這裡介紹的),你可以直接使用它。

    應用

    關於這個幾個函式如何應用,Keil的幫助文件裡給出了一個例項:

     

    #include <stdlib.h>
    
    unsigned char xdata malloc_mempool [0x1000];
    
    
    void tst_init_mempool (void) {
      int i;
      xdata void *p;
    
      init_mempool (&malloc_mempool, sizeof(malloc_mempool));
    
      p = malloc (100);
    
      for (i = 0; i < 100; i++)
        ((char *) p)[i] = i;
    
      free (p);
    }

     

    開銷

    Keil提供的此種方案可以讓你像標準C程式一樣使用malloc和free;這種方式的一大好處是,你可以在此後重複使用一段記憶體。

    想要靈活自然就要付出代價。

    空間代價主要在於Allocted block的“頭部”,下面就來詳細分析:
    在Keil中xdata*和unsigned int都是兩個位元組,所以一個節點的大小sizeof(__mem__) == 4
    每次malloc(size)的效率就(不考慮free block,即allocated block的利用率):
    size/(size+4)
    所以你應該儘量多申請一些記憶體,如果你只申請4個位元組,利用率只有50%.
    (據之前malloc分析,其實可以再“摳門”一些,讓malloc所得block的頭部只記錄長度(因為next欄位沒有使用),每次malloc就少“浪費”兩個位元組)

    時間上,在malloc,free陸續呼叫多次之後,記憶體池在也不是當初的一大塊了,它將被分為很多個小塊,他們被串接在free list之上。
    此時呼叫malloc就不是那麼簡單的事了,malloc從free list的頭部開始查詢,直到找到一個“夠大的”free block這個過程是有時間開銷的。單連結串列的查詢是O(n)複雜度,但問題是這裡的n不能由你直接決定。所以malloc的時間效能也就不那麼穩定了。
    呼叫free也是同樣,在free list上掛的節點變多時,每次free都要從頭開始找,找到能做block的前驅的block(被free原始碼中的q所指)之後,再將當前的block插入到其後。完成該操作必須修改q所指節點,而你沒有指向該block指標,必然要從頭查詢。所以free通常情況下的時間複雜度是O(n),這裡的n和malloc同樣不能確定。

    缺陷

    要使用malloc,必須先呼叫init_mempool為malloc,free建立一個“記憶體池”;通常可以把一個xdata陣列的空間交由malloc和free管理。但我們常會糾結:我該給多少位元組給Pool?我的MCU可能只有1024個位元組可用,也可能更少。如果給多了,我就沒有足夠的空間存放其他資料了;如果給少了,可能很快malloc就不能從池中取得足夠的記憶體,甚至耗盡整個Pool。而這裡的init_mempool只能呼叫一次;因為如果發生第二次呼叫,唯一的一個free list的頭部(AVIL)會被切斷,此前的整個連結串列都將“失去控制”!

    總結

    儘管Keil這個方案存在著一些小的缺陷,但是總體來說還是不錯的,可以說是——在有限的情況下做到了較好的靈活性。

    注:
    1.我所使用的Keil 版本:V4.24.00 for C51
    幾個原始碼檔案連線:
    INIT_MEM.C: http://www.oschina.net/action/code/download?code=23770&id=39701
    MALLOC.C: http://www.oschina.net/action/code/download?code=23770&id=39702
    FREE.C: http://www.oschina.net/action/code/download?code=23770&id=39703
    CALLOC.C:http://www.oschina.net/action/code/download?code=23770&id=39704
    REALLOC.C:http://www.oschina.net/action/code/download?code=23770&id=39705

 

相關文章