Pooled Allocation(池式分配)實例——Keil 內存管理

  • 引言:說到動態申請(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; // 從freelist頭開始找
      
    while (1)
    {
        if ((p = q->next) == NULL) // 已經是最後一個節點
        {
            return (NULL);                /* FAILURE */
        }
    
        if (p->len >= size) // 找到一個“夠用的”block
          break;
    
       q = p;
    } // 此時p指向“夠用的”block,q指向它之前的節點(q->next == 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; // 將當前block從鏈表上刪除
     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

發佈了31 篇原創文章 · 獲贊 40 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章