iOS libMalloc源码分析-ScalableZone(small&large)

本篇是ScalableZone系列的第二篇,介绍small和large的内存的机制,由于small和tiny在机制上高度相似,本篇着重介绍差异的部分。

small

主要实现在magazine_small.c文件中。和tiny一样,small内存也对应一个rack_s结构体管理内部数据。

arduino 复制代码
typedef struct szone_s {
    //...
    struct rack_s small_rack;
    //...
}

内部结构和tiny类型保持一致,rack_s内部维护若干magazines具体管理small内存,每个magazine管理regions和空闲链表mag_free_list。small magazine的逻辑和tinymagazine保持一致。

相关概念

size分级

small类型将内存划分为512B一级,则15KB的范围一共划分为30级,用SMALL_QUANTUM宏表示。

arduino 复制代码
#define SHIFT_SMALL_QUANTUM (SHIFT_TINY_QUANTUM + 5) // 9
#define SMALL_QUANTUM (1 << SHIFT_SMALL_QUANTUM) // 512 bytes

例如,分配一个1026B大小的内存,对应的msize是3,实际分配1536B的内存。对应的空闲链表mag_free_list数组中存放的空闲内存块,也是按照相同size分级来划分的。例如mag_free_list[1]存放1KB的空闲块。

region

small magazine下分配的region和tiny region结构类似,small region也是由基本内存块block组成的空间,单个block的大小对应size分级的基本单位512B,用small_block_t类型表示,small_block_t是uint32_t数组,容量是512B/4 = 128。NUM_SMALL_BLOCKS表示region内包含的block个数,一个region可以存储16319个block大小的内存数据。

c 复制代码
typedef uint32_t small_block_t[SMALL_QUANTUM / sizeof(uint32_t)];
#define NUM_SMALL_BLOCKS 16319

small_region的定义如下:

ini 复制代码
typedef struct small_region {
    region_trailer_t trailer;
    msize_t small_meta_words[NUM_SMALL_BLOCKS];
    oob_free_entry_s small_oob_free_entries[SMALL_OOB_COUNT];
    uint8_t pad[SMALL_REGION_PAD];
    region_cookie_t region_cookie;
    small_block_t blocks[NUM_SMALL_BLOCKS];
} * small_region_t;

和tiny region的结构类似,主要包含两块数据存储和meta信息,主要字段如下:

  • trailer:region链表结构,辅助magazine管理region,和tiny相同。

  • blocks:是实际存放数据的区域,容量是NUM_SMALL_BLOCKS的block数组,可以存储NUM_SMALL_BLOCKS个block大小的数据。

  • small_meta_words[]:存储block内存块状态的数组,和tiny不同的是,不通过bit位存储,每32bit对应一个数组元素,而是每个block都对应一个数组元素,因此数组共NUM_SMALL_BLOCKS个元素。如图所示,region中每个内存块的block都可以在small_meta_words数值找到对应的index位置。

region的总大小包含以上3个区域:

arduino 复制代码
#define SMALL_REGION_SIZE ((SMALL_HEAP_SIZE + SMALL_METADATA_SIZE + PAGE_MAX_SIZE - 1) & ~(PAGE_MAX_SIZE - 1))
#define SMALL_HEAP_SIZE (NUM_SMALL_BLOCKS * SMALL_QUANTUM)
#define SMALL_METADATA_SIZE (sizeof(region_trailer_t) + NUM_SMALL_BLOCKS * sizeof(msize_t))

#define PAGE_MAX_SHIFT          14
#define PAGE_MAX_SIZE           (1 << PAGE_MAX_SHIFT)
  • SMALL_HEAP_SIZE:blocks数据区大小。
  • SMALL_METADATA_SIZE:region链表+small_meta_words数组大小。 再按照PAGE_MAX_SIZE(16K)对齐。

同时提供了一些宏,获取相应区域起始地址:

  • 根据ptr获取region的地址

    scss 复制代码
    #define SMALL_REGION_FOR_PTR(ptr) ((small_region_t)((uintptr_t)(ptr) & ~((1 << SMALL_BLOCKS_ALIGN) - 1)))
  • 获取region的header:即block状态数组small_meta_words的地址

    scss 复制代码
    #define SMALL_META_HEADER_FOR_REGION(region) (((small_region_t)region)->small_meta_words)
    #define SMALL_META_HEADER_FOR_PTR(ptr) (((small_region_t)SMALL_REGION_FOR_PTR(ptr))->small_meta_words)
  • 获取region的blocks区域:

    csharp 复制代码
    #define SMALL_REGION_HEAP_BASE(region) ((void *)((small_region_t)region)->blocks)
  • 获取ptr地址在region中的偏移

    scss 复制代码
    #define SMALL_HEAP_OFFSET_FOR_PTR(ptr) ((uintptr_t)(ptr) - (uintptr_t)SMALL_REGION_HEAP_BASE(SMALL_REGION_FOR_PTR(ptr)))
  • 获取ptr对应的block在small_meta_words的index和地址

    scss 复制代码
    #define SMALL_META_INDEX_FOR_PTR(ptr) ((SMALL_HEAP_OFFSET_FOR_PTR(ptr) >> SHIFT_SMALL_QUANTUM) & (NUM_SMALL_CEIL_BLOCKS - 1))
    #define SMALL_METADATA_FOR_PTR(ptr) (SMALL_META_HEADER_FOR_PTR(ptr) + SMALL_META_INDEX_FOR_PTR(ptr))

内存状态管理

首先,small magazine也使用mag_bitmap数组来记录magazine下各msize的空闲内存情况。其次,和tiny的pair字段不同,region结构中通过small_meta_words数组记录region内部的内存块的状态和大小,但是不分成header和inuse两个字段分别存储内存块的有无和使用状态。

small_meta_words是msize_t数组,msize_t定义如下:

arduino 复制代码
typedef unsigned short msize_t;

unsigned short可以存储16bit的信息,msize_t的最高bit位(1<<15)存储是否空闲,其余bit位存储msize数值。最高位为1表示空闲,0表示未空闲。接下来结合代码分析。

未分配

初始情况下未分配任何内存块,small_meta_words默认是全0,用黄色表示。

分配使用

例如分配了一个msize=2的内存块,则small_meta_words[index]设置如下:

arduino 复制代码
static MALLOC_INLINE void small_meta_header_set_in_use(msize_t *meta_headers, msize_t index, msize_t msize)
{
    meta_headers[index] = msize;
}

msize_t的最高位位0,表示使用中,其他bit位存储msize值。

未使用

当内存块被回收时,标记为空闲状态,设置第一个block和最后一个block的最高位bit为1。

scss 复制代码
static MALLOC_INLINE void
small_meta_header_set_is_free(msize_t *meta_headers, msize_t index, msize_t msize)
{
    meta_headers[index] = msize | SMALL_IS_FREE;
}

static MALLOC_INLINE void
small_free_mark_free(rack_t *rack, free_list_t entry, msize_t msize)
{
    void *ptr = small_free_list_get_ptr(entry);
    msize_t *meta_headers = SMALL_META_HEADER_FOR_PTR(ptr);
    uintptr_t start_index = SMALL_META_INDEX_FOR_PTR(ptr);
    uintptr_t end_index = SMALL_META_INDEX_FOR_PTR(ptr + SMALL_BYTES_FOR_MSIZE(msize) - 1);

    small_meta_header_set_is_free(meta_headers, start_index, msize);
    small_meta_header_set_is_free(meta_headers, end_index, msize);
}

之所以设置也设置最后一个block的size和和free状态,是为了后续回收操作时,方便查找相邻的前一块内存地址。

例如,回收一个msize=3的内存块,如图,内存块包含3个block,第1个和第3个block对应的bit最高位设置为1。 当重新使用该空闲的内存块时,重置为0.

