Zephyr内存管理之slab

本文介绍Zephyr内存管理器和Slab的实现。

Zephyr内存管理 链接到标题

Zephyr内核提供了slab和heap两种动态内存分配方式,同时Zephyr也可以通过配置使用newlib中的malloc/free进行动态内存分配。对于实时操作系统来说,希望动态分配内存的执行时间是确定的,建议在实际开发中使用Zephyr内核提供的slab和heap来进行动态内存分配。Zephyr原本还有一个Pool的管理方式,现在已经决定将废弃k_mem_pool,目前最新的代码k_mem_pool虽然存在,但其后端任然是使用heap实现。

本文先介绍说明Slab。

Slab 链接到标题

Slab分配器是一个Zephyr内核对象,通过Slab分配器可以从指定的内存区域动态分配内存块,Slab管理的内存块大小相同且固定,可以避免产生内存碎片,并能高效快速的分配和释放内存。Slab作为Zephyr内核对象,允许在分配不到内存块时进行等待,等到超时退出或者其它Slab用户释放内存为止。Slab常用于对内存需求是固定大小的情况。在一个系统中可以定义多个Slab,每个Slab能分配出来的内存块大小不一样,以满足不同功能模块的需求。

Slab实现 链接到标题

初始化Slab时,会先声明一片内存作为Slab的缓存区,Slab将这边缓存区划分为等大小的内存块,并使用一个单链表将这些内存块串联起来进行分配管理。缓存区内存的起始地址必须2的幂对齐,且要大于4.每个块大小为4的倍数个字节块,Slab中块的数量必须大于0。一个Slab缓存区内存的大小就是块大小剩余块数量。

每个Slab都将维护一个struct k_mem_slab,在k_mem_slab_init中对其进行初始化:

struct k_mem_slab {
	_wait_q_t wait_q;
	uint32_t num_blocks;
	size_t block_size;
	char *buffer;
	char *free_list;
	uint32_t num_used;
};
  • wait_q: 在slab无空闲块时,申请slab内存的thread将被放入该wait_q

  • num_blocks: slab管理的block总数

  • block_size:slab管理的block大小

  • buffer: slab的缓存区,大小为num_blocks*block_size

  • free_list: slab空闲链表,slab中空闲的块将以单链表的的形式串接起来

  • num_used:slab中已经被分配的block数量

k_mem_slab_init中将建立slab的free_list,将buffer按照block_size大小分割为num_blocks块。每个块的最低4字节保存指向下一个块的指针,从高地址开始依次顺序的将各个块链成一个单链表,如下图(1)。

初始化slab 链接到标题

初始化slab有两种方式,一种是使用下面宏进行定义

#define K_MEM_SLAB_DEFINE(name, slab_block_size, slab_num_blocks, slab_align) \
	char __noinit __aligned(WB_UP(slab_align)) \
	   _k_mem_slab_buf_##name[(slab_num_blocks) * WB_UP(slab_block_size)]; \
	Z_STRUCT_SECTION_ITERABLE(k_mem_slab, name) = \
		Z_MEM_SLAB_INITIALIZER(name, _k_mem_slab_buf_##name, \
					WB_UP(slab_block_size), slab_num_blocks)

通过宏K_MEM_SLAB_DEFINE定义slab,会定义一个全局数组_k_mem_slab_buf_##name,大小为slab_num_blocks*slab_block_size,在定义一个全局的struct k_mem_slab变量name,该变量被放到名为k_mem_slab的section中,并通过宏Z_MEM_SLAB_INITIALIZER对该结构体变量进行初始化赋值,这里除了free_list无法赋值外,其它的字段都可以直接初始化:

#define Z_MEM_SLAB_INITIALIZER(obj, slab_buffer, slab_block_size, \
			       slab_num_blocks) \
	{ \
	.wait_q = Z_WAIT_Q_INIT(&obj.wait_q), \
	.num_blocks = slab_num_blocks, \
	.block_size = slab_block_size, \
	.buffer = slab_buffer, \
	.free_list = NULL, \
	.num_used = 0, \
	_OBJECT_TRACING_INIT \
	}

free_list将在slab模块初始化时SYS_INIT(init_mem_slab_module, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_OBJECTS);init_mem_slab_module中对k_mem_slab section进行遍历取出每个定义好的struct k_mem_slab的全局变量,对其buffer进行分割,建立出free_list

