共计 20447 个字符,预计需要花费 52 分钟才能阅读完成。
一、Mach 虚拟内存
1.1 Mach 内存简介
iOS 零碎架构可分为内核驱动层(Kernel and Device Drivers Layer)、外围操作系统层(Core OS)、外围服务层(Core Services layer)、媒体层(Media layer 可触摸层 & 应用层(Cocoa&Application layer),内核驱动层就是咱们常常提到的 Darwin,Darwin 是苹果公司于 2000 年公布的一个开源操作系统,是由 XNU 和一些其余的 Darwin 库组成,XNU 是由苹果公司公布的操作系统内核,XNU 蕴含三局部:Mach、BSD、I/O Kit。
Mach 是一个由卡内基梅隆大学开发的计算机操作系统微内核,是 XNU 内核,是作为 UNIX 内核的代替,次要解决 UNIX 所有皆文件导致形象机制有余的问题,为古代操作系统做了进一步的形象工作。Mach 负责操作系统最根本的工作,包含过程和线程形象、处理器调度、过程间通信、音讯机制、虚拟内存治理、内存保护等。在 iOS 零碎架构中,内存治理是由在 Mach 层中进行的,BSD 只是对 Mach 接口进行了 POSIX 封装,不便用户态过程调用。
1.2 Mach 虚拟内存的特点
1.2.1 虚构段页式内存治理
页是内存治理的根本单位, 在 Intel 和 ARM 中,通常为 4K,罕用的查看虚拟内存的命令:hw.pagesize 查看默认页面大小; vm\_page\_free\_count: 以后闲暇的 RAM 页数;vm\_stat(1) – 从零碎范畴的角度提供无关虚拟内存的统计信息。
在 iOS ARM64 机型中 page size 是 16K,在 JetsamEvent 结尾的系统日志里 pageSize 代表以后设施物理内存页的大小。
1.2.2 iOS 零碎没有替换空间
手机自带的磁盘空间也很小,属于宝贵资源,同时跟桌面硬件比起来,手机的闪存 I/O 速度太慢,所以 iOS 零碎没有替换空间;对于 Mac 零碎,参考 Apple 官网文档 About the Virtual Memory System,Mac 上有替换空间有换页行为,也就是当物理内存不够了,就把不沉闷的内存页暂存到磁盘上,以此换取更多的内存空间。
1.2.3 内存压缩技术
内存压缩技术是从 OS X Mavericks (10.9) 开始引入的 (iOS 则是 iOS 7.0 开始),能够参考官网文档: OS X Mavericks Core Technology Overview, 在内存缓和时可能将最近应用过的内存占用压缩至原有大小的一半以下,并且可能在须要时解压复用。简略了解为零碎会在内存缓和的时候寻找 inactive memory pages 而后开始压缩,达到开释内存的成果,以 CPU 工夫来换取内存空间,NSPurgeableData 是应用该技术典型的数据结构。所以掂量内存指标肯定要记录 compressed 内存,另外还须要记录被压缩的 page 的信息。
1.2.4 内存报警
通过后面的内存压缩环节后,设施可用内存若仍处于危险状态,iOS 零碎须要各个 App 过程配合解决,会向各过程发送内存报警要求配合开释内存,具体来说,Mach 内核零碎的 vm\_pageout 守护程序会查问过程列表及其驻留页面数,向驻留页面数最高的过程发送 NOTE\_VM\_PRESSURE,被选中的过程会响应这个压力告诉,理论体现就是 APP 收到零碎的 didReceiveMemoryWarning 内存报警,开释局部内存以达到升高手机内存负载的指标。
在收到内存报警时,App 升高内存负载,能够在很大水平上避免出现 OOM,具体源码剖析见第三节。
1.2.5 Jetsam 机制
当过程不能通过开释内存缓解内存压力时,Jestam 机制开始染指,这是 iOS 实现的一个低内存清理的解决机制,也称为 MemoryStatus,这个机制有点相似于 Linux 的“Out-of-Memory”杀手,最后的目标就是杀掉耗费太多内存的过程,这个机制只有在 iOS 零碎有,在 Mac 零碎是没有的。零碎在强杀 App 前,会先做优先级判断,那么,这个优先级判断的根据是什么呢?
iOS 零碎内核里有一个数组,专门用于保护线程的优先级。这个优先级规定就是:内核线程的优先级是最高的,操作系统的优先级其次,App 的优先级排在最初,并且,前台 App 程序的优先级是高于后盾运行 App 的,线程应用优先级时,CPU 占用多的线程的优先级会被升高。
1.3 Mach 内存治理数据结构
Mach 虚拟内存这一层齐全以一种机器无关的形式来治理虚拟内存,这一层通过 vm_map、vm_map_entry、vm_objec 和 vm_page 四种要害的数据结构来治理虚拟内存。
第一、vm_map: 示意地址空间内的多个虚拟内存区域。每一个区域都由一个独立的条目 vm_map_entry 示意,这些条目通过一个双向链表 vm map links 保护,参考 XNU 开源代码(https://opensource.apple.com/…),代码门路:osftnk/vm/vm_map.h,咱们能够分明地看到 vm_map 构造。
第二、vm_map_entry: 这个数据结构向上承接了 vm_map,向下指向 vm_object,该数据结构有很多权限拜访标记位,任何一个 vm_map entry 都示意了虚拟内存中一块间断的区域,每一个这样的区域都能够通过指定的拜访爱护权限进行爱护,在 XNU 源代码门路 (osftnk/vm/vm_map.h) 可看到具体数据结构定义。
第三、vm_object: 这是一个外围数据结构,将后面介绍的 vm_map_entry 与理论内存相关联,该数据结构次要蕴含一个 vm_page 的链表、memory object 分页器、标记位(用来示意底层的内存状态如初始化、已创立、已就绪或 pageout 等状态)和一些计数器(援用计数、驻留计数和联动计数等),XNU 源代码门路:osfmk/vm/vm_object.h;
第四、vm_page: 重点蕴含 offset 偏移量和很多状态位:驻留内存、正在清理、替换出、加密、重写、和脏等,XNU 源代码门路(osftnk/vm/vm_page.h)。
1.4 Mach 内核提供的内存操作接口
XNU 内存治理的外围机制是虚拟内存治理,在 Mach 层中进行的,Mach 管制了分页器,并且提供了各种 vm_ 和 mach_vm_ 音讯接口。
Mach 内核是依照 page size 大小来调配的内存的,对于苹果的 arm64 机型来说的 page size 是 16K 大小,然而咱们通常在应用程序中在堆上申请内存的时候,单位都是字节,很显然内核提供的函数不适宜间接提供给下层应用,这儿存在一个 GAP, 在 iOS 零碎中 libsystem_malloc.dylib 就是用来补救 GAP 的。
libsystem_malloc.dylib 是 iOS 内核之外的一个内存库,开源地址:https://opensource.apple.com/…。当咱们 App 过程须要的创立新的对象时,如调用 [NSObject alloc],或开释对象调用 release 办法时,申请先会走到 libsystem_malloc.dylib 的 malloc() 和 free()函数,而后 libsystem_malloc 会向 iOS 的零碎内核发动内存申请或开释内存,具体来说就是调用操作系统 Mach 内核提供的内存调配接口去分配内存或开释内存,苹果操作系统 Mach 内核提供了如下内存操作的相干接口。
<section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”> 函数名 </span></section> | <section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”> 阐明 </span></section> |
<section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>mach_vm_allocate</span></section> | <section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>allocates “zero fill” memory in the specfied map</span></section> |
<section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>mach_vm_deallocate</span></section> | <section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>deallocates the specified range of addresses in the specified address map</span></section> |
<section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>mach_vm_protect</span></section> | <section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>sets the protection of the specified range in the specified map</span></section> |
<section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>mach_vm_map</span></section> | <section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>maps a memory object to a task’s address space</span></section> |
<section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>mach_vm_page_query</span></section> | <section style=”text-align: justify;margin-left: 8px;margin-right: 8px;”><span style=”font-size: 14px;”>query page infomation</span></section> |
libsystem\_malloc 就是通过 mach\_vm\_allocate 和 mach\_vm\_map 来申请 page size 整数倍大小的内存,而后缓存这些内存页,造成一个内存池。当 malloc 调用的时候,能够依据传入的 size 大小来利用不同的调配策略,从这些缓存的内存中,调配一个 size 大小的内存地址返回给下层调用,同时记录这次调配操作的元数据。当调用 free 的时候,能够依据调配的地址,找到元数据,进而回收调配的内存。
# 二、内存调配函数 alloc 源码剖析
为了理解内存调配底层原理,咱们从 alloc 函数源码剖析说起,下载 objc 开源库,而后从 iOS 做内存调配的函数[NSObject alloc] 开始一起剖析。
## 2.1 objc_rootAlloc 函数
`
+ (id)alloc {
return _objc_rootAlloc(self);
}
id _objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/checkNil/, true/allocWithZone/);
}`
调用函数 callAlloc,并传入两个值 checkNil 为 false 以及 allocWithZone 为 true。
## 2.2 callAlloc 函数
`
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
#if OBJC2
if (slowpath(checkNil && !cls)) return nil;
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
return _objc_rootAllocWithZone(cls, nil);
}
#endif
/ 省略 /
}`
首先_OBJC2_宏定义,代表 objc 的版本,当初编译器应用的都是 Objective-C2.0,进入 if 语句,slowpath(通知编译器,传入的条件后果为假的可能性很大),因为 objc_rootAlloc 传入的 checkNil 为 false,所以不会返回 nil,接着执行 fastpath(通知编译器,传入的条件后果为真的可能性很大), 这个判断就是去检测传入的这个类是否实现了 allocWithZone 办法, 如果没有实现进入下一个函数。
## 2.3 objc_rootAllocWithZone 函数
调用_class_createInstanceFromZone
`
NEVER_INLINE id _objc_rootAllocWithZone(Class cls, malloc_zone_t *zone __unused)
{
// allocWithZone under OBJC2 ignores the zone parameter
return _class_createInstanceFromZone(cls, 0, nil,
OBJECT_CONSTRUCT_CALL_BADALLOC);
}`
## 2.4 class_createInstanceFromZone 外围函数
`
static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone,
int construct_flags = OBJECT_CONSTRUCT_NONE,
bool cxxConstruct = true,
size_t *outAllocatedSize = nil)
{// 断言机制,避免类并发创立
ASSERT(cls->isRealized());
// 读取类的标记位,减速类对象的创立
bool hasCxxCtor = cxxConstruct && cls->hasCxxCtor();
bool hasCxxDtor = cls->hasCxxDtor();
bool fast = cls->canAllocNonpointer();
size_t size;
// 计算内存空间大小
size = cls->instanceSize(extraBytes);
if (outAllocatedSize) *outAllocatedSize = size;
id obj;
if (zone) {
obj = (id)malloc_zone_calloc((malloc_zone_t *)zone, 1, size);
} else {
obj = (id)calloc(1, size);
}
/ 省略 /
}`
咱们能够看到先调用 instanceSize 函数计算出创建对象须要的内存空间大小,而后再调用 malloc\_zone\_calloc 或者 calloc 去分配内存空间。
## 2.5 instanceSize 计算内存空间大小
`
inline size_t instanceSize(size_t extraBytes) const {
if (fastpath(cache.hasFastInstanceSize(extraBytes))) {
return cache.fastInstanceSize(extraBytes);
}
size_t size = alignedInstanceSize() + extraBytes;
if (size < 16) size = 16;
return size;
}
`
为了缩小计算工夫,先判断缓存是否有值,如果有先从缓存取值,否则须要进入计算逻辑,从下面的代码逻辑中咱们看到入参 extraBytes 值为 0,返回值就是 alignedInstanceSize,源码如下:
`
uint32_t alignedInstanceSize() {
return word_align(unalignedInstanceSize());
}
uint32_t unalignedInstanceSize() const {
ASSERT(isRealized());
return data()->ro()->instanceSize;
}`
咱们晓得 OC 类构造中,data 字段存储类相干信息,其中 ro 数据结构存储了以后类在编译期就曾经确定的属性、办法以及遵循的协定,所以从以后对象所属类的 ro 中获取 instanceSize 代表了调配对象所需内存空间。
`
# define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
return (x + WORD_MASK) & ~WORD_MASK;
}`
接下来调用 word\_align 做内存对齐操作,从上述源码能够发现类对象的创立是按 16 字节对齐,有余 16 字节的返回 16,这是 iOS 在堆上调配 OC 对象根本准则,都是以 16 倍数做调配。
## 2.6 malloc_zone_calloc 函数
malloc_zone_calloc 或者 calloc 去分配内存空间,这两个函数正是 libmalloc 中重要的内存调配函数,libmalloc 库中门路 src/malloc 能够看到源码
https://opensource.apple.com/…
`
void *
calloc(size_t num_items, size_t size)
{
return _malloc_zone_calloc(default_zone, num_items, size, MZ_POSIX);
}
MALLOC_NOINLINE
static void *
_malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size,
malloc_zone_options_t mzo)
{
MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);
void *ptr;
if (malloc_check_start) {
internal_check();
}
ptr = zone->calloc(zone, num_items, size);
if (os_unlikely(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);
if (os_unlikely(ptr == NULL)) {
malloc_set_errno_fast(mzo, ENOMEM);
}
return ptr;
}
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
return _malloc_zone_calloc(zone, num_items, size, MZ_NONE);
}`
# 三、内存报警源码剖析
## 3.1 总体流程图
## 3.2 系统启动初始化
mach 系统启动后先做一系列内核初始化工作,函数调用门路为 arm_init->machine_startup->kernel_bootstrap->kernel_bootstrap_thread,arm_init 函数,XNU 代码门路:/osfmk/arm/arm_init.c
`
void arm_init(boot_args *args)
{/ 省略 /
machine_startup(args);
}`
machine\_routines 函数,XNU 代码门路:/osfmk/arm/machine_routines.c
`
void
machine_startup(__unused boot_args * args)
{
machine_conf();
/*
* Kick off the kernel bootstrap.
*/
kernel_bootstrap();
/ NOTREACHED /
}`
kernel_bootstrap 函数,XNU 代码门路: /osfmk/kern/startup.c
`
void
kernel_bootstrap(void)
{
/*
* Create a kernel thread to execute the kernel bootstrap.
*/
kernel_bootstrap_log(“kernel_thread_create”);
result = kernel_thread_create((thread_continue_t)kernel_bootstrap_thread, NULL, MAXPRI_KERNEL, &thread);
/ 省略 /
}`
kernel\_bootstrap\_thread 函数,XNU 代码门路:/osfmk/kern/startup.c,其中 vm\_pageout 办法进行内存报警初始化,bsd\_init 办法进行 Jetsam 机制初始化。
`
static void
kernel_bootstrap_thread(void)
{
/ 省略 /
//Jetsam 机制初始化
bsd_init();
// 内存报警机制
vm_pageout();
}`
## 3.3 报警线程创立机会
系统启动时在实现内核初始化工作后,会调用 vm_pageout()办法,创立 vm_pageout 守护程序,在 vm_pageout 函数次要性能是治理页面替换的策略,判断哪些页面须要写回到磁盘,此外的一项性能就是通过 kernel_thread_start_priority 初始化内存报警线程,刚创立的 VM_pressure 线程设置为阻塞状态,期待唤醒,XNU 代码门路:osfmk/vm/vm_pageout.c。
`
void
vm_pageout(void)
{
/ 省略 /
result = kernel_thread_start_priority((thread_continue_t)vm_pressure_thread, NULL,
BASEPRI_DEFAULT,
&thread);
if (result != KERN_SUCCESS) {
panic(“vm_pressure_thread: create failed”);
}
thread_deallocate(thread);
/ 省略 /
}`
## 3.4 创立内存报警线程
创立内存报警线程,线程名称为 VM_pressure,XNU 代码门路:osfmk/vm/vm_pageout.c,具体实现如下所示:
`
#if VM_PRESSURE_EVENTS
void
vm_pressure_thread(void)
{
static boolean_t thread_initialized = FALSE;
if (thread_initialized == TRUE) {
vm_pageout_state.vm_pressure_thread_running = TRUE;
consider_vm_pressure_events();
vm_pageout_state.vm_pressure_thread_running = FALSE;
}
thread_set_thread_name(current_thread(), “VM_pressure”);
thread_initialized = TRUE;
assert_wait((event_t) &vm_pressure_thread, THREAD_UNINT);
thread_block((thread_continue_t)vm_pressure_thread);
}
#endif / VM_PRESSURE_EVENTS /`
## 3.5 唤醒报警线程
### 3.5.1 内存发生变化时调用
在手机的内存发生变化的时候就会调用 memorystatus_pages_update 函数,XNU 代码门路:bsd/kern/kern_memorystatus.c, 其中调用外围函数 vm_pressure_response,这是内存报警机制的外围模块。
`
#if VM_PRESSURE_EVENTS
void
vm_pressure_thread(void)
{
static boolean_t thread_initialized = FALSE;
if (thread_initialized == TRUE) {
vm_pageout_state.vm_pressure_thread_running = TRUE;
consider_vm_pressure_events();
vm_pageout_state.vm_pressure_thread_running = FALSE;
}
thread_set_thread_name(current_thread(), “VM_pressure”);
thread_initialized = TRUE;
assert_wait((event_t) &vm_pressure_thread, THREAD_UNINT);
thread_block((thread_continue_t)vm_pressure_thread);
}
#endif / VM_PRESSURE_EVENTS /`
### 3.5.2 确定新的内存状态值
在 vm_pressure_response 办法中,通过掂量内存指标来确定是否唤起内存报警线程,进而向各 APP 发送 didReceiveMemoryWarning,这是内存报警源码的外围模块,XNU 代码门路:osfmk/vm/vm_pageout.c。
`
void vm_pressure_response(void)
{
/ 省略 /
old_level = memorystatus_vm_pressure_level;
switch (memorystatus_vm_pressure_level) {
case kVMPressureNormal:
{
if (VM_PRESSURE_WARNING_TO_CRITICAL()) {
new_level = kVMPressureCritical;
} else if (VM_PRESSURE_NORMAL_TO_WARNING()) {
new_level = kVMPressureWarning;
}
break;
}
case kVMPressureWarning:
case kVMPressureUrgent:
{
if (VM_PRESSURE_WARNING_TO_NORMAL()) {
new_level = kVMPressureNormal;
} else if (VM_PRESSURE_WARNING_TO_CRITICAL()) {
new_level = kVMPressureCritical;
}
break;
}
case kVMPressureCritical:
{
if (VM_PRESSURE_WARNING_TO_NORMAL()) {
new_level = kVMPressureNormal;
} else if (VM_PRESSURE_CRITICAL_TO_WARNING()) {
new_level = kVMPressureWarning;
}
break;
}
default:
return;
}
if (new_level != -1) {
memorystatus_vm_pressure_level = (vm_pressure_level_t) new_level;
if ((memorystatus_vm_pressure_level != kVMPressureNormal) || (old_level != memorystatus_vm_pressure_level)) {
if (vm_pageout_state.vm_pressure_thread_running == FALSE) {
thread_wakeup(&vm_pressure_thread);
}
if (old_level != memorystatus_vm_pressure_level) {
thread_wakeup(&vm_pageout_state.vm_pressure_changed);
}
}
}
}`
memorystatus_vm_pressure_level 是全局变量,代表上一次内存状态,接下来依据其不同的值,调用如下办法确定新的内存状态值 new\_level,分如下四种状况。
第一、上次处于 kVMPressureNormal 状态,判断函数 VM_PRESSURE_WARNING_TO_CRITICAL()的值,若为 true 新内存值为 kVMPressureCritical,否则判断函数 VM_PRESSURE_NORMAL_TO_WARNING()的值,若为 true 新内存值为 kVMPressureWarning,否则为默认值 -1;
第二、上次处于 kVMPressureWarning、kVMPressureUrgent 状态,判断函数 VM_PRESSURE_WARNING_TO_NORMAL()的值,若为 true,新内存值为 kVMPressureNormal,否则判断函数 VM_PRESSURE_NORMAL_TO_WARNING()的值,若为 true 新内存值为 kVMPressureCritical,否则为默认值 -1;
第三、上次处于 kVMPressureCritical 状态,判断函数 VM_PRESSURE_WARNING_TO_NORMAL()的值,若为 true,新内存值为 kVMPressureNormal,否则判断函数 VM_PRESSURE_CRITICAL_TO_WARNING()的值,若为 true 新内存值为 kVMPressureWarning,否则为默认值 -1;
### 3.5.3 水位等级详情
在 XNU 代码门路:osfmk/vm/vm\_compressor.h,有如下宏定义
`
#define AVAILABLE_NON_COMPRESSED_MEMORY (vm_page_active_count + vm_page_inactive_count + vm_page_free_count + vm_page_speculative_count)
#define AVAILABLE_MEMORY (AVAILABLE_NON_COMPRESSED_MEMORY + VM_PAGE_COMPRESSOR_COUNT)
#define VM_PAGE_COMPRESSOR_COMPACT_THRESHOLD (((AVAILABLE_MEMORY) * 10) / (vm_compressor_minorcompact_threshold_divisor ? vm_compressor_minorcompact_threshold_divisor : 10))
#define VM_PAGE_COMPRESSOR_SWAP_THRESHOLD (((AVAILABLE_MEMORY) * 10) / (vm_compressor_majorcompact_threshold_divisor ? vm_compressor_majorcompact_threshold_divisor : 10))
#define VM_PAGE_COMPRESSOR_SWAP_UNTHROTTLE_THRESHOLD (((AVAILABLE_MEMORY) * 10) / (vm_compressor_unthrottle_threshold_divisor ? vm_compressor_unthrottle_threshold_divisor : 10))
#define VM_PAGE_COMPRESSOR_SWAP_RETHROTTLE_THRESHOLD (((AVAILABLE_MEMORY) * 11) / (vm_compressor_unthrottle_threshold_divisor ? vm_compressor_unthrottle_threshold_divisor : 11))
#define VM_PAGE_COMPRESSOR_SWAP_HAS_CAUGHTUP_THRESHOLD (((AVAILABLE_MEMORY) * 11) / (vm_compressor_catchup_threshold_divisor ? vm_compressor_catchup_threshold_divisor : 11))
#define VM_PAGE_COMPRESSOR_SWAP_CATCHUP_THRESHOLD (((AVAILABLE_MEMORY) * 10) / (vm_compressor_catchup_threshold_divisor ? vm_compressor_catchup_threshold_divisor : 10))
#define VM_PAGE_COMPRESSOR_HARD_THROTTLE_THRESHOLD (((AVAILABLE_MEMORY) * 9) / (vm_compressor_catchup_threshold_divisor ? vm_compressor_catchup_threshold_divisor : 9))`
在 XNU 代码门路:osfmk/vm/vm\_compressor.c,有如下赋值,对于 iOS 零碎,走 !XNU\_TARGET\_OS\_OSX 分支
`
#if !XNU_TARGET_OS_OSX
vm_compressor_minorcompact_threshold_divisor = 20;
vm_compressor_majorcompact_threshold_divisor = 30;
vm_compressor_unthrottle_threshold_divisor = 40;
vm_compressor_catchup_threshold_divisor = 60;
#else / !XNU_TARGET_OS_OSX /
/ 省略 /`
#### 3.5.3.1 VM_PRESSURE_WARNING_TO_CRITICAL
VM_PRESSURE_WARNING_TO_CRITICAL() 判断内存状态是否从报警到重大,XNU 代码门路:osfmk/vm/vm_pageout.c,在 iOS 零碎中,VM_CONFIG_COMPRESSOR_IS_ACTIVE 为 YES, 走 else 逻辑。
`
boolean_t VM_PRESSURE_WARNING_TO_CRITICAL(void)
{
if (!VM_CONFIG_COMPRESSOR_IS_ACTIVE) {
**
return FALSE;
} else {
return vm_compressor_low_on_space() || (AVAILABLE_NON_COMPRESSED_MEMORY < ((12 * VM_PAGE_COMPRESSOR_SWAP_UNTHROTTLE_THRESHOLD) / 10)) ? 1 : 0;
}
}`
通过后面的宏定义和赋值带入计算表达式,得出如下论断:非压缩可用内存小于总可用内存的 12/40。
#### 3.5.3.2 VM_PRESSURE_NORMAL_TO_WARNING
VM_PRESSURE_NORMAL_TO_WARNING()判断内存状态是否从失常到报警,代码门路:osfmk/vm/vm_pageout.c
`
boolean_t
VM_PRESSURE_NORMAL_TO_WARNING(void)
{
/ 省略 /
return (AVAILABLE_NON_COMPRESSED_MEMORY < VM_PAGE_COMPRESSOR_COMPACT_THRESHOLD) ? 1 : 0;
}`
同理,带入计算表达式,得出如下论断:非压缩可用内存小于总可用内存的 1 /2。
#### 3.5.3.3 VM_PRESSURE_WARNING_TO_NORMAL
VM_PRESSURE_WARNING_TO_NORMAL()判断内存状态是否从报警到失常
`
boolean_t
VM_PRESSURE_WARNING_TO_NORMAL(void)
{
/ 省略 /
return (AVAILABLE_NON_COMPRESSED_MEMORY > ((12 * VM_PAGE_COMPRESSOR_COMPACT_THRESHOLD) / 10)) ? 1 : 0;
}`
同理,带入计算表达式,得出如下论断: 非压缩可用内存大于总可用内存 (压缩 + 非压缩) 的 3 /5。
#### 3.5.3.4 VM_PRESSURE_CRITICAL_TO_WARNING
VM_PRESSURE_CRITICAL_TO_WARNING()判断内存状态是否从重大到报警
`
boolean_t
VM_PRESSURE_CRITICAL_TO_WARNING(void)
{
/ 省略 /
return (AVAILABLE_NON_COMPRESSED_MEMORY > ((14 * VM_PAGE_COMPRESSOR_SWAP_UNTHROTTLE_THRESHOLD) / 10)) ? 1 : 0;
}`
同理,带入计算表达式,得出如下论断: 非压缩可用内存大于总可用内存 (压缩 + 非压缩) 的 7 /20。
#### 3.5.4 判断是否唤起报警线程
如下两个条件满足一个就会唤起 vm_pressure_thread。
第一、新的内存状态值不等于 kVMPressureNormal。
第二、新的内存状态和老的内存状态不一样。
## 3.6 报警线程操作
### 3.6.1 memorystatus_update_vm_pressure 实现
从 3.3 节中咱们晓得内存报警线程唤醒后执行 consider_vm_pressure_events(),XNU 代码门路:/bsd/kern/kern_memorystatus_notify.c
`
void consider_vm_pressure_events(void)
{
vm_dispatch_memory_pressure();
}
static void vm_dispatch_memory_pressure(void)
{
memorystatus_update_vm_pressure(FALSE);
}`
最终会调用函数 memorystatus_update_vm_pressure,XNU 代码门路:/bsd/kern/kern_memorystatus_notify.c
`
kern_return_t
memorystatus_update_vm_pressure(boolean_t target_foreground_process)
{
/ 省略 /
if (level_snapshot != kVMPressureNormal) {
/*
* 是否处于上一个报警周期
* next_warning_notification_sent_at_ts 代表下一次发送报警告诉的最短时间
*/
level_snapshot = memorystatus_vm_pressure_level;
if (level_snapshot == kVMPressureWarning || level_snapshot == kVMPressureUrgent) {
if (next_warning_notification_sent_at_ts) {
/
* curr_ts 示意以后工夫,小于下一次发送报警告诉的最短时间
* 延后执行
*/
if (curr_ts < next_warning_notification_sent_at_ts) {
delay(INTER_NOTIFICATION_DELAY 4 / 1 sec */);
return KERN_SUCCESS;
}
// 下一次发送报警告诉的最短时间设置为零
next_warning_notification_sent_at_ts = 0;
memorystatus_klist_reset_all_for_level(kVMPressureWarning);
}
} else if (level_snapshot == kVMPressureCritical) {
/ 省略 /
}
}
while (1) {
level_snapshot = memorystatus_vm_pressure_level;
if (prev_level_snapshot > level_snapshot) {
/*prev_level_snapshot:示意上一一次的等级
* 上一次等级小于本次等级,启用滑动窗口逻辑
*/
if (smoothing_window_started == FALSE) {
smoothing_window_started = TRUE;
microuptime(&smoothing_window_start_tstamp);
}
/ 省略 /
}
prev_level_snapshot = level_snapshot;
smoothing_window_started = FALSE;
memorystatus_klist_lock();
// 从 task 列表里选取一个 task,筹备发动内存正告告诉
kn_max = vm_pressure_select_optimal_candidate_to_notify(&memorystatus_klist, level_snapshot, target_foreground_process);
// 没有获取能够发动正告的 task
if (kn_max == NULL) {
memorystatus_klist_unlock();
if (level_snapshot != kVMPressureNormal) {
// 延后告诉
if (level_snapshot == kVMPressureWarning || level_snapshot == kVMPressureUrgent) {
nanoseconds_to_absolutetime(WARNING_NOTIFICATION_RESTING_PERIOD * NSEC_PER_SEC, &curr_ts);
/ Next warning notification (if nothing changes) won’t be sent before…/
next_warning_notification_sent_at_ts = mach_absolute_time() + curr_ts;
}
if (level_snapshot == kVMPressureCritical) {
nanoseconds_to_absolutetime(CRITICAL_NOTIFICATION_RESTING_PERIOD * NSEC_PER_SEC, &curr_ts);
/ Next critical notification (if nothing changes) won’t be sent before…/
next_critical_notification_sent_at_ts = mach_absolute_time() + curr_ts;
}
}
return KERN_FAILURE;
}
// 获取选中过程信息
target_proc = knote_get_kq(kn_max)->kq_p;
target_pid = target_proc->p_pid;
task = (struct task *)(target_proc->task);
// 调用 is_knote_registered_modify_task_pressure_bits
// 告诉选中过程内存报警
if (level_snapshot != kVMPressureNormal) {
if (level_snapshot == kVMPressureWarning || level_snapshot == kVMPressureUrgent) {
if (is_knote_registered_modify_task_pressure_bits(kn_max, NOTE_MEMORYSTATUS_PRESSURE_WARN, task, 0, kVMPressureWarning) == TRUE) {
found_candidate = TRUE;
}
} else {
if (level_snapshot == kVMPressureCritical) {
if (is_knote_registered_modify_task_pressure_bits(kn_max, NOTE_MEMORYSTATUS_PRESSURE_CRITICAL, task, 0, kVMPressureCritical) == TRUE) {
found_candidate = TRUE;
}
}
}
} else {
if (kn_max->kn_sfflags & NOTE_MEMORYSTATUS_PRESSURE_NORMAL) {
task_clear_has_been_notified(task, kVMPressureWarning);
task_clear_has_been_notified(task, kVMPressureCritical);
found_candidate = TRUE;
}
}
if (found_candidate == FALSE) {
proc_rele(target_proc);
memorystatus_klist_unlock();
continue;
}
/ 省略 /
}
return KERN_SUCCESS;
}`
### 3.6.2 is_knote_registered_modify_task_pressure_bits 告诉线程报警
is_knote_registered_modify_task_pressure_bits 告诉线程报警,XNU 代码门路:/bsd/kern/kern_memorystatus_notify.c
`
static boolean_t
is_knote_registered_modify_task_pressure_bits(struct knote *kn_max, int knote_pressure_level, task_t task, vm_pressure_level_t pressure_level_to_clear, vm_pressure_level_t pressure_level_to_set)
{
if (kn_max->kn_sfflags & knote_pressure_level) {
if (pressure_level_to_clear && task_has_been_notified(task, pressure_level_to_clear) == TRUE) {
task_clear_has_been_notified(task, pressure_level_to_clear);
}
task_mark_has_been_notified(task, pressure_level_to_set);
return TRUE;
}
return FALSE;
}`
task_mark_has_been_notified,XNU 代码门路:/bsd/kern/task_policy.c
`
void task_mark_has_been_notified(task_t task, int pressurelevel)
{
if (task == NULL) {
return;
}
if (pressurelevel == kVMPressureWarning) {
task->low_mem_notified_warn = 1;
} else if (pressurelevel == kVMPressureCritical) {
task->low_mem_notified_critical = 1;
}
}`
# 四、总结
本文介绍了 Mach 虚拟内存的特点、内存治理的数据结构以及 Mach 内核提供的内存操作接口,同时对 OC 内存调配外围函数 alloc 做了源码剖析,此外对 iOS 端内存报警机制做了具体的源码剖析,对于 Jestam 机制和 libmalloc 源码在后续文章做具体介绍,敬请期待。
——END——
参考资料:
[1] objc 源码:https://opensource.apple.com/…
[2] libsystem_malloc.dylib 源码:https://opensource.apple.com/…
[3] XNU 源码:https://github.com/apple/darw…
[4]《深刻解析 Mac OS X & iOS 操作系统》
[5] Mach 内核介绍:
https://developer.apple.com/l…
[6] Mach 系统结构:
https://developer.apple.com/l…
[7] Mach 虚拟内存零碎:
https://developer.apple.com/l…
[8] Mach 内存替换空间:
https://images.apple.com/medi…
举荐浏览:
百度 APP iOS 端内存优化实际 - 内存管控计划
百度 APP iOS 端内存优化实际 - 大块内存监控计划