scss 复制代码
static MALLOC_INLINE void
small_meta_header_set_not_free(msize_t *meta_headers, msize_t index)
{
    meta_headers[index] &= ~SMALL_IS_FREE;
}

static MALLOC_INLINE void
small_free_mark_unfree(rack_t *rack, free_list_t entry, msize_t msize)
{
    void *ptr = small_free_list_get_ptr(entry);
    msize_t *meta_headers = SMALL_META_HEADER_FOR_PTR(ptr);
    uintptr_t start_index = SMALL_META_INDEX_FOR_PTR(ptr);
    uintptr_t end_index = SMALL_META_INDEX_FOR_PTR(ptr + SMALL_BYTES_FOR_MSIZE(msize) - 1);

    small_meta_header_set_not_free(meta_headers, start_index);
    small_meta_header_set_not_free(meta_headers, end_index);
}

清除

当一块内存块不存在时,对应的bit位需要被清除,例如相邻空闲内存块存在合并的操作,将其中一个内存块清除,分别设置第一个block和最后一个block的bit位为0。

scss 复制代码
static MALLOC_INLINE void
small_meta_header_set_middle(msize_t *meta_headers, msize_t index)
{
    meta_headers[index] = 0;
}

static MALLOC_INLINE void
small_free_mark_middle(rack_t *rack, free_list_t entry, msize_t msize)
{
    // Marks both the start and end block of a free-list entry as "middle" (unfree).
    void *ptr = small_free_list_get_ptr(entry);
    msize_t *meta_headers = SMALL_META_HEADER_FOR_PTR(ptr);
    uintptr_t start_index = SMALL_META_INDEX_FOR_PTR(ptr);
    uintptr_t end_index = SMALL_META_INDEX_FOR_PTR(ptr + SMALL_BYTES_FOR_MSIZE(msize) - 1);

    small_meta_header_set_middle(meta_headers, start_index);
    small_meta_header_set_middle(meta_headers, end_index);
}

例如回收一个msize=2的内存块ptr时,合并相邻的前一块msize=2的空闲内存previous,分别将previous和ptr的标记位清除。 再调用small_free_mark_free方法设置合并后的内存块msize=4为空闲状态。

空闲内存管理

和tiny相同,small通过magazine的mag_free_list字段维护空闲链表,管理被回收的空闲内存块。

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

链表管理

基础结构

smal使用通用的free_list_t结构,作为空闲链表的节点数据,是一个union类型

arduino 复制代码
typedef union {
    small_inplace_free_entry_t small_inplace;
    medium_inplace_free_entry_t medium_inplace;
    inplace_free_entry_t inplace;
    oob_free_entry_t oob;
    void *p;
} free_list_t;

typedef struct _small_inplace_free_entry_s {
    inplace_linkage_s previous;
    inplace_linkage_s next;
} small_inplace_free_entry_s, *small_inplace_free_entry_t;

typedef struct {
    void *ptr;
    uint8_t checksum;
} inplace_linkage_s;

使用同一块内存,在不同场景下用途不同,small涉及small_inplace、oob、p三个字段的使用,通过small_inplace和p设置和获取链表的前后关系:

arduino 复制代码
//设置
static MALLOC_INLINE void small_inplace_checksum_ptr(rack_t *rack, inplace_linkage_s *linkage, void *ptr)
{
    uintptr_t checksum = free_list_gen_checksum((uintptr_t)ptr ^ rack->cookie ^ (uintptr_t)rack);
    linkage->checksum = checksum;
    linkage->ptr = ptr;
}

static MALLOC_INLINE void small_inplace_free_entry_set_previous(rack_t *rack, small_inplace_free_entry_t entry, free_list_t previous)
{
    small_inplace_checksum_ptr(rack, &entry->previous, previous.p);
}

static MALLOC_INLINE void small_inplace_free_entry_set_next(rack_t *rack, small_inplace_free_entry_t entry, free_list_t next)
{
    small_inplace_checksum_ptr(rack, &entry->next, next.p);
}

//获取
static MALLOC_INLINE free_list_t small_inplace_unchecksum_ptr(rack_t *rack, inplace_linkage_s *linkage)
{
    return (free_list_t){ .p = linkage->ptr };
}

static MALLOC_INLINE free_list_t small_inplace_free_entry_get_previous(rack_t *rack, small_inplace_free_entry_t ptr)
{
    return small_inplace_unchecksum_ptr(rack, &ptr->previous);
}

static MALLOC_INLINE free_list_t small_inplace_free_entry_get_next(rack_t *rack, small_inplace_free_entry_t ptr)
{
    return small_inplace_unchecksum_ptr(rack, &ptr->next);
}

前后内存块地址存入linkage类型previous和next的ptr字段中,读取时通过linkage类型的ptr字段构建一个free_list_t结构,并赋值字段p,然后返回。

封装层如下:

scss 复制代码
//设置previous关联
static MALLOC_INLINE void small_free_list_set_previous(rack_t *rack, free_list_t entry, free_list_t previous)
{
    if (small_is_oob_free_entry(entry)) {
        small_oob_free_entry_set_previous(entry.oob, previous);
    } else {
        small_inplace_free_entry_set_previous(rack, entry.small_inplace, previous);
    }
}
//获取previous关联
static MALLOC_INLINE free_list_t small_free_list_get_previous(rack_t *rack, free_list_t ptr)
{
    if (small_is_oob_free_entry(ptr)) {
        return small_oob_free_entry_get_previous(ptr.oob);
    } else {
        return small_inplace_free_entry_get_previous(rack, ptr.small_inplace);
    }
}
//设置next关联
static MALLOC_INLINE void small_free_list_set_next(rack_t *rack, free_list_t entry, free_list_t next)
{
    if (small_is_oob_free_entry(entry)) {
        small_oob_free_entry_set_next(entry.oob, next);
    } else {
        small_inplace_free_entry_set_next(rack, entry.small_inplace, next);
    }
}
//获取next关联
static MALLOC_INLINE free_list_t small_free_list_get_next(rack_t *rack, free_list_t ptr)
{
    if (small_is_oob_free_entry(ptr)) {
        return small_oob_free_entry_get_next(ptr.oob);
    } else {
        return small_inplace_free_entry_get_next(rack, ptr.small_inplace);
    }
}

这里先判断地址ptr是否是一个oob空闲块,如果是则走oob方式设置和获取前后关系,否则使用small_inplace字段。

oob

oob即out-of-band空闲内存,当一个内存块的size超过内存页大小(vm_kernel_page_size)时,回收时会将其地址加入region的一个专门字段中维护,即small_oob_free_entries数组中,保证内存页dirty。

arduino 复制代码
typedef struct small_region {
    //...
    oob_free_entry_s small_oob_free_entries[SMALL_OOB_COUNT];
    //..
} * small_region_t;

和free_list_t结构类似,oob数组维护SMALL_OOB_COUNT个oob_free_entry_t结构,对应SMALL_OOB_COUNT个oob内存块,通过注释可以了解,oob_free_entry_s的使用场景。定义如下:

arduino 复制代码
// Out-of-band free list entry. Out-of-band free list entries are used
// in specific cases where a free-list entry is the *only* data on a given page,
// and the presence of that entry causes the page to stay dirty.
typedef struct {
    uintptr_t prev;
    uintptr_t next;
    uint16_t ptr;
} MALLOC_PACKED oob_free_entry_s, *oob_free_entry_t;

包含了prev、next维护链表前后关系,ptr存储内存块地址。SMALL_OOB_SIZE是存储oob_free_entry的区域,是region的总大小减去其他内存区大小,SMALL_OOB_SIZE是对应的数量。

