iOS libMalloc源码分析-ScalableZone(tiny)

入门篇介绍了zone在初始化时规定了nanozone管理256字节以内内存,其余内存由scalablezone管理,上篇介绍了nanozone的机制,本篇继续介绍scalablezone的内存机制。

整体策略与szone

对于256字节以上的内存,使用scalablezone分配,由于待分配内存的大小差异较大,scalablezone在分配时不采用同一套逻辑,而是先将分配大小划分成tiny、small、medium、large4个级别,针对这4个级别,采用不同的分配逻辑处理,其中tiny、small、medium(iOS下不使用,这里不介绍)的整体逻辑相同,但是具体策略有差异,large有一套自己的机制,iOS上没有medium策略。

首先,调用create_scalable_szone函数创建scalablezone,scalablezone的类型是szone_s,是基础malloc_zone_t结构的扩展,szone_s定义如下:

c 复制代码
typedef struct szone_s {
    malloc_zone_t basic_zone; // first page will be given read-only protection
    uint8_t pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)];

    struct rack_s tiny_rack;
    struct rack_s small_rack;
    struct rack_s medium_rack;

    _malloc_lock_s large_szone_lock MALLOC_CACHE_ALIGN; // One customer at a time for large
    unsigned num_large_objects_in_use;
    unsigned num_large_entries;
    large_entry_t *large_entries; // hashed by location; null entries don't count
    size_t num_bytes_in_large_objects;
#if CONFIG_LARGE_CACHE
    int large_entry_cache_oldest;
    int large_entry_cache_newest;
    large_entry_t large_entry_cache[LARGE_ENTRY_CACHE_SIZE_HIGH]; // "death row" for large malloc/free
    int large_cache_depth;
    size_t large_cache_entry_limit;
    boolean_t large_legacy_reset_mprotect;
    size_t large_entry_cache_reserve_bytes;
    size_t large_entry_cache_reserve_limit;
    size_t large_entry_cache_bytes; // total size of death row, bytes
#endif
    /* The purgeable zone constructed by create_purgeable_zone() would like to hand off tiny and small
     * allocations to the default scalable zone. Record the latter as the "helper" zone here. */
    struct szone_s *helper_zone;
} szone_t;

包含以下字段:

  • basic_zone:基础结构malloc_zone_t,存储内存管理的各入口函数地址,在入门篇文章中已介绍。
  • tiny_rack/small_rack/medium_rack:rack_s类型,用于tiny、small、medium内存的具体管理,下文中介绍。
  • large类型内存管理相关字段,包括统计信息,管理large内存块,缓存逻辑相关。
  • helper_zone:辅助zone。

create_scalable_szone函数创建scalablezone。

ini 复制代码
szone_t *create_scalable_szone(size_t initial_size, unsigned debug_flags)
{
    szone_t *szone;
    szone = mvm_allocate_pages(SZONE_PAGED_SIZE, 0, DISABLE_ASLR, VM_MEMORY_MALLOC);
    if (!szone) {
        return NULL;
    }
    unsigned int max_mags = mag_max_magazines();
    uint32_t num_magazines = (max_mags > 1) ? MIN(max_mags, TINY_MAX_MAGAZINES) : 1;
    rack_init(&szone->tiny_rack, RACK_TYPE_TINY, num_magazines, debug_flags);
    rack_init(&szone->small_rack, RACK_TYPE_SMALL, num_magazines, debug_flags);
    //...
#if CONFIG_LARGE_CACHE
    //large cache相关字段初始化
#endif

    szone->basic_zone.version = 13;
    szone->basic_zone.size = (void *)szone_size;
    szone->basic_zone.malloc = (void *)szone_malloc;
    szone->basic_zone.calloc = (void *)szone_calloc;
    szone->basic_zone.valloc = (void *)szone_valloc;
    szone->basic_zone.free = (void *)szone_free;
    szone->basic_zone.realloc = (void *)szone_realloc;
    szone->basic_zone.destroy = (void *)szone_destroy;
    szone->basic_zone.batch_malloc = (void *)szone_batch_malloc;
    szone->basic_zone.batch_free = (void *)szone_batch_free;
    szone->basic_zone.introspect = (struct malloc_introspection_t *)&szone_introspect;
    szone->basic_zone.memalign = (void *)szone_memalign;
    //...
    return szone;
}

初始化逻辑包含以下部分:

  1. mvm_allocate_pages创建一个szone_t内存,即scalablezone。
  2. rack_init函数初始化tiny、small的rack数据,初始化分配逻辑。
  3. 初始化large cach相关逻辑
  4. 初始化malloc_zone_t base_zone的字段,作为scalablezone管理内存的入口API。

例如,scalablezone的malloc逻辑由szone_malloc实现:

arduino 复制代码
void *szone_malloc(szone_t *szone, size_t size)
{
    return szone_malloc_should_clear(szone, size, 0);
}

内部调用szone_malloc_should_clear实现malloc,如下:

ini 复制代码
#if MALLOC_TARGET_64BIT
#define TINY_LIMIT_THRESHOLD (1008)
#else // MALLOC_TARGET_64BIT
#define TINY_LIMIT_THRESHOLD (496)
#endif // MALLOC_TARGET_64BIT

#if MALLOC_TARGET_IOS
#define SMALL_LIMIT_THRESHOLD (15 * 1024)
#else // MALLOC_TARGET_IOS
#define SMALL_LIMIT_THRESHOLD (32 * 1024)
#endif // MALLOC_TARGET_IOS

MALLOC_NOINLINE void *
szone_malloc_should_clear(szone_t *szone, size_t size, boolean_t cleared_requested)
{
    void *ptr;
    msize_t msize;
    if (size <= TINY_LIMIT_THRESHOLD) {
        msize = TINY_MSIZE_FOR_BYTES(size + TINY_QUANTUM - 1);
        if (!msize) {
            msize = 1;
        }
        ptr = tiny_malloc_should_clear(&szone->tiny_rack, msize, cleared_requested);
    } else if (size <= SMALL_LIMIT_THRESHOLD) {
        msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
        if (!msize) {
            msize = 1;
        }
        ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
    } else {
        size_t num_kernel_pages = round_large_page_quanta(size) >> large_vm_page_quanta_shift;
        if (num_kernel_pages == 0) { /* Overflowed */
            ptr = 0;
        } else {
            ptr = large_malloc(szone, num_kernel_pages, 0, cleared_requested);
        }
    }
    return ptr;
}

可以看到,根据分配size大小,使用不同的分配策略。

  • tiny:tiny_malloc_should_clear,1008B以内
  • small:small_malloc_should_clear1009B~15KB
  • large:15KB以上

又例如,free逻辑由szone_free实现:

arduino 复制代码
void szone_free(szone_t *szone, void *ptr)
{
    _szone_free(szone, ptr, false);
}

static void _szone_free(szone_t *szone, void *ptr, bool try)
{
    //...
    if ((tiny_region = tiny_region_for_ptr_no_lock(&szone->tiny_rack, ptr)) != NULL) {
        if (TINY_INDEX_FOR_PTR(ptr) >= NUM_TINY_BLOCKS) {
            malloc_zone_error(szone->debug_flags, true, "Pointer %p to metadata being freed\n", ptr);
            return;
        }
        free_tiny(&szone->tiny_rack, ptr, tiny_region, 0, false);
        return;
    }
    //...
    if ((small_region = small_region_for_ptr_no_lock(&szone->small_rack, ptr)) != NULL) {
        if (SMALL_META_INDEX_FOR_PTR(ptr) >= NUM_SMALL_BLOCKS) {
            malloc_zone_error(szone->debug_flags, true, "Pointer %p to metadata being freed (2)\n", ptr);
            return;
        }
        free_small(&szone->small_rack, ptr, small_region, 0);
        return;
    }
    //...
    bool claimed = free_large(szone, ptr, try);
    if (!try || claimed) {
        return;
    }
}

类似malloc逻辑,free也具体调用free_tiny、free_small、free_large处理不同size内存的回收。用一张图表示scalablezone处理内存的逻辑如下:

接下来介绍tiny、small、large的相关内存管理机制。

tiny

tiny主要的实现在magazine_tiny.c文件中,首先结合代码,介绍tiny管理内存的相关数据结构及其概念。

相关概念

size分级

根据要分配的size,按照指定size的倍数计算对应的分级(msize),实际分配指定size的倍数分配,例如tiny类型,将内存划分为16B一级,则1008B的范围一共划分为63级(如下图所示)。 例如,分配一个17B大小的内存,对应的msize是2,实际分配32B的内存。

rack

如上所述,rack_s是管理内存的全局数据结构,查看rack_s的定义,核心字段如下:

arduino 复制代码
OS_ENUM(rack_type, uint32_t,
    RACK_TYPE_NONE = 0,
    RACK_TYPE_TINY,
    RACK_TYPE_SMALL,
    RACK_TYPE_MEDIUM,
);

typedef struct rack_s {
    rack_type_t type; //类型
    
    //...
    size_t num_regions;
    region_t initial_regions[INITIAL_NUM_REGIONS];

    int num_magazines;
    magazine_t *magazines;
    
    //...
} rack_t;
  • type:类型,对应tiny、small、medium三种策略类型。
  • num_regions:分配的region内存块个数,region是向scalablezone向系统分配的虚拟内存的基础单位,不同类型分配的region的大小不一致。
  • initial_regions:初始化时分配的region数组。
  • num_magazines:持有的magazine个数,magazine是管理scalable内存的核心概念,具体负责内存的管理,包括region块的分配与管理、空闲块的使用与管理等。
  • magazines:magazine数组,一种策略类型具体由若干个magazine管理。

创建scalable_szone时,通过rack_init初始化rack数据。

magazine

结构关系

magazine是管理内存的核心结构,每一种rack(tiny、small、medium),会创建多个magazine,rack在操作内存时,首先需要选取其中一个magazine具体操作,体现在数据结构上,rack_s下持有若干个magazine。

arduino 复制代码
typedef struct rack_s {
    //...
    // array of per-processor magazines
    magazine_t *magazines;
}

具体的对应关系如图: rack创建多少个magazines,与设备的cpu核心数相关,具体逻辑追溯到rack初始化的逻辑中。

scss 复制代码
szone_t *
create_scalable_szone(size_t initial_size, unsigned debug_flags)
{
    //...
    unsigned int max_mags = mag_max_magazines();
    uint32_t num_magazines = (max_mags > 1) ? MIN(max_mags, TINY_MAX_MAGAZINES) : 1;
    rack_init(&szone->tiny_rack, RACK_TYPE_TINY, num_magazines, debug_flags);
}

void rack_init(rack_t *rack, rack_type_t type, uint32_t num_magazines, uint32_t debug_flags)
{
    //...
    rack->num_magazines = num_magazines;
    //...
    if (num_magazines > 0) {
        size_t magsize = round_page_quanta(sizeof(magazine_t) * (num_magazines + 1));
        magazine_t *magazines = mvm_allocate_pages(magsize, 0, MALLOC_ADD_GUARD_PAGE_FLAGS|DISABLE_ASLR, VM_MEMORY_MALLOC);
        if (!magazines) {
            MALLOC_REPORT_FATAL_ERROR(0, "unable to allocate magazine array");
        }
        rack->magazines = &magazines[1];
    }
}

调用create_scalable_szone初始化tiny的rack数据,在rack_init方法中传入num_magazines并创建magazines,num_magazines通过mag_max_magazines方法获取.

arduino 复制代码
unsigned int mag_max_magazines(void)
{
    return max_magazines;
}

static void _malloc_initialize(const char *apple[], const char *bootargs)
{
    logical_ncpus = *(uint8_t *)(uintptr_t)_COMM_PAGE_LOGICAL_CPUS;
    //...
    if (max_magazines) {
        max_magazines = MIN(max_magazines, logical_ncpus);
    } else {
        max_magazines = logical_ncpus;
    }
}

max_magazines由_COMM_PAGE_LOGICAL_CPUS获取,含义是number of logical CPUs(逻辑CPU的核心数)。因此magazines的个数由逻辑CPU的核心数决定。可以理解为magazine和CPU存在对应关系,如图,当分配内存时,根据当前线程执行的CPU,选择对应的magazine。 例如设备共4个CPU核心,当前运行的CPU index是1,则返回magazines[1]。

具体代码如下:

ini 复制代码
mag_index_t mag_index = tiny_mag_get_thread_index() % rack->num_magazines;
magazine_t *tiny_mag_ptr = &(rack->magazines[mag_index]);

mag_index_t tiny_mag_get_thread_index(void)
{
    if (likely_if(_os_cpu_number_override == -1)) {
        return _malloc_cpu_number();
    } else {
        return _os_cpu_number_override;
    }
}

通过tiny_mag_get_thread_index方法获取当前线程的cpu核心对应的mag_index下标,通过magazines[mag_index]返回magazine对象。CPU核心数和magazine对象的关系大致如图,

这样操作内存时,每个cpu核心对应一个magazine,多magazine同时操作内存,可以发挥多cpu核心并行的能力。

内存管理

magazine是如何管理内存的,首先数据结构如下:

arduino 复制代码
typedef struct magazine_s {
    //...
    void *mag_last_free;
    msize_t mag_last_free_msize;
    //...
    region_t mag_last_free_rgn;
    
    free_list_t mag_free_list[MAGAZINE_FREELIST_SLOTS]; //257
    uint32_t mag_bitmap[MAGAZINE_FREELIST_BITMAP_WORDS]; //9
    
    size_t mag_bytes_free_at_end; //最后申请的heap region中未使用的大小
    size_t mag_bytes_free_at_start;
    region_t mag_last_region; // 最后一次向内核申请的heap region地址
    
    size_t mag_num_bytes_in_objects;
    size_t num_bytes_in_magazine;
    unsigned mag_num_objects;
    
    region_trailer_t *firstNode;
    region_trailer_t *lastNode;
    
    //...
}

