esp-idf的内存管理——内存管理组件的初始化

目录

  • 1 堆的初始化概览
  • 2 堆的初始化源码分析
  • 3 归还启动栈
  • 参考

1 堆的初始化概览

idf会在启动过程中初始化heap组件,初始化主要做了如下几件事:

  1. 找出所有连续的内存区域
  2. 这些内存区域如果有毗邻且caps一致的则合并之
  3. 在这些内存区域上建立堆
  4. 通过heap_t将各个堆串成单向链表以便管理,表头为registered_heaps
  5. 在启动的末期(当前是在main_task中)将原本用于启动栈的内存归还给堆

初始化完成之后,内存中会形成这样的结构:
esp-idf的内存管理——内存管理组件的初始化_第1张图片

至此,铺垫结束,下面看源码。

2 堆的初始化源码分析

在初始化阶段,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做了这么几件事:

  1. 找出所有的memory region,并尽可能合并
  2. 为每个memory region创建一个heap_t,并进行注册(也就是创建堆)
  3. 将heap_t串到registered_heaps

下面要看的是:

  • soc_get_available_memory_region_max_count
  • soc_get_available_memory_regions
  • register_heap

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)

3 归还启动栈

当前,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, &registered_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

你可能感兴趣的:(#,esp-idf的内存管理,esp-idf,heap,嵌入式)