less 复制代码
#define SMALL_OOB_COUNT ((SMALL_REGION_SIZE - SMALL_HEAP_SIZE - SMALL_METADATA_SIZE - sizeof(region_cookie_t)) / sizeof(oob_free_entry_s))
#define SMALL_OOB_SIZE (SMALL_OOB_COUNT * sizeof(oob_free_entry_s))

oob内存的相关操作如下:

  1. 当一个内存块被回收时,首先判断是否满足oob内存的条件,即msize>= vm_kernel_page_size.

    arduino 复制代码
    static MALLOC_INLINE boolean_t
    small_needs_oob_free_entry(void *ptr, msize_t msize)
    {
        vm_size_t ss = vm_kernel_page_size;
        uintptr_t pptr = trunc_page_quanta((uintptr_t)ptr);
        return ((trunc_page_quanta((uintptr_t)ptr) == (uintptr_t)ptr) && (SMALL_BYTES_FOR_MSIZE(msize) >= vm_kernel_page_size));
    }
  2. 如果满足,则遍历small_oob_free_entries数组,查找可用的oob_free_entry_s结构包装内存信息。

    ini 复制代码
    static MALLOC_INLINE oob_free_entry_t
    small_oob_free_find_empty(void *ptr, msize_t msize)
    {
        small_region_t region = SMALL_REGION_FOR_PTR(ptr);
        for (int i=0; i < SMALL_OOB_COUNT; i++) {
            if (region->small_oob_free_entries[i].ptr == 0) {
                return &region->small_oob_free_entries[i];
            }
        }
        return NULL;
    }

    即ptr==0的可用oob_free_entry_s。并将内存块地址ptr存储进结构中。

    arduino 复制代码
    #define SMALL_IS_OOB (1 << 15)
    static MALLOC_INLINE void
    small_oob_free_entry_set_ptr(oob_free_entry_t oobe, void *ptr)
    {
        oobe->ptr = SMALL_IS_OOB | (SMALL_REGION_OFFSET_FOR_PTR(ptr) >> SHIFT_SMALL_QUANTUM);
    }

    存储的数据包括:ptr在region中的相对偏移地址和SMALL_IS_OOB标识符。也有相应的get方法将oob结构中的数据还原成原内存地址。

    arduino 复制代码
    static MALLOC_INLINE void *
    small_oob_free_entry_get_ptr(oob_free_entry_t oobe)
    {
        if (!(oobe->ptr & SMALL_IS_OOB)) {
            return NULL;
        }
        small_region_t region = SMALL_REGION_FOR_PTR(oobe);
        uint16_t block = oobe->ptr & ~SMALL_IS_OOB;
        return (void *)((uintptr_t)region + (block << SHIFT_SMALL_QUANTUM));
    }
  3. 设置和获取oob结构之间的前后链表关系。

    arduino 复制代码
    static MALLOC_INLINE void small_oob_free_entry_set_previous(oob_free_entry_t oobe, free_list_t previous)
    {
        oobe->prev = (uintptr_t)previous.p;
    }
    
    static MALLOC_INLINE free_list_t small_oob_free_entry_get_previous(oob_free_entry_t oobe)
    {
        return (free_list_t){ .p = (void *)oobe->prev };
    }
    
    static MALLOC_INLINE void small_oob_free_entry_set_next(oob_free_entry_t oobe, free_list_t next)
    {
        oobe->next = (uintptr_t)next.p;
    }
    
    static MALLOC_INLINE free_list_t small_oob_free_entry_get_next(oob_free_entry_t oobe)
    {
        return (free_list_t){ .p = (void *)oobe->next };
    }
  4. 查找内存地址对应的oob存储结构,同样遍历small_oob_free_entries数组,匹配ptr是否相等。

    ini 复制代码
    static MALLOC_INLINE oob_free_entry_t
    small_oob_free_find_ptr(void *ptr, msize_t msize)
    {
        small_region_t region = SMALL_REGION_FOR_PTR(ptr);
        for (int i=0; i < SMALL_OOB_COUNT; i++) {
            oob_free_entry_t oob = &region->small_oob_free_entries[i];
            if (small_oob_free_entry_get_ptr(oob) == ptr &&
                oob->ptr & SMALL_IS_OOB) {
                return &region->small_oob_free_entries[i];
            }
        }
        return NULL;
    }
  5. oob_free_entry_t结构最终存储在free_list_t结构中,作为链表的节点。

相关操作

综上,空闲内存块地址记录在free_list_t结构中, small_free_list_get_ptr方法返回free_list_t中存储的内存地址。

c 复制代码
static MALLOC_INLINE void *small_free_list_get_ptr(free_list_t ptr)
{
    if (!ptr.p) {
        return NULL;
    } else if (small_is_oob_free_entry(ptr)) {
        return small_oob_free_entry_get_ptr(ptr.oob);
    } else {
        return (void *)ptr.p;
    }
}
添加进链表

当内存块被回收,添加进空闲链表结构中,代码如下:

scss 复制代码
static free_list_t small_free_list_add_ptr(rack_t *rack, magazine_t *small_mag_ptr, void *ptr, msize_t msize)
{
    //1.计算mszie和slot
    grain_t slot = SMALL_FREE_SLOT_FOR_MSIZE(rack, msize);
    free_list_t free_head = small_mag_ptr->mag_free_list[slot];
    //2.构建free_list_t结构
    free_list_t free_ptr = small_free_list_from_ptr(rack, ptr, msize);
    //3.设置链表关系
    small_free_list_set_previous(rack, free_ptr, (free_list_t){ .p = NULL });
    small_free_list_set_next(rack, free_ptr, free_head);
    //4.标记状态位
    small_free_mark_free(rack, free_ptr, msize);
    //5.设置链表关系,magazeinz的bit位
    if (small_free_list_get_ptr(free_head)) {
        small_free_list_set_previous(rack, free_head, free_ptr);
    } else {
        BITMAPN_SET(small_mag_ptr->mag_bitmap, slot);
    }
    //6.更新空闲链表
    small_mag_ptr->mag_free_list[slot] = free_ptr;
    return free_ptr;
}

步骤如下:

  1. 计算mszie和slot,定位对应的空闲链表,并获取head节点
  2. 根据内存块地址ptr构建free_list_t节点,内部实现:
ini 复制代码
static MALLOC_INLINE free_list_t
small_free_list_from_ptr(rack_t *rack, void *ptr, msize_t msize)
{
    free_list_t entry;
    entry.p = ptr;
    if (small_needs_oob_free_entry(ptr, msize)) {
        oob_free_entry_t oobe = small_oob_free_find_empty(ptr, msize);
        if (oobe) {
            small_oob_free_entry_set_ptr(oobe, ptr);
            entry.oob = oobe;
        }
    }
    return entry;
}

这里判断是否满足oob情况,如果是,则构建oob结构包装ptr,维护在entry的oob字段下,否则默认使用p字段维护ptr。

  1. 设置链表关系,将当前新的节点加入链表头。
  2. small_free_mark_free标记为空闲,更新region内的状态数据,如果是msize的第一个空闲块,则记录magazine的mag_bitmap状态位。
  3. 最后更新mag_free_list空闲链表。
从链表删除

如果从空闲链表中查找到可用的内存块,需要删除链表中相应的的节点信息。

arduino 复制代码
static void small_free_list_remove_ptr(rack_t *rack, magazine_t *small_mag_ptr, free_list_t entry, msize_t msize)
{
    small_free_mark_middle(rack, entry, msize);
    small_free_list_remove_ptr_no_clear(rack, small_mag_ptr, entry, msize);
}

entry是节点,首先更新状态位为重新使用。其次small_free_list_remove_ptr_no_clear方法更新链表。