主要字段如下:

  • firstNode、lastNode:region链表,待分配内存从region中分配。
  • mag_last_free:缓存的前一次回收内存块的地址
  • mag_last_free_msize:缓存的前一次回收内存块的size分级
  • mag_last_free_rgn:前一次回收内存所在的region
  • mag_free_list:空闲链表,记录了之前回收的内存,实现内存复用的核心字段
  • mag_bitmap:内存空闲状态数组,判断内存复用的辅助字段。
  • mag_bytes_free_at_end:region中剩余内存空间的结束地址。
  • mag_bytes_free_at_start:region中剩余内存空间的起始地址。

总体来看,包含2个核心数据结构和一些辅助功能所需的字段,用一张图说明。 字段中涉及的相关概念,例如region、mag_free_list空闲链表,下文具体介绍。

region

上文所述,magazine会管理一系列region空间,其他分配器的逻辑类似,region是实际存储分配内存的物理空间,magazine分配内存时,会一次性分配一块较大的内存区域region(图中黄色区域),该区域大小范围固定。而具体的内存从region中分配,当region剩余空间不足时,分配一块新的region。

内部结构

对于tiny内存,msize按照16B为基本单位,称为一个block内存,region内分配的不同size的内存块本质上由若干地址连续的block构成。因此,region是一块包含若干地址连续的16B内存块的较大物理空间,结构定义如下:

arduino 复制代码
typedef struct tiny_region {
    //meta
    region_trailer_t trailer; //链表
    tiny_header_inuse_pair_t pairs[CEIL_NUM_TINY_BLOCKS_WORDS]; //bit状态信息
    region_free_blocks_t free_blocks_by_slot[NUM_TINY_SLOTS]; 
    uint8_t pad[TINY_REGION_PAD];
    region_cookie_t region_cookie;
    //block内存
    tiny_block_t blocks[NUM_TINY_BLOCKS];
} * tiny_region_t;

其中,tiny_block_t对应一个16B的block内存块,block是tiny内存块的基本单位,blocks字段是block数组,用于实际存储region内的业务数据,NUM_TINY_BLOCKS是数组容量,即region中包含的block块个数,决定了region可供使用的内存容量。

arduino 复制代码
#define SHIFT_TINY_QUANTUM 4ull
#define TINY_QUANTUM (1 << SHIFT_TINY_QUANTUM)
#define NUM_TINY_BLOCKS 64504

NUM_TINY_BLOCKS是一个固定值64504,TINY_QUANTUM表示单个block大小是16B,因此region内存的容量是NUM_TINY_BLOCKS*TINY_QUANTUM。

除了负责数据存储的blocks字段外,其余字段用于region状态的管理,包括region链表的维护,block内存状态,用于辅助后续region内存操作与管理,这些字段称为region的meta数据字段。

下图反映了region空间的内存结构分布(真实的region是一块物理内存连续的空间,与图中展示的region存在差异,希望不要造成误解),region内存包含blocks区域和meta区域两部分,blocks区域的内存块由16B block基础内存块构成。 因此,region内存的大小(TINY_REGION_SIZE)等于blocks区大小(TINY_HEAP_SIZE)和meta区大小(TINY_METADATA_SIZE)之和。

scss 复制代码
//blocks大小
#define TINY_HEAP_SIZE (NUM_TINY_BLOCKS * TINY_QUANTUM)
//meta大小
#define TINY_METADATA_SIZE (sizeof(region_trailer_t) + sizeof(tiny_header_inuse_pair_t) * CEIL_NUM_TINY_BLOCKS_WORDS + (sizeof(region_free_blocks_t) * NUM_TINY_SLOTS))
//region大小
#define TINY_REGION_SIZE ((TINY_HEAP_SIZE + TINY_METADATA_SIZE + PAGE_MAX_SIZE - 1) & ~(PAGE_MAX_SIZE - 1))

同时,还提供了一些封装宏访问region内部内存,例如TINY_REGION_METADATA是meta区起始地址,TINY_REGION_HEAP_BASE是blocks区起始地址,TINY_REGION_HEAP_END是blocks区结束地址。

scss 复制代码
#define TINY_REGION_METADATA(region) ((uintptr_t)&((tiny_region_t)region)->trailer)
#define TINY_REGION_HEAP_BASE(region) ((void *)(((tiny_region_t)region)->blocks))
#define TINY_REGION_HEAP_END(region) ((void *)(((uintptr_t)TINY_REGION_HEAP_BASE(region)) + TINY_HEAP_SIZE))

同时,提供了TINY_INDEX_FOR_PTR宏,根据region中的任意内存地址ptr,返回对应的block内存在region中的index。

scss 复制代码
#define TINY_HEAP_OFFSET_FOR_PTR(ptr) ((uintptr_t)(ptr) - (uintptr_t)TINY_REGION_HEAP_BASE(TINY_REGION_FOR_PTR(ptr)))
#define TINY_INDEX_FOR_PTR(ptr) ((TINY_HEAP_OFFSET_FOR_PTR(ptr) >> SHIFT_TINY_QUANTUM) & (NUM_TINY_CEIL_BLOCKS - 1))

具体计算逻辑是,首先计算ptr距离blocks起始地址的偏移量,然后除以单个block的size(16字节),再&(NUM_TINY_CEIL_BLOCKS - 1)得到block得index,offset如图: 反之,提供TINY_PTR_FOR_INDEX宏,可以根据block的index,计算对应的block内存地址。

scss 复制代码
#define TINY_PTR_FOR_INDEX(index, region) (void *)((uintptr_t)TINY_REGION_HEAP_BASE(region) + ((index) << SHIFT_TINY_QUANTUM))

也可以通过block区中的一个block内存块地址,找到其所在region区的meta字段内存地址。

scss 复制代码
#define TINY_BLOCK_HEADER_FOR_PTR(ptr) ((void *)&(((tiny_region_t)TINY_REGION_FOR_PTR(ptr))->pairs))

接下来介绍meta信息相关字段。

  • trailer,region链表结构字段,维护magazine下分配的所有region内存,对应的数据结构:

    arduino 复制代码
    typedef struct region_trailer {
        struct region_trailer *prev;
        struct region_trailer *next;
        //...
    } region_trailer_t;
    • 通过prev、next指针指向前后region。
  • pairs[]数组,记录region内block内存的使用状态,辅助magazine进行内存操作,其含义作用下文详细介绍。

  • free_blocks_by_slot[]数组,记录region内不同msize空闲内存块。

链表结构

如上所述,region通过meta的region_trailer_t结构的trailer字段维护链表,如图所示,通过链表可以遍历查找到magazine下分配的所有region。 magazine的firstNode字段指向的第一块region,lastNode字段指向最后一块region。

  1. 将region加入链表的末尾

    ini 复制代码
    //将region加入tiny_mag_ptr的last node
    static void recirc_list_splice_last(rack_t *rack, magazine_t *mag_ptr, region_trailer_t *node)
    {
        if (NULL == mag_ptr->lastNode) {
            mag_ptr->firstNode = node;
            node->prev = NULL;
        } else {
            node->prev = mag_ptr->lastNode;
            mag_ptr->lastNode->next = node;
        }
        mag_ptr->lastNode = node;
        node->next = NULL;
        node->recirc_suitable = FALSE;
    }
  2. 将region加入链表的头部

    ini 复制代码
    static void recirc_list_splice_first(rack_t *rack, magazine_t *mag_ptr, region_trailer_t *node)
    {
        if (NULL == mag_ptr->firstNode) {
            mag_ptr->lastNode = node;
            node->next = NULL;
        } else {
            node->next = mag_ptr->firstNode;
            mag_ptr->firstNode->prev = node;
        }
        mag_ptr->firstNode = node;
        node->prev = NULL;
        node->recirc_suitable = FALSE;
    }
  3. region节点从region trailer链表中移除

    ini 复制代码
    static void recirc_list_extract(rack_t *rack, magazine_t *mag_ptr, region_trailer_t *node)
    {
        // excise node from list
        if (NULL == node->prev) {
            mag_ptr->firstNode = node->next;
        } else {
            node->prev->next = node->next;
        }
        if (NULL == node->next) {
            mag_ptr->lastNode = node->prev;
        } else {
            node->next->prev = node->prev;
        }
        node->next = node->prev = NULL;
        mag_ptr->recirculation_entries--;
    }

hash存储

除了magazine维护的region链表外,当分配了一块region后,会计算region的一个hash值,并记录到一块hash ring的内存结构中,标识rack下是否存在这块region,内存结构如图:

rack维护的相关字段如下:

arduino 复制代码
typedef struct rack_s {
    //...
    region_hash_generation_t *region_generation;
}

typedef struct region_hash_generation {
    size_t num_regions_allocated;
    size_t num_regions_allocated_shift;
    region_t *hashed_regions;
    struct region_hash_generation *nextgen;
} region_hash_generation_t;

定义了region_hash_generation_t类型的region_generation字段,数据结构内部维护了hashed_regions字段,存储region的hash index数据。当hashed_regions的容量无法满足region的增长时,会新分配一个region_generation作为当前generation的nextgen,存储扩容的hashed_regions,并将数据同步到新的hashed_regions。

相关实现

结合图示,分析具体实现。

hash添加

当新分配一个region时,调用rack_region_insert加入region的hash index到hash ring中。

rust 复制代码
void rack_region_insert(rack_t *rack, region_t region)
{
    //rack的regions个数大于的2被大于当前generation的记录分配的region个数时,新增generation,并迁移数据。
    if (rack->region_generation->num_regions_allocated < (2 * rack->num_regions)) {
        region_t *new_regions;
        size_t new_size;
        size_t new_shift = rack->region_generation->num_regions_allocated_shift; // In/Out parameter
        new_regions = hash_regions_grow_no_lock(rack->region_generation->hashed_regions,
                                                rack->region_generation->num_regions_allocated, &new_shift, &new_size);

        rack->region_generation->nextgen->hashed_regions = new_regions;
        rack->region_generation->nextgen->num_regions_allocated = new_size;
        rack->region_generation->nextgen->num_regions_allocated_shift = new_shift;
        //新generation作为当前generation
        rack->region_generation = rack->region_generation->nextgen;
    }

    //ragion的hash index加入generation的hashed_regions中
    hash_region_insert_no_lock(rack->region_generation->hashed_regions,
                               rack->region_generation->num_regions_allocated,
                               rack->region_generation->num_regions_allocated_shift,
                               region);
    rack->num_regions++;
}
  1. 判断rack中regions个数(num_regions),是否超过当前generation的记录分配的region个数的一半,如果超过,则需要扩容hashed_regions,新建一个generation,指向扩容后的new_hashed_regions,并作为当前generation。

  2. 扩容逻辑如下:

    ini 复制代码
    static region_t *hash_regions_grow_no_lock(region_t *regions, size_t old_size, size_t *mutable_shift, size_t *new_size)
    {
        *new_size = old_size + old_size;
        *mutable_shift = *mutable_shift + 1;
        region_t *new_regions = hash_regions_alloc_no_lock(*new_size);
    
        size_t index;
        for (index = 0; index < old_size; ++index) {
            region_t r = regions[index];
            if (r != HASHRING_OPEN_ENTRY && r != HASHRING_REGION_DEALLOCATED) {
                hash_region_insert_no_lock(new_regions, *new_size, *mutable_shift, r);
            }
        }
        return new_regions;
    }

    分配一块新内存new_regions,容量是旧的2倍,并将旧region中存储的hash index数据迁移到新的hashed_regions内存中。如图所示:

  3. 将region的hash index加入generation的hashed_regions中。加入的具体实现是:

    ini 复制代码
    static void hash_region_insert_no_lock(region_t *regions, size_t num_entries, size_t shift, region_t r)
    {
        size_t index, hash_index;
        rgnhdl_t entry;
        index = hash_index = (((uintptr_t)r >> HASH_BLOCKS_ALIGN) * 11400714819323198549ULL) >> (64 - shift);
        do {
            entry = regions + index;
            if (*entry == HASHRING_OPEN_ENTRY || *entry == HASHRING_REGION_DEALLOCATED) {
                *entry = r;
                return;
            }
            if (++index == num_entries) {
                index = 0;
            }
        } while (index != hash_index);
    }

    首先计算region的hash,以hash作为index,查找index位是否存储数据,如果已存储数据,则不断++index偏移至下一位查找是否有空位,直到找到空位,当index位于buffer数组末尾,则从0继续遍历,整个遍历查找过程是一个环,如果查找回到最初的index,则终止。其中,这里判断是当前hashed_regions[index]的值是否是枚举值HASHRING_OPEN_ENTRY和HASHRING_REGION_DEALLOCATED,HASHRING_OPEN_ENTRY是默认值0,表示没有存过数据,HASHRING_REGION_DEALLOCATED是-1表示之前存储的值被清空了。 如图,计算出region的hash index,由于对应的hashed_regions[hash index]上已经存储数据了,则偏移直到找到空位存储。

hash查找

可以通过查询hash_regions中的index,判断rack中是否存在region。

ini 复制代码
static rgnhdl_t hash_lookup_region_no_lock(region_t *regions, size_t num_entries, size_t shift, region_t r)
{
    size_t index, hash_index;
    rgnhdl_t entry;
    if (!num_entries) {
        return 0;
    }
    index = hash_index = (((uintptr_t)r >> HASH_BLOCKS_ALIGN) * 11400714819323198549ULL) >> (64 - shift);
    do {
        entry = regions + index;
        if (*entry == 0) {
            return 0;
        }
        if (*entry == r) {
            return entry;
        }
        if (++index == num_entries) {
            index = 0;
        }
    } while (index != hash_index);
    return 0;
}

