Rice我叫加饭? · 2月13日

RMP,超级高效且线程安全的内存分配算法

RMP

RMP(Rice Memory Pool)全称内存池分配算法,它是超级高效,并且线程安全的内存池组件。

开源链接:https://github.com/RiceChen0/rmp

RMP 背景

RMP 设计初衷

  1. 动态申请内存的长度相近,并且申请次数频繁,期望有一个高效却合理处理内存分配和释放的软件组件。
  2. 第一点的场景在 RTOS 环境中,期望有一个满足线程安全的内存分配和释放的软件组件。

RMP 特点

  1. 效率性:提供一个高效分配的内存分配算法。
  2. 安全性:针对 RTOS 环境,提供线程安全的内存分配方式。
  3. 跨平台性:支持任意的 RTOS 环境,裸机环境。
  4. 资源损耗:几乎可以忽略 RAM 和 ROM 的消耗。
  5. 模式支持:支持静态模式和动态模式。
  6. 通信方式:支持阻塞方式和非阻塞方式。

应用场景

  1. 场景 1:在一些协议设计场景中,且协议报文长度相近
  • 处理方式 1:采用动态内存申请分配协议报文存储空间。存在问题:反复的内存申请释放,会导致内存碎片。
  • 处理方式 2:采用一个大内存,用户拆成多个块,自行管理。存在问题:增加代码的复杂度。
  1. 场景 2:在 RTOS 环境中,邮箱的传输过程,邮箱指传输报文的起始地址,它不像消息队列一样内部实现会做数据拷贝。所以需要防护传输报文的内存。
  • 处理方式 1:在邮箱发送线程申请报文内存,在邮箱接收线程释放报文内存。存在问题:反复的内存申请释放,会导致内存碎片。
  • 处理方式 2:采用一个大内存,用户拆成多个块,自行管理,在邮箱发送线程获取内存块,在邮箱接收线程释放内存块。存在问题:增加代码的复杂度。
  • 处理方式 3:如果你使用的系统是 RT-RThread,可以利用 RT-Thread 的内核对象--内存池,大部分系统没有提供该机制。

RMP 软件设计

RMP 初始化框图

image.png

RMP 初始化软件设计说明

  1. RMP 的设计是将一个大的内存块平均划分为多个大小相同的内存块。
  2. 通过下一个内存的起始 4 个字节保存上一个内存块的地址起始地址。
  3. 最后一个内存块的起始地址,通过一个 free_list 保存。这样就可以形成一个链式存储。
  4. 这种方式可以通过 free_list 快速的查找到任意的内存块。

RMP 内存申请框图

Image

RMP 内存申请软件设计说明

  1. 当内存块申请的时候,通过 block4 的前 4 个字节找到 block3 的起始地址。
  2. free_list 保存 block3 的起始地址。
  3. 返回 block3 的起始地址。
  4. 这时内存池将减少一内存块。
  5. 以此类推。

RMP 内存释放框图

Image

RMP 内存释放软件设计说明

  1. 当 block2 释放的时候,block2 的前四个字节保存 free_list 指向的地址。
  2. free_list 保存 block2 的起始地址。
  3. 这是内存池将增加一个内存块。
  4. 以此类推。

RMP 目录结构

├─adapter  
│  └─rtthread                     
│      ├─rmp_mutex.c            // rtthread mutex适配层  
│      └─rmp_sem.c              // rtthread sem适配层  
├─example                         
│  └─rmp_rtt_example.c          // rtthread 平台实例  
├─include  
│  ├─rmp_def.h                  // rmp 通用接口定义  
│  └─rmp.h                      // rmp 对外头文件  
└─src  
   └─rmp.c                      // rmp 核心代码源文件  

RMP 接口说明

image.png

  • 动态创建内存池

根据单个内存块的大小和内存块个数,申请内存池。
内存池的空间通过 malloc 方式申请

mp_t *rmp_create(uint32_t size, uint32_t count);  

image.png

  • 删除动态创建的内存池

内存池的空间通过 free 方式释放。

voidrmp_delete(rmp_t *mp);  

image.png

  • 内存池初始化

内存池的空间由用户提供。
告知内存池单个内存块的大小和内存块个数

voidrmp_init(rmp_t *mp, void *mem, uint32_t size, uint32_t count);  

image.png

  • 内存池去初始化
voidrmp_deinit(rmp_t *mp);  

image.png

  • 阻塞方式从内存池申请内存块

在 RTOS 环境,它是阻塞方式从内存池申请内存块
在非 RTOS 环境,它是非阻塞方式从内存池申请内存块

