前言: iOS 高级之美 是本人总结了一些工作实际开发研究以及面试重点,围绕底层进行
源码分析
-LLDB 调试
-源码断点
-汇编调试
,让读者真正感受 Runtime底层之美~😊
目录如下:iOS 高级之美(一)—— iOS_objc4-756.2 最新源码编译调试
iOS 高级之美(二)—— OC对象底层上篇
iOS 高级之美(三)—— OC对象底层下篇
iOS 高级之美(四)—— isa原理分析
iOS 高级之美(五)—— 类结构分析
iOS 高级之美(六)—— malloc分析
我们前面分析了对象的创建,其中一个非常重要的点:申请内存空间!
然而 obj = (id)calloc(1, size)
这一段代码所在位置不再是 libObjc4
,它定位到了 libmalloc
, 至于愈合定位的大家可以参考笔者前面的文章。这个篇章我们针对 malloc
展开而分析
那么
calloc
方法做了什么呢,让我们来一探究竟!
一、malloc_zone_t 分析
这个家伙是一个非常重要的家伙,我们先来看看 malloc_zone_t
的结构
typedef struct _malloc_zone_t {
void *reserved1; /* RESERVED FOR CFAllocator DO NOT USE */
void *reserved2; /* RESERVED FOR CFAllocator DO NOT USE */
size_t (* MALLOC_ZONE_FN_PTR(size))(struct _malloc_zone_t *zone, const void *ptr); /* returns the size of a block or 0 if not in this zone; must be fast, especially for negative answers */
void *(* MALLOC_ZONE_FN_PTR(malloc))(struct _malloc_zone_t *zone, size_t size);
void *(* MALLOC_ZONE_FN_PTR(calloc))(struct _malloc_zone_t *zone, size_t num_items, size_t size); /* same as malloc, but block returned is set to zero */
void *(* MALLOC_ZONE_FN_PTR(valloc))(struct _malloc_zone_t *zone, size_t size); /* same as malloc, but block returned is set to zero and is guaranteed to be page aligned */
void (* MALLOC_ZONE_FN_PTR(free))(struct _malloc_zone_t *zone, void *ptr);
void *(* MALLOC_ZONE_FN_PTR(realloc))(struct _malloc_zone_t *zone, void *ptr, size_t size);
void (* MALLOC_ZONE_FN_PTR(destroy))(struct _malloc_zone_t *zone);
const char *zone_name;
unsigned (* MALLOC_ZONE_FN_PTR(batch_malloc))(struct _malloc_zone_t *zone, size_t size, void **results, unsigned num_requested);
struct malloc_introspection_t * MALLOC_INTROSPECT_TBL_PTR(introspect);
unsigned version;
void *(* MALLOC_ZONE_FN_PTR(memalign))(struct _malloc_zone_t *zone, size_t alignment, size_t size);
void (* MALLOC_ZONE_FN_PTR(free_definite_size))(struct _malloc_zone_t *zone, void *ptr, size_t size);
size_t (* MALLOC_ZONE_FN_PTR(pressure_relief))(struct _malloc_zone_t *zone, size_t goal);
boolean_t (* MALLOC_ZONE_FN_PTR(claimed_address))(struct _malloc_zone_t *zone, void *ptr);
} malloc_zone_t;
malloc_zone_t
是一个非常基础结构,里面包含一堆函数指针,用来存储一堆相关的处理函数的具体实现的地址,例如malloc
、free
、realloc
等函数的具体实现。后续会基于malloc_zone_t
进行扩展。
二、calloc 的流程
2.1 calloc -> malloc_zone_calloc
的流程
void * calloc(size_t num_items, size_t size)
{
void *retval;
retval = malloc_zone_calloc(default_zone, num_items, size);
if (retval == NULL) {
errno = ENOMEM;
}
return retval;
}
- 这个
default_zone
其实是一个“假的”zone,同时它也是malloc_zone_t
类型。它存在的目的就是要引导程序进入一个创建真正的zone
的流程。 - 下面来看一下
default_zone
的引导流程。
2.2 default_zone
引导
void * malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (malloc_logger) {
malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
(uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
}
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
return ptr;
}
ptr = zone->calloc(zone, num_items, size)
- 此时传进来的
zone
的类型是 上面calloc
传入的defaultzone
,所以zone->calloc
的调用实现要看defaultzone
的定义。
2.3 defaultzone
的定义
static virtual_default_zone_t virtual_default_zone
__attribute__((section("__DATA,__v_zone")))
__attribute__((aligned(PAGE_MAX_SIZE))) = {
NULL,
NULL,
default_zone_size,
default_zone_malloc,
default_zone_calloc,
default_zone_valloc,
default_zone_free,
default_zone_realloc,
default_zone_destroy,
DEFAULT_MALLOC_ZONE_STRING,
default_zone_batch_malloc,
default_zone_batch_free,
&default_zone_introspect,
10,
default_zone_memalign,
default_zone_free_definite_size,
default_zone_pressure_relief,
default_zone_malloc_claimed_address,
};
- 从上面的结构可以看出
defaultzone->calloc
实际的函数实现为default_zone_calloc
。
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
zone = runtime_default_zone();
return zone->calloc(zone, num_items, size);
}
- 引导创建真正的
zone
- 使用真正的
zone
进行calloc
2.4 zone
分析
在创建正在的 zone
时,其实系统是有对应的一套创建策略的。在跟踪 runtime_default_zone
方法后,最终会进入如下调用
static void
_malloc_initialize(void *context __unused)
{
...... - 省略多余代码
//创建helper_zone,
malloc_zone_t *helper_zone = create_scalable_zone(0, malloc_debug_flags);
//创建 nano zone
if (_malloc_engaged_nano == NANO_V2) {
zone = nanov2_create_zone(helper_zone, malloc_debug_flags);
} else if (_malloc_engaged_nano == NANO_V1) {
zone = nano_create_zone(helper_zone, malloc_debug_flags);
}
//如果上面的if else if 成立,这进入 nonazone
if (zone) {
malloc_zone_register_while_locked(zone);
malloc_zone_register_while_locked(helper_zone);
// Must call malloc_set_zone_name() *after* helper and nano are hooked together.
malloc_set_zone_name(zone, DEFAULT_MALLOC_ZONE_STRING);
malloc_set_zone_name(helper_zone, MALLOC_HELPER_ZONE_STRING);
} else {
//使用helper_zone分配内存
zone = helper_zone;
malloc_zone_register_while_locked(zone);
malloc_set_zone_name(zone, DEFAULT_MALLOC_ZONE_STRING);
}
//缓存default_zone
initial_default_zone = zone;
.....
}
- 创建
helper_zone
- 创建
nano zone
- 如果上面的
if else if 成立
,这进入nonazone
- 使用
helper_zone
分配内存 - 缓存
default_zone
在这里 会存在两种 zone
nanozone_t
scalable_zone
2.5 nanozone_t
分析
typedef struct nano_meta_s {
OSQueueHead slot_LIFO MALLOC_NANO_CACHE_ALIGN;
unsigned int slot_madvised_log_page_count;
volatile uintptr_t slot_current_base_addr;
volatile uintptr_t slot_limit_addr;
volatile size_t slot_objects_mapped;
volatile size_t slot_objects_skipped;
bitarray_t slot_madvised_pages;
// position on cache line distinct from that of slot_LIFO
volatile uintptr_t slot_bump_addr MALLOC_NANO_CACHE_ALIGN;
volatile boolean_t slot_exhausted;
unsigned int slot_bytes;
unsigned int slot_objects;
} *nano_meta_admin_t;
// vm_allocate()'d, so page-aligned to begin with.
typedef struct nanozone_s {
// first page will be given read-only protection
malloc_zone_t basic_zone;
uint8_t pad[PAGE_MAX_SIZE - sizeof(malloc_zone_t)];
// remainder of structure is R/W (contains no function pointers)
// page-aligned
// max: NANO_MAG_SIZE cores x NANO_SLOT_SIZE slots for nano blocks {16 .. 256}
//以Mag、Slot为维度,维护申请的band内存部分 slot 的范围为 1~16
struct nano_meta_s meta_data[NANO_MAG_SIZE][NANO_SLOT_SIZE];//
_malloc_lock_s band_resupply_lock[NANO_MAG_SIZE];
uintptr_t band_max_mapped_baseaddr[NANO_MAG_SIZE];
size_t core_mapped_size[NANO_MAG_SIZE];
unsigned debug_flags;
uintptr_t cookie;
malloc_zone_t *helper_zone;
} nanozone_t;
-
nanozone_t
同样是malloc_zone_t
类型。在nano_create_zone
函数内部会完成对calloc
等函数的重新赋值。
2.6 nano_create_zone
分析
malloc_zone_t *
nano_create_zone(malloc_zone_t *helper_zone, unsigned debug_flags)
{
nanozone_t *nanozone;
int i, j;
//构造nano zone
/* Note: It is important that nano_create_zone resets _malloc_engaged_nano
* if it is unable to enable the nanozone (and chooses not to abort). As
* several functions rely on _malloc_engaged_nano to determine if they
* should manipulate the nanozone, and these should not run if we failed
* to create the zone.
*/
// MALLOC_ASSERT(_malloc_engaged_nano == NANO_V1);
/* get memory for the zone. */
nanozone = nano_common_allocate_based_pages(NANOZONE_PAGED_SIZE, 0, 0, VM_MEMORY_MALLOC, 0);
if (!nanozone) {
_malloc_engaged_nano = NANO_NONE;
return NULL;
}
//构造对zone 的一些函数进行重新赋值
/* set up the basic_zone portion of the nanozone structure */
nanozone->basic_zone.version = 10;
nanozone->basic_zone.size = (void *)nano_size;
nanozone->basic_zone.malloc = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_malloc_scribble : (void *)nano_malloc;
nanozone->basic_zone.calloc = (void *)nano_calloc;
nanozone->basic_zone.valloc = (void *)nano_valloc;
nanozone->basic_zone.free = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_free_scribble : (void *)nano_free;
nanozone->basic_zone.realloc = (void *)nano_realloc;
nanozone->basic_zone.destroy = (void *)nano_destroy;
nanozone->basic_zone.batch_malloc = (void *)nano_batch_malloc;
nanozone->basic_zone.batch_free = (void *)nano_batch_free;
nanozone->basic_zone.introspect = (struct malloc_introspection_t *)&nano_introspect;
nanozone->basic_zone.memalign = (void *)nano_memalign;
nanozone->basic_zone.free_definite_size = (debug_flags & MALLOC_DO_SCRIBBLE) ? (void *)nano_free_definite_size_scribble
: (void *)nano_free_definite_size;
nanozone->basic_zone.pressure_relief = (void *)nano_pressure_relief;
nanozone->basic_zone.claimed_address = (void *)nano_claimed_address;
nanozone->basic_zone.reserved1 = 0; /* Set to zero once and for all as required by CFAllocator. */
nanozone->basic_zone.reserved2 = 0; /* Set to zero once and for all as required by CFAllocator. */
mprotect(nanozone, sizeof(nanozone->basic_zone), PROT_READ); /* Prevent overwriting the function pointers in basic_zone. */
/* Nano zone does not support MALLOC_ADD_GUARD_PAGES. */
if (debug_flags & MALLOC_ADD_GUARD_PAGES) {
malloc_report(ASL_LEVEL_INFO, "nano zone does not support guard pages\n");
debug_flags &= ~MALLOC_ADD_GUARD_PAGES;
}
/* set up the remainder of the nanozone structure */
nanozone->debug_flags = debug_flags;
if (phys_ncpus > sizeof(nanozone->core_mapped_size) /
sizeof(nanozone->core_mapped_size[0])) {
MALLOC_REPORT_FATAL_ERROR(phys_ncpus,
"nanozone abandoned because NCPUS > max magazines.\n");
}
/* Initialize slot queue heads and resupply locks. */
OSQueueHead q0 = OS_ATOMIC_QUEUE_INIT;
for (i = 0; i < nano_common_max_magazines; ++i) {
_malloc_lock_init(&nanozone->band_resupply_lock[i]);
for (j = 0; j < NANO_SLOT_SIZE; ++j) {
nanozone->meta_data[i][j].slot_LIFO = q0;
}
}
/* Initialize the security token. */
nanozone->cookie = (uintptr_t)malloc_entropy[0] & 0x0000ffffffff0000ULL; // scramble central 32bits with this cookie
nanozone->helper_zone = helper_zone;
return (malloc_zone_t *)nanozone;
}
- 构造
nano zone
- 构造对
zone
的一些函数进行重新赋值 -
Nano zone
不支持 MALLOC_ADD_GUARD_PAGES - 建立其余的
nanozone
结构 - 初始化插槽队列头并重新供应锁
- 初始化安全令牌。
2.7 nano_calloc
分析
过程参考 defaultzone
。回到上面 default_zone_calloc
函数内。下一步就是使用 nanozone_t
调用 calloc
。
下面是 nano_calloc
的实现
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
size_t total_bytes;
if (calloc_get_size(num_items, size, 0, &total_bytes)) {
return NULL;
}
// 如果要开辟的空间小于 NANO_MAX_SIZE 则进行nanozone_t的malloc。
if (total_bytes <= NANO_MAX_SIZE) {
void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
if (p) {
return p;
} else {
/* FALLTHROUGH to helper zone */
}
}
//否则就进行helper_zone的流程
malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
return zone->calloc(zone, 1, total_bytes);
}
- 如果要开辟的空间小于
NANO_MAX_SIZE
则进行 - 否则就进行
helper_zone
的流程
2.8 _nano_malloc_check_clear
分析
这里我们也可以看出使用 nanozone_t
的限制为不超过256B
。继续看 _nano_malloc_check_clear
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);
void *ptr;
size_t slot_key;
// 获取16字节对齐之后的大小,slot_key非常关键,为slot_bytes/16的值,也是数组的二维下下标
size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
//根据_os_cpu_number经过运算获取 mag_index(meta_data的一维索引)
mag_index_t mag_index = nano_mag_index(nanozone);
//确定当前cpu对应的mag和通过size参数计算出来的slot,去对应metadata的链表中取已经被释放过的内存区块缓存
nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);
//检测是否存在已经释放过,可以直接拿来用的内存,已经被释放的内存会缓存在 chained_block_s 链表
//每一次free。同样会根据 index 和slot 的值回去 pMeta,然后把slot_LIFO的指针指向释放的内存。
ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
if (ptr) {
...省略无关代码
//如果缓存的内存存在,这进行指针地址检查等异常检测,最后返回
//第一次调用malloc时,不会执行这一块代码。
} else {
//没有释放过的内存,所以调用函数 获取内存
ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
}
if (cleared_requested && ptr) {
memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
}
return ptr;
}
获取16字节对齐之后的大小,
slot_key
非常关键,为slot_bytes/16
的值,也是数组的二维下下标根据
_os_cpu_number
经过运算获取mag_index
(meta_data
的一维索引)确定当前
cpu
对应的mag
和通过size
参数计算出来的slot
,去对应metadata
的链表中取已经被释放过的内存区块缓存检测是否存在已经释放过,可以直接拿来用的内存,已经被释放的内存会缓存在
chained_block_s
链表每一次
free
。同样会根据index
和slot
的值回去pMeta
,然后把slot_LIFO
的指针指向释放的内存。如果缓存的内存存在,这进行指针地址检查等异常检测,最后返回
没有释放过的内存,所以调用函数 获取内存
该方法主要是通过 cpu
与 slot
确定 index
,从chained_block_s
链表中找出是否存在已经释放过的缓存。如果存在则进行指针检查之后返回,否则进入查询 meta data
或者开辟 band
。
2.9 segregated_next_block
分析
static MALLOC_INLINE void *
segregated_next_block(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
while (1) {
//当前这块pMeta可用内存的结束地址
uintptr_t theLimit = pMeta->slot_limit_addr; // Capture the slot limit that bounds slot_bump_addr right now
//原子的为pMeta->slot_bump_addr添加slot_bytes的长度,偏移到下一个地址
uintptr_t b = OSAtomicAdd64Barrier(slot_bytes, (volatile int64_t *)&(pMeta->slot_bump_addr));
//减去添加的偏移量,获取当前可以获取的地址
b -= slot_bytes; // Atomic op returned addr of *next* free block. Subtract to get addr for *this* allocation.
if (b < theLimit) { // Did we stay within the bound of the present slot allocation?
//如果地址还在范围之内,则返回地址
return (void *)b; // Yep, so the slot_bump_addr this thread incremented is good to go
} else {
//已经用尽了
if (pMeta->slot_exhausted) { // exhausted all the bands availble for this slot?
pMeta->slot_bump_addr = theLimit;
return 0; // We're toast
} else {
// One thread will grow the heap, others will see its been grown and retry allocation
_malloc_lock_lock(&nanozone->band_resupply_lock[mag_index]);
// re-check state now that we've taken the lock
//多线程的缘故,重新检查是否用尽
if (pMeta->slot_exhausted) {
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
return 0; // Toast
} else if (b < pMeta->slot_limit_addr) {
//如果小于最大限制地址,当重新申请一个新的band后,重新尝试while
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
continue; // ... the slot was successfully grown by first-taker (not us). Now try again.
} else if (segregated_band_grow(nanozone, pMeta, slot_bytes, mag_index)) {
//申请新的band成功,重新尝试while
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
continue; // ... the slot has been successfully grown by us. Now try again.
} else {
pMeta->slot_exhausted = TRUE;
pMeta->slot_bump_addr = theLimit;
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
return 0;
}
}
}
}
}
当前这块
pMeta
可用内存的结束地址原子的为
pMeta->slot_bump_addr
添加slot_bytes
的长度,偏移到下一个地址b -= slot_bytes
减去添加的偏移量,获取当前可以获取的地址如果地址还在范围之内,则返回地址
return (void *)b
pMeta->slot_exhausted
多线程的缘故,重新检查是否用尽_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
如果小于最大限制地址,当重新申请一个新的band
后,重新尝试while
_malloc_lock_unlock(&nanozone->band_resupply_lock[mag_index]);
申请新的band
成功,重新尝试while
如果是第一次调用 segregated_next_block
函数,band
不存在,缓存也不会存在,所以会调用segregated_band_grow
。来开辟新的 band
2.10 segregated_band_grow
分析
boolean_t
segregated_band_grow(nanozone_t *nanozone, nano_meta_admin_t pMeta, size_t slot_bytes, unsigned int mag_index)
{
用来计算slot_current_base_addr 的联合体
nano_blk_addr_t u; // the compiler holds this in a register
uintptr_t p, s;
size_t watermark, hiwater;
if (0 == pMeta->slot_current_base_addr) { // First encounter?
//利用nano_blk_addr_t 来计算slot_current_base_addr。
u.fields.nano_signature = NANOZONE_SIGNATURE;
u.fields.nano_mag_index = mag_index;
u.fields.nano_band = 0;
u.fields.nano_slot = (slot_bytes >> SHIFT_NANO_QUANTUM) - 1;
u.fields.nano_offset = 0;
//根据设置的属性计算 slot_current_base_addr
p = u.addr;
pMeta->slot_bytes = (unsigned int)slot_bytes;
pMeta->slot_objects = SLOT_IN_BAND_SIZE / slot_bytes;
} else {
p = pMeta->slot_current_base_addr + BAND_SIZE; // Growing, so stride ahead by BAND_SIZE
u.addr = (uint64_t)p;
if (0 == u.fields.nano_band) { // Did the band index wrap?
return FALSE;
}
assert(slot_bytes == pMeta->slot_bytes);
}
pMeta->slot_current_base_addr = p;
//BAND_SIZE = 1 << 21 = 2097152 = 256kb
mach_vm_address_t vm_addr = p & ~((uintptr_t)(BAND_SIZE - 1)); // Address of the (2MB) band covering this (128KB) slot
if (nanozone->band_max_mapped_baseaddr[mag_index] < vm_addr) {
//如果最大能存储的地址 仍然小于目标地址,则小开辟新的band
#if !NANO_PREALLOCATE_BAND_VM
// Obtain the next band to cover this slot
//// mac 和模拟器 或重新使用
// Obtain the next band to cover this slot
//重新申请新的 band,调用mach_vm_map 从pmap 转换。
kern_return_t kr = mach_vm_map(mach_task_self(), &vm_addr, BAND_SIZE, 0, VM_MAKE_TAG(VM_MEMORY_MALLOC_NANO),
MEMORY_OBJECT_NULL, 0, FALSE, VM_PROT_DEFAULT, VM_PROT_ALL, VM_INHERIT_DEFAULT);
void *q = (void *)vm_addr;
if (kr || q != (void *)(p & ~((uintptr_t)(BAND_SIZE - 1)))) { // Must get exactly what we asked for
if (!kr) {
mach_vm_deallocate(mach_task_self(), vm_addr, BAND_SIZE);
}
return FALSE;
}
#endif
nanozone->band_max_mapped_baseaddr[mag_index] = vm_addr;
}
// Randomize the starting allocation from this slot (introduces 11 to 14 bits of entropy)
if (0 == pMeta->slot_objects_mapped) { // First encounter?
pMeta->slot_objects_skipped = (malloc_entropy[1] % (SLOT_IN_BAND_SIZE / slot_bytes));
pMeta->slot_bump_addr = p + (pMeta->slot_objects_skipped * slot_bytes);
} else {
pMeta->slot_bump_addr = p;
}
pMeta->slot_limit_addr = p + (SLOT_IN_BAND_SIZE / slot_bytes) * slot_bytes;
pMeta->slot_objects_mapped += (SLOT_IN_BAND_SIZE / slot_bytes);
u.fields.nano_signature = NANOZONE_SIGNATURE;
u.fields.nano_mag_index = mag_index;
u.fields.nano_band = 0;
u.fields.nano_slot = 0;
u.fields.nano_offset = 0;
s = u.addr; // Base for this core.
// Set the high water mark for this CPU's entire magazine, if this resupply raised it.
watermark = nanozone->core_mapped_size[mag_index];
hiwater = MAX(watermark, p - s + SLOT_IN_BAND_SIZE);
nanozone->core_mapped_size[mag_index] = hiwater;
return TRUE;
}
nano_blk_addr_t u
用来计算slot_current_base_addr
的联合体利用
nano_blk_addr_t
来计算slot_current_base_addr
根据设置的属性计算
slot_current_base_addr
如果最大能存储的地址 仍然小于目标地址,则小开辟新的
band
mac 和模拟器 或重新使用
重新申请新的
band
,调用mach_vm_map
从pmap
转换。
当进入 segregated_band_grow
时,如果当前的 band
不够用,则使用 mach_vm_map
经由 pmap
重新映射物理内存到虚拟内存。
关于通过 nano_blk_addr_t
的联合体结构如下,其每个成员所占的 bit位数
已经写出。
struct nano_blk_addr_s {
uint64_t
nano_offset:NANO_OFFSET_BITS, //17 locates the block
nano_slot:NANO_SLOT_BITS, //4 bucket of homogenous quanta-multiple blocks
nano_band:NANO_BAND_BITS, //17
nano_mag_index:NANO_MAG_BITS, //6 the core that allocated this block
nano_signature:NANOZONE_SIGNATURE_BITS; // the address range devoted to us.
};
#endif
// clang-format on
typedef union {
uint64_t addr;
struct nano_blk_addr_s fields;
} nano_blk_addr_t;
下面通过 LLDB 分析
在 free
的阶段,也是使用如上的方式获取 对应的 slot,mag_index
。
下面来梳理下 nana_zone
分配过程:
- 确定当前
cpu
对应的mag
和通过size参数
计算出来的slot
,去对应chained_block_s
的链表中取已经被释放过的内存区块缓存,如果取到检查指针地址是否有问题,没有问题就直接返回;- 初次进行
nano malloc
时,nano zon
并没有缓存,会直接在nano zone
范围的地址空间上直接分配连续地址内存;- 如当前
Band
中当前Slot
耗尽则向系统申请新的Band
(每个Band
固定大小2M
,容纳了16个128k
的槽),连续地址分配内存的基地址、limit地址
以及当前分配到的地址由meta data
结构维护起来,而这些meta data
则以Mag
、Slot
为维度(Mag个数是处理器个数,Slot是16个
)的二维数组形式,放在nanozone_t
的meta_data
字段中。
流程如下
2.11 scalable zone(helper_zone)
分析
在 szone
上分配的内存包括 tiny、small和large
三大类,其中 tiny
和 small
的分配、释放过程大致相同,larg类型有自己的方式管理。同样会通过
create_scalable_zone来构造
zone。 这里不在复述
create_scalable_zone`,直接看内存的分配策略
2.12 szone_malloc_should_clear
分析
MALLOC_NOINLINE void *
szone_malloc_should_clear(szone_t *szone, size_t size, boolean_t cleared_requested)
{
void *ptr;
msize_t msize;
//64位 <= 1008B 32位<= 496B
if (size <= SMALL_THRESHOLD) {
// tiny size: <=1008 bytes (64-bit), <=496 bytes (32-bit)
// think tiny
msize = TINY_MSIZE_FOR_BYTES(size + TINY_QUANTUM - 1);
if (!msize) {
msize = 1;
}
ptr = tiny_malloc_should_clear(&szone->tiny_rack, msize, cleared_requested);
} else if (size <= szone->large_threshold) {
//64位 <= 128KB 32位 <= 128KB
// small size: <=15k (iOS), <=64k (large iOS), <=128k (macOS)
// think small
msize = SMALL_MSIZE_FOR_BYTES(size + SMALL_QUANTUM - 1);
if (!msize) {
msize = 1;
}
ptr = small_malloc_should_clear(&szone->small_rack, msize, cleared_requested);
} else {
// large: all other allocations
size_t num_kernel_pages = round_page_quanta(size) >> vm_page_quanta_shift;
if (num_kernel_pages == 0) { /* Overflowed */
ptr = 0;
} else {
ptr = large_malloc(szone, num_kernel_pages, 0, cleared_requested);
}
}
#if DEBUG_MALLOC
if (LOG(szone, ptr)) {
malloc_report(ASL_LEVEL_INFO, "szone_malloc returned %p\n", ptr);
}
#endif
/*
* If requested, scribble on allocated memory.
*/
if ((szone->debug_flags & MALLOC_DO_SCRIBBLE) && ptr && !cleared_requested && size) {
memset(ptr, SCRIBBLE_BYTE, szone_size(szone, ptr));
}
return ptr;
}
这里以看出在 szone
上分配的内存包括 tiny
、small
和large
三大类,我们以 tiny为例
开始下面的分析
2.12 tiny_malloc_should_clear
分析
void *
tiny_malloc_should_clear(rack_t *rack, msize_t msize, boolean_t cleared_requested)
{
void *ptr;
mag_index_t mag_index = tiny_mag_get_thread_index() % rack->num_magazines;
//获取magazine. magazines 是一个由64个magazine_t组成的数组
magazine_t *tiny_mag_ptr = &(rack->magazines[mag_index]);
MALLOC_TRACE(TRACE_tiny_malloc, (uintptr_t)rack, TINY_BYTES_FOR_MSIZE(msize), (uintptr_t)tiny_mag_ptr, cleared_requested);
#if DEBUG_MALLOC
if (DEPOT_MAGAZINE_INDEX == mag_index) {
malloc_zone_error(rack->debug_flags, true, "malloc called for magazine index -1\n");
return (NULL);
}
if (!msize) {
malloc_zone_error(rack->debug_flags, true, "invariant broken (!msize) in allocation (region)\n");
return (NULL);
}
#endif
SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);
#if CONFIG_TINY_CACHE
ptr = tiny_mag_ptr->mag_last_free;
//如果开启了tiny 的缓存。
if (tiny_mag_ptr->mag_last_free_msize == msize) {
// we have a winner
//优先查看上次最后释放的区块是否和此次请求的大小刚好相等(都是对齐之后的slot大小),如果是则直接返回。
tiny_mag_ptr->mag_last_free = NULL;
tiny_mag_ptr->mag_last_free_msize = 0;
tiny_mag_ptr->mag_last_free_rgn = NULL;
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
}
#if DEBUG_MALLOC
if (LOG(szone, ptr)) {
malloc_report(ASL_LEVEL_INFO, "in tiny_malloc_should_clear(), tiny cache ptr=%p, msize=%d\n", ptr, msize);
}
#endif
return ptr;
}
#endif /* CONFIG_TINY_CACHE */
while (1) {
//先从freelist 查找
ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
if (ptr) {
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
}
return ptr;
}
//从一个后备magazine中取出一个可用region,完整地拿过来放到当前magazine,再走一遍上面的步骤。
if (tiny_get_region_from_depot(rack, tiny_mag_ptr, mag_index, msize)) {
//再次尝试从freelist 中获取
ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
if (ptr) {
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
if (cleared_requested) {
memset(ptr, 0, TINY_BYTES_FOR_MSIZE(msize));
}
return ptr;
}
}
// The magazine is exhausted. A new region (heap) must be allocated to satisfy this call to malloc().
// The allocation, an mmap() system call, will be performed outside the magazine spin locks by the first
// thread that suffers the exhaustion. That thread sets "alloc_underway" and enters a critical section.
// Threads arriving here later are excluded from the critical section, yield the CPU, and then retry the
// allocation. After some time the magazine is resupplied, the original thread leaves with its allocation,
// and retry-ing threads succeed in the code just above.
if (!tiny_mag_ptr->alloc_underway) {
//如果没有正在申请新的的 regin 操作,则进行申请操作
void *fresh_region;
// time to create a new region (do this outside the magazine lock)
//设置当前正在申请新的 堆
tiny_mag_ptr->alloc_underway = TRUE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
//申请新的堆 1m
fresh_region = mvm_allocate_pages_securely(TINY_REGION_SIZE, TINY_BLOCKS_ALIGN, VM_MEMORY_MALLOC_TINY, rack->debug_flags);
SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);
// DTrace USDT Probe
MAGMALLOC_ALLOCREGION(TINY_SZONE_FROM_RACK(rack), (int)mag_index, fresh_region, TINY_REGION_SIZE);
if (!fresh_region) { // out of memory!
tiny_mag_ptr->alloc_underway = FALSE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
return NULL;
}
//从最近的一个 region 或者新申请的 region中malloc
ptr = tiny_malloc_from_region_no_lock(rack, tiny_mag_ptr, mag_index, msize, fresh_region);
// we don't clear because this freshly allocated space is pristine
tiny_mag_ptr->alloc_underway = FALSE;
OSMemoryBarrier();
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
CHECK(szone, __PRETTY_FUNCTION__);
return ptr;
} else {
SZONE_MAGAZINE_PTR_UNLOCK(tiny_mag_ptr);
yield();
SZONE_MAGAZINE_PTR_LOCK(tiny_mag_ptr);
}
}
/* NOTREACHED */
}
获取
magazine
.magazines
是一个由64个magazine_t
组成的数组如果开启了
tiny
的缓存优先查看上次最后释放的区块是否和此次请求的大小刚好相等(都是对齐之后的
slot大小
),如果是则直接返回。ptr = tiny_malloc_from_free_list(rack, tiny_mag_ptr, mag_index, msize);
先从freelist
查找从一个后备
magazine
中取出一个可用region
,完整地拿过来放到当前magazine
,再走一遍上面的步骤。void *fresh_region;
如果没有正在申请新的的regin
操作,则进行申请操作tiny_mag_ptr->alloc_underway = TRUE;
设置当前正在申请新的 堆fresh_region = mvm_allocate_pages_securely(TINY_REGION_SIZE, TINY_BLOCKS_ALIGN, VM_MEMORY_MALLOC_TINY, rack->debug_flags);
申请新的堆 --- 1Mptr = tiny_malloc_from_region_no_lock(rack, tiny_mag_ptr, mag_index, msize, fresh_region);
从最近的一个region
或者新申请的region
中malloc
每次调用 free
函数,会直接把要释放的内存优先放到mag_last_free
指针上,在下次 alloc
时,也会优先检查mag_last_free
是否存在大小相等的内存,如果存在就直接返回。
2.14 tiny_malloc_from_free_list
& tiny_get_region_from_depot
分析
tiny_malloc_from_free_list
函数的作用是从free_list
中不断进行各种策略尝试。从上面的流程可以看出,在查找已经释放的内存缓存,会采用2步缓存查找(策略1,2),及两步备用内存的开辟(策略3,4)。
当
free_list
流程仍然找不到可以使用内存,就会使用tiny_get_region_from_depot
每一个类型的 rack
指向的 magazines
,都会在下标为-1 , magazine_t
当做备用:depot
,该方法的作用是从备用的 depot
查找出是否有满足条件的 region
如果存在,更新 depot
和 region
的关联关系,然后在关联当前的magazine_t
和 region
。之后在再次重复 free_list
过程
2.15 mvm_allocate_pages_securely
的分析
走到这一步,就需要申请新的
heap
了,这里需要理解虚拟内存和物理内存的映射关系。你其实只要记住两点:
vm_map
代表就是一个进程运行时候涉及的虚拟内存,pmap
代表的就是和具体硬件架构相关的物理内存。重新申请的核心函数为
mach_vm_map
,其概念如图
2.16 tiny_malloc_from_region_no_lock
的分析
重新申请了新的内存 (region)
之后,挂载到当前的 magazine
下并分配内存。
这个方法的主要作用是把新申请的内存地址,转换为region
,并进行相关的关联。及更新对应的 magazine
。整个 scalable_zone
的结构体关系,及流程如下
2.17 nano_zone
总结
malloc
库会检查指针地址,如果没有问题,则以链表的形式将这些区块按大小存储起来。这些链表的头部放在 meta_data数组
中对应的 [mag][slot]
元素中。
其实从缓存获取空余内存和释放内存时都会对指向这篇内存区域的指针进行检查,如果有类似地址不对齐、未释放/多次释放、所属地址与预期的 mag、slot
不匹配等情况都会以报错结束。
2.18 scalable_zone
分析
首先检查指针指向地址是否有问题。
如果last free指针
上没有挂载内存区块,则放到last free
上。如果有
last free
,置换内存,并把last free
原有内存区块挂载到free list
上(在挂载的free list
前,会先根据region
位图检查前后区块是否能合并成更大区块,如果能会合并成一个)。合并后所在的
region
如果空闲字节超过一定条件,则将把此region
放到后备的magazine
中(-1)。如果整个
region
都是空的,则直接还给系统内核。
三、流程总结
四、拓展补充
malloc_zone_t
提供了一个模板类,或者理解为malloc_zone_t
提供一类接口(高度抽象了alloc一个对象所需要的特征),free
,calloc
等。由所有拓展的结构体来实现真正的目标函数。
同上对于上层
Objc
,提供了抽象接口(依赖倒置),这样就降低了调用者(Objc)
与实现模块间的耦合。