Zephyr内存管理之slab

Creative Commons
本作品采用知识共享署名

本文介绍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中对其进行初始化:

1
2
3
4
5
6
7
8
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有两种方式,一种是使用下面宏进行定义

1
2
3
4
5
6
#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无法赋值外,其它的字段都可以直接初始化:

1
2
3
4
5
6
7
8
9
10
11
#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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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进行初始化, 下面两种方式是等效的
宏定义初始化

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

代码初始化

1
2
3
4
5
6
7
8
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);
}

初始化函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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),代码分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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),代码分析如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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