Zephyr内存管理之Pool

本文通过分析代码介绍Zephyr Pool内存管理的实现。

sys_pool为Zephyr中Pool的内存分配管理算法,支持创建用户和内核两种模式的Pool,在Zephyr内存管理之Heap一文中已提到过在早期的Zephyr中内核使用Pool进行动态内存管理,k_malloc/k_free等函数都是使用的Pool管理内存。在最新的Zephyr中已经通过默认配置CONFIG_MEM_POOL_HEAP_BACKEND=y,将内核的动态内存管理变为Heap来实现。虽然Pool已经从Zephyr的Kernel中移除,但Zephyr的内置的minimal stdlib动态分配内存和Lvgl的porting依然使用的是Pool,因此分析Pool的内存管理原理还是有实际意义。

Pool管理的是一片固定大小的连续内存区域,用户可以在Pool管理的内存区域中动态分配指定长度的内存。sys_pool采用伙伴系统内存内存管理算法,sys_pool本身时是线程安全的管理算法。其代码在下列几个位置:

  • include/sys/mempool_base.h : sys_pool基础的数据结构,宏和函数
  • lib/os/mempool.h :sys_pool对外的数据结构和函数
  • lib/os/mempool.c :sys_pool实现代码

Block 链接到标题

使用伙伴管理系统的Pool中内存以Block的形式进行管理,从Pool中分配内存就是取出一个可用的Block,在初始化Block时会指定最大的Block和最先的Block,每次分配内存的过程就是从较大的Block向较小的Block进行分割,直到分割出来的大小和分配内存大小一致。从Pool中能够分配最大内存不能超过最大的Block,分配的内存小于最小的Block就会给与最小的Block。在Zephyr中伙伴系统的数量是4,也就是说一次分割会将大的Block分为4个小的Block,然后取出其中一个Block进行分配或者进行下一次分割。

在Pool中最大的Block处于level 0,每分割一次level增加1,在每个level中对block依次编号,一个block的位置和大小可以由level和block编号唯一确认。一个Block的数据结构如下:

struct sys_mem_pool_block {
	struct sys_mem_pool *pool;
	uint32_t level : 4;
	uint32_t block : 28;
};

pool 指向block所属的pool控制块。 level block的level,占4bit,最大为15,最大的block最多可以被分为$4^{15}=65536$个block block block的编号,占28bit

任意一个被分配的block的最开始都会放置一个struct sys_mem_pool_block用于管理该block,从该结构体之后的内存才是分配出的可用内存。Pool中空闲的block会在最开始放置sys_dnode_t,将该Block放入空闲链表。

Pool管理 链接到标题

Pool定义 链接到标题

在使用Pool前需要先定义Pool,定义Pool主要是完成Pool内存的分配和Pool管理结构的分配,在mempool.h中提供了下面宏定义一个Pool:

#define SYS_MEM_POOL_DEFINE(name, ignored, minsz, maxsz, nmax, align, section)

name, pool的名字 ignored, 忽略字段无意义 minsz, pool中最小的block size,长度必须是4的倍数且大于0 maxsz, pool中最大的block size,长度必须是minsz的倍数,且倍数是4的幂(1,4,16,64,…) nmax, 最大block的数量, pool管理的内存空间maxsz*nmax align, pool buffer起始地址对齐,必须是2的幂,且必须 要大于等于4 section, pool放置的section名

使用SYS_MEM_POOL_DEFINE定义Pool代码如下:

#define SYS_MEM_POOL_DEFINE(name, ignored, minsz, maxsz, nmax, align, section) \
	char __aligned(WB_UP(align)) Z_GENERIC_SECTION(section)		\
		_mpool_buf_##name[WB_UP(maxsz) * nmax			\
				  + _MPOOL_BITS_SIZE(maxsz, minsz, nmax)]; \
	struct sys_mem_pool_lvl Z_GENERIC_SECTION(section)		\
		_mpool_lvls_##name[Z_MPOOL_LVLS(maxsz, minsz)];		\
	Z_GENERIC_SECTION(section) struct sys_mem_pool name = {		\
		.base = {						\
			.buf = _mpool_buf_##name,			\
			.max_sz = WB_UP(maxsz),				\
			.n_max = nmax,					\
			.n_levels = Z_MPOOL_LVLS(maxsz, minsz),		\
			.levels = _mpool_lvls_##name,			\
			.flags = SYS_MEM_POOL_USER			\
		}							\
	};	
  • 定义一个align对齐的全局数组作为Pool buffer。该数组前maxsz*nmax字节为Pool内存分配区,的该数组的末尾还有一段bitmap用于标记block是否被使用。
  • 根据内存分配区的大小计算出block level总数n_levels,定义一个含有n_levels个struct sys_mem_pool_lvl的结构体数组,用于管理不同level的block。
  • 定义一个结构体变量struct sys_mem_pool,该结构体变量用于管理和存储Pool的信息。