scss 复制代码
static void small_free_list_remove_ptr_no_clear(rack_t *rack, magazine_t *small_mag_ptr, free_list_t entry, msize_t msize)
{
    grain_t slot = SMALL_FREE_SLOT_FOR_MSIZE(rack, msize);
    free_list_t next, previous;

    previous = small_free_list_get_previous(rack, entry);
    next = small_free_list_get_next(rack, entry);
    
    if (!small_free_list_get_ptr(previous)) {
        small_mag_ptr->mag_free_list[slot] = next;
        if (!small_free_list_get_ptr(next)) {
            BITMAPN_CLR(small_mag_ptr->mag_bitmap, slot);
        }
    } else {
        //...
        small_free_list_set_next(rack, previous, next);
    }
    if (small_free_list_get_ptr(next)) {
        free_list_t next_prev = small_free_list_get_previous(rack, next);
        //...
        small_free_list_set_previous(rack, next, previous);
    }
    if (small_is_oob_free_entry(entry)) {
        small_oob_free_entry_set_free(entry.oob);
    }
}

主要逻辑是通过更新previous、next指针,将entry节点从链表中删除。如果是oob内存,则将oob数组中的信息清空。

其他

magazine、region管理、cache、depot region等概念,small和tiny的逻辑相同。底层管理虚拟内存的接口也是一致,这里不展开介绍,接下来分析一下核心流程,即内存的分配和回收,由于机制上和tiny也是高度相似,这里简要分析一下。

核心流程

内存分配

ini 复制代码
void *small_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
    int64_t heapSize = (NUM_SMALL_BLOCKS * SMALL_QUANTUM);
    size_t regionTrailerSize = sizeof(region_trailer_t);
    size_t mSize = sizeof(msize_t);
    int64_t metaSize = (regionTrailerSize + NUM_SMALL_BLOCKS * mSize);
    int64_t pageMaxSize = PAGE_MAX_SIZE;
    int64_t regionSize = ((heapSize + metaSize + PAGE_MAX_SIZE - 1) & ~(PAGE_MAX_SIZE - 1));
    
    void *ptr;
    mag_index_t mag_index = small_mag_get_thread_index() % rack->num_magazines;
    magazine_t *small_mag_ptr = &(rack->magazines[mag_index]);
    
#if CONFIG_SMALL_CACHE
    //...
#endif    
    while (1) {
        ptr = small_malloc_from_free_list(rack, small_mag_ptr, mag_index, msize);
        if (ptr) {
            return ptr;
        }
        if (small_get_region_from_depot(rack, small_mag_ptr, mag_index, msize)) {
            ptr = small_malloc_from_free_list(rack, small_mag_ptr, mag_index, msize);
            if (ptr) {
                return ptr;
            }
        }
        if (!small_mag_ptr->alloc_underway) {
            void *fresh_region;
            small_mag_ptr->alloc_underway = TRUE;
            fresh_region = mvm_allocate_pages(SMALL_REGION_SIZE,
                    SMALL_BLOCKS_ALIGN,
                    MALLOC_FIX_GUARD_PAGE_FLAGS(rack->debug_flags),
                    VM_MEMORY_MALLOC_SMALL);
            
            ptr = small_malloc_from_region_no_lock(rack, small_mag_ptr, mag_index, msize, fresh_region);
            small_mag_ptr->alloc_underway = FALSE;
            return ptr;
        }
    }
    //...
}

逻辑和tiny代码高度相似,步骤如下:

  1. 选取magazine,尝试获取cache。
  2. 如果未命中cache,调用small_malloc_from_free_list从空闲链表中查找合适的空闲内存块。
  3. 如果没有找到,则调用small_get_region_from_depot将depot magazine的region迁移至当前magazine中,然后再次查找。
  4. 如果仍未找到,则调用mvm_allocate_pages分配一块新内存fresh_region,并从新region中分配一块内存。

其中,CONFIG_SMALL_CACHE、small_malloc_from_free_list、small_get_region_from_depot、small_malloc_from_region_no_lock逻辑和CONFIG_TINY_CACHE、tiny_malloc_from_free_list、tiny_get_region_from_depot、tiny_malloc_from_region_no_lock相同,可参考上文。

内存回收

ini 复制代码
void free_small(rack_t *rack, void *ptr, region_t small_region, size_t known_size)
{
    msize_t msize;
    mag_index_t mag_index = MAGAZINE_INDEX_FOR_SMALL_REGION(SMALL_REGION_FOR_PTR(ptr));
    magazine_t *small_mag_ptr = &(rack->magazines[mag_index]);
    if (known_size) {
        msize = SMALL_MSIZE_FOR_BYTES(known_size + SMALL_QUANTUM - 1);
    } else {
        msize = SMALL_PTR_SIZE(ptr);
        if (SMALL_PTR_IS_FREE(ptr)) {
            free_small_botch(rack, ptr);
            return;
        }
    }
#if CONFIG_SMALL_CACHE
    //...
#endif
    region_trailer_t *trailer = REGION_TRAILER_FOR_SMALL_REGION(small_region);
    mag_index_t refreshed_index;
    mag_index = refreshed_index;
    small_mag_ptr = &(rack->magazines[mag_index]);
    if (small_free_no_lock(rack, small_mag_ptr, mag_index, small_region, ptr, msize)) {
    }
}

回收的步骤也和tiny相似,首先选取magazine,更新cache信息,然后调用small_free_no_lock方法回收。

ini 复制代码
static MALLOC_INLINE boolean_t
small_free_no_lock(rack_t *rack, magazine_t *small_mag_ptr, mag_index_t mag_index, region_t region, void *ptr, msize_t msize)
{
    msize_t *meta_headers = SMALL_META_HEADER_FOR_PTR(ptr);
    unsigned index = SMALL_META_INDEX_FOR_PTR(ptr);
    size_t original_size = SMALL_BYTES_FOR_MSIZE(msize);
    void *next_block = ptr + original_size;
    msize_t next_index = index + msize;
    
    if (index > 0 && (meta_headers[index - 1] & SMALL_IS_FREE)) {
        msize_t previous_msize = meta_headers[index - 1] & ~SMALL_IS_FREE;
        grain_t previous_index = index - previous_msize;

        if (meta_headers[previous_index] == (previous_msize | SMALL_IS_FREE)) {
            void *previous_ptr = (void *)((uintptr_t)ptr - SMALL_BYTES_FOR_MSIZE(previous_msize));
            free_list_t previous = small_free_list_find_by_ptr(rack, small_mag_ptr, previous_ptr, previous_msize);
            small_free_list_remove_ptr(rack, small_mag_ptr, previous, previous_msize);
            ptr = previous_ptr;
            small_meta_header_set_middle(meta_headers, index); // This block is now a middle block.
            msize += previous_msize;
            index -= previous_msize;
        } else {
            __builtin_trap();
        }
    }
    if ((next_block < SMALL_REGION_HEAP_END(region)) && (meta_headers[next_index] & SMALL_IS_FREE)) {
        msize_t next_msize = meta_headers[next_index] & ~SMALL_IS_FREE;
        free_list_t next = small_free_list_find_by_ptr(rack, small_mag_ptr, next_block, next_msize);
        small_free_list_remove_ptr(rack, small_mag_ptr, next, next_msize);
        msize += next_msize;
    }
    
    free_list_t freee = small_free_list_add_ptr(rack, small_mag_ptr, ptr, msize);
    
    small_mag_ptr->mag_num_bytes_in_objects -= original_size;
    region_trailer_t *trailer = REGION_TRAILER_FOR_SMALL_REGION(region);
    size_t bytes_used = trailer->bytes_used - original_size;
    trailer->bytes_used = (unsigned int)bytes_used;
    
    needs_unlock = small_free_try_recirc_to_depot(rack, small_mag_ptr, mag_index, region, freee, msize, original_ptr, original_size);
    return needs_unlock;
}