查找和添加的规则是相同的。

hash删除

当一个region被回收时,调用rack_region_remove方法删除对应的hash index。

rust 复制代码
bool rack_region_remove(rack_t *rack, region_t region, region_trailer_t *trailer)
{
    bool rv = true;
    //...
    rgnhdl_t pSlot = hash_lookup_region_no_lock(
            rack->region_generation->hashed_regions,
            rack->region_generation->num_regions_allocated,
            rack->region_generation->num_regions_allocated_shift,
            region);
    //...
    *pSlot = HASHRING_REGION_DEALLOCATED;
    return rv;
}

其中,pSlot是查找到的hash index,置位HASHRING_REGION_DEALLOCATED,这样下次可以复用。这里不会回收hashed_regions内存,而是置空数据给后续region的hash index复用。

内存状态管理

上文所述,magazine中分配的内存实际从region中分配,并回收复用。为了管理magazine中的内存,首先需要记录这些内存的状态,因为只有知道每块内存的状态,才能有效判断,实现复用的逻辑,可以分为以下3种:

  1. 未分配:未被分配的内存空间,即region中的剩余内存空间。
  2. 分配并被使用:从region中分配了一块指定size的内存块,并且正在使用中。
  3. 未使用:被回收的内存块,可以被复用。

用一张图来说明: magazine使用了一些数据结构和机制记录这些状态。

bit位状态

使用mag_bitmap[]数组记录每一级msize是否存在内存,例如下图:

slot是msize的值,例如16B的slot是0,32B的slot是1,bitmap数组中每个index对应的值bitmap[index]可以存储4字节32bit位的内容,则mag_bitmap[0]可以表示slot0~31范围内的状态。图中,bit位0、1、2、3、8、12、17、18、26为1,则对应msize的空闲链表存在空闲内存块,对于tiny内存,存在16B、32B、48B、64B、144B、208B、288B、304B、432B的空闲内存块。

magazine定义了一些宏来操作bit位,以64位为例:

scss 复制代码
#define BITMAPV_SET(bitmap, slot) (bitmap[(slot) >> 5] |= 1 << ((slot)&31))
#define BITMAPV_CLR(bitmap, slot) (bitmap[(slot) >> 5] &= ~(1 << ((slot)&31)))
#define BITMAPV_BIT(bitmap, slot) ((bitmap[(slot) >> 5] >> ((slot)&31)) & 1)

block状态管理

mag_bitmap是magazine记录的magazine全局的内存块状态信息,同时对于每一块region,也会维护其内部内存块的状态。

region中内存块由若干个16B大小的block构成,如图所示,淡黄色是msize=1的内存块,由1个block组成,黄色是msize=2的内存块,由2个block组成。

通过记录这些block的状态,来实现记录内存块状态的例如一个msize=2的内存块,共包含2个block,则region会记录这2个block的状态,是否被分配在使用中,或者是否被回收,同时记录哪几个block属于同一个内存块,同时还要能够访问前后内存地址相邻的内存块地址,方便回收时的合并操作。

block的状态信息存储在pairs字段中,定义如下:

arduino 复制代码
typedef struct tiny_header_inuse_pair {
    uint32_t header;
    uint32_t inuse;
} tiny_header_inuse_pair_t;

tiny_header_inuse_pair_t pairs[CEIL_NUM_TINY_BLOCKS_WORDS];

pairs字段可以理解为region的Header信息,是tiny_header_inuse_pair_t(简称pair)结构体数组,数组长度通过CEIL_NUM_TINY_BLOCKS_WORDS宏表示。

arduino 复制代码
#define CEIL_NUM_TINY_BLOCKS_WORDS (((NUM_TINY_BLOCKS + 31) & ~31) >> 5)

每个pair结构体元素包含header和inuse2个字段,长度分别是32bit,因此最多可以存储32个block内存块的状态信息。而一块tiny region共有64504个block,CEIL_NUM_TINY_BLOCKS_WORDS表示存储这些block共需要的pair个数。

而每个pair结构体的字段通过记录这些内存块中block的bit值,来存储当前内存块的状态。其中header字段的bit位记录某个内存块是否被分配以及分配的msize长度,inuse字段记录该内存块的使用状态,是正在使用还是被回收未使用。

未分配

初始化情况下,region内未分配任何内存块,则所有block的bit均为0,下图是一个pair结构体下32个block的bit值,均为0(黄色表示)。

其中header和inuse上的bit位均为0.

分配使用

当分配内存块时,bit位更新,分为2种情况,如果分配的内存msize是1,执行set_tiny_meta_header_in_use_1函数逻辑。如果分配的内存msize大于1,执行set_tiny_meta_header_in_use函数逻辑。

ini 复制代码
static MALLOC_INLINE void set_tiny_meta_header_in_use_1(const void *ptr) // As above with msize == 1
{
    uint32_t *block_header = TINY_BLOCK_HEADER_FOR_PTR(ptr);
    msize_t index = TINY_INDEX_FOR_PTR(ptr);
    msize_t midx = (index >> 5) << 1;
    uint32_t val = (1 << (index & 31));
    block_header[midx] |= val;   // BITARRAY_SET(block_header, index);
    block_header[midx + 1] |= val; // BITARRAY_SET(in_use, index);
    index++;
    midx = (index >> 5) << 1;
    val = (1 << (index & 31));
    block_header[midx] |= val; // BITARRAY_SET(block_header, (index+clr_msize))
}

static MALLOC_INLINE void set_tiny_meta_header_in_use(const void *ptr, msize_t msize)
{
    uint32_t *block_header = TINY_BLOCK_HEADER_FOR_PTR(ptr);
    msize_t index = TINY_INDEX_FOR_PTR(ptr);
    msize_t clr_msize = msize - 1;
    msize_t midx = (index >> 5) << 1;
    uint32_t val = (1 << (index & 31));
    block_header[midx] |= val;  // BITARRAY_SET(block_header, index);
    block_header[midx + 1] |= val; // BITARRAY_SET(in_use, index);
    index++;
    midx = (index >> 5) << 1;
    unsigned start = index & 31;
    unsigned end = start + clr_msize;

#if defined(__LP64__)
    if (end > 63) {
        unsigned mask0 = (0xFFFFFFFFU >> (31 - start)) >> 1;
        unsigned mask1 = (0xFFFFFFFFU << (end - 64));
        block_header[midx + 0] &= mask0; // clear header
        block_header[midx + 1] &= mask0; // clear in_use
        block_header[midx + 2] = 0;      // clear header
        block_header[midx + 3] = 0;      // clear in_use
        block_header[midx + 4] &= mask1; // clear header
        block_header[midx + 5] &= mask1; // clear in_use
    } else
#endif
        if (end > 31) {
            unsigned mask0 = (0xFFFFFFFFU >> (31 - start)) >> 1;
            unsigned mask1 = (0xFFFFFFFFU << (end - 32));
            block_header[midx + 0] &= mask0;
            block_header[midx + 1] &= mask0;
            block_header[midx + 2] &= mask1;
            block_header[midx + 3] &= mask1;
        } else {
            unsigned mask = (0xFFFFFFFFU >> (31 - start)) >> 1;
            mask |= (0xFFFFFFFFU << end);
            block_header[midx + 0] &= mask;
            block_header[midx + 1] &= mask;
        }

    // we set the block_header bit for the following block to reaffirm next block is a block
    index += clr_msize;
    midx = (index >> 5) << 1;
    val = (1 << (index & 31));
    block_header[midx] |= val; // BITARRAY_SET(block_header, (index+clr_msize));
}

其中,midx是pair的index,首先>>5定位表示每32个block的bit值存储在同一个pair中,<<1是因为block_header指针是32位,指向的pair长度包含2个32位字段,所以block_header表示的数组长度乘2,如图:

block_header[midx]和block_header[midx+1]对应pair中的header和inuse。val是block index对应的bit位。

设置的规则是:

  1. 设置header字段的bit位数值,规则是根据当前分配的内存的msize长度,进行设置,内存块包含的第一个block对应的bit位设置为1,其余block对应的bit位设置为0。
  2. 为了标记内存块的边界范围,需要设置内存块中最后一个block的后一个block的bit为数值为1。因为确定内存块大小的的逻辑是从起始block的bit位开始,直到找到下一个为1的bit位,该bit位表示一个新的内存块的起始位置。
  3. 设置inuse字段的bit位数值,规则是内存块包含的第一个block对应的bit位设置为1,其余block对应的bit位设置为0。

当分配一个msize=1内存,pair字段对应的bit值如图。 由于这里分配的内存块msize是1,仅包含一个block,则header的bit位数值是1 1,insue的bit位数值是1。

当分配的内存块msize大于1,例如msize=5,分为两种情况:

1)内存块下的5个block位于同一个pair index,从起始block开始,包含5个bit位数据1 0 0 0 0,下一个内存开始的bit设置为1,标识边界,同时inuse的bit位标记为1 0 0 0 0,只需要起始block的bit位标记为1,其余为0. 2)如果包含的block位于不同的pair index,则需要记录在不同的header中。

未使用

当发生回收逻辑时,内存块会被加入回收缓存链表,则对应的bit位需要标记为空闲未使用状态。

ini 复制代码
static MALLOC_INLINE void
set_tiny_meta_header_free(const void *ptr, msize_t msize)
{
    // !msize is acceptable and means 65536
    uint32_t *block_header = TINY_BLOCK_HEADER_FOR_PTR(ptr);
    msize_t index = TINY_INDEX_FOR_PTR(ptr);
    msize_t midx = (index >> 5) << 1;
    uint32_t val = (1 << (index & 31));

    block_header[midx] |= val;      // BITARRAY_SET(block_header, index);
    block_header[midx + 1] &= ~val; // BITARRAY_CLR(in_use, index);
    //...
}

inuse字段bit位更新,首个block的bit位更新为0,这里同样需要更新header的bit位,如果该内存之前分配过,则header的bit位没有变化,如图: 如果该内存是从一块大的空闲内存块中分割的,则这是一块新的空闲内存,如图,例如需要分配一块msize=5的内存块,匹配到一块的msize=8的空闲内存块,于是分割成两块,msize=5的内存块标记为使用状态,分割后生成一个msize=3的空闲内存块,header上的bit位值更新成1。

清除

当一块内存块不存在时,相应的bit位需要被清除,例如相邻空闲内存块存在合并的操作,则对应的bit位也需要更新,例如前后msize=5和misze=3的前后内存块合并成一块msize=8的更大空闲内存块,将misze=3的内存块的第一个block对应的bit位设置为0。

bit值变更如图: 设置代码如下:

scss 复制代码
static MALLOC_INLINE void
set_tiny_meta_header_middle(const void *ptr)
{
    // indicates this block is in the middle of an in use block
    uint32_t *block_header;
    uint32_t *in_use;
    msize_t index;

    block_header = TINY_BLOCK_HEADER_FOR_PTR(ptr);
    in_use = TINY_INUSE_FOR_HEADER(block_header);
    index = TINY_INDEX_FOR_PTR(ptr);

    BITARRAY_CLR(block_header, index);
    BITARRAY_CLR(in_use, index);
}

#define TINY_BLOCK_HEADER_FOR_REGION(region) ((void *)&(((tiny_region_t)region)->pairs))
#define TINY_INUSE_FOR_HEADER(_h) ((void *)&(((tiny_header_inuse_pair_t *)(_h))->inuse))

in_use和block_header可以通过region的字段直接获取,BITARRAY_CLR宏定义了清空bit位值的方法,同时封装了计算下标的操作,等效于上文的block_header index计算逻辑。

arduino 复制代码
static void BITARRAY_CLR(uint32_t *bits, msize_t index)
{
    bits[(index >> 5) << 1] &= ~(1 << (index & 31));
}
状态判断

更新了bit信息之后,就可以根据header和inuse字段的bit值判断当前内存块的状态,例如判断一块内存是否处于空闲状态。

ini 复制代码
static boolean_t tiny_meta_header_is_free(const void *ptr)
{
    uint32_t *block_header;
    uint32_t *in_use;
    msize_t index;
    block_header = TINY_BLOCK_HEADER_FOR_PTR(ptr);
    in_use = TINY_INUSE_FOR_HEADER(block_header);
    index = TINY_INDEX_FOR_PTR(ptr);
    if (!BITARRAY_BIT(block_header, index)) {
        return 0;
    }
    return !BITARRAY_BIT(in_use, index);
}
  1. 获取内存块第一个block的index
  2. 读取block_header字段对应bit位的数值,如果为0,说明该内存块不存在,返回false
  3. 如果内存块存在,读取in_use字段对应bit位的数值,如果为0,说明是空闲,返回true,否则返回false。

下图对应上述3种情况。

空闲内存管理

除了记录内存状态,magazine需要使用其他数据结构管理内存,例如回收内存的复用。其中,空闲链表mag_free_list是核心结构,当region中有内存块被回收时,按照size分级,将回收的内存块加入到magazine的空闲链表结构mag_free_list中管理。例如tiny下mag_free_list共63个(如上图),说明图中f1~f5位于不同的Region,由于size分级相同,位于同一个mag_free_list中。下图描述Region中内存的回收过程。

  1. Region1和Region2中分配若干size不等的内存,Region2中存在部分剩余空间未分配(白色区域)
  2. Region1和Region2中部分内存被回收,例如先后回收了f1~f5,其中f1、f3、f4的msize相同是1,加入同一个mag_free_list中,f2、f5的msize相同是2,加入同一个mag_free_list中。
  3. 再次分配内存,首先从对应size级别的mag_free_list中分配,例如分配2次16B的内存,先后取f4、f3,分配1次32B内存,取f5。
  4. mag_bitmap字段记录了msize下是否存在之被回收的空闲内存,是bit位数组,每个bit位对应一个msize,1表示存在,0表示不存在。这样,分配内存时,首先查看mag_bitmap下对应的bit位是为1,如果为1,则继续从对应的mag_free_list中返回内存。如果为0,则判断更大的bit位是否为1,即是否存在msize更大的空闲内存块。

