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代码的分析,欢迎大家交流评论~

相关推荐
pf_data14 小时前
手机换新,怎么把旧iPhone手机数据传输至新iPhone16手机
ios·智能手机·iphone
键盘敲没电1 天前
【iOS】KVC
ios·objective-c·xcode
吾吾伊伊,野鸭惊啼1 天前
2024最新!!!iOS高级面试题,全!(二)
ios
吾吾伊伊,野鸭惊啼1 天前
2024最新!!!iOS高级面试题,全!(一)
ios
不会敲代码的VanGogh1 天前
【iOS】——应用启动流程
macos·ios·objective-c·cocoa
Swift社区1 天前
Apple 新品发布会亮点有哪些 | Swift 周报 issue 61
ios·swiftui·swift
逻辑克1 天前
使用 MultipeerConnectivity 在 iOS 中实现近场无线数据传输
ios
dnekmihfbnmv1 天前
好用的电容笔有哪些推荐一下?年度最值得推荐五款电容笔分享!
ios·电脑·ipad·平板
Magnetic_h2 天前
【iOS】单例模式
笔记·学习·ui·ios·单例模式·objective-c
归辞...2 天前
「iOS」——单例模式
ios·单例模式·cocoa