步骤包括:

  1. 获取内存地址相邻的空闲内存块,尝试合并。合并操作更新链表和状态信息。即删除旧的节点,添加合并后新的节点进链表。
  2. 更新统计信息mag_num_bytes_in_objects、bytes_used。
  3. 判断是否迁移至small_free_try_recirc_to_depot,和tiny_free_try_recirc_to_depot的逻辑相似,可以参考tiny相关介绍。

以上是small代码的分析。

large

主要实现在magazine_large.c文件中,large内存管理分为分配与回收两大块,本文在结合代码分析的过程中,介绍相关的概念和实现机制。

相关概念

不同于tiny、small的内存分配方式,使用large方式分配,具备以下特点:

  1. 由于分配的较大块的内存,超过15KB以上,因此每次分配时,根据实际需要的size分配,而不使用size分级策略,使实际分配的size固定为几档。
  2. 每次分配和回收时,直接使用底层接口mvm_allocate_pages和mvm_deallocate_pages方法分配回收虚拟内存,没有region和freelist机制一次性分配较大内存,并在内部复用。

因此,large的内存管理较tiny、small较为简单,使用相关数据结构管理分配的内存块。

large_entry管理

large_entry_t是管理内存块的基础数据结构,当分配一个large内存块时,都会创建一个large_entry_t数据,添加进全局结构中管理,通过维护large_entry_t,管理分配的内存块。数据结构如下:

arduino 复制代码
typedef struct large_entry_s {
    vm_address_t address;
    vm_size_t size;
    boolean_t did_madvise_reusable;
} large_entry_t;

address存储内存块地址,size存储内存块大小,did_madvise_reusable表示是否可执行madvise重用逻辑。如图所示: 每个entry对应一个分配的内存,entry加入一个全局结构中管理,相关字段在szone中定义:

arduino 复制代码
typedef struct szone_s {
    //...
    _malloc_lock_s large_szone_lock MALLOC_CACHE_ALIGN;
    unsigned num_large_objects_in_use;
    unsigned num_large_entries;
    large_entry_t *large_entries;
    size_t num_bytes_in_large_objects;
    //...
} szone_t;

字段如下:

  • num_large_objects_in_use:分配在使用的内存块个数
  • num_large_entries:可以容纳的entry个数
  • large_entries:实际存储entry的hash数组
  • num_bytes_in_large_objects:分配内存块的总大小

添加entry

存储entry的large_entries是一个hash数组,存储entry的机制和一般的hash数组类似。添加entry的代码如下:

ini 复制代码
static void large_entry_insert_no_lock(szone_t *szone, large_entry_t range)
{
    unsigned num_large_entries = szone->num_large_entries;
    unsigned hash_index = (((uintptr_t)(range.address)) >> vm_page_quanta_shift) % num_large_entries;
    unsigned index = hash_index;
    large_entry_t *entry;
    do {
        entry = szone->large_entries + index;
        if (0 == entry->address) {
            *entry = range;
            return;
        }
        index++;
        if (index == num_large_entries) {
            index = 0;
        }
    } while (index != hash_index);
}

如下图,加入entry进large_entries的步骤如下:

  1. 根据entry的存储的内存块地址address,计算出hash_index,该index决定entry插入hash数组中的位置。
  2. 根据数组的首地址和index计算出偏移位置,将entry插入该位置,如果该位置已经存储其他entry的数据,则index++,直到找到没有entry或者空entry的位置。空entry是指之前添加过entry,但是entry的数据已经清0。
  3. 整体偏移判断的过程是类似遍历环状结构,如果index偏移到数组的末尾位置num_large_entries,则回到0位置继续,直到index再次和hash_index相等终止,即回到开始偏移的位置。

插入过程如图所示: 如图,白色表示未存储entry的位置,淡黄色表示已存储entry的位置。一开始计算出的hash_index位置上已经存储了entry,则偏移index两次,直到找到空位置后插入entry。

为保证hash数组中的位置足够存储entry,当large_entries存储空间达到某阈值时,需要扩容,相关代码如下:

ini 复制代码
bool should_grow = (szone->num_large_objects_in_use + 1) * 4 > szone->num_large_entries;
if (should_grow) {
    large_entry_t *entries = large_entries_grow_no_lock(szone, range_to_deallocate);
}

判断条件是当前使用的内存块个数+1后*4大于hash数组目前的容量num_large_entries时扩容,执行扩容逻辑函数large_entries_grow_no_lock。

ini 复制代码
static large_entry_t *large_entries_grow_no_lock(szone_t *szone, vm_range_t *range_to_deallocate)
{
    unsigned old_num_entries = szone->num_large_entries;
    large_entry_t *old_entries = szone->large_entries;
    
    unsigned new_num_entries =
    (old_num_entries) ? old_num_entries * 2 + 1 : (unsigned)((large_vm_page_quanta_size / sizeof(large_entry_t)) - 1);
    large_entry_t *new_entries = large_entries_alloc_no_lock(szone, new_num_entries);
    unsigned index = old_num_entries;
    large_entry_t oldRange;
    if (new_entries == NULL) {
        return NULL;
    }

    szone->num_large_entries = new_num_entries;
    szone->large_entries = new_entries;
    
    while (index--) {
        oldRange = old_entries[index];
        if (oldRange.address) {
            large_entry_insert_no_lock(szone, oldRange);
        }
    }

    if (old_entries) {
        large_entries_free_no_lock(szone, old_entries, old_num_entries, range_to_deallocate);
    } else {
        range_to_deallocate->address = (vm_address_t)0;
        range_to_deallocate->size = 0;
    }

    return new_entries;
}

整体步骤如下:

  1. 计算要扩容的大小,如果之前没有过当前large_entries为NULL,则分配一个虚拟内存页的大小,否则在原基础上扩容1倍后加1,因此,这里的扩容的机制是2倍扩容。

  2. 根据扩容大小,调用large_entries_alloc_no_lock方法分配新内存,返回新数组的地址。

    arduino 复制代码
    static large_entry_t *large_entries_alloc_no_lock(szone_t *szone, unsigned num)
    {
        size_t size = num * sizeof(large_entry_t);
        unsigned flags = MALLOC_APPLY_LARGE_ASLR(szone->debug_flags & (DISABLE_ASLR | DISABLE_LARGE_ASLR));
        return mvm_allocate_pages(round_large_page_quanta(size), 0, flags, VM_MEMORY_MALLOC_LARGE);
    }

    这里调用mvm_allocate_pages进行分配一块虚拟内存。

  3. 更新szone的large_entries和num_large_entries字段,并将原数组中的entry内容拷贝至新的数组中。迁移过程如图,遍历原数组内存逐一添加进新数组,在新数组中位置保持和原位置不变。

  4. 调用large_entries_free_no_lock方法回收逻辑。

    arduino 复制代码
    void large_entries_free_no_lock(szone_t *szone, large_entry_t *entries, unsigned num, vm_range_t *range_to_deallocate)
    {
        size_t size = num * sizeof(large_entry_t);
        range_to_deallocate->address = (vm_address_t)entries;
        range_to_deallocate->size = round_large_page_quanta(size);
    }

    该方法不真正回收内存,而是赋值给range_to_deallocate暂存,是range_to_deallocate传入扩容方法的参数,用于存储要释放的原内存,在执行完扩容后释放。

  5. 返回新数组地址。

综合来看,添加entry的逻辑如下:

arduino 复制代码
```
void *large_malloc(szone_t *szone, size_t num_kernel_pages, unsigned char alignment, boolean_t cleared_requested)
{
    //...
    addr = mvm_allocate_pages(size, alignment, MALLOC_APPLY_LARGE_ASLR(szone->debug_flags), VM_MEMORY_MALLOC_LARGE);
    if (addr == NULL) {
        return NULL;
    }

    SZONE_LOCK(szone);
    bool success = large_entry_grow_and_insert_no_lock(szone, (vm_address_t) addr, (vm_size_t) size,
        &range_to_deallocate);
    SZONE_UNLOCK(szone);
    if (!success) {
        return NULL;
    }

    if (range_to_deallocate.size) {
        // we deallocate outside the lock
        mvm_deallocate_pages((void *)range_to_deallocate.address, range_to_deallocate.size, 0);
    }
    return addr;
}
```
  1. 调用mvm_allocate_pages方法分配large内存块。

  2. 调用large_entry_grow_and_insert_no_lock方法创建entry,并添加进entries数组中。

    ini 复制代码
    static bool large_entry_grow_and_insert_no_lock(szone_t *szone, vm_address_t addr, vm_size_t size,
        vm_range_t *range_to_deallocate)
    {
        bool should_grow = (szone->num_large_objects_in_use + 1) * 4 > szone->num_large_entries;
        if (should_grow) {
            large_entry_t *entries = large_entries_grow_no_lock(szone, range_to_deallocate);
            if (entries == NULL) {
                return false;
            }
        }
    
        large_entry_t large_entry;
        large_entry.address = addr;
        large_entry.size = size;
        large_entry_insert_no_lock(szone, large_entry);
    
        szone->num_large_objects_in_use++;
        szone->num_bytes_in_large_objects += size;
        return true;
    }

    内部先执行扩容逻辑,生成新的entry,记录内存块的地址和size,加入entry数组中,并更新szone的其他字段,包括num_large_objects_in_use和num_bytes_in_large_objects。

  3. 如果其中发生扩容逻辑,则range_to_deallocate存储原数组内存,调用mvm_deallocate_pages方法回收旧数组内存。

查询entry

由于entry存储某个large内存的地址和size,可以通过地址找到对应的entry。

ini 复制代码
large_entry_t *
large_entry_for_pointer_no_lock(szone_t *szone, const void *ptr)
{
    unsigned num_large_entries = szone->num_large_entries;
    unsigned hash_index;
    unsigned index;
    large_entry_t *range;
    if (!num_large_entries) {
        return NULL;
    }
    hash_index = ((uintptr_t)ptr >> vm_page_quanta_shift) % num_large_entries;
    index = hash_index;

    do {
        range = szone->large_entries + index;
        if (range->address == (vm_address_t)ptr) {
            return range;
        }
        if (0 == range->address) {
            break;
        }
        index++;
        if (index == num_large_entries) {
            index = 0;
        }
    } while (index != hash_index);

    return NULL;
}

具体逻辑和插入entry一致:

  1. 根据地址ptr,基于相同紫规则计算出hash_index。
  2. szone->large_entries + index作为查询entry的初始入口位置,匹配ptr和当前位置上存储的entry的address,匹配不上则index++,如果匹配成功,则结束遍历。
  3. 当index再次等于hash_index,则遍历结束,如果未找到,则查找失败,返回NULL。

清除entry

如上文所述,entry被创建出来加入数组后,数组中存储的是entry结构体的数据,而不是地址,当对应的large内存被回收时,entry数据被清空即可。

ini 复制代码
static vm_range_t large_entry_free_no_lock(szone_t *szone, large_entry_t *entry)
{
    vm_range_t range;
    range.address = entry->address;
    range.size = entry->size;
    entry->address = 0;
    entry->size = 0;
    //rehash
    large_entries_rehash_after_entry_no_lock(szone, entry);
    return range;
}
  1. 首先将entry存储的内存块address、size数据存储到range中。

  2. 清空entry数据

  3. 由于清空了其中一个entry数据,为保证hash机制,需要对所有entry的位置重新调整,保证每个entry基于之前的规则插入到正确的位置,例如下图,原先插入entry时,计算出hash_index位置已经有entry1数据了,index++,得到hash_index+2的位置写入entry数据,当entry2被清除时,按照规则,entry需要被重新迁移至hash_index+1的位置上。 调用large_entries_rehash_after_entry_no_lock进行重新hash插入的操作

    ini 复制代码
    static void
    large_entries_rehash_after_entry_no_lock(szone_t *szone, large_entry_t *entry)
    {
        unsigned num_large_entries = szone->num_large_entries;
        uintptr_t hash_index = entry - szone->large_entries;
        uintptr_t index = hash_index;
        large_entry_t range;
        do {
            index++;
            if (index == num_large_entries) {
                index = 0;
            }
            range = szone->large_entries[index];
            if (0 == range.address) {
                return;
            }
            szone->large_entries[index].address = (vm_address_t)0;
            szone->large_entries[index].size = 0;
            large_entry_insert_no_lock(szone, range);
        } while (index != hash_index);
    }

    逻辑是遍历large_entries数组中每个位置,首先将存储的entry数据清空,再将entry数据重新插入数组中。

  4. 返回range给后续回收操作。

缓存机制

为了提升内存分配的性能,large内存也支持cache机制,维护了一个全局结构缓存entry数据,回收内存块时,没有调用mvm_deallocate_pages方法回收给系统,而是将对应的entry加入缓存中,下次分配内存时先从缓存中查找匹配合适size的entry,如果存在,则返回entry对应的内存块,不需要调用mvm_allocate_pages分配一块新内存。

缓存机制相关的字段也维护在了szone中。

arduino 复制代码
typedef struct szone_s {
//...
#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];
    int large_cache_depth;
    size_t large_cache_entry_limit;
    //...
    size_t large_entry_cache_bytes;
#endif
//...
} szone_t;

字段含义如下:

  • large_entry_cache:缓存entry数据的核心数据结构,是一个长度是LARGE_ENTRY_CACHE_SIZE_HIGH的数组,在szone_s内存创建时分配。LARGE_ENTRY_CACHE_SIZE_HIGH是定义数组容量的宏,64位下值是64。
arduino 复制代码
#if MALLOC_TARGET_64BIT
#define LARGE_ENTRY_CACHE_SIZE_HIGH 64
#define LARGE_ENTRY_SIZE_ENTRY_LIMIT_HIGH (512 * 1024 * 1024)
#define LARGE_ENTRY_CACHE_SIZE_LOW 16
#define LARGE_ENTRY_SIZE_ENTRY_LIMIT_LOW (128 * 1024 * 1024)
#else
#define LARGE_ENTRY_CACHE_SIZE_HIGH 8
#define LARGE_ENTRY_SIZE_ENTRY_LIMIT_HIGH (32 * 1024 * 1024)
#define LARGE_ENTRY_CACHE_SIZE_LOW LARGE_ENTRY_CACHE_SIZE_HIGH
#define LARGE_ENTRY_SIZE_ENTRY_LIMIT_LOW LARGE_ENTRY_SIZE_ENTRY_LIMIT_HIGH
#endif
  • large_cache_depth:允许缓存entry的最大个数。
  • large_cache_entry_limit:缓存的内存块总大小限制,即缓存的entry对应的内存块的总大小必须小于limit值。
  • large_entry_cache_bytes:当前缓存的entry对应的内存块的总size。
  • large_entry_cache_oldest:按照加入entry缓存的时间先后顺序,最早加入缓存的entry,其在缓存数组中的index位置。
  • large_entry_cache_newest:最新加入缓存的entry,其在缓存数组中的index位置。

初始化

在创建szone时,分配并初始化了相关字段。

ini 复制代码
#define LARGE_CACHE_EXPANDED_THRESHOLD (32ull * 1024 * 1024 * 1024)
uint64_t magazine_large_expanded_cache_threshold = LARGE_CACHE_EXPANDED_THRESHOLD;