接下来,结合代码个图示详细讲解内存复用的实现机制。

数据结构

链表指针

mag_free_list链表结构的定义如下:

arduino 复制代码
typedef struct magazine_s {
    free_list_t mag_free_list[MAGAZINE_FREELIST_SLOTS];
} magazine_t;

在tiny中,free_list_t结构被转换成了tiny_free_list_t结构,如下:

arduino 复制代码
typedef struct {
    inplace_union previous;
    inplace_union next;
} tiny_free_list_t;

其中,previous字段是指向前一个空闲内存块的指针数据,next字段是指向后一个空闲内存块的指针数据,这样就可以遍历查询链表中所有空闲内存块地址。如上图所示。之所以使用前16字节可以存储链表字段,是因为一个内存块至少包含16字节的长度(msize=1)。当内存块被回收时,会清空原本存储的数据,然后内存块的前16字节存储tiny_free_list_t字段数据。

msize数据

除了存储空闲链表字段外,还会存储空闲内存块的长度msize数据,方便后续操作,例如查找当前空闲块在物理地址相邻的前一个空闲块,如图所示:

当回收msize=1的内存块,则回收的内存块共16字节,前后8字节分别存储链表的prev、next指针。如果回收msize>1的内存块,例如msize=3,共48字节,前16字节分别存储prev、next指针之外,后2字节存储msize,同时最后2字节也存储当前内存块的msize。之所以最后2字节也存储msize,这样,当物理地址相邻的后一个内存块也会回收时,通过内存块地址ptr[-1]可以知道前一个空闲块的msize,快速定位到前一个空闲块的起始地址,辅助内存合并等操作,如图,内存块2被回收时,通过ptr[-1]获取空闲块1的msize。 设置msize信息的具体代码在set_tiny_meta_header_free函数中。

scss 复制代码
static MALLOC_INLINE void
set_tiny_meta_header_free(const void *ptr, msize_t msize)
{
    //...
    if (msize > 1) {
        void *follower = FOLLOWING_TINY_PTR(ptr, msize);
        TINY_PREVIOUS_MSIZE(follower) = msize;
        TINY_FREE_SIZE(ptr) = msize;
    }
    if (msize == 0) {
        TINY_FREE_SIZE(ptr) = msize;
    }
}

#define TINY_FREE_SIZE(ptr) (((msize_t *)(ptr))[8])
#define TINY_PREVIOUS_MSIZE(ptr) ((msize_t *)(ptr))[-1]

其中FOLLOWING_TINY_PTR(ptr, msize)获取下一个内存块的起始地址,TINY_PREVIOUS_MSIZE获取该地址的前2字节,即当前内存块的最后2字节存储msize,TINY_FREE_SIZE(ptr)是起始地址偏移16字节后的2字节存储msize。

magazine相关

mag_free_list

如上文所述,magazine使用空闲链表mag_free_list[slot]来管理所有空闲的内存块,即magazine下所有region中的相同msize的空闲内存块,都会被维护到同一个空闲链表mag_free_list[slot]中,如图所示,magazine下共分配了region1和region2两块region,其中region1中包含两块msize=3的空闲块,region2中包含一块,3个空闲块组成了。

magazine的mag_free_list[slot]记录的是链表的第一个空闲内存块的地址。

mag_bitmap

如上文所述,和mag_free_list字段对应,magazine使用mag_bitmap字段记录magazine是否存在msize的空闲内存块,每个mag_bitmap[i]是uint32_t类型4字节32个bit位,因此可以存储32个msize的信息,具体可以参考"bit位设置"的介绍。

region相关

free_blocks_by_slot

除了magazine维护的空闲链表,每个region也记录了该region内的所有空闲内存块,存储在free_blocks_by_slot字段中。

arduino 复制代码
typedef struct tiny_region {
    // Indices of the first and last free block in this region. Value is the
    // block index + 1 so that 0 indicates no free block in this region for the
    // corresponding slot.
    region_free_blocks_t free_blocks_by_slot[NUM_TINY_SLOTS];
} * tiny_region_t;

free_blocks_by_slot数组存储的是相同msize的第一个空闲块的block index和最后一个空闲块的block index,index是从1开始,而不是0,如果free_blocks_by_slot[msize]是0,表示region中没有长度是msize的空闲块。数据结构如下:

arduino 复制代码
typedef struct {
    uint16_t first_block;
    uint16_t last_block;
} region_free_blocks_t;

region_free_blocks_t结构的first_block存储了region中最近一次被回收的空闲内存块的block index,last_block存储了最先被回收的空闲内存块的block index。

如图所示,例如region中先后回收了f1、f2、f3、f4个内存块,对应的起始block index分别是5、7、10、19,其中f1、f2、f4的msize=1,则region_free_blocks_t[0]的first_block存储f4的block index+1=20,last_block存储f1的block index+1=6。region中msize=3的内存块只有f3被回收,则region_free_blocks_t[2]的first_block和last_block的值都是10+1=11。

之所以region中也记录空闲内存块,是为了方便操作空闲链表的操作,例如快速定位region内最近一次被回收的内存块。

相关操作

分析了数据结构后,接下来结合代码分析空闲链表的相关操作。在梳理逻辑之前,首先介绍一下magazine的多个region中空闲内存块的链表结构。

如图,mag_free_list中维护的空闲内存节点的整体顺序和region内存的前后顺序一致,对于同一块region中的内存,则将最新回收的内存插入最前面。例如上图,内存1~5依次被回收,24位于region1,则24插入链表前部,4回收时机较迟,作为首节点,2位于4后面,13位于region2,位于链表中部,3较晚回收,3在1前面,5位于region3,放在链表最后。region内的空闲内存节点顺序和region_free_blocks_t[msize]的的block index顺序一致,最新回收的内存的block index是first_block,最早回收的内存的block index是last_block。

添加进链表

当一个内存块被回收时,需要将其添加进空闲链表,代码如下:

ini 复制代码
static void tiny_free_list_add_ptr(rack_t *rack, magazine_t *tiny_mag_ptr, void *ptr, msize_t msize)
{
    grain_t slot = (!msize || (msize > NUM_TINY_SLOTS)) ? NUM_TINY_SLOTS : msize - 1;
    tiny_free_list_t *free_ptr = ptr;
    tiny_free_list_t *free_head = tiny_mag_ptr->mag_free_list[slot].p;
    //1.设置空闲状态
    set_tiny_meta_header_free(ptr, msize);
    //2.设置bit位状态
    if (free_head) {
    } else {
        BITMAPV_SET(tiny_mag_ptr->mag_bitmap, slot);
    }
    
    tiny_region_t region = TINY_REGION_FOR_PTR(ptr);
    //region内msize对应的空闲内存index
    region_free_blocks_t *free_blocks = &region->free_blocks_by_slot[slot];
    //第一个空闲块
    uint16_t first_free_block_index = free_blocks->first_block;
    //当前内存块的block index
    uint16_t this_block_index = TINY_INDEX_FOR_PTR(ptr);
    //存在msize对应的空闲内存index
    if (first_free_block_index) {
        //firstblock对应的空闲块
        tiny_free_list_t *old_first_free = TINY_PTR_FOR_INDEX(first_free_block_index - 1, region);
        //前一个内存块,位于其他region内
        tiny_free_list_t *prev_ptr = free_list_unchecksum_ptr(rack, &old_first_free->previous);
        if (!prev_ptr) {
            //没有prev,则作为mag_free_list的首个元素
            tiny_mag_ptr->mag_free_list[slot].p = free_ptr;
        } else {
            //有prev,则插入prev的后面
            prev_ptr->next.u = free_list_checksum_ptr(rack, free_ptr); // XXX
        }
        //设置prev关系
        free_ptr->previous.u = old_first_free->previous.u;        
        //设置next和prev关系,free_ptr成为链表中在region区的首个节点
        free_ptr->next.u = free_list_checksum_ptr(rack, old_first_free);
        old_first_free->previous.u = free_list_checksum_ptr(rack, free_ptr);
        //设置为空闲blocks的第一个节点
        free_blocks->first_block = this_block_index + 1;
    } else {
        //当前region没有firstblock,ptr是region下第一个空闲block
        tiny_free_list_t *prev_free = NULL;
        tiny_free_list_t *next_free;
        mag_index_t mag_index = MAGAZINE_INDEX_FOR_TINY_REGION(region);
        //遍历mag下的regions,找出前一个region的lastblock,prev_free是对应的内存块
        if (mag_index != DEPOT_MAGAZINE_INDEX
                && tiny_mag_ptr->mag_free_list[slot].p) {
            region_trailer_t *trailer = REGION_TRAILER_FOR_TINY_REGION(region);
            prev_free = tiny_earlier_region_last_free(tiny_mag_ptr, trailer, slot);
        }
        if (!prev_free) {
            //不存在prev,则作为mag_free_list的首个元素
            next_free = tiny_mag_ptr->mag_free_list[slot].p;
            tiny_mag_ptr->mag_free_list[slot].p = free_ptr;
        } else {
            //存在prev,则插入prev之后
            next_free = free_list_unchecksum_ptr(rack, &prev_free->next);
            prev_free->next.u = free_list_checksum_ptr(rack, free_ptr);
        }
        free_ptr->previous.u = free_list_checksum_ptr(rack, prev_free);

        //重新设置prev和next
        if (next_free) {
            next_free->previous.u = free_list_checksum_ptr(rack, free_ptr);
        }
        free_ptr->next.u = free_list_checksum_ptr(rack, next_free);
        //成为当前region的firstblock
        free_blocks->first_block = free_blocks->last_block =
                this_block_index + 1;
    }
}

结合图示分析:

  1. 当内存ptr被回收,首先调用set_tiny_meta_header_free方法设置当前内存为空闲状态,并更新设置msize信息。

  2. 根据内存的msize定位mag_free_list,如果mag_free_list链表为空,则设置mag_bitmap,标记magazine存在msize的空闲内存。

  3. 根据region的free_blocks字段,查找region内存是否存在空闲内存块,如果存在,则将ptr插入该内存节点之前,并更新当前region的free_blocks->first_block为ptr的block index。下图中内存块5是当前region的first_block,ptr插入3和5之间。

  4. 如果不存在,则通过tiny_earlier_region_last_free方法,查找离当前region最近的前一个region中的空闲内存块,将ptr插入该内存节点之后,并更新当前region的free_blocks->first_block和last_block为ptr的block index。下图中,ptr所在的region没有空闲块,则向前查找region,1是最近的空闲块,ptr放在1之后。 其中tiny_earlier_region_last_free方法通过遍历region链表,查找是否存在相同msize的空闲块,直到遍历到当前region为止。

    ini 复制代码
    static void * tiny_earlier_region_last_free(magazine_t *tiny_mag_ptr,
        region_trailer_t *trailer, grain_t slot)
    {
        //...
        while (next_trailer && next_trailer != trailer && count++ < 5) {
            tiny_region_t r = TINY_REGION_FOR_PTR(next_trailer);
            uint16_t block = r->free_blocks_by_slot[slot].last_block;
            if (block) {
                target_block = block;
                target_trailer = next_trailer;
            }
            next_trailer = next_trailer->next;
        }
    }
  5. 如果前面的region不存在空闲内存,则mag_free_list[msize]的首节点是ptr。

从链表删除

当空闲内存块被重新使用,需要从空闲链表结构中删除。

ini 复制代码
static void tiny_free_list_remove_ptr(rack_t *rack, magazine_t *tiny_mag_ptr, void *ptr, msize_t msize)
{
    grain_t slot = tiny_slot_from_msize(msize);
    tiny_free_list_t *free_ptr = ptr, *next, *previous;
    //后一个空闲块
    next = free_list_unchecksum_ptr(rack, &free_ptr->next);
    //前一个空闲块
    previous = free_list_unchecksum_ptr(rack, &free_ptr->previous);
    
    if (!previous) { //没有prev,则删除的是链表首节点 
        //next设置为首节点
        tiny_mag_ptr->mag_free_list[slot].p = next;
        if (!next) {
            BITMAPV_CLR(tiny_mag_ptr->mag_bitmap, slot);
        }
    } else {
        //设置prev的next指针指向next内存
        previous->next = free_ptr->next;
    }
    if (next) { //存在next内存
        //设置next的prev指针指向prev内存
        next->previous = free_ptr->previous;
    }
    
    tiny_region_t region = TINY_REGION_FOR_PTR(ptr);
    region_free_blocks_t *free_blocks = &region->free_blocks_by_slot[slot];
    uint16_t this_block_index = TINY_INDEX_FOR_PTR(ptr);
    //是否是region中first_block或者last_block
    boolean_t is_first = free_blocks->first_block == this_block_index + 1;
    boolean_t is_last = free_blocks->last_block == this_block_index + 1;

    if (is_first && is_last) {
        //是first_block和last_block,则region中只有该msize空闲块,移除后,region内不存在msize空闲块,更新为0
        free_blocks->first_block = free_blocks->last_block = 0;
    } else if (is_first) {
        //是first,则存在next内存,移除后first替换为next
        free_blocks->first_block = TINY_INDEX_FOR_PTR(next) + 1;
    } else if (is_last) {
        //是last,则存在prev内存,移除后last替换为previous
        free_blocks->last_block = TINY_INDEX_FOR_PTR(previous) + 1;
    }
}

