封装通常会降低效率,但能够带来诸如通用性提升等好处,idf在tlsf
的基础上增加封装,其作用可以概括为以下两点:
tlsf
没办法满足idf的需要,比如不支持内存的caps
,没有堆调试特性,因而增加上层封装以提供更多的功能还是从结构来看上层封装的具体设计与实现。上层封装更具体的说,有两层,分别是multi heap
层和更上层的heap caps
层(不妨就这么称呼吧)。其实现分别位于multi_heap.c
以及heap_caps.c
。先看multi heap
层,这一层提供了可选的堆调试配置项,这里仅介绍不含堆调试的部分(堆调试以及上层的堆初始化会在后面的博客中专门介绍)。
multi heap
译为多堆
,因为esp32系列芯片不止一块内存,比如SRAM、RTC fast mem等,且每块内存也可能因为内存保留而被进一步分割。因此最终就会存在多个连续且相互之间不连续的内存区域,这些内存区域都会建堆进行管理,那么就会有多个堆,故而叫多堆(我猜的)。multi heap
层定义了struct multi_heap_info
来管理堆,这个结构的内容如下:
typedef struct multi_heap_info {
void *lock;
size_t free_bytes;
size_t minimum_free_bytes;
size_t pool_size;
tlsf_t heap_data;
} heap_t;
字段不是很多,下面一一介绍:
pool
)有个恶心的地方,struct multi_heap_info
被重命名为heap_t
(尽管只在源文件中,没有暴露出去),而heap caps
层的元数据结构也叫heap_t
,注意区分。
一个完整的堆包括元数据和数据部分,idf将其中的数据部分称为pool
。需要注意的是,不同层次上来看,pool
所表示的范围是不一样的。比如tlsf
堆的pool
就是真正的数据部分,而对于multi heap
来说,pool
还包括tlsf
堆的元数据。
heap caps
层定义了heap_t
来管理堆,这个结构的内容如下:
typedef struct heap_t_ {
uint32_t caps[SOC_MEMORY_TYPE_NO_PRIOS];
intptr_t start;
intptr_t end;
multi_heap_lock_t heap_mux;
multi_heap_handle_t heap;
SLIST_ENTRY(heap_t_) next;
} heap_t;
上述字段的含义如下:
soc_memory_type_desc_t
的caps
字段拷贝而来,会影响内存申请的优先顺序multi_heap.c
以及heap_caps.c
的层次关系如下:
先梳理一下接口调用情况:
然后梳理一遍源码:
malloc
函数调用heap_caps_malloc_default
实现功能,后者主要调用heap_caps_malloc_base
实现功能:
IRAM_ATTR void *heap_caps_malloc_default( size_t size )
{
/* 若未使能在外部RAM(指SPI RAM)中申请内存,则始终在内部RAM中申请 */
/* 可以调用接口heap_caps_malloc_extmem_enable来使能,并指定malloc_alwaysinternal_limit */
if (malloc_alwaysinternal_limit==MALLOC_DISABLE_EXTERNAL_ALLOCS) {
return heap_caps_malloc( size, MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL);
} else {
void *r;
/* 若使能在外部RAM(指SPI RAM)中申请内存,待申请内存小于阈值则在内部RAM申请 */
if (size <= (size_t)malloc_alwaysinternal_limit) {
r=heap_caps_malloc_base( size, MALLOC_CAP_DEFAULT | MALLOC_CAP_INTERNAL );
/* 否则在外部RAM申请(外部RAM容量比内部RAM大很多) */
} else {
r=heap_caps_malloc_base( size, MALLOC_CAP_DEFAULT | MALLOC_CAP_SPIRAM );
}
if (r==NULL) {
/* 申请失败的话就放款caps再挣扎一次 */
r=heap_caps_malloc_base( size, MALLOC_CAP_DEFAULT );
}
/* 还是申请失败 */
if (r==NULL){
/* 则执行内存申请失败时的回调函数 */
/* 回调函数可通过heap_caps_register_failed_alloc_callback注册 */
/* 并且可以配置在回调函数执行完之后系统abort */
heap_caps_alloc_failed(size, MALLOC_CAP_DEFAULT, __func__);
}
return r;
}
}
heap_caps_malloc_base
IRAM_ATTR static void *heap_caps_malloc_base( size_t size, uint32_t caps)
{
void *ret = NULL;
/* 申请内存的大小有限制(这个限制取决于具体硬件) */
if (size > HEAP_SIZE_MAX) {
return NULL;
}
if (caps & MALLOC_CAP_EXEC) {
/* 指定的caps若含有MALLOC_CAP_EXEC,则意味着需要的内存是IRAM(I/DRAM也行,但地址得是ibus地址) */
/* 此时caps不能再包含MALLOC_CAP_8BIT或MALLOC_CAP_DMA */
/* MALLOC_CAP_8BIT或MALLOC_CAP_DMA应该用于数据(DRAM) */
if ((caps & MALLOC_CAP_8BIT) || (caps & MALLOC_CAP_DMA)) {
return NULL;
}
caps |= MALLOC_CAP_32BIT; // IRAM is 32-bit accessible RAM
}
if (caps & MALLOC_CAP_32BIT) {
/* 32-bit可访问的当然要求4字节对齐 */
size = (size + 3) & (~3); // int overflow checked above
}
/* 遍历heap_t的caps数组 */
for (int prio = 0; prio < SOC_MEMORY_TYPE_NO_PRIOS; prio++) {
heap_t *heap;
/* 遍历所有堆串成的单向链表 */
/* heap_t的caps数组和堆注册(介绍堆初始化的时候会进一步说明)的顺序共同决定了内存申请的优先级 */
SLIST_FOREACH(heap, ®istered_heaps, next) {
if (heap->heap == NULL) {
continue;
}
if ((heap->caps[prio] & caps) != 0) {
/* 若当前heap->caps[prio]全部或部分含有指定的caps */
/* 则查看当前堆所有属性是否可以完全满足指定的caps */
if ((get_all_caps(heap) & caps) == caps) {
/* 可以满足的话,就可以进行内存申请了 */
/* 若需要在IRAM中申请内存,但当前堆在I/DRAM */
/* 那么申请也是可以申请的,但要额外做一些事情 */
if ((caps & MALLOC_CAP_EXEC) && esp_ptr_in_diram_dram((void *)heap->start)) {
/* 先申请内存,且多申请4字节 */
ret = multi_heap_malloc(heap->heap, size + 4);
if (ret != NULL) {
/* 若能成功申请到,则将dbus地址转换为相应的ibus地址 */
/* 且把dbus地址藏在位于开头的多申请出的4个字节里 */
/* 最终返回给用户的是ibus地址 */
return dram_alloc_to_iram_addr(ret, size + 4);
}
} else {
/* 通常会在这个分支申请内存(没什么特殊情况) */
ret = multi_heap_malloc(heap->heap, size);
if (ret != NULL) {
return ret;
}
}
}
}
}
}
/* 没有申请到内存,则返回NULL */
return NULL;
}
在未使能堆调试功能时,multi_heap_malloc
的实现很简单:
void *multi_heap_malloc(multi_heap_handle_t heap, size_t size)
__attribute__((alias("multi_heap_malloc_impl")));
......
void *multi_heap_malloc_impl(multi_heap_handle_t heap, size_t size)
{
if (size == 0 || heap == NULL) {
return NULL;
}
/* 上锁 */
multi_heap_internal_lock(heap);
/* 申请可用内存块 */
void *result = tlsf_malloc(heap->heap_data, size);
if(result) {
/* 若成功申请到内存 */
/* 则减少空闲内存记录值 */
heap->free_bytes -= tlsf_block_size(result);
heap->free_bytes -= tlsf_alloc_overhead();
/* 更新历史最低空闲内存总容量 */
if (heap->free_bytes < heap->minimum_free_bytes) {
heap->minimum_free_bytes = heap->free_bytes;
}
}
/* 解锁 */
multi_heap_internal_unlock(heap);
return result;
}
同样的,先梳理一下接口调用情况:
然后梳理一遍源码:
free
函数调用heap_caps_free
实现功能,heap_caps_free
主要调用multi_heap_free
实现功能:
IRAM_ATTR void heap_caps_free( void *ptr)
{
if (ptr == NULL) {
return;
}
/* 对于I/DRAM中的ibus地址,申请的时候是做了一些特殊操作的 */
if (esp_ptr_in_diram_iram(ptr)) {
/* 把藏起来的dbus地址取出 */
uint32_t *dramAddrPtr = (uint32_t *)ptr;
ptr = (void *)dramAddrPtr[-1];
}
/* 遍历所有堆,检查这是不是一个合法的地址(位于某个堆的地址范围内) */
heap_t *heap = find_containing_heap(ptr);
assert(heap != NULL && "free() target pointer is outside heap areas");
/* 释放内存,地址是申请的时候藏起来的dbus地址 */
multi_heap_free(heap->heap, ptr);
}
在未使能堆调试功能时,multi_heap_free
的实现很简单:
void multi_heap_free(multi_heap_handle_t heap, void *p)
__attribute__((alias("multi_heap_free_impl")));
......
void multi_heap_free_impl(multi_heap_handle_t heap, void *p)
{
if (heap == NULL || p == NULL) {
return;
}
assert_valid_block(heap, block_from_ptr(p));
/* 上锁 */
multi_heap_internal_lock(heap);
/* 释放之后增加相应的空闲内存 */
heap->free_bytes += tlsf_block_size(p);
heap->free_bytes += tlsf_alloc_overhead();
/* 释放内存块 */
tlsf_free(heap->heap_data, p);
/* 解锁 */
multi_heap_internal_unlock(heap);
}
堆的完整性检测接口主要有:
这些接口逻辑很简答,没什么好说的,它们主要调用multi_heap_check
实现功能,而multi_heap_check
会先后调用tlsf
堆的tlsf_check
和tlsf_check_pool
对tlsf
堆进行完整性检查,不多赘述了。
搞明白元数据和层次关系,其它接口应该也就不难理解了,本文就不赘述了。
[1] esp-idf