static int init_mem_slab_module(struct device *dev)
{
	int rc = 0;

	Z_STRUCT_SECTION_FOREACH(k_mem_slab, slab) {    //遍历k_mem_slab
		rc = create_free_list(slab);                //创建free_list
		if (rc < 0) {
			goto out;
		}
	}

out:
	return rc;
}

第二种初始化方式,在运行时使用k_mem_slab_init进行初始化, 下面两种方式是等效的 宏定义初始化

K_MEM_SLAB_DEFINE(test_slab, TEST_BLOCK_SIZE, TEST_BLOCK_COUNT, sizeof(void *))

代码初始化

struct k_mem_slab test_slab;
static uint8_t __noinit __aligned(sizeof(void *))
		test_slab_buf[(TEST_BLOCK_SIZE * TEST_BLOCK_COUNT)];

void thread()
{
    k_mem_slab_init(&test_slab, test_slab_buf, TEST_BLOCK_SIZE, TEST_BLOCK_COUNT);
}

初始化函数如下

int k_mem_slab_init(struct k_mem_slab *slab, void *buffer,
		    size_t block_size, uint32_t num_blocks)
{
	int rc = 0;
    //slab内存初始化
	slab->num_blocks = num_blocks;
	slab->block_size = block_size;
	slab->buffer = buffer;
	slab->num_used = 0U;

    //建立free list
	rc = create_free_list(slab);
	if (rc < 0) {
		goto out;
	}

    //初始化wait q
	z_waitq_init(&slab->wait_q);
	SYS_TRACING_OBJ_INIT(k_mem_slab, slab);

	z_object_init(slab);

out:
	return rc;
}

分配slab内存 链接到标题

将直接将free_list指向的块分配给申请者,果分配内存时发现free_listNULL说明slab内的块已经被分配完,将根据timeout进行等待。如图中(2)(3),代码分析如下:

int k_mem_slab_alloc(struct k_mem_slab *slab, void **mem, k_timeout_t timeout)
{
	k_spinlock_key_t key = k_spin_lock(&lock);
	int result;

	if (slab->free_list != NULL) {      //free_list不为NULL,有空闲内存块,直接分配
		*mem = slab->free_list;
		slab->free_list = *(char **)(slab->free_list);
		slab->num_used++;
		result = 0;
	} else if (K_TIMEOUT_EQ(timeout, K_NO_WAIT)) {  //没有空闲块且不等待,返回错误
		/* don't wait for a free block to become available */
		*mem = NULL;
		result = -ENOMEM;
	} else {        //等待其它线程释放
		result = z_pend_curr(&lock, key, &slab->wait_q, timeout);
		if (result == 0) {
            //等待成功获取到空闲内存块
			*mem = _current->base.swap_data;
		}
		return result;
	}

	k_spin_unlock(&lock, key);

	return result;

释放slab内存 链接到标题

释放slab内存时,会先检查slab内的wait_q是否有其它线程在等待分配内存块,如果有则将要释放的内存块提供给等待的线程。如果没有线程等待,释放的内存块将会被加入到free_list.如图中(4)(5),代码分析如下:

void k_mem_slab_free(struct k_mem_slab *slab, void **mem)
{
	k_spinlock_key_t key = k_spin_lock(&lock);
    //检查是否有线程在等待空闲的内存
	struct k_thread *pending_thread = z_unpend_first_thread(&slab->wait_q);

	if (pending_thread != NULL) {       //如果有线程等待,将释放的内存块提供给等待线程
		z_thread_return_value_set_with_data(pending_thread, 0, *mem);
		z_ready_thread(pending_thread);
		z_reschedule(&lock, key);
	} else {        //如果没有线程在等待,将释放的内存块加入到free_list中
		**(char ***)mem = slab->free_list;
		slab->free_list = *(char **)mem;
		slab->num_used--;
		k_spin_unlock(&lock, key);
	}
}

Slab使用建议 链接到标题

Slab是一个内核对象,在分配时会存在等待资源的情况,释放时可能会从其它线程获取资源的情况,因此Slab分配和释放都有可能会引起线程的调度。 在需要定长内存的分配情况下,优先使用Slab。当从一个线程发送大量数据到另一个线程时,可以使用Slab,之发送内存块地址,可以避免不必要的数据拷贝动作。

参考 链接到标题

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