结合图示分析:

  1. 例如空闲内存块ptr被再次使用时,首先通过prev、next指针查找链表上前后空闲块。
  2. 如果没有prev,则ptr是mag_free_list链表的首节点,则将next设置为首节点,如果next也不存在,则ptr移除后链表中已经不存在msize的空闲块了,调用BITMAPV_CLR清空将mag_bitmap上的对应bit标记位置为0。
  3. 如果有prev,则prev的next指针指向next内存。这样ptr从链表中删除。
  4. 更新region的free_blocks字段,如果first_block和last_block均等于ptr的block index,则当前region只存在一个msize的空闲块ptr,移除后,first_block和last_block均置为0。
  5. 如果first_block和last_block不等,则region中至少包含两个msize的空闲块。
    1. 如果first_block是ptr的block index,则ptr是链表中在region范围内的第一个空闲节点,ptr移除后,first_block替换为链表中后一个空闲块的block index。
    2. 如果last_block是ptr的block index,则ptr是链表中在region范围内的最后一个空闲节点,ptr移除后,last_block替换为链表中前一个空闲块的block index。
查看前后空闲块

获取相邻前一个空闲块的内存起始地址。

ini 复制代码
static void *tiny_previous_preceding_free(void *ptr, msize_t *prev_msize)
{
    uint32_t *block_header;
    uint32_t *in_use;
    msize_t index;
    msize_t previous_msize;
    msize_t previous_index;
    void *previous_ptr;
    
    block_header = TINY_BLOCK_HEADER_FOR_PTR(ptr);
    in_use = TINY_INUSE_FOR_HEADER(block_header);
    index = TINY_INDEX_FOR_PTR(ptr);

    if (!index) {
        return NULL;
    }
    if ((previous_msize = get_tiny_previous_free_msize(ptr)) > index) {
        return NULL;
    }

    previous_index = index - previous_msize;
    previous_ptr = TINY_PTR_FOR_INDEX(previous_index, TINY_REGION_FOR_PTR(ptr));
    if (!BITARRAY_BIT(block_header, previous_index)) {
        return NULL;
    }
    if (BITARRAY_BIT(in_use, previous_index)) {
        return NULL;
    }
    if (get_tiny_free_size(previous_ptr) != previous_msize) {
        return NULL;
    }

    // conservative check did match true check
    *prev_msize = previous_msize;
    return previous_ptr;
}

ptr是当前空闲内存块的地址,步骤如下:

  1. index是ptr的block index,使用get_tiny_previous_free_msize尝试获取前一个空闲内存的msize。

    scss 复制代码
    #define TINY_PREVIOUS_MSIZE(ptr) ((msize_t *)(ptr))[-1]
    #define TINY_FREE_SIZE(ptr) (((msize_t *)(ptr))[8])
    
    static msize_t get_tiny_previous_free_msize(const void *ptr)
    {
        if (ptr != TINY_REGION_HEAP_BASE(TINY_REGION_FOR_PTR(ptr))) {
            void *prev_block = (void *)((uintptr_t)ptr - TINY_QUANTUM);
            uint32_t *prev_header = TINY_BLOCK_HEADER_FOR_PTR(prev_block);
            msize_t prev_index = TINY_INDEX_FOR_PTR(prev_block);
            if (BITARRAY_BIT(prev_header, prev_index)) {
                return 1;
            }
            msize_t *prev_msize_ptr = &TINY_PREVIOUS_MSIZE(ptr);
            return _malloc_read_uint16_via_rsp(prev_msize_ptr);
        }
        return 0;
    }

    首先判断向前偏移一个block的内存,如果BITARRAY_BIT为1,则存在内存块,msize是1,如果不是,则使用TINY_PREVIOUS_MSIZE宏取ptr地址的向前偏移2字节的内存,因为存储了前一个空闲块的msize信息,返回值为previous_msize。

  2. previous_ptr是向前偏移previous_msize长度后,前一个空闲块的起始地址,previous_index是对应的block index。

  3. 判断previous_index的block_header bit位,如果是1,则存在该内存,进一步判断inuse bit位,如果是0,则是空闲块,调用get_tiny_free_size取8字节后的2字节存储的msize和之前的previous_msize比较,进一步校验。这些通过后,表示存在相邻的前一块空闲块,长度是previous_msize。

相较于获取前一块内存地址,获取相邻地址后一块空闲内存块的地址简单,ptr地址直接加上msize得到next起始地址。

ini 复制代码
size_t original_size = TINY_BYTES_FOR_MSIZE(msize);
void *next_block = ((unsigned char *)ptr + original_size);

如图所示: 接下来,结合代码分析tiny内存的分配与回收的整体逻辑。

底层内存管理

当分配和回收region时触发系统层的分配操作,实质调用底层接口管理虚拟内存,libmalloc做了封装,支持参数控制。

内存分配

ini 复制代码
void *mvm_allocate_pages(size_t size, unsigned char align, uint32_t debug_flags, int vm_page_label) {
    boolean_t add_prelude_guard_page = debug_flags & MALLOC_ADD_PRELUDE_GUARD_PAGE;
    boolean_t add_postlude_guard_page = debug_flags & MALLOC_ADD_POSTLUDE_GUARD_PAGE;
    boolean_t purgeable = debug_flags & MALLOC_PURGEABLE;
    boolean_t use_entropic_range = !(debug_flags & DISABLE_ASLR);
    vm_address_t vm_addr;
    uintptr_t addr;
    vm_size_t allocation_size = round_page_quanta(size);
    mach_vm_offset_t allocation_mask = ((mach_vm_offset_t)1 << align) - 1;
    int alloc_flags = VM_FLAGS_ANYWHERE | VM_MAKE_TAG(vm_page_label);
    kern_return_t kr;
    
    if (!allocation_size) {
        allocation_size = vm_page_quanta_size;
    }
    if (add_postlude_guard_page || add_prelude_guard_page) {
        if (add_prelude_guard_page && align > vm_page_quanta_shift) {
            allocation_size += (1 << align) + large_vm_page_quanta_size;
        } else {
            allocation_size += add_prelude_guard_page && add_postlude_guard_page ?
                    2 * large_vm_page_quanta_size : large_vm_page_quanta_size;
        }
    }
    if (purgeable) {
        alloc_flags |= VM_FLAGS_PURGABLE;
    }
    if (allocation_size < size) {
        return NULL;
    }
retry:
    vm_addr = use_entropic_range ? entropic_address : vm_page_quanta_size;
    kr = mach_vm_map(mach_task_self(), &vm_addr, allocation_size,
            allocation_mask, alloc_flags, MEMORY_OBJECT_NULL, 0, FALSE,
            VM_PROT_DEFAULT, VM_PROT_ALL, VM_INHERIT_DEFAULT);
    if (kr == KERN_NO_SPACE && use_entropic_range) {
        vm_addr = vm_page_quanta_size;
      kr = mach_vm_map(mach_task_self(), &vm_addr, allocation_size,
              allocation_mask, alloc_flags, MEMORY_OBJECT_NULL, 0, FALSE,
              VM_PROT_DEFAULT, VM_PROT_ALL, VM_INHERIT_DEFAULT);
    }
    if (kr) {
        if (kr != KERN_NO_SPACE) {
            //...
        }
        return NULL;
    }
    //...
    if (add_postlude_guard_page || add_prelude_guard_page) {
        //...
        mvm_protect((void *)addr, size, PROT_NONE, debug_flags);
    }
    return (void *)addr;
}

核心调用是通过xnu的mach_vm_map分配一块allocation_size大小的虚拟内存,allocation_size是经过对齐后的大小,debug_flags控制分配逻辑,例如add_prelude_guard_page和add_postlude_guard_page,通过mvm_protect方法对指定内存的操作权限进行控制。

内存回收

ini 复制代码
void mvm_deallocate_pages(void *addr, size_t size, unsigned debug_flags)
{
    boolean_t added_prelude_guard_page = debug_flags & MALLOC_ADD_PRELUDE_GUARD_PAGE;
    boolean_t added_postlude_guard_page = debug_flags & MALLOC_ADD_POSTLUDE_GUARD_PAGE;
    vm_address_t vm_addr = (vm_address_t)addr;
    vm_size_t allocation_size = size;
    kern_return_t kr;

    if (added_prelude_guard_page) {
        vm_addr -= large_vm_page_quanta_size;
        allocation_size += large_vm_page_quanta_size;
    }
    if (added_postlude_guard_page) {
        allocation_size += large_vm_page_quanta_size;
    }
    kr = vm_deallocate(mach_task_self(), vm_addr, allocation_size);
    if (kr) {
        malloc_zone_error(debug_flags, false, "Can't deallocate_pages region at %p\n", addr);
    }
}

核心方法是通过vm_deallocate回收对齐后的内存空间。

内存权限

arduino 复制代码
#define PROT_NONE       0x00    /* [MC2] no permissions */
#define PROT_READ       0x01    /* [MC2] pages can be read */
#define PROT_WRITE      0x02    /* [MC2] pages can be written */
#define PROT_EXEC       0x04    /* [MC2] pages can be executed */

void mvm_protect(void *address, size_t size, unsigned protection, unsigned debug_flags)
{
    kern_return_t err;

    if ((debug_flags & MALLOC_ADD_PRELUDE_GUARD_PAGE) && !(debug_flags & MALLOC_DONT_PROTECT_PRELUDE)) {
        err = mprotect((void *)((uintptr_t)address - large_vm_page_quanta_size), large_vm_page_quanta_size, protection);
    }
    if ((debug_flags & MALLOC_ADD_POSTLUDE_GUARD_PAGE) && !(debug_flags & MALLOC_DONT_PROTECT_POSTLUDE)) {
        err = mprotect((void *)(round_page_quanta(((uintptr_t)address + size))), large_vm_page_quanta_size, protection);
    }
}

调用mprotect设置虚拟内存的访问权限,address和size控制了内存范围。

了解tiny相关结构后,接下来分析tiny是如何分配和和回收内存的。

depot magazine

depot magazine是rack下一个特殊的magazine,下标是-1.

arduino 复制代码
#define DEPOT_MAGAZINE_INDEX -1
rack->magazines[DEPOT_MAGAZINE_INDEX]

depot magazine下维护的region是从原magazine中的region迁移过来的,触发条件是,当内存回收时,region中的空闲内存占比达到一定阈值时,发生迁移,主要包含两部分:

  1. region从原magazine迁移至depot magazine。
  2. region内的所有空闲内存块从原mag_free_list空闲链表迁移至depot magazine的mag_free_list。

迁移操作如图,深黄色要迁移的region,regions链表和freelist迁移至depot magazine中。 逻辑整体逻辑如下:

arduino 复制代码
static MALLOC_INLINE boolean_t
tiny_free_try_recirc_to_depot(rack_t *rack,
                              magazine_t *tiny_mag_ptr,
                              mag_index_t mag_index,
                              region_t region,
                              void *headptr,
                              size_t headsize,
                              void *ptr,
                              msize_t msize)
{
    region_trailer_t *node = REGION_TRAILER_FOR_TINY_REGION(region);
    size_t bytes_used = node->bytes_used;

    if (DEPOT_MAGAZINE_INDEX != mag_index) {
        //当空闲内存达到一定程度,移到depot magazine
        if (tiny_magazine_below_recirc_threshold(tiny_mag_ptr)) {
            return tiny_free_do_recirc_to_depot(rack, tiny_mag_ptr, mag_index);
        }
    }
}

首先进行阈值判断:

scss 复制代码
static MALLOC_INLINE boolean_t tiny_magazine_below_recirc_threshold(magazine_t *mag_ptr)
    size_t a = mag_ptr->num_bytes_in_magazine;  // Total bytes allocated to this magazine
    size_t u = mag_ptr->mag_num_bytes_in_objects; // In use (malloc'd) from this magaqzine
    return a - u > ((3 * TINY_HEAP_SIZE) / 2)
            && u < DENSITY_THRESHOLD(a);
}

#define DENSITY_THRESHOLD(a) ((a) - ((a) >> 2)) 

条件是当magazine中未使用的内存大小大于总内存的3/4时,发生迁移操作。

调用tiny_free_do_recirc_to_depot方法,代码如下:

ini 复制代码
static boolean_t tiny_free_do_recirc_to_depot(rack_t *rack, magazine_t *tiny_mag_ptr, mag_index_t mag_index)
{
    region_trailer_t *node = tiny_mag_ptr->lastNode;
    region_t sparse_region = TINY_REGION_FOR_PTR(node);
    //...
    recirc_list_extract(rack, tiny_mag_ptr, node);
    int objects_in_use = tiny_free_detach_region(rack, tiny_mag_ptr, sparse_region);
    magazine_t *depot_ptr = &(rack->magazines[DEPOT_MAGAZINE_INDEX]);
    //...
    size_t bytes_inplay = tiny_free_reattach_region(rack, depot_ptr, sparse_region);
    tiny_mag_ptr->mag_num_bytes_in_objects -= bytes_inplay;
    tiny_mag_ptr->num_bytes_in_magazine -= TINY_HEAP_SIZE;
    tiny_mag_ptr->mag_num_objects -= objects_in_use;
    //...
    depot_ptr->mag_num_bytes_in_objects += bytes_inplay;
    depot_ptr->num_bytes_in_magazine += TINY_HEAP_SIZE;
    depot_ptr->mag_num_objects += objects_in_use;

    recirc_list_splice_last(rack, depot_ptr, node);
    //...
}

包括以下流程:

  1. 调用recirc_list_extract方法将region从magazine的region链表中删除
ini 复制代码
region_trailer_t *node = tiny_mag_ptr->lastNode;
recirc_list_extract(rack, tiny_mag_ptr, node);
  1. 调用tiny_free_detach_region方法将region中的所有内存块从magazine的freelist中删除。
