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在另外一篇文章中介绍。

相关推荐
_可乐无糖1 小时前
Fastbot-iOS(iOS monkey)schema参数的指定方式
ui·ios·自动化
书弋江山1 小时前
IOS 关于ARKi使用
ios
Milk夜雨4 小时前
C语言中的贪心算法
c语言·开发语言·数据结构·算法·ios
opentogether7 小时前
iOS 中的 nil、Nil、NULL、NSNull 僵尸对象和野指针
ios·cocoa·xcode
艾小逗12 小时前
uniapp下载&打开实现方案,支持安卓ios和h5,下载文件到指定目录,安卓文件管理内可查看到
android·ios·uni-app·uniapp文件下载
北京_宏哥13 小时前
《爆肝整理》保姆级系列教程-玩转Charles抓包神器教程(7)-Charles苹果手机手机抓包知否知否?
ios·前端框架·charles
刘小哈哈哈17 小时前
实现一个iOS晃动动画
ios
__zhangheng1 天前
Info.plist contained no UIScene configuration dictionary (looking for configura
macos·ios·objective-c·cocoa·swift
iOS民工1 天前
iOS SSZipArchive 解压后 中文文件名乱码问题
ios
皮蛋很白1 天前
IOS safari 播放 mp4 遇到的坎儿
前端·ios·safari·video.js