LwIP系列(2):动态内存池管理(memp.c)详细分析

前言

我们在学习Lwip源码时,内存管理是绕不开的一个重点,我们在看相关的代码时,经常会看到memp_mallocmem_malloc, 其中:
(1)memp_malloc是从内存池中申请内存,具体实现在memp.c + memp.h。
(2)mem_malloc则是从内存堆中申请内存,具体实现在mem.c + mem.h中。
这两个API的区分也很容易,“p”是pool的简称,所以memp代表从内存池,mem是从内存堆。

内存池与内存堆的区别?

内存堆

内存堆其实很好理解,可以简单的认为编辑器默认的malloc就是从内存堆中申请的,与**【堆】** 对应的是 【栈】 ,堆的生长方向是从低地址->高地址,栈的生长方向是从高地址->低地址。使用malloc申请的变量都是从【堆】中申请,临时变量或局部变量是从【栈】中默认申请。更详细的区别本文就不赘述了,后面可以单独的文章说明。
所以内存堆,就是自己实现一套malloc程序。

内存池

内存池的理解重点在“池”,所谓【池】其实就是要提前挖好坑,提前占位,类似线程池,都是提前先定义或申请好,等到需要申请使用时,直接从内存池中拿出1块内存使用即可。再具体一些就是,提前申请好固定内存类型数组,这个内存数组就是内存池,从内存池中申请内存,就是从内存数组中拿出1个可用的成员。释放内存到内存池,相当于内存再恢复到内存数组中(这样说可能还是有点不严谨,凑合理解吧)。

为什么需要内存池

因为内存池管理有优点,比如:

  1. 速度块,因为内存池在程序编译时,就自动在内存栈中提前申请了内存池数组,所以再申请的时候,就可以很快的从内存池中取处一片内存使用。这一点对于以太网通信就非常重要了,因为以太网的速率是非常块的,而lwip又是在资源比较弱的单片机上实用,所以内存池的管理,是一种以空间换时间的方法。
  2. 避免内存泄漏。由于内存池是提前从栈中申请的内存数组,申请和释放都是围绕着这个内存数组的,所以顶多会有一点浪费(比如内存数组申请的个数多了),而不会造成内存泄漏。这一点内存堆就不能保证。

当然内存池也有缺点,比如:

  1. 内存池必须是固定结构、固定大小,所以不够灵活,在lwip中,一般是通过opt.h 进行配置的。
  2. 可能会造成使用浪费,注意这里说的是“浪费”而不是“泄漏”,浪费的含义是,我们一半会分配内存池数组稍微大一些,防止不够用。

内存池,内存堆的使用场景

  1. 内存池一般用在PBUF_POOL类型的需求,比如以太网原始数据接收。还有就是各种固定的tcb、pcb使用,比如tcp_pcb、udb_pcb、pbuf_pcb 等等。
  2. 内存堆一般用在PBUF_RAM,还有一些不太固定格式、不太常用的内存使用上。

LwIP 动态内存池管理分析

如果我们直接查看memp.c ,可能很多人开始都会一脸懵逼,起码我是的,因为这个文件中,作者用了大量的宏定义高级用法,我们很难一下子看懂内存池管理的具体逻辑,所以我们可以借助IDE的预编译功能,将memp.c 通过预编译,翻译成没有宏定义的文件,方便我们查看。这里我们可以借助MDK的输出预编译文件的功能,具体设置如下图:
LwIP系列(2):动态内存池管理(memp.c)详细分析_第1张图片

简化版程序分析

上述的方法能够输出预编译 memp.i ,应该就能很容易分析memp.c的实现原理了,这里我们再通过一个类似的、简化版、方便理解的示例程序,进一步分析memp.c的原理。
示例程序code如下:

#include 
#include 


typedef struct slist_s{
    struct slist_s *next;
} slist_t;

struct memp_desc{
    int size;
    int num;
    char *pool_buf;
    slist_t **list;
};

