LwIP的内存管理(2)

        主要内容参照5. LwIP的内存管理 — [野火]LwIP应用开发实战指南—基于野火STM32 文档,整理出来自用。

一、动态内存堆的两种实现方式

        LwIP 的动态内存堆管理(heap)分为两种:

  • C 标准库自带的内存管理策略
  • LwIP 自身实现的内存堆管理策略

        这两种方式通过宏MEM_LIBC_MALLOC选择,且二者只能选其一。

        此外,LwIP 在自身内存堆和内存池的实现上设计灵活:

  • 内存池可由内存堆实现
  • 内存堆也可由内存池实现

        通过MEM_USE_POOLSMEMP_MEM_MALLOC两个宏定义选择,二者同样只能选其一。

二、内存堆的组织结构

        内存堆的组织结构包括内存数据结构与一些重要的全局变量,具体如下:

struct mem
{
    /** index (-> ram[next]) of the next struct */
    mem_size_t next;                                        (1)
    /** index (-> ram[prev]) of the previous struct */
    mem_size_t prev;                                        (2)
    /** 1: this area is used; 0: this area is unused */
    u8_t used;                                      (3)
#if MEM_OVERFLOW_CHECK
    /** this keeps track of the user allocation size for guard checks */
    mem_size_t user_size;
#endif
};

#define MIN_SIZE             12                     (4)

LWIP_DECLARE_MEMORY_ALIGNED(ram_heap, MEM_SIZE_ALIGNED+(2U*SIZEOF_STRUCT_MEM)); (5)

#define LWIP_RAM_HEAP_POINTER       ram_heap                        (6)

/** pointer to the heap (ram_heap):
for alignment, ram is now a pointer instead of an array */
static u8_t *ram;                                   (7)

/** the last entry, always unused! */
static struct mem *ram_end;                         (8)

#if !NO_SYS
static sys_mutex_t mem_mutex;                               (9)
#endif

static struct mem * LWIP_MEM_LFREE_VOLATILE lfree;          (10)
  • (1)(2):nextprev并非指针,而是目的地址相对于整个内存堆起始地址的偏移量。
  • (3):used字段用于标记该内存是否已被使用。
  • (4):申请的内存最小为 12 字节,因为一个内存块至少需要保存mem结构体的信息,该结构体对齐后的内存大小为 12 字节。
  • (5):定义内存堆大小,编译器处理后为u8_t ram_heap[(((MEM_SIZE_ALIGNED + (2U * SIZEOF_STRUCT_MEM)) + MEM_ALIGNMENT - 1U))]。其中MEM_SIZE_ALIGNED是内存堆大小MEM_SIZE经过内存对齐后的大小;SIZEOF_STRUCT_MEM是结构体mem经过内存对齐后的大小;MEM_ALIGNMENT是 CPU 的对齐字节数,一般为 4。
  • (6):LWIP_RAM_HEAP_POINTER宏相当于给ram_heap[]重新命名,ram_heap[]是内核的内存堆空间。
  • (7):ram是全局指针变量,指向内存堆对齐后的起始地址,确保内存堆起始地址按 CPU 对齐方式对齐。
  • (8):ram_endmem类型指针,指向内存堆中最后一个内存块。
  • (9):用于保护内存堆的互斥量,暂时未用。
  • (10):lfreemem类型指针,指向内存堆中低地址的空闲内存块,即空闲内存块链表指针。

三、内存堆初始化

        内核初始化时,会调用mem_init()函数进行内存堆初始化,主要过程是对上述内存堆组织结构进行初始化,设置内存堆起始地址,初始化空闲列表。函数源码如下:

void
mem_init(void)
{
    struct mem *mem;

    LWIP_ASSERT("Sanity check alignment",
                (SIZEOF_STRUCT_MEM & (MEM_ALIGNMENT - 1)) == 0);

    /* align the heap */
    ram = (u8_t *)LWIP_MEM_ALIGN(LWIP_RAM_HEAP_POINTER);            (1)
    /* initialize the start of the heap */
    mem = (struct mem *)(void *)ram;                                (2)
    mem->next = MEM_SIZE_ALIGNED;                           (3)
    mem->prev = 0;                                          (4)
    mem->used = 0;                                          (5)
    /* initialize the end of the heap */
    ram_end = ptr_to_mem(MEM_SIZE_ALIGNED);                 (6)
    ram_end->used = 1;                                              (7)
    ram_end->next = MEM_SIZE_ALIGNED;
    ram_end->prev = MEM_SIZE_ALIGNED;
    MEM_SANITY();

    /* initialize the lowest-free pointer to the start of the heap */
    lfree = (struct mem *)(void *)ram;                              (8)

    MEM_STATS_AVAIL(avail, MEM_SIZE_ALIGNED);

    if (sys_mutex_new(&mem_mutex) != ERR_OK)                        (9)
    {
        LWIP_ASSERT("failed to create mem_mutex", 0);
    }
}
  • (1):进行内存堆空间对齐,LWIP_RAM_HEAP_POINTER宏定义为ram_mem,内存堆对齐后的起始地址记录在ram中。
  • (2):在内存堆起始位置放置一个mem类型的结构体,因为初始化后的内存堆是一个大的空闲内存块,每个空闲内存块前面都需放置一个mem结构体。
  • (3):下一个内存块的偏移量为MEM_SIZE_ALIGNED,相当于直接到内存堆的结束地址。
  • (4):上一个内存块为空。
  • (5):标记为未被使用。
  • (6):指针移动到内存堆末尾位置,放置一个mem类型的结构体,初始化表示内存堆结束的内存块。
  • (7):标记该内存块已被使用,因其是结束位置,不能被分配,nextprev字段都指向自身,仅表示内存堆结束,无内存可分配。
  • (8):空闲内存块链表指针指向内存堆起始地址,因当前只有一个内存块。
  • (9):创建内存堆分配时使用的互斥量,无操作系统时该语句等效于空。

        经过mem_init()函数后,内存堆被初始化为两个内存块:第一个是整个内存堆大小的空闲内存块,第二个是大小为 0、标记为已使用状态的结束内存块,无法分配。系统运行中,随着内存分配与释放,lfree指针指向地址不断改变,始终指向低地址空闲内存块,而ram_end不变,指向最后一个内存块即内存堆结束地址。

四、内存分配

        内存分配函数根据用户指定申请大小分配内存空间,大小需大于MIN_SIZE。LwIP 使用首次拟合方法,在空闲内存块链表中遍历寻找第一个合适大小的内存块分配,若可分割,则分割出用户需要的大小,剩余空闲内存块重新插入链表。

   mem_malloc()函数是 LwIP 中的内存分配函数,参数为用户指定的内存字节数,成功返回内存块地址,失败返回NULL。分配的内存空间受内存对齐影响,可能比申请的略大,例如申请 22 字节,按 4 字节对齐则分配 24 字节。

        内存块申请成功后返回起始地址,但未初始化,可能含随机数据,用户需立即初始化或写入有效数据。在操作系统环境中,内存堆是全局变量,申请内存块不安全,LwIP 使用互斥量保护临界资源,多线程同时申请或释放时会因互斥量产生延迟。函数源码如下:

void *
mem_malloc(mem_size_t size_in)
{
    mem_size_t ptr, ptr2, size;
    struct mem *mem, *mem2;
    LWIP_MEM_ALLOC_DECL_PROTECT();

    if (size_in == 0)
    {
        return NULL;
    }

    size = (mem_size_t)LWIP_MEM_ALIGN_SIZE(size_in);        (1)

    if (size < MIN_SIZE_ALIGNED)
    {

        size = MIN_SIZE_ALIGNED;                    (2)
    }

    if ((size > MEM_SIZE_ALIGNED) || (size < size_in))
    {
        return NULL;                                        (3)
    }

    sys_mutex_lock(&mem_mutex);                     (4)

    LWIP_MEM_ALLOC_PROTECT();

    /* 遍历空闲内存块链表 */
    for (ptr = mem_to_ptr(lfree); ptr < MEM_SIZE_ALIGNED - size;
            ptr = ptr_to_mem(ptr)->next)            (5)
    {
        mem = ptr_to_mem(ptr);                              (6)

        if((!mem->used)&&(mem->next-(ptr + SIZEOF_STRUCT_MEM))>= size) (7)
        {
            if (mem->next - (ptr + SIZEOF_STRUCT_MEM) >=
                    (size + SIZEOF_STRUCT_MEM + MIN_SIZE_ALIGNED))
            {
                ptr2 = (mem_size_t)(ptr + SIZEOF_STRUCT_MEM + size);   (8)
                LWIP_ASSERT("invalid next ptr",ptr2 != MEM_SIZE_ALIGNED);
                /* create mem2 struct */
                mem2 = ptr_to_mem(ptr2);                    (9)
                mem2->used = 0;                             (10)
                mem2->next = mem->next;
                mem2->prev = ptr;
                /* and insert it between mem and mem->next */
                mem->next = ptr2;
                mem->used = 1;                                      (11)

                if (mem2->next != MEM_SIZE_ALIGNED)
                {
                    ptr_to_mem(mem2->next)->prev = ptr2;            (12)
                }
                MEM_STATS_INC_USED(used, (size + SIZEOF_STRUCT_MEM));
            }
            else
            {
                mem->used = 1;                                      (13)
                MEM_STATS_INC_USED(used, mem->next - mem_to_ptr(mem));
            }

            if (mem == lfree)                                       (14)
            {
                struct mem *cur = lfree;
            /*Find next free block after mem and update lowest free pointer */
                while (cur->used && cur != ram_end)
                {
                    cur = ptr_to_mem(cur->next);            (15)
                }
                lfree = cur;                                        (16)
                LWIP_ASSERT("mem_malloc: !lfree->used",
                            ((lfree == ram_end) || (!lfree->used)));
            }
            LWIP_MEM_ALLOC_UNPROTECT();
            sys_mutex_unlock(&mem_mutex);                   (17)
            LWIP_ASSERT("mem_malloc: allocated memory not above ram_end.",
            (mem_ptr_t)mem +SIZEOF_STRUCT_MEM+size <=(mem_ptr_t)ram_end);
            LWIP_ASSERT("mem_malloc: allocated memory properly aligned.",
            ((mem_ptr_t)mem + SIZEOF_STRUCT_MEM) % MEM_ALIGNMENT == 0);
            LWIP_ASSERT("mem_malloc: sanity check alignment",
                        (((mem_ptr_t)mem) & (MEM_ALIGNMENT - 1)) == 0);


        MEM_SANITY();
        return (u8_t *)mem + SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET; (18)
        }
    }
    MEM_STATS_INC(err);
    LWIP_MEM_ALLOC_UNPROTECT();
    sys_mutex_unlock(&mem_mutex);                           (19)
    LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SERIOUS,
        ("mem_malloc: could not allocate %"S16_F" bytes\n", (s16_t)size));
    return NULL;
}
  • (1):对用户申请的内存大小进行对齐操作。
  • (2):若申请的内存大小小于最小内存对齐大小MIN_SIZE_ALIGNED,则设为默认最小值。
  • (3):若申请的内存大小大于整个内存堆对齐后的大小,返回NULL,申请失败。
  • (4):获得互斥量,仅在操作系统环境起作用。
  • (5):遍历空闲内存块链表,寻找第一个满足用户需求的内存块。
  • (6):得到该内存块起始地址。
  • (7):若内存块未使用,且大小不小于用户需求大小加上mem结构体大小,则满足需求。
  • (8):若内存块可分割,通过起始地址与用户需求大小偏移,得到剩余内存起始块地址ptr2
  • (9):将该地址后的内存空间作为分割后的新内存块mem2,转换为mem结构体记录信息。
  • (10):标记mem2为未使用,插入空闲内存块链表。
  • (11):标记被分配的内存块mem为已使用状态。
  • (12):若mem2的下一个内存块不是链表最后一个,将其prev指向mem2
  • (13):若不可分割,直接标记分配的内存块为已使用。
  • (14):若被分配的内存块是lfree指向的,需重新为lfree赋值。
  • (15):找到第一个低地址的空闲内存块。
  • (16):将lfree指向该内存块。
  • (17):释放互斥量。
  • (18):返回内存块可用起始地址,因块头需用mem结构体保存信息。
  • (19):若分配失败,释放互斥量并退出。