szone_t *create_scalable_szone(size_t initial_size, unsigned debug_flags)
{
    //...
    uint64_t memsize = platform_hw_memsize();
    if (memsize >= magazine_large_expanded_cache_threshold) {
        szone->large_cache_depth = LARGE_ENTRY_CACHE_SIZE_HIGH;
        szone->large_cache_entry_limit = LARGE_ENTRY_SIZE_ENTRY_LIMIT_HIGH;
    } else {
        szone->large_cache_depth = LARGE_ENTRY_CACHE_SIZE_LOW;
        szone->large_cache_entry_limit = LARGE_ENTRY_SIZE_ENTRY_LIMIT_LOW;
    }
}

首先调用platform_hw_memsize函数获取设备总内存,如果超过32G,则设置较高规格的参数,否则使用较低规格,以64位设备为例,如果内存大于32G,则large_cache_depth和large_cache_entry_limit分别是64和512M,否则是16和128M。

添加

内存块被回收时,会将对应的entry添加进缓存数组中,结合代码介绍:

ini 复制代码
bool free_large(szone_t *szone, void *ptr, bool try)
{
    //...
    entry = large_entry_for_pointer_no_lock(szone, ptr);
    if (entry) {
        if (large_cache_enabled && entry->size <= szone->large_cache_entry_limit) {
            int idx = szone->large_entry_cache_newest, stop_idx = szone->large_entry_cache_oldest;
            large_entry_t this_entry = *entry;
            boolean_t reusable = TRUE;
            //检测重复回收情况
            while (1) { 
                vm_size_t curr_size = szone->large_entry_cache[idx].size;
                vm_address_t addr = szone->large_entry_cache[idx].address;
                if (addr == entry->address) {
                    malloc_zone_error(szone->debug_flags, true, "pointer %p being freed already on death-row\n", ptr);
                    return true;
                }
                if (idx == stop_idx) { // exhausted live ring?
                    break;
                }
                if (idx) {
                    idx--; // bump idx down
                } else {
                    idx = szone->large_cache_depth - 1; // wrap idx
                }
            }
            vm_range_to_deallocate = large_entry_free_no_lock(szone, entry);
            entry = NULL;
            
            //...
            szone->num_large_objects_in_use--;
            szone->num_bytes_in_large_objects -= this_entry.size;
            
            if (reusable) {
                int idx = szone->large_entry_cache_newest; // Most recently occupied
                vm_address_t addr;
                size_t adjsize;
                //case1
                if (szone->large_entry_cache_newest == szone->large_entry_cache_oldest &&
                    0 == szone->large_entry_cache[idx].address) {
                        addr = 0;
                        adjsize = 0;
                } else {
                    //case2
                    if (idx == szone->large_cache_depth - 1) {
                        idx = 0;
                    } else {
                        idx++;
                    }
                    //case3
                    if (idx == szone->large_entry_cache_oldest) {
                        addr = szone->large_entry_cache[idx].address;
                        adjsize = szone->large_entry_cache[idx].size;
                        szone->large_entry_cache_bytes -= adjsize;
                    } else {
                        addr = 0;
                        adjsize = 0;
                    }
                }
                szone->large_entry_cache_bytes += this_entry.size;
                szone->large_entry_cache[idx] = this_entry;
                szone->large_entry_cache_newest = idx;

                if (0 == addr) {
                    return true;
                }
                
                if (szone->large_entry_cache_oldest == szone->large_cache_depth - 1) {
                    szone->large_entry_cache_oldest = 0;
                } else {
                    szone->large_entry_cache_oldest++;
                }
                mvm_deallocate_pages((void *)addr, (size_t)adjsize, 0);
            }
        }
    }
}
  1. 查找缓存数组large_entry_cache中的位置,从large_entry_cache_newest开始,large_entry_cache_oldest结束。

  2. 重复回收判断逻辑,遍历large_entry_cache中已经存储的所有entry,匹配要回收的entry,如果两者的address和size数据相同,说明该内存已经被回收加入缓存中了,属于重复回收,直接报错。

  3. 开始查找存储位置,当前位置用idx表示,分为以下几种情况:

    1. 初始情况下,large_entry_cache数组为空,large_entry_cache_newest和large_entry_cache_oldest均为0,直接将entry存入large_entry_cache[idx],如图,entry0是首个加入缓存的entry,large_entry_cache_newest和large_entry_cache_oledest均指向entry0。
    2. 如果large_entry_cache_newest不等于large_entry_cache_oldest,且idx未达到large_entry_cache_oldest,则缓存数组未满,large_entry_cache_newest更新加1,将entry加入large_entry_cache_newest位置上,large_entry_cache_oldest保持不变,如图,entry3是新加入缓存数组的entry,large_entry_cache_newest指向entry3。
    3. 如果idx达到large_entry_cache_oldest,说明缓存数组已存满,需要将large_entry_cache_oldest上存储的原entry数据更新,并将对应的原内存回收,并更新large_entry_cache_oldest值,如图所示,large_entry_cache_newest指向的entry9是新加入的entry,替换entry0的数据,同时entry0对应的内存被回收,同时large_entry_cache_oldest指向entry1。
  4. 更新entry后,更新large_entry_cache_bytes的值,large_entry_cache_bytes是缓存的内存总size,加上新添加的entry的内存size,减去被替换entry的内存size。

查找

当分配内存时,根据要分配的size从缓存结构中查找一块size匹配的entry,返回对应的内存块。具体实现代码如下:

ini 复制代码
static large_entry_t large_malloc_best_fit_in_cache(szone_t *szone, size_t size, unsigned char alignment)
{
    int best = -1, idx = szone->large_entry_cache_newest, stop_idx = szone->large_entry_cache_oldest;
    size_t best_size = SIZE_T_MAX;
    large_entry_t entry;
    memset(&entry, 0, sizeof(entry));
    while (1) {
        size_t this_size = szone->large_entry_cache[idx].size;
        vm_address_t addr = szone->large_entry_cache[idx].address;

        if (0 == alignment || 0 == (((uintptr_t)addr) & (((uintptr_t)1 << alignment) - 1))) {
            if (size == this_size || (size < this_size && this_size < best_size)) {
                best = idx;
                best_size = this_size;
                if (size == this_size) {
                    break;
                }
            }
        }
        if (idx == stop_idx) { // exhausted live ring?
            break;
        }

        if (idx) {
            idx--; // bump idx down
        } else {
            idx = szone->large_cache_depth - 1; // wrap idx
        }
    }
    if (best == -1 || (best_size - size) >= size) { // limit fragmentation to 50%
        return entry;
    }
    entry = szone->large_entry_cache[best];
    remove_from_death_row_no_lock(szone, best);
    return entry;
}
  1. 开始遍历,起始位置是large_entry_cache_newest,是最新缓存的位置,结束位置是large_entry_cache_oldest,是最旧缓存的位置,由于添加缓存时idx不断加1,因此随着数组位置不断加1,缓存从旧到新。因此,查询缓存时,从最新的缓存开始匹配,idx不断减1进行遍历,idx=0时,重置回large_cache_depth-1.
  2. 匹配逻辑是,如果当前缓存entry的size等于要分配的size,则直接匹配成功,记录位置,终止遍历。如果缓存entry的size大于要分配的size,则暂时记录位置,继续遍历匹配,查看是否有entry的size大于且更贴近要分配的size。best存储位置。如果entry的size小于要分配的size,则跳过。
  3. 当idx等于stop_idx,即large_entry_cache_oldest,说明已经遍历到最早的缓存了,结束遍历。
  4. 如果best等于-1,则查找失败,返回空entry,否则返best位置上的entry。
  5. 调用remove_from_death_row_no_lock方法调整缓存结构,因为匹配成功后,需要将entry从缓存数组中移除。

调整

