idf会在启动过程中初始化heap
组件,初始化主要做了如下几件事:
caps
一致的则合并之heap_t
将各个堆串成单向链表以便管理,表头为registered_heaps
main_task
中)将原本用于启动栈的内存归还给堆至此,铺垫结束,下面看源码。
在初始化阶段,do_core_init
会调用heap_caps_init
来初始化堆。简单浏览一遍这个函数:
void heap_caps_init(void)
{
/* 获取所有可以作为堆的memory regions */
size_t num_regions = soc_get_available_memory_region_max_count();
soc_memory_region_t regions[num_regions];
num_regions = soc_get_available_memory_regions(regions);
/* 合并地址上相邻且类型相同的memory region */
for (size_t i = 1; i < num_regions; i++) {
soc_memory_region_t *a = regions[i - 1];
soc_memory_region_t *b = regions[i];
if (b->start == (intptr_t)(a->start + a->size) && b->type == a->type ) {
a->type = -1;
b->start = a->start;
b->size += a->size;
}
}
/* 计数memory region的个数 */
size_t num_heaps = 0;
for (size_t i = 0; i < num_regions; i++) {
if (regions[i].type != -1) {
num_heaps++;
}
}
/* 每个memory region使用一个heap_t管理。这个结构先在栈上申请,等到堆注册好之后,
再从堆上申请一块内存,把栈上的heap_t拷贝到堆上。 */
heap_t temp_heaps[num_heaps];
size_t heap_idx = 0;
ESP_EARLY_LOGI(TAG, "Initializing. RAM available for dynamic allocation:");
/* 遍历合并后的各个memory region */
for (size_t i = 0; i < num_regions; i++) {
soc_memory_region_t *region = regions[i];
const soc_memory_type_desc_t *type = &soc_memory_types[region->type];
heap_t *heap = &temp_heaps[heap_idx];
if (region->type == -1) {
continue;
}
heap_idx++;
assert(heap_idx <= num_heaps);
/* 把soc_memory_type_desc_t的caps数组拷贝到heap_t的caps数组 */
/* 感觉这里直接使用指针更好,多少能省点内存 */
memcpy(heap->caps, type->caps, sizeof(heap->caps));
/* 记录当前memory region的起始和结束地址 */
heap->start = region->start;
heap->end = region->start + region->size;
/* 初始化堆使用的锁,对堆的访问应当是互斥的 */
MULTI_HEAP_LOCK_INIT(&heap->heap_mux);
if (type->startup_stack) {
/* 如果当前memory region被用于启动栈,那么就先不进行注册 */
/* 在FreeRTOS跑起来的时候(意味着启动过程结束),具体在main_task中,会归还给堆 */
heap->heap = NULL;
} else {
/* 注册堆 */
register_heap(heap);
}
/* 可以理解为把当前heap_t的next字段置为NULL */
/* 何必包一层宏呢,为了用一下类似SLIST_FOREACH这样的宏,感觉意义也不大,不如直接一点 */
/* 这里还不会把heap_t串成一个链表,串的动作在后面 */
SLIST_NEXT(heap, next) = NULL;
ESP_EARLY_LOGI(TAG, "At %08X len %08X (%d KiB): %s",
region->start, region->size, region->size / 1024, type->name);
}
assert(heap_idx == num_heaps);
assert(SLIST_EMPTY(registered_heaps));
heap_t *heaps_array = NULL;
for (size_t i = 0; i < num_heaps; i++) {
if (heap_caps_match(&temp_heaps[i], MALLOC_CAP_8BIT|MALLOC_CAP_INTERNAL)) {
/* 遍历heap_t,找到第一个其caps包含MALLOC_CAP_8BIT|MALLOC_CAP_INTERNAL的 */
/* 从中申请内存,用于存放暂存于栈上的heap_t */
heaps_array = multi_heap_malloc(temp_heaps[i].heap, sizeof(heap_t) * num_heaps);
if (heaps_array != NULL) {
break;
}
}
}
assert(heaps_array != NULL);
/* 把栈上的heap_t拷贝到堆上 */
memcpy(heaps_array, temp_heaps, sizeof(heap_t)*num_heaps);
/* 穿串儿:把heap_t串到registered_heaps */
for (size_t i = 0; i < num_heaps; i++) {
if (heaps_array[i].heap != NULL) {
/* 把struct multi_heap_info的lock指向heap_t的heap_mux */
/* 这里绕了一个弯,为什么要绕这个弯呢? */
/* multi_heap.c包含multi_heap_platform.h,也即multi_heap_lock_t暴露了 */
/* 直接把lock定义成multi_heap_lock_t不是更简洁吗? */
multi_heap_set_lock(heaps_array[i].heap, &heaps_array[i].heap_mux);
}
if (i == 0) {
SLIST_INSERT_HEAD(registered_heaps, &heaps_array[0], next);
} else {
SLIST_INSERT_AFTER(&heaps_array[i-1], &heaps_array[i], next);
}
}
}
总结一下,heap_caps_init
做了这么几件事:
下面要看的是:
soc_get_available_memory_region_max_count
用于获取所有可获得的memory region的总数量。其内容也很简单:
size_t soc_get_available_memory_region_max_count(void)
{
/* soc_memory_region_count是soc_memory_regions数组的元素个数 */
/* s_get_num_reserved_regions返回保留的memory region的个数 */
return soc_memory_region_count + s_get_num_reserved_regions();
}
至于为什么要加上保留的memory region,是因为保留一块memory region可能会将整块memory region一分为二,但因为保留内存所造成的memory region增加的数量,最大不超过保留的数量。获得了数量之后,就在栈上使用类型为soc_memory_region_t
的变长数组regions
来存放这些memory region。
s_get_num_reserved_regions
中,如果ROM_HAS_LAYOUT_TABLE
宏不为0,就会在返回的数量上再加1。这是因为ROM中的库函数使用的静态数据没法使用宏SOC_RESERVE_MEMORY_REGION
来保留,需要动态去保留:
static size_t s_get_num_reserved_regions(void)
{
size_t result = ( &soc_reserved_memory_region_end
- &soc_reserved_memory_region_start );
#if ROM_HAS_LAYOUT_TABLE
return result + 1;
#else
return result;
#endif
}
再看一眼s_prepare_reserved_regions
就更清楚了:
static void s_prepare_reserved_regions(soc_reserved_region_t *reserved, size_t count)
{
#if ROM_HAS_LAYOUT_TABLE
/* 首先动态(运行时)保留ROM中的库函数使用的静态数据 */
const ets_rom_layout_t *layout = ets_rom_layout_p;
reserved[0].start = (intptr_t)layout->dram0_rtos_reserved_start;
reserved[0].end = SOC_DIRAM_DRAM_HIGH;
/* 然后把保留的各memory region拷贝到reserved数组 */
memcpy(reserved + 1, &soc_reserved_memory_region_start, (count - 1) * sizeof(soc_reserved_region_t));
#else
memcpy(reserved, &soc_reserved_memory_region_start, count * sizeof(soc_reserved_region_t));
#endif
/* 对保留的memory region按照起始地址排序 */
qsort(reserved, count, sizeof(soc_reserved_region_t), s_compare_reserved_regions);
for (size_t i = 0; i < count; i++) {
/* 把保留的memory region的起始和结束地址都进行字对齐 */
reserved[i].start = reserved[i].start & ~3;
reserved[i].end = (reserved[i].end + 3) & ~3;
assert(reserved[i].start <= reserved[i].end);
if (i < count - 1) {
assert(reserved[i + 1].start > reserved[i].start);
/* 保留的区域不可重叠 */
if (reserved[i].end > reserved[i + 1].start) {
abort();
}
}
}
}
实际可用的memory region的数量通常会小于最大数量,接下来soc_get_available_memory_regions
就会得到所有实际可用的memory region,将这些memory region的信息填入regions
数组,并返回实际的数量:
size_t soc_get_available_memory_regions(soc_memory_region_t *regions)
{
/* 输出的memory region,将会在其中填充实际可获得的memory region */
soc_memory_region_t *out_region = regions;
/* 输入的memory region,也即memory_layout.c中定义的soc_memory_regions数组中记录的各memory region */
/* 这是对内存的简单划分,没有考虑到保留内存的情况,因此并不是实际可获得的 */
soc_memory_region_t in_regions[soc_memory_region_count];
memcpy(in_regions, soc_memory_regions, sizeof(in_regions));
soc_memory_region_t *in_region = in_regions;
/* reserved数组用于记录保留的各个memory region */
size_t num_reserved = s_get_num_reserved_regions();
soc_reserved_region_t reserved[num_reserved];
s_prepare_reserved_regions(reserved, num_reserved);
/* 遍历各输入memory region,对每个输入region,检查所有保留的region,若保留的region与其有交集,则剔除保留的部分 */
/* 将剔除后的(可能被分割成多个的)region通过regions输出给调用者 */
/* 注意,此时保留的memory region是按照起始地址排过序的 */
while (in_region != in_regions + soc_memory_region_count) {
soc_memory_region_t in = *in_region;
intptr_t in_start = in.start;
intptr_t in_end = in_start + in.size;
bool copy_in_to_out = true;
bool move_to_next = true;
for (size_t i = 0; i < num_reserved; i++) {
if (reserved[i].end <= in_start) {
/* 当前保留的region在输入region的前面,则直接检查一下个保留的region */
continue;
} else if (reserved[i].start >= in_end) {
/* 所有保留的region都在输入region的后面,则整个输入region都是实际可获得的 */
break;
} else if (reserved[i].start <= in_start &&
reserved[i].end >= in_end) {
/* 当前保留的region完整的覆盖了输入region,则整个输入region都是被保留的 */
copy_in_to_out = false;
break;
} else if (in_start < reserved[i].start &&
in_end > reserved[i].end) {
/* 当前保留的region被包含在输入region之内,则输入region被分为前后两截,中间是保留的region */
in_end = reserved[i].start;
/* 可以确定的是,前面那截是实际可获得的 */
in.size = in_end - in_start;
/* 但因为没有遍历完所有保留的region,因此后面那截可能仍然包含保留区域 */
/* 所以调整输入region,去掉保留的region以及前面那截 */
in_region->size -= (reserved[i].end - in_region->start);
in_region->start = reserved[i].end;
/* 下一轮遍历保留region时,仍然使用调整后的输入region(不能遍历下一个输入region) */
move_to_next = false;
/* 跳出本轮遍历保留region的过程 */
break;
} else if (reserved[i].start <= in_start) {
/* 当前保留region和输入region的前面重叠,去掉保留的部分 */
/* 剩下的部分可能还需要保留,因此继续遍历保留region即可 */
in.start = reserved[i].end;
in_start = in.start;
in.size = in_end - in_start;
} else {
/* 当前保留region和输入region的后面重叠,前面的部分是实际可获得的 */
/* 遍历下一个保留region时,应当会命中分支"reserved[i].start >= in_end" */
in_end = reserved[i].start;
in.size = in_end - in_start;
}
}
/* 堆本身也有一定的元数据开销,实际可获得的region特别小的话,就直接丢弃 */
if (in.size <= 16) {
copy_in_to_out = false;
}
if (copy_in_to_out) {
/* 输出实际可获得的region */
*out_region++ = in;
}
if (move_to_next) {
/* 遍历下一个输入region */
in_region++;
}
}
/* 返回所有实际可获得的region的数量 */
return (out_region - regions);
}
真正建立堆的函数是register_heap
,该函数最终会调用tlsf
层的接口来为memory region构建堆管理所需的元数据。具体的:
/* 注册堆时,传入的是heap_t类型的指针,其指向的heap_t记录了相应memory region的起始地址和结束地址 */
static void register_heap(heap_t *region)
{
/* 计算堆的大小 */
size_t heap_size = region->end - region->start;
/* 堆的大小不能超过硬件支持的最大RAM的大小,否则显然是有问题的 */
assert(heap_size <= HEAP_SIZE_MAX);
/* 调用multi_heap_register注册堆,该函数返回的地址就是region->start */
region->heap = multi_heap_register((void *)region->start, heap_size);
if (region->heap != NULL) {
ESP_EARLY_LOGD(TAG, "New heap initialised at %p", region->heap);
}
}
在未使能POISONING
时,函数multi_heap_register
只是multi_heap_register_impl
的别名:
multi_heap_handle_t multi_heap_register(void *start, size_t size)
__attribute__((alias("multi_heap_register_impl")));
使能POISONING
后,该函数是multi_heap_register_impl
的简单包装,若配置了CONFIG_HEAP_POISONING_COMPREHENSIVE
,则会在注册堆之前,向整个memory region填充特殊字节FREE_FILL_PATTERN
(0xfe),表示free
的状态:
multi_heap_handle_t multi_heap_register(void *start, size_t size)
{
#ifdef SLOW
if (start != NULL) {
memset(start, FREE_FILL_PATTERN, size);
}
#endif
return multi_heap_register_impl(start, size);
}
因此,真正注册堆的函数是multi_heap_register_impl
,在这个函数中,就可以看到tlsf
层的接口了:
multi_heap_handle_t multi_heap_register_impl(void *start_ptr, size_t size)
{
assert(start_ptr);
/* 堆本身有元数据开销,因此传入的memory region太小是无法建立堆的 */
if(size < (tlsf_size() + tlsf_block_size_min() + sizeof(heap_t))) {
//Region too small to be a heap.
return NULL;
}
/* 需要吐槽的是,这里的heap_t并非上文中的heap_t,而是struct multi_heap_info */
heap_t *result = (heap_t *)start_ptr;
/* 扣除元数据struct multi_heap_info的开销 */
size -= sizeof(heap_t);
/* 交给tlsf的内存是扣除struct multi_heap_info之后,后面的部分(这部分的起始地址记录在heap_data字段) */
result->heap_data = tlsf_create_with_pool(start_ptr + sizeof(heap_t), size);
if(!result->heap_data) {
return NULL;
}
result->lock = NULL;
/* 刚建立的堆就是tlsf的元数据加一整个空闲块 */
result->free_bytes = size - tlsf_size();
/* pool的大小是包括tlsf元数据的 */
result->pool_size = size;
result->minimum_free_bytes = result->free_bytes;
/* 返回的就是start_ptr */
return result;
}
根据tlsf_add_pool
的操作,result->free_bytes
的设置有点不合理,更合理的设置应该是:
align_down(size - tlsf_size() - pool_overhead, ALIGN_SIZE)
当前,idf在main_task
中将作为启动栈的内存归还给堆:
static void main_task(void* args)
{
#if !CONFIG_FREERTOS_UNICORE
// Wait for FreeRTOS initialization to finish on APP CPU, before replacing its startup stack
while (port_xSchedulerRunning[1] == 0) {
;
}
#endif
/* 将启动栈归还给堆 */
heap_caps_enable_nonos_stack_heaps();
......
app_main();
vTaskDelete(NULL);
}
heap_caps_enable_nonos_stack_heaps
实现归还操作:
void heap_caps_enable_nonos_stack_heaps(void)
{
heap_t *heap;
SLIST_FOREACH(heap, ®istered_heaps, next) {
/* 在heap_caps_init中并未注册用于启动栈的内存 */
/* 只是将管理这段内存的heap->heap置为NULL */
/* 因此此处就利用这一点来找到尚未注册的用于启动栈的内存 */
if (heap->heap == NULL) {
register_heap(heap);
if (heap->heap != NULL) {
multi_heap_set_lock(heap->heap, &heap->heap_mux);
}
}
}
}
[1] esp-idf