ini 复制代码
int tiny_free_detach_region(rack_t *rack, magazine_t *tiny_mag_ptr, region_t r)
{
    uintptr_t start = (uintptr_t)TINY_REGION_HEAP_BASE(r);
    uintptr_t current = start;
    uintptr_t limit = (uintptr_t)TINY_REGION_HEAP_END(r);
    boolean_t is_free;
    msize_t msize;
    region_trailer_t *trailer = REGION_TRAILER_FOR_TINY_REGION(r);

    while (current < limit) {
        msize = get_tiny_meta_header((void *)current, &is_free);
        if (is_free && !msize && (current == start)) {
            break;
        }
        if (!msize) {
            break;
        }
        if (is_free) {
            //从空闲链表中删除空闲内存
            tiny_free_list_remove_ptr(rack, tiny_mag_ptr, (void *)current, msize);
        }
        current += TINY_BYTES_FOR_MSIZE(msize);
    }
    return trailer->objects_in_use;
}

方法内部遍历region中的所有空闲内存块,调用tiny_free_list_remove_ptr将其从mag_free_list中删除。 3. 调用tiny_free_reattach_region方法将region中的所有内存块添加进depot magazine的freelist中。

ini 复制代码
size_t tiny_free_reattach_region(rack_t *rack, magazine_t *tiny_mag_ptr, region_t r)
{
    uintptr_t start = (uintptr_t)TINY_REGION_HEAP_BASE(r);
    uintptr_t current = start;
    uintptr_t limit = (uintptr_t)TINY_REGION_HEAP_END(r);
    boolean_t is_free;
    msize_t msize;
    size_t bytes_used = REGION_TRAILER_FOR_TINY_REGION(r)->bytes_used;

    while (current < limit) {
        msize = get_tiny_meta_header((void *)current, &is_free);
        if (is_free && !msize && (current == start)) {
            // first block is all free
            break;
        }
        if (!msize) {
            break;
        }
        if (is_free) {
            //加入free链表
            tiny_free_list_add_ptr(rack, tiny_mag_ptr, (void *)current, msize);
        }
        current += TINY_BYTES_FOR_MSIZE(msize);
    }
    return bytes_used;
}

方法内部遍历region中的所有空闲内存块,调用tiny_free_reattach_region将其添加进mag_free_list中。

  1. 更新magazine的统计信息字段,例如mag_num_bytes_in_objects。
  2. 调用recirc_list_splice_last方法将region节点添加进depot magazine的region trailer链表
scss 复制代码
recirc_list_splice_last(rack, depot_ptr, node);

recirc_list_extract、recirc_list_splice_last的具体实现上文已经介绍。

核心流程

介绍完相关结构,接下来还是以tiny内存为例,介绍内存分配回收的具体流程。

内存分配

tiny_malloc_should_clear方法是内存分配的核心方法,核心实现步骤如下:

ini 复制代码
void *tiny_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
    void *ptr;
    mag_index_t mag_index = tiny_mag_get_thread_index() % rack->num_magazines;
    magazine_t *tiny_mag_ptr = &(rack->magazines[mag_index]);
    //...
#if CONFIG_TINY_CACHE
    ptr = tiny_mag_ptr->mag_last_free;
    if (tiny_mag_ptr->mag_last_free_msize == msize) {
        tiny_mag_ptr->mag_last_free = NULL;
        tiny_mag_ptr->mag_last_free_msize = 0;
        tiny_mag_ptr->mag_last_free_rgn = NULL;
        tiny_check_zero_and_clear(ptr, msize, cleared_requested);
        return ptr;
    }
#endif
    while (1) {
        ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
        if (ptr) {
            tiny_check_zero_and_clear(ptr, msize, cleared_requested);
            return ptr;
        }
#if CONFIG_RECIRC_DEPOT
        if (tiny_get_region_from_depot(rack, tiny_mag_ptr, mag_index, msize)) {
            ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
            if (ptr) {
                tiny_check_zero_and_clear(ptr, msize, cleared_requested);
                return ptr;
            }
        }
#endif
        if (!tiny_mag_ptr->alloc_underway) {
            void *fresh_region;
            
            tiny_mag_ptr->alloc_underway = TRUE;
                 
            fresh_region = mvm_allocate_pages(TINY_REGION_SIZE,
                                        TINY_BLOCKS_ALIGN,
                                        MALLOC_FIX_GUARD_PAGE_FLAGS(rack->debug_flags),
                                        VM_MEMORY_MALLOC_TINY);
            if (!fresh_region) { // out of memory!
                tiny_mag_ptr->alloc_underway = FALSE;
                return NULL;
            }
            ptr = tiny_malloc_from_region_no_lock(rack, tiny_mag_ptr, mag_index, msize, fresh_region);
        } else {
            yield();
        }
    }
}

核心步骤整理如下:

  1. 选取处理内存的magazine对象tiny_mag_ptr。
  2. cache匹配逻辑,如果命中则直接返回缓存mag_last_free,不命中继续。
  3. 从缓存链表freeelist和现有的region中查找可用内存,成功返回,失败继续。
  4. 查找depot magazine是否有空闲内存,如果有,则将region以及region内的空闲内存迁移到tiny_mag_ptr,不存在则继续。
  5. 分配一块新的region,并从新region中分配内存。如果失败,返回NULL。

每一步骤涉及的内容较多,下面结合代码与图示具体展开讲解。

cache逻辑

为了提升分配时的性能,当最近一次内存被回收时,使用mag_last_free和mag_last_free_msize记录这次被回收的内存和msize,而不添加进空闲链表mag_free_list。当下一次分配内存时,如果size大小匹配缓存大小,则直接取缓存内存mag_last_free,而不从空闲链表mag_free_list中查找。这样提升了分配效率,如果缓存没匹配上,则从mag_free_list中查找。执行cache匹配逻辑,cache会在内存回收时更新。

更新

由于magazine中存储的cache是最近一次被回收的内存,因此每次回收内存时,会触发cache的更新逻辑。

ini 复制代码
void free_tiny(rack_t *rack, void *ptr, region_t tiny_region, size_t known_size,
        boolean_t partial_free)
{
    //取当前cache
    void *ptr2 = tiny_mag_ptr->mag_last_free;
    msize_t msize2 = tiny_mag_ptr->mag_last_free_msize;
    region_t rgn2 = tiny_mag_ptr->mag_last_free_rgn;
    //更新cache
    tiny_mag_ptr->mag_last_free = ptr;
    tiny_mag_ptr->mag_last_free_msize = msize;
    tiny_mag_ptr->mag_last_free_rgn = tiny_region;
    
    msize = msize2;
    ptr = ptr2;
    tiny_region = rgn2;
    flags |= TINY_FREE_FLAG_FROM_CACHE;
    
    //之前cache回收添加进freelist
    if (tiny_free_no_lock(rack, tiny_mag_ptr, mag_index, tiny_region, ptr, msize, flags)) {
    }
}

首先取出magazine的cache,cache更新为当前回收的内存,将之前cache回收添加进freelist。更新cache相关的三个字段:

  • mag_last_free:回收的内存地址,作为cache内存地址
  • mag_last_free_msize:回收的内存的msize,表示缓存内存块的大小,cache匹配时使用
  • mag_last_free_rgn:回收内存的region。

无论之前的cache是否被使用,都会被更新成新回收的内存和msize,如图:

使用

分配内存时,首先匹配cache,如果要分配的内存msize和mag_last_free_msize相等,则命中cache,直接返回mag_last_free,并且清空cache相关字段。如果下次内存分配前没有发生内存回收,则下次分配时无缓存,执行后续逻辑。

ini 复制代码
void *tiny_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
    //...
#if CONFIG_TINY_CACHE
    ptr = tiny_mag_ptr->mag_last_free;
    //匹配成功,直接返回cache,清空cache字段。
    if (tiny_mag_ptr->mag_last_free_msize == msize) {
        tiny_mag_ptr->mag_last_free = NULL;
        tiny_mag_ptr->mag_last_free_msize = 0;
        tiny_mag_ptr->mag_last_free_rgn = NULL;
        tiny_check_zero_and_clear(ptr, msize, cleared_requested);
        return ptr;
    }
#endif
}

如果cache未命中,则调用tiny_malloc_from_free_list方法从已有的region中查找一块合适的内存空间返回。

空闲内存查找

从magazine管理的空闲链表mag_free_list或者当前region内的剩余空间查找可用的内存块返回。

ini 复制代码
void *tiny_malloc_from_free_list(rack_t *rack, magazine_t *tiny_mag_ptr, mag_index_t mag_index, msize_t msize)
{
    tiny_free_list_t *ptr;
    msize_t this_msize;
    //计算msize
    grain_t slot = tiny_slot_from_msize(msize);
    //查找空闲链表
    free_list_t *free_list = tiny_mag_ptr->mag_free_list;
    free_list_t *the_slot = free_list + slot;
    tiny_free_list_t *next;
    free_list_t *limit;
    uint64_t bitmap;
    //剩余size
    msize_t leftover_msize;
    tiny_free_list_t *leftover_ptr;
    
    //精确匹配,链表头节点
    ptr = the_slot->p;
    if (ptr) { //存在空闲节点
        next = free_list_unchecksum_ptr(rack, &ptr->next);
        if (next) {
            next->previous = ptr->previous; //从链表中删除ptr
        } else {
            BITMAPV_CLR(tiny_mag_ptr->mag_bitmap, slot); //没有剩余的内存块,slot位对应的bitmap置为0
        }
        the_slot->p = next; //next变为链表的头节点
        this_msize = msize; //记录this_msize
        //更新region信息
        tiny_update_region_free_list_for_remove(slot, ptr, next);
        tiny_check_and_zero_inline_meta_from_freelist(rack, ptr, msize);
        //查找成功
        goto return_tiny_alloc;
    }
    
    //取出表示大于msize的bitmap
    bitmap = ((uint64_t *)(tiny_mag_ptr->mag_bitmap))[0] & ~((1ULL << slot) - 1);
    //空闲链表中没有任何内存块,则走try_tiny_malloc_from_end逻辑
    if (!bitmap) {
        goto try_tiny_malloc_from_end;
    }
    //找到最低一个bit不为0的bitmap的位置
    slot = BITMAPV_CTZ(bitmap);
    limit = free_list + NUM_TINY_SLOTS;
    free_list += slot;
    if (free_list < limit) {
        ptr = free_list->p;
        if (ptr) {
            next = free_list_unchecksum_ptr(rack, &ptr->next);
            free_list->p = next;
            if (next) {
                next->previous = ptr->previous;
            } else {
                //没有剩余的内存块,slot位对应的bitmap置为0
                BITMAPV_CLR(tiny_mag_ptr->mag_bitmap, slot);
            }
            this_msize = get_tiny_free_size(ptr);
            tiny_update_region_free_list_for_remove(slot, ptr, next);
            tiny_check_and_zero_inline_meta_from_freelist(rack, ptr, this_msize);
            //剩余size加入free链表,返回ptr
            goto add_leftover_and_proceed;
        }
    }
    
    ptr = limit->p;
    if (ptr) {
        this_msize = get_tiny_free_size(ptr);
        next = free_list_unchecksum_ptr(rack, &ptr->next);
        //剩余size仍然大于NUM_TINY_SLOTS
        if (this_msize - msize > NUM_TINY_SLOTS) {
            leftover_msize = this_msize - msize;
            leftover_ptr = (tiny_free_list_t *)((unsigned char *)ptr + TINY_BYTES_FOR_MSIZE(msize));

            tiny_free_list_t tmp_ptr = *ptr;
            tiny_check_and_zero_inline_meta_from_freelist(rack, ptr, this_msize);

            //剩余内存仍然加入limit的free链表中
            limit->p = leftover_ptr;
            if (next) {
                next->previous.u = free_list_checksum_ptr(rack, leftover_ptr);
            }
            leftover_ptr->previous = tmp_ptr.previous;
            leftover_ptr->next = tmp_ptr.next;
            //对应bitmap设置为free
            set_tiny_meta_header_free(leftover_ptr, leftover_msize);
            this_msize = msize;
            tiny_update_region_free_list_for_remove(NUM_TINY_SLOTS, ptr, leftover_ptr);

            goto return_tiny_alloc;
        }
        if (next) {
            next->previous = ptr->previous;
        }
        limit->p = next;
        tiny_update_region_free_list_for_remove(slot, ptr, next);
        tiny_check_and_zero_inline_meta_from_freelist(rack, ptr, this_msize);
        //剩余size加入free链表,返回ptr
        goto add_leftover_and_proceed;
        /* NOTREACHED */
    }
    
try_tiny_malloc_from_end:
    //最后申请的heap region中未使用的大小大于需要的size
    if (tiny_mag_ptr->mag_bytes_free_at_end >= TINY_BYTES_FOR_MSIZE(msize)) {
        //mag_last_region是低地址,因此可用区域的地址ptr是mag_last_region-mag_bytes_free_at_end
        ptr = (tiny_free_list_t *)((uintptr_t)TINY_REGION_HEAP_END(tiny_mag_ptr->mag_last_region) - tiny_mag_ptr->mag_bytes_free_at_end);
        //更新未使用大小
        tiny_mag_ptr->mag_bytes_free_at_end -= TINY_BYTES_FOR_MSIZE(msize);
        if (tiny_mag_ptr->mag_bytes_free_at_end) {
            // let's add an in use block after ptr to serve as boundary
            //ptr~ptr+msize对应的block范围标记为使用中
            set_tiny_meta_header_in_use_1((unsigned char *)ptr + TINY_BYTES_FOR_MSIZE(msize));
        }
        this_msize = msize;
        goto return_tiny_alloc;
    }
    return NULL;

add_leftover_and_proceed: //处理剩余内存
    if (!this_msize || (this_msize > msize)) {
        // XXX This works even when (this_msize == 0) because the unsigned
        // subtraction wraps around to the correct result
        //剩余esize
        leftover_msize = this_msize - msize;
        //对应的地址
        leftover_ptr = (tiny_free_list_t *)((unsigned char *)ptr + TINY_BYTES_FOR_MSIZE(msize));
        //加入free链表
        tiny_free_list_add_ptr(rack, tiny_mag_ptr, leftover_ptr, leftover_msize);
        this_msize = msize;
    }
    
return_tiny_alloc:
    tiny_mag_ptr->mag_num_objects++;
    tiny_mag_ptr->mag_num_bytes_in_objects += TINY_BYTES_FOR_MSIZE(this_msize);

    // Check that the region cookie is intact and update the region's bytes in use count
    tiny_region_t region = TINY_REGION_FOR_PTR(ptr);
    region_check_cookie(region, &REGION_COOKIE_FOR_TINY_REGION(region));

    region_trailer_t *trailer = REGION_TRAILER_FOR_TINY_REGION(region);
    size_t bytes_used = trailer->bytes_used + TINY_BYTES_FOR_MSIZE(this_msize);
    trailer->bytes_used = (unsigned int)bytes_used;
    trailer->objects_in_use++;

    // Emptiness discriminant
    if (bytes_used < DENSITY_THRESHOLD(TINY_HEAP_SIZE)) {
    } else {
        trailer->recirc_suitable = FALSE;
    }
    if (this_msize > 1) {
        set_tiny_meta_header_in_use(ptr, this_msize);
    } else {
        set_tiny_meta_header_in_use_1(ptr);
    }
    return ptr;
}