五、内存释放

        内存释放操作相对简单,LwIP 根据用户释放的内存块地址,偏移mem结构体大小得到正确起始地址,依据mem中信息进行释放、合并等操作,将used字段清零表示未使用。

        为防止内存碎片,LwIP 通过算法合并相邻空闲内存块,释放时若与上一个或下一个空闲内存块地址连续,则进行合并。源码如下:

void
mem_free(void *rmem)
{
    struct mem *mem;
    LWIP_MEM_FREE_DECL_PROTECT();

    if (rmem == NULL)                                       (1)
    {
        LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_TRACE |
                    LWIP_DBG_LEVEL_SERIOUS,
                    ("mem_free(p == NULL) was called.\n"));
        return;
    }
    if ((((mem_ptr_t)rmem) & (MEM_ALIGNMENT - 1)) != 0)
    {
        LWIP_MEM_ILLEGAL_FREE("mem_free: sanity check alignment");
        LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE,
                    ("mem_free: sanity check alignment\n"));
        /* protect mem stats from concurrent access */
        MEM_STATS_INC_LOCKED(illegal);
        return;
    }

    mem = (struct mem *)(void *)((u8_t *)rmem -
        (SIZEOF_STRUCT_MEM + MEM_SANITY_OFFSET));   (2)

    if ((u8_t *)mem < ram ||
            (u8_t *)rmem + MIN_SIZE_ALIGNED > (u8_t *)ram_end) (3)
    {
        LWIP_MEM_ILLEGAL_FREE("mem_free: illegal memory");
        LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE,
                    ("mem_free: illegal memory\n"));
        /* protect mem stats from concurrent access */
        MEM_STATS_INC_LOCKED(illegal);
        return;
    }

    /* protect the heap from concurrent access */
    LWIP_MEM_FREE_PROTECT();

    /* mem has to be in a used state */
    if (!mem->used)                                 (4)
    {
        LWIP_MEM_ILLEGAL_FREE("mem_free: illegal \
    memory: double free");
        LWIP_MEM_FREE_UNPROTECT();
        LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE,
                    ("mem_free: illegal memory: double free?\n"));
        /* protect mem stats from concurrent access */
        MEM_STATS_INC_LOCKED(illegal);
        return;
    }

    if (!mem_link_valid(mem))                               (5)
    {
        LWIP_MEM_ILLEGAL_FREE("mem_free: illegal memory:\
    non-linked: double free");
        LWIP_MEM_FREE_UNPROTECT();
        LWIP_DEBUGF(MEM_DEBUG | LWIP_DBG_LEVEL_SEVERE,
                ("mem_free: illegal memory: non-linked: double free?\n"));
        /* protect mem stats from concurrent access */
        MEM_STATS_INC_LOCKED(illegal);
        return;
    }

    /* mem is now unused. */
    mem->used = 0;                                  (6)

    if (mem < lfree)
    {
        /*the newly freed struct is now the lowest */
    lfree = mem; (7)
    }

    MEM_STATS_DEC_USED(used, mem->next -
    (mem_size_t)(((u8_t *)mem - ram)));


    /* finally, see if prev or next are free also */
    plug_holes(mem); (8)


    MEM_SANITY();


    LWIP_MEM_FREE_UNPROTECT();
    }

