通过runtime
的源码跟踪alloc
,最终我们发现会调用libmalloc
中的calloc
方法,那么calloc
做了什么呢,让我们来一探究竟
前言
本文将从runtime
的alloc
的最后流程讲起,为了方便回忆,先放一张runtime
创建实例的流程图。。
重上面的图片我们看出 runtime
的最终会调用calloc
,而calloc
方法被定义在libmalloc中。
本文使用的libmalloc 源码版本为 libmalloc-166.251.2 版本。
malloc_zone_t
先来看一个十分重要的机构体
1 | typedef struct _malloc_zone_t { |
malloc_zone_t
是一个非常基础结构,里面包含一堆函数指针,用来存储一堆相关的处理函数的具体实现的地址,例如malloc、free、realloc等函数的具体实现。后续会基于malloc_zone_t
进行扩展。
calloc
在runtime
的alloc
阶段的最后调用的calloc
函数实现如下。
1 | void * |
default_zone 引导
这个default_zone
其实是一个“假的”zone,同时它也是malloc_zone_t
类型。它存在的目的就是要引导程序进入一个创建真正的zone 的流程。下面来看一下default_zone的引导流程。
1 | void * |
defaultzone 的定义
1 | static virtual_default_zone_t virtual_default_zone |
从上面的结构可以看出 defaultzone->calloc
实际的函数实现为default_zone_calloc
。
1 | static void * |
zone
在创建正在的zone时,其实系统是有对应的一套创建策略的。在跟踪runtime_default_zone
方法后,最终会进入如下调用
1 | static void |
在这里 会存在两种zone
- nanozone_t
- scalable_zone
nanozone_t
1 |
|
nanozone_t
同样是malloc_zone_t
类型。在nano_create_zone
函数内部会完成对calloc
等函数的重新赋值。
nano_create_zone
1 | malloc_zone_t * |
nano_calloc
过程参考defaultzone
。回到上面default_zone_calloc
函数内。下一步就是使用nanozone_t
调用calloc
。
下面是nano_calloc
的实现
1 | static void * |
_nano_malloc_check_clear
这里我们也可以看出使用nanozone_t
的限制为不超过256B。继续看_nano_malloc_check_clear
1 |
|
该方法主要是通过cpu与slot确定index,从chained_block_s
链表中找出是否存在已经释放过的缓存。如果存在则进行指针检查之后返回,否则进入查询meta data
或者开辟band。
segregated_next_block
1 | static MALLOC_INLINE void * |
如果是第一次调用segregated_next_block
函数,band不存在,缓存也不会存在,所以会调用segregated_band_grow
。来开辟新的band
segregated_band_grow
1 | boolean_t |
当进入segregated_band_grow
时,如果当前的band不够用,则使用 mach_vm_map
经由pmap
重新映射物理内存到虚拟内存。
关于通过nano_blk_addr_t
的联合体结构如下,其每个成员所占的bit位数已经写出。
1 | struct nano_blk_addr_s { |
结合下面的例子
在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
字段中。
流程如下
scalable zone(helper_zone)
在szone上分配的内存包括tiny、small和large三大类,其中tiny和small的分配、释放过程大致相同,large类型有自己的方式管理。同样会通过create_scalable_zone
来构造zone。
这里不在复述create_scalable_zone
,直接看内存的分配策略
szone_malloc_should_clear
1 | MALLOC_NOINLINE void * |
这里以看出在szone上分配的内存包括tiny、small和large三大类,我们以tiny为例
tiny_malloc_should_clear
1 | void * |
每次调用free
函数,会直接把要释放的内存优先放到mag_last_free
指针上,在下次alloc时,也会优先检查mag_last_free
是否存在大小相等的内存,如果存在就直接返回。
tiny_malloc_from_free_list
这个函数的作用是从 free_list中不断进行各种策略尝试。
1 | void * |
从上面的流程可以看出,在查找已经释放的内存缓存,会采用2步缓存查找(策略1,2),及两步备用内存的开辟(策略3,4)。
当free_list流程仍然找不到可以使用内存,就会使用tiny_get_region_from_depot
tiny_get_region_from_depot
1 | static boolean_t |
每一个类型的rack
指向的magazines
,都会在下标为-1magazine_t
当做备用:depot,该方法的作用是从备用的depot
查找出是否有满足条件的region
如果存在,更新depot
和region
的关联关系,然后在关联当前的magazine_t
和region
。之后在再次重复free_list
过程。
mvm_allocate_pages_securely
走到这一步,就需要申请新的heap了,这里需要理解虚拟内存和物理内存的映射关系。
你其实只要记住两点:vm_map
代表就是一个进程运行时候涉及的虚拟内存,pmap
代表的就是和具体硬件架构相关的物理内存。
重新申请的核心函数为mach_vm_map
,其概念如图
tiny_malloc_from_region_no_lock
重新申请了新的内存(region)之后,挂载到当前的magazine下并分配内存。
1 | static void * |
这个方法的主要作用是把新申请的内存地址,转换为region,并进行相关的关联。及更新对应的magazine。整个scalable_zone的结构体关系,及流程如下
free
对于free操作同样会选择对应的zone 进行
nano_zone
malloc库会检查指针地址,如果没有问题,则以链表的形式将这些区块按大小存储起来。这些链表的头部放在meta_data数组中对应的[mag][slot]元素中。
其实从缓存获取空余内存和释放内存时都会对指向这篇内存区域的指针进行检查,如果有类似地址不对齐、未释放/多次释放、所属地址与预期的mag、slot不匹配等情况都会以报错结束。
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)与实现模块间的耦合。