代码较多,但是整体思路以下3点:

  1. 根据需要的msize,首先从空闲链表mag_free_list中查找一块合适的空闲内存块。
  2. 如果空闲链表中没有找到,则从region的剩余空间中划分一块msize的内存块
  3. 如果都没有找到,则返回NULL。 从mag_free_list的查找策略,分为以下2点:
  4. 首先查找msize大小完全匹配的空闲内存块,找到返回
  5. 其次查找较大msize的内存块,如果找到,则拆分分2块,前一块内存大小是需要的msize返回,后一块是剩余的内存,继续加入空闲链表中,剩余的内存分为2种情况
    1. 剩余msize小于limitsize,加入对应mag_free_list[slot]
    2. 剩余msize大于等于limitsize,仍然放在mag_free_list[limit]里
      limitsize是tiny内存的最大(1008字节)尺寸/16字节。

为了直观的介绍,用下图表示region的内存情况。 如图,mag_free_list[]中存储了各个msize的空闲链表,tiny的范围是1008字节以内,共63个空闲个mag_free_list,对于mag_free_list[msize](msize<63)存储的各节点的内存size相等,是msize*16,对于最大的mag_free_list[msize](msize=63),存储的各节点的内存size不一定相等,可能存在大于1008的内存块,因为空闲块在回收时会进行合并操作,导致合并后的空闲块大于1008B。mag_bytes_free_at_end表示region内从未分配出去的剩余内存空间的范围大小,因为分配的原则是首先查找分配后回收的内存,在没有找到的情况下才会找region的剩余空间。

接下来具体分析:

  1. 例如msize是需要分配的内存size,ptr用于存找到的内存块地址,根据msize计算其在mag_free_list的下标slot。

  2. 首先精确匹配,查找mag_free_list[slot]中是否有空闲内存块,如果有,则查找成功,通过更新next、prev指针将ptr从空闲链表中删除。如果删除后链表为空,则将mag_bitmap对应的标记位清除,表示magazine没有msize的空闲内存。同时调用tiny_update_region_free_list_for_remove方法更新region的free_blocks_by_slot字段。操作结束后,进入return_tiny_alloc流程。

  3. 如果精确匹配没找到,则从mag_free_list[i](i>slot)存储较大空闲块的链表中查找。具体实现如下:

    • 读取mag_bitmap上存储的数据,返回所有bit位是1且高于msize的bit位的下标,这些下标对应的size级别均大于msize。如果没有找到bit位,则不继续处理,进入try_tiny_malloc_from_end流程。

    • 如果找到,则通过BITMAPV_CTZ(bitmap)从中取出最低的bit位下标,对应的size级别最小。如图,例如需要分配msize=2(32B的)内存,对应的bit位如下,取出大于该bit位的所有值为1的bit位数据,取出其中最低的bit位,对应的msize=4。

    • 定位到对应的slot下标和mag_free_list[slot]链表入口,如果slot小于limit(limit是tiny内存mag_free_list[]最大的下标63),则将mag_free_list[slot]的空闲内存块拆分成两块,前半部分作为分配的内存返回,后半部分作为一块新的空闲内存重新加入对应的mag_free_list中,如图所示: 例如,找到msize=4的内存块,首先修改前后指针将内存块从原链表删除,然后进入add_leftover_and_proceed流程,进入拆分逻辑,例如需要msize=2的内存,则取前32B的内存块返回,同时剩余的32B的内存块加入对应的mag_free_list[slot]中。

    • 如果没找到,则查找最后一个链表mag_free_list[limit]中的空闲内存块,如果找到,修改前后指针将内存块从原链表删除,拆分成两块,前msize部分作为ptr返回,针对剩余内存块(leftover_ptr)判断,如果块leftover_ptr的size仍然大于limit size(1008),则仍然将leftover_ptr加入mag_free_list[limit]中。如图所示,内存块1056B,取前32B返回,剩余部分1024B,仍然加入mag_free_list[limit]。 同时将leftover_ptr加入pairs状态标记位中,leftover_ptr是新的内存块。如下图,拆分后,pairs状态标记位多记录了一个leftover_ptr内存信息。然后走return_tiny_alloc流程。

    • 如果leftover_ptr的size小于等于limit size,则和步骤c一样进入add_leftover_and_proceed流程。

  4. 当前面的空闲链表逻辑均没有可用的内存块时,则进入try_tiny_malloc_from_end流程,即查看magazine的当前region(mag_last_region)剩余的空间大小mag_bytes_free_at_end是否满足分配msize需求,如果满足,则分配后进入return_tiny_alloc流程,否则直接返回NULL,表示内存查找失败。如图,mag_bytes_free_at_end满足分配msize,则分配一块msize的内存,mag_bytes_free_at_end-=msize。

  5. add_leftover_and_proceed流程,处理拆分大内存块后的逻辑,将剩余内存leftover_ptr加入对应size的空闲链表中,并在pairs中设置状态标记为空闲,进入return_tiny_alloc流程

  6. return_tiny_alloc流程,最后流程,将当前分配到的ptr,设置pairs中设置状态标记为使用中,并更新magezine和region相关的统计信息,包括

    ini 复制代码
    tiny_mag_ptr->mag_num_objects++;
    tiny_mag_ptr->mag_num_bytes_in_objects
    
    trailer->bytes_used = (unsigned int)bytes_used;
    trailer->objects_in_use++;
    • mag_num_objects:magazine分配的总内存块数
    • mag_num_bytes_in_objects:magazine分配的总内存大小
    • bytes_used:region使用的总内存大小
    • objects_in_use:region使用的总内存块数

整体逻辑可以用一张流程图来表示:

depot查找

如果调用tiny_malloc_from_free_list方法没有找到可用的内存块,则调用tiny_get_region_from_depot方法查看depot magazine中的region中是否有合适的内存块。

ini 复制代码
if (tiny_get_region_from_depot(rack, tiny_mag_ptr, mag_index, msize)) {
    //迁移后接着查找
    ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
    if (ptr) {
        return ptr;
    }
}

static boolean_t tiny_get_region_from_depot(rack_t *rack, magazine_t *tiny_mag_ptr, mag_index_t mag_index, msize_t msize)
{
    magazine_t *depot_ptr = &(rack->magazines[DEPOT_MAGAZINE_INDEX]);
    region_trailer_t *node;
    region_t sparse_region;
    msize_t try_msize = msize;
    
    while (1) {
        //寻找一个大于等于申请大小msize的节点,返回该节点的heap region
        sparse_region = tiny_find_msize_region(rack, depot_ptr, DEPOT_MAGAZINE_INDEX, try_msize);
        if (NULL == sparse_region) { // Depot empty?
            return 0;
        }

        node = REGION_TRAILER_FOR_TINY_REGION(sparse_region);
        if (0 == node->pinned_to_depot) {
            // Found one!
            break;
        }
        try_msize++;

        if (try_msize > NUM_TINY_SLOTS) {
            SZONE_MAGAZINE_PTR_UNLOCK(depot_ptr);
            return 0;
        }
    }
    //通过trailer,从node链表中移除region
    recirc_list_extract(rack, depot_ptr, node);

    //将空闲内存从depot_ptr的free链表中删除
    int objects_in_use = tiny_free_detach_region(rack, depot_ptr, sparse_region);

    // Transfer ownership of the region
    MAGAZINE_INDEX_FOR_TINY_REGION(sparse_region) = mag_index;

    //将空闲内存加入tiny_mag_ptr的free链表
    size_t bytes_inplay = tiny_free_reattach_region(rack, tiny_mag_ptr, sparse_region);

    depot_ptr->mag_num_bytes_in_objects -= bytes_inplay;
    depot_ptr->num_bytes_in_magazine -= TINY_HEAP_SIZE;
    depot_ptr->mag_num_objects -= objects_in_use;

    tiny_mag_ptr->mag_num_bytes_in_objects += bytes_inplay;
    tiny_mag_ptr->num_bytes_in_magazine += TINY_HEAP_SIZE;
    tiny_mag_ptr->mag_num_objects += objects_in_use;

    //通过trailer,将region加入tiny_mag_ptr的链表
    recirc_list_splice_last(rack, tiny_mag_ptr, node);
    return 1;
    
}

整体逻辑如下:

  1. 遍历depot magazine中的freelist,查找大小满足msize的空闲内存块,如果存在,返回所在的region。try_msize是要匹配的size,初识是msize,如果没有找到,则try_msize++,只要size大于等于msize,都满足条件。如果未找到,返回0,表示从depot magazine中查找失败。查找逻辑如下:

    ini 复制代码
    static region_t tiny_find_msize_region(rack_t *rack, magazine_t *tiny_mag_ptr, mag_index_t mag_index, msize_t msize)
    {
        tiny_free_list_t *ptr;
        grain_t slot = tiny_slot_from_msize(msize);
        free_list_t *free_list = tiny_mag_ptr->mag_free_list;
        free_list_t *the_slot = free_list + slot;
        free_list_t *limit;
        #if defined(__LP64__)
            uint64_t bitmap;
        #else
            uint32_t bitmap;
        #endif
            ptr = the_slot->p;
        if (ptr) {
            return TINY_REGION_FOR_PTR(ptr);
        }
        #if defined(__LP64__)
            bitmap = ((uint64_t *)(tiny_mag_ptr->mag_bitmap))[0] & ~((1ULL << slot) - 1);
        #else
            bitmap = tiny_mag_ptr->mag_bitmap[0] & ~((1 << slot) - 1);
        #endif
        if (!bitmap) {
            return NULL;
        }
    
        slot = BITMAPV_CTZ(bitmap);
        limit = free_list + NUM_TINY_SLOTS;
        free_list += slot;
    
        if (free_list < limit) {
            ptr = free_list->p;
            if (ptr) {
                return TINY_REGION_FOR_PTR(ptr);
            }
        }
        ptr = limit->p;
        if (ptr) {
            return TINY_REGION_FOR_PTR(ptr);
        }
        return NULL;
    }

    逻辑和tiny_malloc_from_free_list相似,首先计算msize对应的slot,然后先从对应的mag_free_list[slot]中精确匹配,如果失败,则从mag_free_listslot找size更大的空闲内存块,最后查找mag_free_list[limit],如果找到,返回内存块所在的region,否则返回NULL,查找失败。

  2. 如果查找成功,则将depot中的region从region链表中迁移至tiny_mag_ptr,同时将region中的空闲内存块从freelist中迁移至tiny_mag_ptr的freelist中。如图所示,深黄色是查找到的region,从depot的regions链表和freelist中迁移至新的magazine下。

  3. 更新depot magazine和tiny_mag_ptr的统计信息。

当调用tiny_get_region_from_depot查好成功时,再次调用tiny_malloc_from_free_list方法从magazine中查找,因为上述逻辑只是把内存迁移至magazine的相应结构中,所以需要再查找一次。

region分配

如果查找失败,说明magazine中的没有可用的空闲内存块,则从新的region中分配内存,包含两部分:

  1. 新分配一块region空间
  2. 从region中分配msize的内存,将region加入magazine结构中管理。
虚拟内存分配

首先调用mvm_allocate_pages方法分配一块region虚拟内存。

ini 复制代码
fresh_region = mvm_allocate_pages(TINY_REGION_SIZE,
                    TINY_BLOCKS_ALIGN,
                    MALLOC_FIX_GUARD_PAGE_FLAGS(rack->debug_flags),
                    VM_MEMORY_MALLOC_TINY);

TINY_REGION_SIZE是region的大小范围,按照TINY_BLOCKS_ALIGN大小对齐,同时支持传递一些flags参数,控制分配行为。

内存块分配

新region分配后,调用tiny_malloc_from_region_no_lock方法,从region中分配一块内存块,并将region加入rack和magazine的结构中维护。

ini 复制代码
ptr = tiny_malloc_from_region_no_lock(rack, tiny_mag_ptr, mag_index, msize, fresh_region);
return ptr;