ini 复制代码
static int remove_from_death_row_no_lock(szone_t *szone, int idx)
{
    int i, next_idx = -1;
    if (szone->large_entry_cache_oldest < szone->large_entry_cache_newest) {
        for (i = idx; i < szone->large_entry_cache_newest; ++i) {
            szone->large_entry_cache[i] = szone->large_entry_cache[i + 1];
        }
        if (idx == szone->large_entry_cache_oldest) {
            next_idx = -1;
        } else {
            next_idx = idx - 1;
        }
        szone->large_entry_cache_newest--; 
    } else if (szone->large_entry_cache_newest < szone->large_entry_cache_oldest) {
        if (idx <= szone->large_entry_cache_newest) {
            // Fill from right.
            for (i = idx; i < szone->large_entry_cache_newest; ++i) {
                szone->large_entry_cache[i] = szone->large_entry_cache[i + 1];
            }

            if (0 < szone->large_entry_cache_newest) {
                szone->large_entry_cache_newest--;
            } else {
                szone->large_entry_cache_newest = szone->large_cache_depth - 1;
            }
            if (idx == 0) {
                next_idx = szone->large_cache_depth - 1;
            } else {
                next_idx = idx - 1;
            }
        }
        else {
            for (i = idx; i > szone->large_entry_cache_oldest; --i) {
                szone->large_entry_cache[i] = szone->large_entry_cache[i - 1];
            }

            if (idx == szone->large_entry_cache_oldest) {
                next_idx = -1;
            } else {
                next_idx = idx;
            }
            if (szone->large_entry_cache_oldest < szone->large_cache_depth - 1) {
                szone->large_entry_cache_oldest++;
            } else {
                szone->large_entry_cache_oldest = 0;
            }
        }
    }
    else {
        szone->large_entry_cache[idx].address = 0;
        szone->large_entry_cache[idx].size = 0;
        next_idx = -1;
    }
    return next_idx;
}

分为以下几种情况:

  1. large_entry_cache_oldest小于large_entry_cache_newest,例如缓存数组未满的情况,如图,例如匹配entry2并返回,移除后,entry3~6向前移动,其余entry位置不变。large_entry_cache_newest位置减1。

  2. large_entry_cache_newest小于large_entry_cache_oldest,例如缓存数组满的情况,根据匹配entry的idx位置分为以下情况:

    1. idx <= szone->large_entry_cache_newest,如图,例如匹配entry10移除后,entry11~13向前移动,其余entry位置不变,large_entry_cache_newest位置减1.
    2. idx > szone->large_entry_cache_newest,如图,例如匹配entry7移除后,entry5~6向后移动,其余entry位置不变,large_entry_cache_oldest位置加1.
  3. large_entry_cache_newest等于large_entry_cache_oldest,例如数组中仅存在一个entry的情况,移除后数组为空,如图。

核心流程

接下来分析内存分配与回收的流程。

内存分配

使用large_malloc方法分配large内存

arduino 复制代码
void *large_malloc(szone_t *szone, size_t num_kernel_pages, unsigned char alignment, boolean_t cleared_requested)
{
    void *addr;
    vm_range_t range_to_deallocate;
    size_t size;
    
    range_to_deallocate.size = 0;
    range_to_deallocate.address = 0;
    
#if CONFIG_LARGE_CACHE
    // Look for a large_entry_t on the death-row cache?
    if (large_cache_enabled && size <= szone->large_cache_entry_limit) {
        addr = large_malloc_from_cache(szone, size, alignment, cleared_requested);
        if (addr != NULL) {
            return addr;
        }
    }
#endif 

    addr = mvm_allocate_pages(size, alignment, MALLOC_APPLY_LARGE_ASLR(szone->debug_flags), VM_MEMORY_MALLOC_LARGE);
    if (addr == NULL) {
        return NULL;
    }

    bool success = large_entry_grow_and_insert_no_lock(szone, (vm_address_t) addr, (vm_size_t) size,
            &range_to_deallocate);
    if (!success) {
        return NULL;
    }

    if (range_to_deallocate.size) {
        mvm_deallocate_pages((void *)range_to_deallocate.address, range_to_deallocate.size, 0);
    }
    return addr;
}

步骤如下:

  1. 调用large_malloc_from_cache方法从缓存中查找可用的内存块,如果成功执结返回内存块地址addr。

    arduino 复制代码
    static void *large_malloc_from_cache(szone_t *szone, size_t size, unsigned char alignment, boolean_t cleared_requested)
    {
        large_entry_t entry;
        while (true) {
            entry = large_malloc_best_fit_in_cache(szone, size, alignment);
            if (entry.address == (vm_address_t)NULL) {
                return NULL;
            } else {
                break;
            }
        }
        vm_range_t range_to_deallocate;
        range_to_deallocate.size = 0;
        range_to_deallocate.address = 0;
        bool success = large_entry_grow_and_insert_no_lock(szone, entry.address, entry.size, &range_to_deallocate);
    
        if (!success) {
            return NULL;
        }
        szone->large_entry_cache_bytes -= entry.size;
        if (range_to_deallocate.size) {
            mvm_deallocate_pages((void *)range_to_deallocate.address, range_to_deallocate.size, 0);
        }
        return (void *)entry.address;
    }
    1. 调用large_malloc_best_fit_in_cache方法从缓存数组中查找可用内存块,如果未找到,则返回NULL,否则将找到的内存块对应的entry加入hash数组中管理。
    2. 更新缓存信息large_entry_cache_bytes。
    3. 如果扩容,range_to_deallocate记录原数组内存,调用mvm_deallocate_pages方法回收。
  2. 如果没有找到,则调用mvm_allocate_pages方法分配一块内存,调用large_entry_grow_and_insert_no_lock方法并生成entry加入hash数组中管理。

  3. 如果扩容,range_to_deallocate记录原数组内存,调用mvm_deallocate_pages方法回收。

  4. 返回entry的内存块address

内存回收

调用free_large方法回收large内存

ini 复制代码
bool free_large(szone_t *szone, void *ptr, bool try)
{
    large_entry_t *entry;
    vm_range_t vm_range_to_deallocate;
    vm_range_to_deallocate.size = 0;
    vm_range_to_deallocate.address = 0;
    entry = large_entry_for_pointer_no_lock(szone, ptr);
    if (entry) {
#if CONFIG_LARGE_CACHE
        if (large_cache_enabled && entry->size <= szone->large_cache_entry_limit) {
            int idx = szone->large_entry_cache_newest, stop_idx = szone->large_entry_cache_oldest;
            large_entry_t this_entry = *entry;
            boolean_t reusable = TRUE;
            //判断是否二次回收
            while (1) {
                //...
            }
            vm_range_to_deallocate = large_entry_free_no_lock(szone, entry);
            entry = NULL;
            szone->num_large_objects_in_use--;
            szone->num_bytes_in_large_objects -= this_entry.size;
            
            //添加缓存
            if (reusable) {
                //...
            }
        }
#endif
        if (!vm_range_to_deallocate.address) {
            szone->num_large_objects_in_use--;
            szone->num_bytes_in_large_objects -= entry->size;

            vm_range_to_deallocate = large_entry_free_no_lock(szone, entry);
        }
    }
    if (vm_range_to_deallocate.address) {
        mvm_deallocate_pages((void *)vm_range_to_deallocate.address, (size_t)vm_range_to_deallocate.size, 0);
    }
    return true;
}

步骤如下:

  1. 调用large_entry_for_pointer_no_lock方法查找回收内存块的entry。
  2. 如果支持缓存逻辑,将entry添加进缓存后直接返回。
  3. 否则调用mvm_deallocate_pages回收内存块,调用large_entry_free_no_lock清空hash数组中的entry数据。

以上是small和large代码的分析,欢迎大家交流评论~

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