void *rmp_alloc(rmp_t *mp);  

image.png

  • 非阻塞方式从内存池申请内存块

在 RTOS 环境,它是非阻塞方式从内存池申请内存块
在非 RTOS 环境,它和 rmp_alloc 功能一样。它是非阻塞方式从内存池申请内存块。

void *rmp_try_alloc(rmp_t *mp);  

image.png

  • 释放内存块

在 RTOS 环境,它是阻塞方式从内存池申请内存块
在非 RTOS 环境,它是非阻塞方式从内存池申请内存块

voidrmp_free(rmp_t *mp, void *ptr);  

image.png

  • 内存池中可用内存块剩余个数
uint32_trmp_available(rmp_t *mp);  

image.png

RMP 验证

实验说明:分别通过静态和动态定义一个内存池,内存池大小为(16 * 4),每个内存块为 16,一共 4 块。连续申请 5 块,再释放第 4 块,再申请一块。

  1. 静态内存池方式:
#include "rmp.h"

int rmp_init(void)
{
    uint8_t buff[16 * 4];
    rmp_t mp;
    
    rmp_init(&mp, buff, 16, 4);

    void *mem1 = rmp_try_alloc(&mp);
    if (mem1 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem1);
    else
        rt_kprintf("mem: NULL\n");

    void *mem2 = rmp_try_alloc(&mp);
    if (mem2 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem2);
    else
        rt_kprintf("mem: NULL\n");

    void *mem3 = rmp_try_alloc(&mp);
    if (mem3 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem3);
    else
        rt_kprintf("mem: NULL\n");

    void *mem4 = rmp_try_alloc(&mp);
    if (mem4 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem4);
    else
        rt_kprintf("mem: NULL\n");

    void *mem5 = rmp_try_alloc(&mp);
    if (mem5 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem5);
    else
        rt_kprintf("mem: NULL\n");

    rmp_free(&mp, mem4);

    void *mem6 = rmp_try_alloc(&mp);
    if (mem6 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem6);
    else
        rt_kprintf("mem: NULL\n");
    return RT_EOK;
}
INIT_COMPONENT_EXPORT(rmp_init);
  1. 动态内存池方式:
#include "rmp.h"

int rmp_init(void)
{
    rmp *mp = NULL;

    mp = rmp_create(16, 4);
    if (mp == NULL)
    {
        rt_kprintf("rmp create failed\n");
        return -1;
    }

    void *mem1 = rmp_try_alloc(mp);
    if (mem1 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem1);
    else
        rt_kprintf("mem: NULL\n");

    void *mem2 = rmp_try_alloc(mp);
    if (mem2 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem2);
    else
        rt_kprintf("mem: NULL\n");

    void *mem3 = rmp_try_alloc(mp);
    if (mem3 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem3);
    else
        rt_kprintf("mem: NULL\n");

    void *mem4 = rmp_try_alloc(mp);
    if (mem4 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem4);
    else
        rt_kprintf("mem: NULL\n");

    void *mem5 = rmp_try_alloc(mp);
    if (mem5 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem5);
    else
        rt_kprintf("mem: NULL\n");

    rmp_free(&mp, mem4);

    void *mem6 = rmp_try_alloc(mp);
    if (mem6 != NULL)
        rt_kprintf("mem: 0x%08X\n", (int)mem6);
    else
        rt_kprintf("mem: NULL\n");
    
    return RT_EOK;
}
INIT_COMPONENT_EXPORT(rmp_init);
  1. 实验运行结果:成功申请 4 次,失败 1 次,然后释放了 1 次,又可以申请 1 次。

Image

总结:

1. rmp 是一个非常高效且安全的内存池组件。

2. rmp 可以减少对 malloc 和 free 的使用,避免内存碎片的产生。

3. rmp 已经集成到 RT-Thread 的软件包中,可在 RT-Thread 的软件包下载

Image

4. 开源链接:https://github.com/RiceChen0/rmp

5. 在设计 rmp 的同时,也发现 RT-Thread 内核自带的内存池是很浪费空间并且时间上不是最佳的,我也给 RT-Thread 社区提了一个对应的 issue。

Image

END

作者:RiceChen
原文:Rice嵌入式

推荐阅读

欢迎关注Rice 嵌入式开发技术分享极术专栏,欢迎添加极术小姐姐微信(id:aijishu20)加入技术交流群,请备注研究方向。
推荐阅读
关注数
1761
内容数
52
一个周末很无聊的嵌入式软件工程师,写写经验,写写总结。
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息