static void *tiny_malloc_from_region_no_lock(rack_t *rack,
                                magazine_t *tiny_mag_ptr,
                                mag_index_t mag_index,
                                msize_t msize,
                                void *aligned_address)
{
    void *ptr;
    //处理之前未使用的剩余空间
    if (tiny_mag_ptr->mag_bytes_free_at_end || tiny_mag_ptr->mag_bytes_free_at_start) {
        tiny_finalize_region(rack, tiny_mag_ptr);
    }
    tiny_region_t region = (tiny_region_t)aligned_address;

    // We set the unused bits of the header in the last pair to be all ones, and those of the inuse to zeroes.
#if NUM_TINY_BLOCKS & 31
    const uint32_t header = 0xFFFFFFFFU << (NUM_TINY_BLOCKS & 31);
#else
    const uint32_t header = 0;
#endif
    region->pairs[CEIL_NUM_TINY_BLOCKS_WORDS - 1].header = header;
    region->pairs[CEIL_NUM_TINY_BLOCKS_WORDS - 1].inuse = 0;
    //设置mag_index
    MAGAZINE_INDEX_FOR_TINY_REGION(region) = mag_index;

    // Insert the new region into the hash ring
    rack_region_insert(rack, region);

    tiny_mag_ptr->mag_last_region = region;
    BYTES_USED_FOR_TINY_REGION(region) = TINY_BYTES_FOR_MSIZE(msize);
    OBJECTS_IN_USE_FOR_TINY_REGION(region) = 1;

    int offset_msize = 0;
    //内存分配地址ptr
    ptr = (void *)(TINY_REGION_HEAP_BASE(region) + TINY_BYTES_FOR_MSIZE(offset_msize));
    set_tiny_meta_header_in_use(ptr, msize);
    tiny_mag_ptr->mag_num_objects++;
    tiny_mag_ptr->mag_num_bytes_in_objects += TINY_BYTES_FOR_MSIZE(msize);
    tiny_mag_ptr->num_bytes_in_magazine += TINY_HEAP_SIZE;

    set_tiny_meta_header_in_use_1((void *)((uintptr_t)ptr + TINY_BYTES_FOR_MSIZE(msize)));
    tiny_mag_ptr->mag_bytes_free_at_end = TINY_BYTES_FOR_MSIZE(NUM_TINY_BLOCKS - msize - offset_msize);
    tiny_mag_ptr->mag_bytes_free_at_start = 0;
    //将region加入tiny_mag_ptr的last node
    recirc_list_splice_last(rack, tiny_mag_ptr, REGION_TRAILER_FOR_TINY_REGION(region));

    return ptr;
}

整体流程如下:

  1. 首先判断,如果magazine还有一些剩余size从未使用,则将这部分内存更新为空闲块加入空闲链表。具体逻辑如图: 白色区域是从未分配过的区域,会将这部分区域更新成回收状态,并将入mag_free_list中,加入之前会判断一下是否可以和空间连续的前一个内存块合并,如果合并,则更新mag_free_list中内存。代码如下:

    ini 复制代码
    if (tiny_mag_ptr->mag_bytes_free_at_end || tiny_mag_ptr->mag_bytes_free_at_start) {
        tiny_finalize_region(rack, tiny_mag_ptr);
    }
    
    void tiny_finalize_region(rack_t *rack, magazine_t *tiny_mag_ptr)
    {
        void *last_block, *previous_block;
        uint32_t *last_header;
        msize_t last_msize, previous_msize, last_index;
    
        //如果还有一些剩余size,尝试合并前一个内存块
        if (tiny_mag_ptr->mag_bytes_free_at_end) {
            last_block = (void *)((uintptr_t)TINY_REGION_HEAP_END(tiny_mag_ptr->mag_last_region) - tiny_mag_ptr->mag_bytes_free_at_end);
            last_msize = TINY_MSIZE_FOR_BYTES(tiny_mag_ptr->mag_bytes_free_at_end);
            last_header = TINY_BLOCK_HEADER_FOR_PTR(last_block);
            last_index = TINY_INDEX_FOR_PTR(last_block);
        
            if (last_index != (NUM_TINY_BLOCKS - 1)) {
                BITARRAY_CLR(last_header, (last_index + 1));
            }
    
            //前一个空闲的block
            previous_block = tiny_previous_preceding_free(last_block, &previous_msize);
            if (previous_block) {
                set_tiny_meta_header_middle(last_block);
                tiny_free_list_remove_ptr(rack, tiny_mag_ptr, previous_block, previous_msize);
                zero_tiny_free_inline_meta_following(previous_block, previous_msize);
                last_block = previous_block;
                last_msize += previous_msize;
            }
    
            tiny_free_list_add_ptr(rack, tiny_mag_ptr, last_block, last_msize);
            tiny_mag_ptr->mag_bytes_free_at_end = 0;
        }
    
        tiny_mag_ptr->mag_last_region = NULL;
    }
  2. 调用rack_region_insert方法将region注册进rack的hash ring结构中。

  3. 从region取偏移meta字段后的首地址作为内存块地址ptr返回,并且调用set_tiny_meta_header_in_use更新block状态。这里有一个特殊逻辑,设置内存块ptr后一个16字节的内存块为use状态,防止回收合并操作。同时更新相关统计信息,包括mag_num_objects等,更新mag_bytes_free_at_end、mag_bytes_free_at_start的值。

  4. 调用recirc_list_splice_last方法将region对应的trailer_t节点加入region链表中。

内存回收

free_tiny是tiny内存回收的入口方法,代码如下:

ini 复制代码
void free_tiny(rack_t *rack, void *ptr, region_t tiny_region, size_t known_size,
        boolean_t partial_free)
{
    msize_t msize;
    boolean_t is_free;
    mag_index_t mag_index = MAGAZINE_INDEX_FOR_TINY_REGION(tiny_region);
    magazine_t *tiny_mag_ptr = &(rack->magazines[mag_index]);
    uint32_t flags = 0;
    
//cache
#if CONFIG_TINY_CACHE
    if (msize < TINY_QUANTUM) {
            void *ptr2 = tiny_mag_ptr->mag_last_free;
            msize_t msize2 = tiny_mag_ptr->mag_last_free_msize;
            region_t rgn2 = tiny_mag_ptr->mag_last_free_rgn;
            if (ptr == ptr2) {
                free_tiny_botch(rack, ptr);
                return;
            }
            //...
            tiny_mag_ptr->mag_last_free = ptr;
            tiny_mag_ptr->mag_last_free_msize = msize;
            tiny_mag_ptr->mag_last_free_rgn = tiny_region;
            //...
            msize = msize2;
            ptr = ptr2;
            tiny_region = rgn2;
            flags |= TINY_FREE_FLAG_FROM_CACHE;
        }
#endif 
    //...
    if (tiny_free_no_lock(rack, tiny_mag_ptr, mag_index, tiny_region, ptr,
            msize, flags)) {
    }
}

主要包含两部分逻辑:

  1. cache更新逻辑,更新tiny_mag_ptr->mag_last_free,更新mag_last_free_msize、mag_last_free_rgn,将原有的cache加入回收逻辑,关于cache的逻辑,上文有具体介绍。
  2. 调用tiny_free_no_lock进行内存回收操作

核心逻辑

tiny_free_no_lock是回收的核心实现,代码如下:

ini 复制代码
boolean_t tiny_free_no_lock(rack_t *rack, magazine_t *tiny_mag_ptr, mag_index_t mag_index,
        region_t region, void *ptr, msize_t msize, uint32_t flags)
{
    void *original_ptr = ptr;
    size_t original_size = TINY_BYTES_FOR_MSIZE(msize);
    void *next_block = ((unsigned char *)ptr + original_size);
    msize_t previous_msize, next_msize;
    void *previous;
    tiny_free_list_t *big_free_block;
    tiny_free_list_t *after_next_block;
    tiny_free_list_t *before_next_block;
    
    //尝试合并前一个空闲的block
    previous = tiny_previous_preceding_free(ptr, &previous_msize);
    if (previous) {
        set_tiny_meta_header_middle(ptr);
        tiny_free_list_remove_ptr(rack, tiny_mag_ptr, previous, previous_msize);
        zero_tiny_free_inline_meta_following(previous, previous_msize);
        ptr = previous;
        msize += previous_msize;
    }
    //尝试合并后一个空闲的block
    if ((next_block < TINY_REGION_HEAP_END(region)) && tiny_meta_header_is_free(next_block)) {
        next_msize = get_tiny_free_size(next_block);
        if (next_msize > NUM_TINY_SLOTS) {
            msize += next_msize;

            big_free_block = (tiny_free_list_t *)next_block;
            after_next_block = free_list_unchecksum_ptr(rack, &big_free_block->next);
            before_next_block = free_list_unchecksum_ptr(rack, &big_free_block->previous);

            if (!before_next_block) {
                tiny_mag_ptr->mag_free_list[NUM_TINY_SLOTS].p = ptr;
            } else {
                before_next_block->next.u = free_list_checksum_ptr(rack, ptr);
            }

            if (after_next_block) {
                after_next_block->previous.u = free_list_checksum_ptr(rack, ptr);
            }

            ((tiny_free_list_t *)ptr)->previous = big_free_block->previous;
            ((tiny_free_list_t *)ptr)->next = big_free_block->next;

            set_tiny_meta_header_middle(big_free_block);
            zero_tiny_free_inline_meta(big_free_block, next_msize);

            set_tiny_meta_header_free(ptr, msize);

            uint16_t next_block_index = TINY_INDEX_FOR_PTR(big_free_block) + 1;
            uint16_t ptr_index = TINY_INDEX_FOR_PTR(ptr) + 1;
            const grain_t slot = NUM_TINY_SLOTS;
            region_free_blocks_t *free_blocks = &((tiny_region_t)region)->free_blocks_by_slot[slot];
            if (free_blocks->first_block == next_block_index) {
                free_blocks->first_block = ptr_index;
            }
            if (free_blocks->last_block == next_block_index) {
                free_blocks->last_block = ptr_index;
            }
            goto tiny_free_ending;
        }
        tiny_free_list_remove_ptr(rack, tiny_mag_ptr, next_block, next_msize);
        set_tiny_meta_header_middle(next_block); // clear the meta_header to enable coalescing backwards
        zero_tiny_free_inline_meta(next_block, next_msize);
        msize += next_msize;
    }
    //重新添加进free链表和freeblock区
    tiny_free_list_add_ptr(rack, tiny_mag_ptr, ptr, msize);

tiny_free_ending:
    tiny_mag_ptr->mag_num_bytes_in_objects -= original_size;

    region_trailer_t *trailer = REGION_TRAILER_FOR_TINY_REGION(region);
    size_t bytes_used = trailer->bytes_used - original_size;
    trailer->bytes_used = (unsigned int)bytes_used;
    
    //...
    boolean_t needs_unlock = TRUE;
#if CONFIG_RECIRC_DEPOT
    needs_unlock = tiny_free_try_recirc_to_depot(rack, tiny_mag_ptr, mag_index, region, original_ptr, original_size, ptr, msize);
#endif // CONFIG_RECIRC_DEPOT
    return needs_unlock;
}

具体步骤如下:

  1. 尝试查找相邻的内存是否空闲,如果空闲,可以进行合并操作,首先调用tiny_previous_preceding_free方法查找前一块空闲内存previous,如果找到则合并。如下图1,合并的操作如下:

    1. 由于合并后内存块ptr不存在了,因此更新ptr的pair状态标记为空,set_tiny_meta_header_middle。
    2. 调用tiny_free_list_remove_ptr方法将previous从原空闲链表mag_free_list中删除。
    3. 更新后的空闲块是合并后的大小,是msize+previous_msize。
  2. 尝试合并后一块内存块next,基本流程和合并previous相似,更新next的pair状态标记为空,将next从mag_free_list中删除,如上图2。这里包含一个特殊逻辑,当next的size超过limit,则合并后的空闲内存块一定会被加入最后一个链表mag_free_list[limit]中,这里单独处理,将next从原来的链表中删除,然后将合并后的ptr加入mag_free_list[limit]的next原位置中,更新next的pair状态标记为空,其余操作一致,如下图。

  3. 调用tiny_free_list_add_ptr方法将合并后的空闲块加入mag_free_list。

  4. 更新统计信息,例如mag_num_bytes_in_objects、bytes_used。

  5. 调用tiny_free_try_recirc_to_depot,判断当前magzine的空闲块比例是否达到阈值,如果达到,需要将相应的region和空闲内存块迁移至depot magazine的结构中。

  6. 如果回收的是depot magazine中内存,则进一步判断内存块所在的region是否内存全部被回收,如是则调用系统方法会说该region内存。逻辑如下:

    arduino 复制代码
    static boolean_t
    tiny_free_try_recirc_to_depot(rack_t *rack,
                              magazine_t *tiny_mag_ptr,
                              mag_index_t mag_index,
                              region_t region,
                              void *headptr,
                              size_t headsize,
                              void *ptr,
                              msize_t msize)
    {
        if (DEPOT_MAGAZINE_INDEX != mag_index) {
        //回收给depot region的逻辑
        } else {
            if (0 < bytes_used || 0 < node->pinned_to_depot) {
            } else {
                //region的bytes_used为0,回收内存给系统
                region_t r_dealloc = tiny_free_try_depot_unmap_no_lock(rack, tiny_mag_ptr, node);
                SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
                if (r_dealloc) {
                    mvm_deallocate_pages(r_dealloc, TINY_REGION_SIZE,
                        MALLOC_FIX_GUARD_PAGE_FLAGS(rack->debug_flags));
                }
                return FALSE;
            }
        }
        return TRUE;
    }
    1. 调用了tiny_free_try_depot_unmap_no_lock将depot magzine下的region从结构中删除
    2. 调用mvm_deallocate_pages方法回收内存。

以上是tiny内存分配的介绍,由于字数限制,small和large在另外一篇文章中介绍。

相关推荐
T1an-12 小时前
最右IOS岗一面
ios
坏小虎5 小时前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年6 小时前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技6 小时前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally7 小时前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim148019 小时前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally21 小时前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手1 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero1 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb
ii_best1 天前
lua语言开发脚本基础、mql命令库开发、安卓/ios基础开发教程,按键精灵新手工具
android·ios·自动化·编辑器