内存池

内存池(memory pool) 是一个内核对象,它允许从指定的内存区域上动态地分配内存块(memory block)。同一个内存池中的内存块的大小是不固定的,这样可以减小由于不同的应用程序需要为大小不同的数据结构分配不同的存储空间所造成的浪费。内存池使用“伙伴(buddy)内存分配”算法,它可以高效地将大块内存分割为小块内存。此外,它还可以在最大限度减小内存碎片的前提下,高效地分配和释放不小不同的内存块。

概念

可以定义任意数量的内存池。每个内存池通过其内存地址进行引用。

内存池的关键属性包括:

  • 块最小尺寸:以字节为单位。大于等于 4X 字节,其 0。
  • 块最大尺寸:以字节为单位。等于 “块最小尺寸”*4^Y,其中 Y 大于等于 0。
  • 最大尺寸块的数量:大于 0。
  • buffer:内存池的块的实际内存区域。它必须大于等于 “块最大尺寸”*“最大尺寸块的数量”。

内存片的 buffer 必须 N 字节对齐,其中 N 是大于 2 的 2 的整数次幂(例如 4,8,16...)。为了保证 buffer 中的所有内存块都对齐到这个边界,块的大小必须是 N 的整数倍。

当线程需要内存块时,它只需要从一个内存池中申请。申请成功后,由线程提供的块描述符的 data 字段表示该内存块的起始地址。当线程使用完内存块后,它必须将其释放给内存池,让其可以重复利用。

如果没有找到所期望的内存块,线程可以等待,直到某个块可用。多个线程可以同时等待某个空的内存池;当某个内存块可用时,它会被分配给优先级最高的、等待时间最久的线程使用。

与堆不同的是,如果有需要,可以定义多个内存片。例如不同的应用程序可以利用不同的内存池。这样可以阻止某个应用程序“绑架”所有资源。

内部操作

内存片的 buffer 是一个数组,数组的元素则是大小固定的块,这样能保证在块与块之间没有空间被浪费。

内存片使用一个链表来跟踪未使用的块。每个未使用块的前 4 个字节用于提供链接信息。

内存池的 buffer 是一个数组,数组的元素的大小是块的最大尺寸,这样能保证块与块之间没有空间被浪费。每个“第 0 级”的块是一个 quad-block,(如果有需要)可以被分为四个小的大小相等的“第 1 级”块。类似地,每个第 1 级块也是 quad-block,也可以被分为四个小的大小相等的“第 2 级”块。依次类推。因此,每个内存块都可以递归地分为四分之一的小块,知道小块的尺寸不满足块最小尺寸。

内存池通过一个叫做 块集(block set) 的数据结构来跟踪它的 buffer 空间的分区情况。内存池为所支持的每一个划分等级或者每一个块尺寸都维持了一个块集。每个块集使用一个叫做 quad-block 状态 的数据结构的数组来跟踪它所关联的尺寸的所有空闲块。

当应用程序请求一个内存块时,内存池首先会判断最小块的尺寸是否满足请求,并检查其相应的块集。如果块集包含有一个空闲块,它会将该块标记为以使用,然后分配过程就结束了。如果该块集不包含空闲块,内存池将尝试将一个更大尺寸的空闲块分类成小的块,或者将小块合并为大的块。如果不能创建这样的块,则分配失败。

注解

默认情况下,内存池会先去分裂一个大的块,失败后才会合并小的块。不过这是可以配置的,可以先合并小的块,或者快过合并小块的过程。在后一种情况下,只有当应用程序明确地发出对整个内存池去碎片的请求时才会合并小块。

内存池的块合并和分裂过程是非常高效的,但是它采用的是递归算法,因此很容易产生显著的开销。此外,合并算法不能将大小不同的相邻块结合在一起,也不能合并不属于同一个父 quad-block 的尺寸相同的相邻块。因此,使用内存池时依然会遇到碎片问题。

当应用程序释放一个已分配的内存块时,仅仅会在该内存块所关联块集中将其标记为空闲。内存池不会尝试合并最近释放的块,这样的好处是可以很方便地在其已存在的组织上进行重分配。

实现

定义内存池

使用类型为 struct k_mem_pool 的变量可以定义一个内存池。不过,由于内存池也需要大量的尺寸可变的数据结构来代表它的块集合和它的 quad-block 的状态,内核不支持在运行时动态地定义内存池。内存池只能使用 K_MEM_POOL_DEFINE 在编译时进行定义和初始化。

下面的代码定义并初始化了一个内存池,这个内存池有三个大小为 4096 字节的块。这些块也可以被划分为最小为 64 字节的 4 字节对齐的子块。(也就是说,内存池支持的块大小是 4096、1024、256 和 64 字节。)注意,该宏定义了内存池的所有数据结构和它的 buffer。

K_MEM_POOL_DEFINE(my_pool, 64, 4096, 3, 4);

分配内存块

函数 k_mem_pool_alloc() 用于分配内存块。

下面的代码会先等待 100 毫秒,以拿到一个 200 字节的可以内存块,然后将其填充为零。如果没有获得合适的内存块,代码会打印一个警告信息。

注意,应用程序实际会接收到一个大小为 256 字节的内存块,因为这是内存池所支持的最接近的尺寸。

struct k_mem_block block;

if (k_mem_pool_alloc(&my_pool, &block, 200, 100) == 0)) {
    memset(block.data, 0, 200);
    ...
} else {
    printf("Memory allocation time-out");
}

释放内存块

函数 k_mem_pool_free() 用于释放内存块。

下面的代码基于上面的例程之上,它申请了 75 字节的内存块,并在不再使用时释放。(基于安全考虑,实际上会从堆内存池使用 256 字节的内存块。)

struct k_mem_block block;

k_mem_pool_alloc(&my_pool, &block, 75, K_FOREVER);
... /* use memory block */
k_mem_pool_free(&block);

内存池手工去碎片

这段代码指示内存池尽可能地将未使用的内存块合并到它们的父 quad-block 里面。每次收到内存块分配请求时,内存池内部都会自动地去除部分碎片,但是在分配大量内存块前对整个内存池做完全的去碎片化的效率更高。

k_mem_pool_defragment(&my_pool);

建议的用法

当需要分配大小不固定的内存时,可以使用内存池。

当一个线程需要给另一个线程发送大量的数据时,可以使用内存池,这样可以避免不必要的数据拷贝。

配置选项

相关的配置选项:

  • CONFIG_MEM_POOL_SPLIT_BEFORE_DEFRAG
  • CONFIG_MEM_POOL_DEFRAG_BEFORE_SPLIT
  • CONFIG_MEM_POOL_SPLIT_ONLY

API

头文件 kernel.h 中提供了如下的内存池 API:

  • K_MEM_POOL_DEFINE
  • k_mem_pool_alloc()
  • k_mem_pool_free()
  • k_mem_pool_defrag()