level 链接到标题

一个Pool的level由其定义时的maxsz和minsz决定,maxsz通过4分裂变为minsz的次数就是一个Pool的level数,例如maxsz每次分成等分的4块,连续分裂3次后变为minsz,即maxsz/4/4/4 = minsz,那么level就是3。 Pool为每个level的block提供一个被名为struct sys_mem_pool_lvl的管理结构,该结构中包含一个free_list和一个bitmap,对应level的空闲的block将加入到free_list,当block被使用时会从free_list中取出,被取出的block在bitmap中对应的bit将被置1表示已经被使用。

struct sys_mem_pool_lvl {
	union {
		uint32_t *bits_p;
		uint32_t bits[sizeof(uint32_t *)/4];
	};
	sys_dlist_t free_list;
};

bits:block使用情况的bitmap,当level管理的block小于等于32个时使用bits存储bitmap bits_pbits_pbits共用4字节空间, 当level管理的block大于32个的时候bits已经无法直接存储,因此当作bits_p用,放置一个指针,指向在Pool buffer末尾更大的bitmap。某个level的block bitmap能够在bits中直接存储,这个level被称作inline level。 free_list:对应level下的空闲block将会加入到该双向链表中进行管理

Pool buffer管理结构 链接到标题

Pool buffer的管理结构如下,定义的时候将对其中的struct sys_mem_pool_base进行初始化

struct sys_mem_pool {
	struct sys_mem_pool_base base;
	struct sys_mutex mutex;
};

base:用于存储和管理Pool的信息 mutex:内存分配和释放操作时上锁,保证sys_pool操作的线程安全。

struct sys_mem_pool_base {
	void *buf;
	size_t max_sz;
	uint16_t n_max;
	uint8_t n_levels;
	int8_t max_inline_level;
	struct sys_mem_pool_lvl *levels;
	uint8_t flags;
};

buf:指向Pool buffer max_sz:最大block的大小 n_max:最大block的数量,实际管理的pool buffer大小就是n_max*max_sz n_levels:Pool的level max_inline_level:最大的inline level,超过该max_inline_level的level bitmap存放在pool buffer后面的bitmap。 levels:指向struct sys_mem_pool_lvl管理结构数组。 flags:Pool属性,SYS_MEM_POOL_KERNELSYS_MEM_POOL_USER可选,当选择kernel时在pool内存管理操作时会锁irq保护。由于目前Pool不再被用于内核,而使用SYS_MEM_POOL_DEFINE定义的pool都将使用SYS_MEM_POOL_USER

Pool初始化 链接到标题

Pool初始化主要是完成free_list的建立和max_inline_level的确认,代码如下:

static inline void sys_mem_pool_init(struct sys_mem_pool *p)
{
	z_sys_mem_pool_base_init(&p->base);
}


void z_sys_mem_pool_base_init(struct sys_mem_pool_base *p)
{
	int i;
	size_t buflen = p->n_max * p->max_sz, sz = p->max_sz;
	uint32_t *bits = (uint32_t *)((uint8_t *)p->buf + buflen);

	p->max_inline_level = -1;

	//为每一级level进行初始化
	for (i = 0; i < p->n_levels; i++) {
		//计算当前level的总计block数量
		size_t nblocks = buflen / sz;

		//初始化当前level block的链表
		sys_dlist_init(&p->levels[i].free_list);

		//如果当前level的block <= 32,归为在线level管理,否则放入bitmap,一个bit对应一个block
		if (nblocks <= sizeof(p->levels[i].bits)*8) {
			p->max_inline_level = i;    //更新max_inline_level
		} else {
			//计算levels的bit在bitmap中的位置
			p->levels[i].bits_p = bits;
			bits += (nblocks + 31)/32;
		}
		
		//计算下一个level的block size大小
		sz = WB_DN(sz / 4);
	}

	//如果有多个最大block, 需要加入到free_list内
	for (i = 0; i < p->n_max; i++) {
		//计算每个block在pool中的地址
		void *block = block_ptr(p, p->max_sz, i);
		//将顶级的block加入到顶级level list中
		sys_dlist_append(&p->levels[0].free_list, block);
	}
}

分配内存 链接到标题

从Pool中分配内存,就是从Pool中找到和要分配内存大小匹配的Block,如果没有匹配的Block就从大的Block分裂出来匹配的Block,分配内存代码分析如下:

void *sys_mem_pool_alloc(struct sys_mem_pool *p, size_t size)
{
	struct sys_mem_pool_block *blk;
	uint32_t level, block;
	char *ret;
	
	//多线程保护
	sys_mutex_lock(&p->mutex, K_FOREVER);

	//分配的内存以block管理,在最开始放置block header信息,后面跟随的是可用内存
	size += WB_UP(sizeof(struct sys_mem_pool_block));
	
	//按照计算的size分配block
	if (z_sys_mem_pool_block_alloc(&p->base, size, &level, &block,
				      (void **)&ret)) {
		ret = NULL;
		goto out;
	}

	//更新分配的block信息
	blk = (struct sys_mem_pool_block *)ret;
	blk->level = level;
	blk->block = block;
	blk->pool = p;
	
	//刨除block header部分内容,返回实际可用的内存地址
	ret += WB_UP(sizeof(struct sys_mem_pool_block));
out:
	sys_mutex_unlock(&p->mutex);
	return ret;
}
int z_sys_mem_pool_block_alloc(struct sys_mem_pool_base *p, size_t size,
			      uint32_t *level_p, uint32_t *block_p, void **data_p)
{
	int i, from_l, alloc_l = -1;
	unsigned int key;
	void *data = NULL;
	size_t lsizes[LVL_ARRAY_SZ(p->n_levels)];

	//从level 0开始查找,找出和size大小最匹配的block所在的level
	lsizes[0] = p->max_sz;
	for (i = 0; i < p->n_levels; i++) {
		if (i > 0) {
			lsizes[i] = WB_DN(lsizes[i-1] / 4);
		}

		if (lsizes[i] < size) {
            //找到大小匹配的Block,退出查找,次数alloc_l是匹配Block所在level
			break;
		}

		alloc_l = i;
	}

	//要分配的size,比最大的block还大,没有匹配上,无法分配内存
	if (alloc_l < 0) {
		*data_p = NULL;
		return -ENOMEM;
	}

	key = pool_irq_lock(p);
	//从最适合的level找有没有空闲的block,如果没有,向上一层找,如果上层没有继续再向上找,直到发现有空闲的block
	for (i = alloc_l; i >= 0; i--) {
		data = block_alloc(p, i, lsizes[i]);

		//此时找到的block 的level可能会比alloc_l小很多,因此要进行block的分割
		if (data != NULL) {
			for (from_l = i; from_l < alloc_l; from_l++) {
				//分割block,并取出第一个,后面三个加入到分割后所属level的list内
				//会将第一个block一直分割下去直到得到对应level的block
				data = block_break(p, data, from_l, lsizes);
				pool_irq_unlock(p, key);
				key = pool_irq_lock(p);
			}
			break;
		}
	}
	pool_irq_unlock(p, key);

	//输出block地址
	*data_p = data;

	if (data == NULL) {
		return -ENOMEM;
	}

	//输出block level
	*level_p = alloc_l;
	//输出block 所处pool中的对应level的number
	*block_p = block_num(p, data, lsizes[alloc_l]);

	return 0;
}

分割block的流程如下:

static void *block_break(struct sys_mem_pool_base *p, void *block, int l,
				size_t *lsizes)
{
	int i, bn;
    //计算出要分割的block的Block number
	bn = block_num(p, block, lsizes[l]);
    //将该block的在bitmap内的bit置1, 被分割的block就是已被使用的block
	set_alloc_bit(p, l + 1, 4*bn);

    //只使用分割后的第一个block,后面3个block被放入free_list内备用
	for (i = 1; i < 4; i++) {
		int lsz = lsizes[l + 1];
		void *block2 = (lsz * i) + (char *)block;

		sys_dlist_append(&p->levels[l + 1].free_list, block2);
	}

	return block;
}

释放内存 链接到标题

释放内存时,即使将Block退回到Pool,如果其伙伴block空闲就进行合并,不空闲就放入free_list,代码如下:

void sys_mem_pool_free(void *ptr)
{
	struct sys_mem_pool_block *blk;
	struct sys_mem_pool *p;

	if (ptr == NULL) {
		return;
	}

	//通过内存计算出block的地址
	ptr = (char *)ptr - WB_UP(sizeof(struct sys_mem_pool_block));
	
	//取block
	blk = (struct sys_mem_pool_block *)ptr;
	p = blk->pool;

	//多线程保护
	sys_mutex_lock(&p->mutex, K_FOREVER);
	//释放对应的block
	z_sys_mem_pool_block_free(&p->base, blk->level, blk->block);
	sys_mutex_unlock(&p->mutex);
}
void z_sys_mem_pool_block_free(struct sys_mem_pool_base *p, uint32_t level,
			      uint32_t block)
{
	size_t lsizes[LVL_ARRAY_SZ(p->n_levels)];
	uint32_t i;

	//计算每个level的block size
	lsizes[0] = p->max_sz;
	for (i = 1; i <= level; i++) {
		lsizes[i] = WB_DN(lsizes[i-1] / 4);
	}

	//释放block
	block_free(p, level, lsizes, block);
}
static void *block_alloc(struct sys_mem_pool_base *p, int l, size_t lsz)
{
	sys_dnode_t *block;
	//检查对应level中是否有空闲的block
	block = sys_dlist_get(&p->levels[l].free_list);
	if (block != NULL) {
		//有空闲的block,在bitmap中将该block mark为被使用
		set_alloc_bit(p, l, block_num(p, block, lsz));
	}
	return block;
}
static void *block_break(struct sys_mem_pool_base *p, void *block, int l,
				size_t *lsizes)
{
	int i, bn;
	//计算要分割的block处于的位置
	bn = block_num(p, block, lsizes[l]);
	//将要分割的block标注为已使用
	set_alloc_bit(p, l + 1, 4*bn);

	//分割后第一个block被返回,剩余的3个block被加入到free_list中,因此i从1开始
	for (i = 1; i < 4; i++) {
		int lsz = lsizes[l + 1];
		void *block2 = (lsz * i) + (char *)block;

		sys_dlist_append(&p->levels[l + 1].free_list, block2);
	}

	return block;
}


static unsigned int bfree_recombine(struct sys_mem_pool_base *p, int level,
				    size_t *lsizes, int bn, unsigned int key)
{
	while (level >= 0) {
		//计算block的实际地址
		int i, lsz = lsizes[level];
		void *block = block_ptr(p, lsz, bn);

		/* Detect common double-free occurrences */
		__ASSERT(alloc_bit_is_set(p, level, bn),
			 "mempool double-free detected at %p", block);

		/* Put it back */
		//将block标注为未使用,并放回到对应level的free_list中
		clear_alloc_bit(p, level, bn);
		sys_dlist_append(&p->levels[level].free_list, block);

		/* Relax the lock (might result in it being taken, which is OK!) */
		pool_irq_unlock(p, key);
		key = pool_irq_lock(p);

		/* Check if we can recombine its superblock, and repeat */
		//检查当前level已经到顶级说明无法合并
		//检查当前level中其它相邻的block正在被使用,也无法合并
		//无法合并的直接退出
		if (level == 0 || partner_alloc_bits(p, level, bn) != 0) {
			return key;
		}

		//可以合并的,将block从free_list中取出,全部取出就是合并
		for (i = 0; i < 4; i++) {
			int b = (bn & ~3) + i;

			sys_dlist_remove(block_ptr(p, lsz, b));
		}

		/* Free the larger block */
		//输出合并互的信息,合并一直会从当前level向顶级level循环,直到顶级或者无法合并才会返回
		level = level - 1;
		bn = bn / 4;
	}
	__ASSERT(0, "out of levels");
	return -1;
}

Pool的使用 链接到标题

本小结简单分析Zephyr的内置的minimal stdlib如何使用Pool,代码参考lib/libc/minimal/source/stdlib/malloc.c, 为了简化分析,本文只分析非用户模式下的情况

1.定义Pool 链接到标题

非用户模式下Pool的buffer放到.data段中,管理的内存大小通过CONFIG_MINIMAL_LIBC_MALLOC_ARENA_SIZE配置

#define POOL_SECTION .data

SYS_MEM_POOL_DEFINE(z_malloc_mem_pool, NULL, 16,
		    CONFIG_MINIMAL_LIBC_MALLOC_ARENA_SIZE, 1, 4, POOL_SECTION);

2.初始化Pool 链接到标题

在系统初始化时,通过malloc_prepare初始化Pool,优先顺序属于APPLICATION级别的,这也说明后面的malloc/free也只能在驱动和kernel初始化完成后才能使用

static int malloc_prepare(struct device *unused)
{
	ARG_UNUSED(unused);

	sys_mem_pool_init(&z_malloc_mem_pool);

	return 0;
}

SYS_INIT(malloc_prepare, APPLICATION, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);

3. malloc/free 链接到标题

malloc/free非常简单,直接封装pool的分配和释放函数

void *malloc(size_t size)
{
	void *ret;

	ret = sys_mem_pool_alloc(&z_malloc_mem_pool, size);
	if (ret == NULL) {
		errno = ENOMEM;
	}

	return ret;
}

void free(void *ptr)
{
	sys_mem_pool_free(ptr);
}

参考 链接到标题

https://docs.zephyrproject.org/latest/reference/kernel/memory/pools.html