• 引言:说到动态申请(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

Pooled Allocation(池式分配)实例——Keil 内存管理的更多相关文章

  1. Pooled Allocation池式分配实例——Keil 内存管理

    最近翻看Kei安装目录,无意中发现C51\LIB下的几个.C文件: CALLOC.CFREE.CINIT_MEM.CMALLOC.CREALLOC.C 看到 MALLOC.C 和 FREE.C 想到可 ...

  2. rt-thread中动态内存分配之小内存管理模块方法的一点理解

    @2019-01-18 [小记] rt-thread中动态内存分配之小内存管理模块方法的一点理解 > 内存初始化后的布局示意 lfree指向内存空闲区首地址 /** * @ingroup Sys ...

  3. <转载>内存管理内幕-动态分配的选择、折衷和实现 对malloc内存分配有个简单的描述,对内存管理有个大致的说明

    这篇文章看后感觉不错,和我在glibc下的hurdmalloc.c文件里关于malloc的实现基本意思相同,同时,这篇文章还介绍了一些内存管理方面的知识,值得推荐. 原文链接地址为:http://ww ...

  4. nginx源码分析—内存池结构ngx_pool_t及内存管理

    Content 0. 序 1. 内存池结构 1.1 ngx_pool_t结构 1.2 其他相关结构 1.3 ngx_pool_t的逻辑结构 2. 内存池操作 2.1 创建内存池 2.2 销毁内存池 2 ...

  5. 内存管理内幕mallco及free函数实现

    原文:https://www.ibm.com/developerworks/cn/linux/l-memory/ 为什么必须管理内存 内存管理是计算机编程最为基本的领域之一.在很多脚本语言中,您不必担 ...

  6. Python 源码剖析(六)【内存管理机制】

    六.内存管理机制 1.内存管理架构 2.小块空间的内存池 3.循环引用的垃圾收集 4.python中的垃圾收集 1.内存管理架构 Python内存管理机制有两套实现,由编译符号PYMALLOC_DEB ...

  7. How Javascript works (Javascript工作原理) (三) 内存管理及如何处理 4 类常见的内存泄漏问题

    个人总结: 1.两种垃圾回收机制: 1)引用标记算法:如果检测到一个对象没有被引用了,就清除它. ***这种算法不能处理循环引用的情况*** 2)标记—清除算法:从根(全局变量)开始向后代变量检测,任 ...

  8. BEP 7:CUDA外部内存管理插件(上)

    BEP 7:CUDA外部内存管理插件(上) 背景和目标 在CUDA阵列接口使得能够共享不同的Python之间的数据库的访问CUDA设备.但是,每个库都与其它库区别对待.例如: Numba在内部管理内存 ...

  9. JavaScript 工作原理之三-内存管理及如何处理 4 类常见的内存泄漏问题(译)

    原文请查阅这里,本文有进行删减,文后增了些经验总结. 本系列持续更新中,Github 地址请查阅这里. 这是 JavaScript 工作原理的第三章. 我们将会讨论日常使用中另一个被开发者越来越忽略的 ...

随机推荐

  1. 跳出iframe

    摘要 有时候需要用到iframe,但里面的单击里面的链接的时候,总是在该iframe中打开. 解决办法 其实解决起来也很简单. 在iframe中的head标签中添加下面的标签即可. <base ...

  2. fedora23也会死机, 怎么办

    现在使用的 fedora23 , 在有些 时候老是 死机, 有的是 firefos引起的, 但 更多的时候, 是 由 终端terminal gnome-terminal引起的, 特别是在 操作 vim ...

  3. editplus如何配置php编译环境?

    为什么要配置php编译? 因为,要先看看 php文件是否能够 编译得过去, 有没有错误, 如果有错误, 不能通过编译, 则肯定不能运行. 所以, 可以先看一下编译 得不得行. 在preferences ...

  4. 关于轻松安装LNMP和LAMP的编译环境

    http://lnmp.org/install.html 系统需求: CentOS/RHEL/Fedora/Debian/Ubuntu/Raspbian Linux系统 需要2GB以上硬盘剩余空间 1 ...

  5. Sublime Text3

    Sublime Text3 激活码: ----- BEGIN LICENSE ----- Andrew Weber Single User License EA7E- 813A03DD 5E4AD9E ...

  6. 编写更好的C#代码

    引言 开发人员总是喜欢就编码规范进行争论,但更重要的是如何能够在项目中自始至终地遵循编码规范,以保证项目代码的一致性.并且团队中的所有人都需要明确编码规范所起到的作用.在这篇文章中,我会介绍一些在我多 ...

  7. [译]git clean

    git clean命令用来从你的工作目录中删除所有没有tracked过的文件. git clean经常和git reset --hard一起结合使用. 记住reset只影响被track过的文件, 所以 ...

  8. Linux下查看文件内容的命令

    查看文件内容的命令: cat     由第一行开始显示内容,并将所有内容输出 tac     从最后一行倒序显示内容,并将所有内容输出 more    根据窗口大小,一页一页的现实文件内容 less ...

  9. tcp/ip协议栈调用关系图

    最近阅读了tcp/ip详解卷2,总结一下整个发送过程和接收过程 sendmsg \/ sendit \/ sosend(这一步将数据从用户空间拷贝到内核空间,并且会在这一步判断发送缓存空间是否充足,是 ...

  10. eclipse中如何将java项目转为java Web项目

    有时候我们在eclipse中导入web项目时,系统当做一个java项目导入进来了.这时候在启动tomcat的服务器时无法找到该项目. 那么可以通过如下操作来将java项目转换为web项目. 1. 选择 ...