- (1):若释放的地址为空,直接返回。
- (2):对释放的地址进行偏移,得到真正内存块的起始地址。
- (3):判断内存块起始地址是否合法,不合法则返回。
- (4):若内存块未被使用,直接返回。
- (5):若内存块在链表中的连接不正常,返回。
- (6):将`used`置0,表示内存块已释放。
- (7):若刚释放的内存块地址比`lfree`指向的地址低,更新`lfree`指针。
- (8):调用`plug_holes()`函数尝试合并内存块,若新释放的内存块与上一个或下一个空闲内存块地址连续,则进行合并。

        对内存释放函数的操作需格外小心,传递的参数必须是内存申请返回的地址,这样才能保证系统根据该地址找到内存块中的mem结构体,实现内存块释放及合并,否则可能打乱整个内存堆,产生内存碎片。

        此外,用户申请内存后需及时释放,避免内存泄漏。内存泄漏指调用内存分配函数后,未及时或错误地释放内存。偶尔的操作影响不大,但周期性申请且不释放,会耗尽内存堆,导致系统死机等问题。

六、使用 C 库的 malloc 和 free 管理内存

        LwIP 支持使用 C 标准库的mallocfree进行内存管理,当宏MEM_LIBC_MALLOC被定义时,编译器会编译相关代码,采用 C 标准库的函数。相关宏定义如下:

#if MEM_LIBC_MALLOC
void
mem_init(void)
{
}
void *
mem_trim(void *mem, mem_size_t size)
{
    LWIP_UNUSED_ARG(size);
    return mem;
}

#ifndef mem_clib_free
#define mem_clib_free free
#endif
#ifndef mem_clib_malloc
#define mem_clib_malloc malloc
#endif
#ifndef mem_clib_calloc
#define mem_clib_calloc calloc
#endif

#define MEM_LIBC_STATSHELPER_SIZE 0

#endif

        若选择使用 C 标准库管理内存,void mem_init(void)void* mem_trim(void *mem, mem_size_t size)函数无实际实现,ram_heap也不会被编译,用户申请的内存块在 C 标准库管理的系统堆中。这要求初始化整个 C 标准库并建立内存堆空间,因此不建议使用这种方式。

七、LwIP 中的配置

LwIP 中内存管理策略的选择由以下宏决定:

  • MEM_LIBC_MALLOC:决定是否使用 C 标准库自带的内存分配策略。默认值为 0,不使用,即使用 LwIP 提供的内存堆分配策略;定义为 1 时,使用 C 标准库自带策略。

MEM_LIBC_MALLOC为 0 时,LwIP 的动态内存管理策略有两种实现形式:

  • 通过内存堆 (HEAP) 管理策略(大数组)

  • 通过内存池 (POOL) 管理策略(事先开辟的内存池)

  • MEMP_MEM_MALLOC:表示是否使用 LwIP 内存堆分配策略实现内存池分配(从内存池获取内存实际是从内存堆分配)。默认值为 0,内存池为独立内存实现,与MEM_USE_POOLS只能选其一。

  • MEM_USE_POOLS:表示是否使用 LwIP 内存池分配策略实现内存堆分配(从内存堆获取内存实际是从内存池分配)。默认值为 0,内存堆为独立内存实现,与MEMP_MEM_MALLOC只能选其一。

        要使用内存池实现内存堆分配,需将MEM_USE_POOLSMEMP_USE_CUSTOM_POOLS定义为 1,MEMP_MEM_MALLOC必须为 0,还需创建lwippools.h文件,添加初始化内存池的代码。

        需注意内存池大小要依次增大,编译时编译器会将这些内存个数及大小添加到系统内存池。用户申请内存时,会选择最合适大小的内存块分配,若最匹配的内存池已用完,则选择更大的,可能浪费内存,但分配效率高,即 “以空间换时间”。

        不同宏定义对应的内存分配策略如下表:

MEMP_MEM_MALLOCMEM_USE_POOLS内存分配策略
00默认方式,内存池与内存堆独立实现
01内存堆由内存池实现
10内存池由内存堆实现
11不允许
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值