本篇是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内存的相关操作如下:
-
当一个内存块被回收时,首先判断是否满足oob内存的条件,即msize>= vm_kernel_page_size.
arduinostatic 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)); }
-
如果满足,则遍历small_oob_free_entries数组,查找可用的oob_free_entry_s结构包装内存信息。
inistatic 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 ®ion->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结构中的数据还原成原内存地址。
arduinostatic 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)); }
-
设置和获取oob结构之间的前后链表关系。
arduinostatic 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 }; }
-
查找内存地址对应的oob存储结构,同样遍历small_oob_free_entries数组,匹配ptr是否相等。
inistatic 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 = ®ion->small_oob_free_entries[i]; if (small_oob_free_entry_get_ptr(oob) == ptr && oob->ptr & SMALL_IS_OOB) { return ®ion->small_oob_free_entries[i]; } } return NULL; }
-
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;
}
步骤如下:
- 计算mszie和slot,定位对应的空闲链表,并获取head节点
- 根据内存块地址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。
- 设置链表关系,将当前新的节点加入链表头。
- small_free_mark_free标记为空闲,更新region内的状态数据,如果是msize的第一个空闲块,则记录magazine的mag_bitmap状态位。
- 最后更新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代码高度相似,步骤如下:
- 选取magazine,尝试获取cache。
- 如果未命中cache,调用small_malloc_from_free_list从空闲链表中查找合适的空闲内存块。
- 如果没有找到,则调用small_get_region_from_depot将depot magazine的region迁移至当前magazine中,然后再次查找。
- 如果仍未找到,则调用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;
}
步骤包括:
- 获取内存地址相邻的空闲内存块,尝试合并。合并操作更新链表和状态信息。即删除旧的节点,添加合并后新的节点进链表。
- 更新统计信息mag_num_bytes_in_objects、bytes_used。
- 判断是否迁移至small_free_try_recirc_to_depot,和tiny_free_try_recirc_to_depot的逻辑相似,可以参考tiny相关介绍。
以上是small代码的分析。
large
主要实现在magazine_large.c文件中,large内存管理分为分配与回收两大块,本文在结合代码分析的过程中,介绍相关的概念和实现机制。
相关概念
不同于tiny、small的内存分配方式,使用large方式分配,具备以下特点:
- 由于分配的较大块的内存,超过15KB以上,因此每次分配时,根据实际需要的size分配,而不使用size分级策略,使实际分配的size固定为几档。
- 每次分配和回收时,直接使用底层接口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的步骤如下:
- 根据entry的存储的内存块地址address,计算出hash_index,该index决定entry插入hash数组中的位置。
- 根据数组的首地址和index计算出偏移位置,将entry插入该位置,如果该位置已经存储其他entry的数据,则index++,直到找到没有entry或者空entry的位置。空entry是指之前添加过entry,但是entry的数据已经清0。
- 整体偏移判断的过程是类似遍历环状结构,如果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;
}
整体步骤如下:
-
计算要扩容的大小,如果之前没有过当前large_entries为NULL,则分配一个虚拟内存页的大小,否则在原基础上扩容1倍后加1,因此,这里的扩容的机制是2倍扩容。
-
根据扩容大小,调用large_entries_alloc_no_lock方法分配新内存,返回新数组的地址。
arduinostatic 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进行分配一块虚拟内存。
-
更新szone的large_entries和num_large_entries字段,并将原数组中的entry内容拷贝至新的数组中。迁移过程如图,遍历原数组内存逐一添加进新数组,在新数组中位置保持和原位置不变。
-
调用large_entries_free_no_lock方法回收逻辑。
arduinovoid 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传入扩容方法的参数,用于存储要释放的原内存,在执行完扩容后释放。
-
返回新数组地址。
综合来看,添加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;
}
```
-
调用mvm_allocate_pages方法分配large内存块。
-
调用large_entry_grow_and_insert_no_lock方法创建entry,并添加进entries数组中。
inistatic 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。
-
如果其中发生扩容逻辑,则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一致:
- 根据地址ptr,基于相同紫规则计算出hash_index。
- szone->large_entries + index作为查询entry的初始入口位置,匹配ptr和当前位置上存储的entry的address,匹配不上则index++,如果匹配成功,则结束遍历。
- 当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;
}
-
首先将entry存储的内存块address、size数据存储到range中。
-
清空entry数据
-
由于清空了其中一个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插入的操作
inistatic 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数据重新插入数组中。
-
返回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);
}
}
}
}
-
查找缓存数组large_entry_cache中的位置,从large_entry_cache_newest开始,large_entry_cache_oldest结束。
-
重复回收判断逻辑,遍历large_entry_cache中已经存储的所有entry,匹配要回收的entry,如果两者的address和size数据相同,说明该内存已经被回收加入缓存中了,属于重复回收,直接报错。
-
开始查找存储位置,当前位置用idx表示,分为以下几种情况:
- 初始情况下,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。
- 如果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。
- 如果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。
-
更新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;
}
- 开始遍历,起始位置是large_entry_cache_newest,是最新缓存的位置,结束位置是large_entry_cache_oldest,是最旧缓存的位置,由于添加缓存时idx不断加1,因此随着数组位置不断加1,缓存从旧到新。因此,查询缓存时,从最新的缓存开始匹配,idx不断减1进行遍历,idx=0时,重置回large_cache_depth-1.
- 匹配逻辑是,如果当前缓存entry的size等于要分配的size,则直接匹配成功,记录位置,终止遍历。如果缓存entry的size大于要分配的size,则暂时记录位置,继续遍历匹配,查看是否有entry的size大于且更贴近要分配的size。best存储位置。如果entry的size小于要分配的size,则跳过。
- 当idx等于stop_idx,即large_entry_cache_oldest,说明已经遍历到最早的缓存了,结束遍历。
- 如果best等于-1,则查找失败,返回空entry,否则返best位置上的entry。
- 调用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;
}
分为以下几种情况:
-
large_entry_cache_oldest小于large_entry_cache_newest,例如缓存数组未满的情况,如图,例如匹配entry2并返回,移除后,entry3~6向前移动,其余entry位置不变。large_entry_cache_newest位置减1。
-
large_entry_cache_newest小于large_entry_cache_oldest,例如缓存数组满的情况,根据匹配entry的idx位置分为以下情况:
- idx <= szone->large_entry_cache_newest,如图,例如匹配entry10移除后,entry11~13向前移动,其余entry位置不变,large_entry_cache_newest位置减1.
- idx > szone->large_entry_cache_newest,如图,例如匹配entry7移除后,entry5~6向后移动,其余entry位置不变,large_entry_cache_oldest位置加1.
-
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;
}
步骤如下:
-
调用large_malloc_from_cache方法从缓存中查找可用的内存块,如果成功执结返回内存块地址addr。
arduinostatic 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; }
- 调用large_malloc_best_fit_in_cache方法从缓存数组中查找可用内存块,如果未找到,则返回NULL,否则将找到的内存块对应的entry加入hash数组中管理。
- 更新缓存信息large_entry_cache_bytes。
- 如果扩容,range_to_deallocate记录原数组内存,调用mvm_deallocate_pages方法回收。
-
如果没有找到,则调用mvm_allocate_pages方法分配一块内存,调用large_entry_grow_and_insert_no_lock方法并生成entry加入hash数组中管理。
-
如果扩容,range_to_deallocate记录原数组内存,调用mvm_deallocate_pages方法回收。
-
返回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;
}
步骤如下:
- 调用large_entry_for_pointer_no_lock方法查找回收内存块的entry。
- 如果支持缓存逻辑,将entry添加进缓存后直接返回。
- 否则调用mvm_deallocate_pages回收内存块,调用large_entry_free_no_lock清空hash数组中的entry数据。
以上是small和large代码的分析,欢迎大家交流评论~