struct test_pcb{
    int a;
    int b;
    int c;
};

static char memp_test_pcb_base[4 * (sizeof(struct test_pcb))];
static slist_t *memp_list_test_pcb;
const struct memp_desc memp_test_pcb_desc = {
    sizeof(struct test_pcb),
    4, 
    memp_test_pcb_base,
    &memp_list_test_pcb
};

void memp_pool_init(struct memp_desc *desc)
{
    int i = 0;
    slist_t *list;

    *desc->list = 0;

    list = (slist_t *)(void *)(desc->pool_buf);
    for(i = 0; i < desc->num; ++i){
        list->next = *(desc->list);
        *(desc->list) = list;

        list = (slist_t *)(void *)((char *)list + desc->size);
    }
}

void *memp_malloc_pool(const struct memp_desc *desc)
{
    slist_t *list;

    list = *(desc->list);

    if(list != NULL){
        *(desc->list) = list->next;
        return (char *)list;
    }

    return 0;
}

void memp_free_pool(const struct memp_desc *desc, void *mem)
{
    slist_t *list;

    list = (slist_t *)(void *)((char *)mem);

    list->next = *(desc->list);
    *(desc->list) = list;
}



int main(void)
{
    int i;
    struct test_pcb *pcb1, *pcb2;

    printf("hello world.\n");

    memp_pool_init(&memp_test_pcb_desc);

    for(i = 0; i < 100; i++){
        pcb1 = memp_malloc_pool(&memp_test_pcb_desc);
        if(pcb1 != NULL){
            printf("malloc [%d] succ.\n", i*2 + 1);
            memset(pcb1, 0, sizeof(struct test_pcb));

            pcb1->a = i + 1;
            pcb1->b = i + 2;
            pcb1->c = i + 3;
            printf("pcb1 a = %d, b = %d, c = %d \n", pcb1->a, pcb1->b, pcb1->c);
        }

        pcb2 = memp_malloc_pool(&memp_test_pcb_desc);
        if(pcb2 != NULL){
            printf("malloc [%d] succ.\n", i*2 + 2);
            memset(pcb2, 0, sizeof(struct test_pcb));

            pcb2->a = i + 4;
            pcb2->b = i + 5;
            pcb2->c = i + 6;
             printf("pcb2 a = %d, b = %d, c = %d \n", pcb2->a, pcb2->b, pcb2->c);
        }

        printf("free [%d] .\n", i*2 + 1);
        memp_free_pool(&memp_test_pcb_desc, pcb1);

        printf("free [%d] .\n", i*2 + 2);
        memp_free_pool(&memp_test_pcb_desc, pcb2);
    }
    return 0;
}

上述代码可以直接运行。
需要说明的是,即便是上面简化版的程序,我们理解起来还是有一定的门槛的,因为上面涉及了很多中C语言中高级用法,比如:

  1. 结构体中包含了该结构体指针类型的成员变量。
  2. 结构体指针也是一种指针,大小也类似于int *指针。
  3. 结构体地址与结构体的首个成员函数的地址是相同的。
  4. 二级指针,二级指针的值是一级指针的地址。
  5. 指针的间接引用是通过【*】符号实现的。
  6. 数据类型的强转,意味着我们可以使用转换后的数据类型顺序来访问原来的数据内存,这一点非常重要,比如我们将char *转换为 struct slist_s *, 这就意味着,我们可以通过操作struct slist_s 类型的成员函数来向char *的内存中写入数据。

附录:Lwip 内存池初始化后的示例图

LwIP系列(2):动态内存池管理(memp.c)详细分析_第2张图片
简单的说:

  1. memp_pools类似于内存池的head + 状态存储单元。
  2. 内存池就是内存数组,每种类型的内存池,对应一个memp_pools的成员变量。
  3. 内存池的读取和释放都是通过memp_pools来实现的。

你可能感兴趣的:(TCP/IP,算法与数据结构,ip,算法)