macOS 内核之内存占用信息

Oct 25, 2019 at 22:42:52

macOS 内核之内存占用信息

macOS 内核之 CPU 占用率信息 | 枫言枫语 一文我们分析了 iOS 和 macOS 获取 CPU 占用信息的方法和内核的实现,本篇我们来看看内存信息的实现。

一、iOS 获取自身 App 内存占用

照例先从 iOS 开始。iOS 由于系统限制,App 层面只能获取自身的内存信息,无法获取其他 App 的内存信息。所以我们先看如何获取自己 App 的内存信息。

系统接口使用很简单,参考滴滴开源的 DoraemonKit 的实现如下:

+ (NSInteger)useMemoryForApp{
    task_vm_info_data_t vmInfo;
    mach_msg_type_number_t count = TASK_VM_INFO_COUNT;
    kern_return_t kernelReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &vmInfo, &count);
    if(kernelReturn == KERN_SUCCESS)
    {
        int64_t memoryUsageInByte = (int64_t) vmInfo.phys_footprint;
        return memoryUsageInByte/1024/1024;
    }
    else
    {
        return -1;
    }
}

//设备总的内存

  • (NSInteger)totalMemoryForDevice{
    return [NSProcessInfo processInfo].physicalMemory/1024/1024;
    }

关键 API 还是 task_info(),取当前进程的信息,第一个参数为当前进程的 mach port(可参考上一篇讲过对这个 mach port 构造的实现),传入参数 TASK_VM_INFO 获取虚拟内存信息,后两个参数是返回值,传引用。

可以看到 task_vm_info_data_t 里的 phys_footprint 就是当前进程的内存占用,以 byte 为单位。腾讯开源的 Matrix亦使用一致的实现。

footprint 这个术语在 Apple 的文档里有曰过: Technical Note TN2434: Minimizing your app's Memory Footprint

有了当前进程的内存,再获取整个手机的内存,比一下就有当前进程的内存占用率了。获取手机的物理内存信息可以用 NSProcessInfo 的 API,如上面 DoraemonKit 的实现。也可以像腾讯的 Matrix 一样用 sysctl() 的接口:

+ (int)getSysInfo:(uint)typeSpecifier
{
    size_t size = sizeof(int);
    int results;
    int mib[2] = {CTL_HW, (int) typeSpecifier};
    sysctl(mib, 2, &results, &size, NULL, 0);
    return results;
}
  • (int)totalMemory
    {
    return [MatrixDeviceInfo getSysInfo:HW_PHYSMEM];
    }

1.1 task_info() 函数实现

kern_return_t
task_info(
    task_t          task,
    task_flavor_t       flavor,
    task_info_t     task_info_out,
    mach_msg_type_number_t  *task_info_count)

这个函数位于 osfmk/kern/task.c 内部实现并不复杂,大家可以直接看源码。

函数的第一个参数是用作内核与发起系统调用的进程做 IPC 通信的 mach port,第二个参数是获取信息的类型,函数里一顿 switch-case 猛如虎,剩下就是回传数据了。

我们看看 TASK_VM_INFO 的 case,这个case 和 TASK_VM_INFO_PURGEABLE 共享逻辑,后者会多一些 purgeable_ 开头的数据返回。

首先内核会判断调用方是内核进程还是用户进程,内核进程取内核的 map,用户进程去该进程的 map,并加锁。接着就是一顿 map 信息读取了。最后解锁。

// osfmk/kern/ledger.c
// 赋值
vm_info->phys_footprint =
                (mach_vm_size_t) get_task_phys_footprint(task);

// 取自 task_ledgers
uint64_t get_task_phys_footprint(task_t task)
{
kern_return_t ret;
ledger_amount_t credit, debit;

ret = ledger_get_entries(task->ledger, task_ledgers.phys_footprint, &credit, &debit);
if (KERN_SUCCESS == ret) {
    return (credit - debit);
}

return 0;

}

task_ledgers 是内核维护的对该进程的"账本",每次为该进程分配和释放内存页的时候就往账本上记录一笔,并且分了多个不同的种类。

// osfmk/kern/task.c
void
init_task_ledgers(void)

这个初始化函数里大概创建了 30 种不同类型的账本,phys_footprint 是其中一个。

// osfmk/i386/pmap.h
// osfmk/arm/pmap.h

// 增加操作,即分配内存,以页为单位
#define pmap_ledger_debit(p, e, a) ledger_debit((p)->ledger, e, a)

// 减少操作,即释放内存,以页为单位
#define pmap_ledger_credit(p, e, a) ledger_credit((p)->ledger, e, a)

每次内核为该进程分配和释放内存时就往上记录一笔,以此来追踪进程的内存占用。这里假设各位读者都已了解虚拟内存以及为何按内存页(Memory Page)来分配的相关知识,如果有疑问可 Google 之。

pmap Mach 内核用来管理内存的一整套系统,代码古老且复杂,一个函数动辄四、五百行。而且 pmap 对于不同的机器有不同的实现,代码中区分了 i386arm 两种实现。本人才疏学浅,一时半会也学不会,只能日后再做学习。不过通过以上代码追踪,我们可以知道为何在 iOS 中读取 phys_footprint 就能得到当前进程的内存占用。

1.2 task_vm_info_data_ 数据结构

task_vm_info_data_t 里除了 phys_footprint 还有很多别的东西,我们可以看看这个结构体的定义:

#define TASK_VM_INFO       22
#define TASK_VM_INFO_PURGEABLE 23

struct task_vm_info {
// 虚拟内存大小,以 byte 为单位
mach_vm_size_t virtual_size;
// Memory Region 个数
integer_t region_count;
// 内存分页大小
integer_t page_size;
// 实际物理内存大小,以 byte 为单位
mach_vm_size_t resident_size;
// _peak 记录峰值,写入时会作比较,比原来的大才会更新
mach_vm_size_t resident_size_peak;

// 带 _peak 的都是运行过程中记录峰值的
mach_vm_size_t  device;
mach_vm_size_t  device_peak;
mach_vm_size_t  internal;
mach_vm_size_t  internal_peak;
mach_vm_size_t  external;
mach_vm_size_t  external_peak;
mach_vm_size_t  reusable;
mach_vm_size_t  reusable_peak;
mach_vm_size_t  purgeable_volatile_pmap;
mach_vm_size_t  purgeable_volatile_resident;
mach_vm_size_t  purgeable_volatile_virtual;
mach_vm_size_t  compressed;
mach_vm_size_t  compressed_peak;
mach_vm_size_t  compressed_lifetime;

/* added for rev1 */
mach_vm_size_t  phys_footprint;

/* added for rev2 */
mach_vm_address_t   min_address;
mach_vm_address_t   max_address;

};
typedef struct task_vm_info task_vm_info_data_t;

二、iOS/Mac 上获取系统内存占用信息

在 macOS 上我们在终端运行 vm_stat 可以看到以下内存信息输出输出:

➜  darwin-xnu git:(master) vm_stat
Mach Virtual Memory Statistics: (page size of 4096 bytes)
Pages free:                              349761.
Pages active:                           1152796.
Pages inactive:                         1090213.
Pages speculative:                        22734.
Pages throttled:                              0.
Pages wired down:                        979685.
Pages purgeable:                         519551.
"Translation faults":                 300522536.
Pages copy-on-write:                   16414066.
Pages zero filled:                     94760760.
Pages reactivated:                      4424880.
Pages purged:                           4220936.
File-backed pages:                       480042.
Anonymous pages:                        1785701.
Pages stored in compressor:             2062437.
Pages occupied by compressor:            598535.
Decompressions:                         4489891.
Compressions:                          11890969.
Pageins:                                6923471.
Pageouts:                                 38335.
Swapins:                                  87588.
Swapouts:                                432061.

这个系统命令就是通过 host_statistics64() 获取的,代码可见这里。使用的是这个接口:

// osfmk/kern/host.c
kern_return_t
host_statistics64(host_t host, host_flavor_t flavor, host_info64_t info, mach_msg_type_number_t * count)

照例第一个参数填 mach_host_self(),用于跟内核 IPC。第二个参数是取的系统统计信息类型,我们要取内存,所以填 HOST_VM_INFO64。剩下两个就是返回的数据了。

返回的数据类型会 cast 成 vm_statistics64_t

// osfmk/mach/vm_statistics.h

/*

  • vm_statistics64
  • History:
  • rev0 - original structure.
  • rev1 - added purgable info (purgable_count and purges).
  • rev2 - added speculative_count.
  • ----
    
  • rev3 - changed name to vm_statistics64.
  •  changed some fields in structure to 64-bit on 
    
  •  arm, i386 and x86_64 architectures.
    
  • rev4 - require 64-bit alignment for efficient access
  •  in the kernel. No change to reported data.
    

*/

struct vm_statistics64 {
natural_t free_count; /* # 空闲内存页数量,没有被占用的 /
natural_t active_count; /
# 活跃内存页数量,正在使用或者最近被使用 /
natural_t inactive_count; /
# 非活跃内存页数量,有数据,但是最近没有被使用过,下一个可能就要干掉他 /
natural_t wire_count; /
# 系统占用的内存页,不可被换出的 /
uint64_t zero_fill_count; /
# Filled with Zero Page 的页数 /
uint64_t reactivations; /
# 重新激活的页数 inactive to active /
uint64_t pageins; /
# 换入,写入内存 /
uint64_t pageouts; /
# 换出,写入磁盘 /
uint64_t faults; /
# Page fault 次数 /
uint64_t cow_faults; /
# of copy-on-writes /
uint64_t lookups; /
object cache lookups /
uint64_t hits; /
object cache hits /
uint64_t purges; /
# of pages purged /
natural_t purgeable_count; /
# of pages purgeable /
/

* NB: speculative pages are already accounted for in "free_count",
* so "speculative_count" is the number of "free" pages that are
* used to hold data that was read speculatively from disk but
* haven't actually been used by anyone so far.
*
/
natural_t speculative_count; /
# of pages speculative */

/* added for rev1 */
uint64_t    decompressions;     /* # of pages decompressed */
uint64_t    compressions;       /* # of pages compressed */
uint64_t    swapins;        /* # of pages swapped in (via compression segments) */
uint64_t    swapouts;       /* # of pages swapped out (via compression segments) */
natural_t   compressor_page_count;  /* # 压缩过个内存 */
natural_t   throttled_count;    /* # of pages throttled */
natural_t   external_page_count;    /* # of pages that are file-backed (non-swap) mmap() 映射到磁盘文件的 */
natural_t   internal_page_count;    /* # of pages that are anonymous malloc() 分配的内存 */
uint64_t    total_uncompressed_pages_in_compressor; /* # of pages (uncompressed) held within the compressor. */

} attribute((aligned(8)));

typedef struct vm_statistics64 *vm_statistics64_t;
typedef struct vm_statistics64 vm_statistics64_data_t;

Page Fault 中文翻译为缺页错误之类,其实就是要访问的内存分页已经在虚拟内存里,但是还没加载到物理内存。这时候如果访问合法就从磁盘加载到物理内存,如果不合法(访问 nullptr 之类)就 crash 这个进程。详细解释可以参考这里

Filled with Zero Page: 操作系统会维护一个 page,里面填满了 0,叫做 zero page。当一个新页被分配的时候,系统就往这个页里填 zero page。我的理解是相当于清空数据保护,防止其他进程读取旧数据吧。

空闲内存计算

speculative pages 是 OS X 10.5 引入的一个内核特性。内核先占用了这些 page,但是还没被真的使用,相当于预约。比如说当一个 App 在顺序读取硬盘数据的时候,内核发现它读完了 1, 2, 3 块, 那么很可能它会读 4。这时候内核先预约一块内存页准备给未来有可能会出现的 4。大概是这么个理解,可以参考这里的回答

在上面的注释中,speculative pages 是被计入 vm_stat.free_count 里的,所以 vm_stat 的实现里,空闲内存的计算减去了这一部分:

pstat((uint64_t) (vm_stat.free_count - vm_stat.speculative_count), 8);

以上我们就得到了系统内存信息了。不过通过 host_statistics64() 接口取到的数据加一起并不等于系统物理内存,这是由内核统计实现决定了,这里有一个讨论有兴趣可以看看

有了 active_count, speculative_countwired_count,我们就可以计算内存占用率了?还差一个 compressed

Memory Compression

内存压缩技术是从 OS X Mavericks (10.9) 开始引入的(iOS 则是 iOS 7 开始),可以参考官方文档:OS X Mavericks Core Technology Overview

简单理解为系统会在内存紧张的时候寻找 inactive memory pages 然后开始压缩,以 CPU 时间来换取内存空间。所以 compressed 也要算进使用中的内存。另外还需要记录被压缩的 page 的信息,记录在 compressor_page_count 里,这个也要算进来。

(active_count + wired_count + speculative_count + compressor_page_count) * page_size

这才是最终的系统内存占用情况,以 byte 为单位。这个接口 host_statistics() 在 iOS 亦适用。

Mac 上的 iStat Menus App 就是这样计算内存占用的,但是,Activity Monitor.app 却有点不同。留意到他的 Memory Used 有一项叫做 App Memory。这个是根据 internal_page_count 来计算的,所以 Activity Monitor.app 的计算是这样的:

(internal_page_count + wired_count + compressor_page_count) * page_size

三、KSCrash 的 usableMemory 函数

KSCrash 是一个开源的 Crash 堆栈信息捕捉库,里面有两个关于内存的函数:

static uint64_t freeMemory(void)
{
    vm_statistics_data_t vmStats = {};
    vm_size_t pageSize = 0;
    if(VMStats(&vmStats, &pageSize))
    {
        return ((uint64_t)pageSize) * vmStats.free_count;
    }
    return 0;
}

static uint64_t usableMemory(void)
{
vm_statistics_data_t vmStats = {};
vm_size_t pageSize = 0;
if(VMStats(&vmStats, &pageSize))
{
return ((uint64_t)pageSize) * (vmStats.active_count +
vmStats.inactive_count +
vmStats.wire_count +
vmStats.free_count);
}
return 0;
}

freeMemory() 是直接返回的 free_countusableMemory() 则是 active_count + inactive_count + wire_count + free_count

根据这两个函数的实现我猜测 freeMemory() 是想表达当前空闲内存的意思,usableMemory() 则是整个系统一共可以使用的内存有多少。

理论上 usableMemory 可以用硬件信息代替,但实际上系统接口返回的数据加一起一般都比物理内存少。使用这种方式计算我猜可能也是想获得更准备的系统实际可用内存吧。

但是根据上文我们已经知道,free_count 还包含了 speculative_count,最好去掉。并且 iOS 7 开始还加入了 memory compression,所以还得加上这个。

KSCrash 用的接口是 host_statistics(),这个接口没有返回 compression 相关的信息,猜测应该是这个项目开始的时候还没有 host_statistics64() 接口,或者当时 iPhone 的 64 位机器还不够普及(iPhone 5s 开始有 64 位机器)。

不过我自己实践了一下,即使用 host_statistics64() 接口,加上 compressionscompressor_page_count 之后的结果和不加的结果差不多。也有可能当时我的手机并没有使用大量内存所以压缩效果不明显就是。

mem: 2712944640
mem2: 2712961024

四、iOS 和 Mac 的差异

4.1 Mac 有 page out 但是 iOS 没有

参考 Apple 官方文档 About the Virtual Memory System,Mac 上会有换页行为,也就是当物理内存不够了,就把不活跃的内存页暂存到磁盘上,以此换取更多的内存空间。

具体的步骤是:

  1. 如果 active list 上的内存页最近没人访问过,就丢进 inactive list 里
  2. 如果一个在 inactive list 上的内存页最近没人访问,那就找到这个页的 VM object
  3. 如果这个 VM object 从来没被 paged (换入或者换出过),就创建一个默认的 pager 给他
  4. 然后用这个默认的 pager 尝试把它换出(page out)
  5. 如果换出成功那就释放该页占用的物理内存,然后把该页从 inactive list 放进 free list

但是在 iOS 上,系统不会有 page out 行为。这大概是 Apple 当年把 Darwin 系统移植到手机上时遇到的最头痛的问题之一:没有 swap 空间。桌面操作系统发展了几十年,有非常成熟的硬件条件,但是手机并不是。手机自带的空间也很小,属于珍贵资源,同时跟桌面硬件比起来,手机的闪存 I/O 速度太慢。所以普遍手机的操作系统都没有设计 swap。

所以一旦空闲内存下降到边界,iOS 的内核就会把 inactive 且没有修改过的内存释放掉,而且还可能会给正在运行的 App 发出内存警告,让 App 及时释放内存不然就之间挂掉,也就是俗称的"爆内存"(OOM Out-of-Memory)。

4.2 iOS 有 jetsam 但是 Mac 没有

负责把 iOS App 干掉的杀手叫做 jetsam,这个东西在 Mac 上没有。

jetsam 如何判断要干掉哪些进程?

这篇 No pressure, Mon! Handling low memory conditions in iOS and Mavericks 和这篇 iOS内存abort(Jetsam) 原理探究 | SatanWoo 对于 jetsam 有些解析。不过 jetsam 相关的代码非常长,直接看的话是真的眼花缭乱。

看完这两篇文章之后我发现几个地方不太清楚,所以还是自己去走了一遍,但是我从最终的 kill 那一步反推回去,读起来比从一开始看 memory status 一步步往下走要容易一些。所以有兴趣看这部分代码的朋友,建议也从 memorystatus_do_kill() 反推回去。

  1. 开机
  2. arm_init()
  3. kernel_bootstrap()
  4. machine_startup()
  5. kernel_bootstrap()
  6. kernel_bootstrap_thread()
  7. bsd_init()
  8. memorystatus_init()
  9. memorystatus_thread()
  10. memorystatus_act_aggressive()
  11. memorystatus_kill_top_process()
  12. memorystatus_kill_proc()
  13. memorystatus_do_kill()
  14. jetsam_do_kill()
  15. exit_with_reason()
  16. 对每个线程调用 thread_terminate()
  17. thread_terminate_internal()
  18. thread_apc_ast()
  19. thread_terminate_self()
  20. threadcnt == 0 时调用 proc_exit()

一共 20 层之多,内核代码果然年代久远。 XD

其中 #1-#8 都是初始化,memorystatus_init() 里面创建了多个(hardcoded 为 3 个)最高优先级的内核线程:

int max_jetsam_threads = JETSAM_THREADS_LIMIT;
#define JETSAM_THREADS_LIMIT   3

kernel_thread_start_priority(memorystatus_thread, NULL, 95 /* MAXPRI_KERNEL */, &jetsam_threads[i].thread);

以下条件命中时,会采取行动:

static boolean_t
memorystatus_action_needed(void)
{
#if CONFIG_EMBEDDED
    return (is_reason_thrashing(kill_under_pressure_cause) ||
            is_reason_zone_map_exhaustion(kill_under_pressure_cause) ||
           memorystatus_available_pages <= memorystatus_available_pages_pressure);
#else /* CONFIG_EMBEDDED */
    return (is_reason_thrashing(kill_under_pressure_cause) ||
            is_reason_zone_map_exhaustion(kill_under_pressure_cause));
#endif /* CONFIG_EMBEDDED */
}

thrashing

kill_under_pressure_causethrashing 的条件:

kMemorystatusKilledFCThrashing
kMemorystatusKilledVMCompressorThrashing
kMemorystatusKilledVMCompressorSpaceShortage

会在这里触发 compressor_needs_to_swap(void),当内存需要换页的时候,arm 架构的实现就会判断当前 vm compressor 状态然后抛出上述三种 cause 之一,按照我的理解应该是内存压缩都开始告急了。

ZoneMapExhaustion

kill_under_pressure_causezone_map_exhaustion 的条件:

kMemorystatusKilledZoneMapExhaustion

这种情况则是由 kill_process_in_largest_zone() 函数发起,如果能找到 alloc 了最大 zone 的一个进程就干掉他,不行就记录 cause,走 jetsam 流程。

memorystatus_available_pages <= memorystatus_available_pages_pressure

或者是可用内存页少于系统设定的阈值,这个阈值计算如下:

unsigned long pressure_threshold_percentage = 15;
unsigned long delta_percentage = 5;

memorystatus_delta = delta_percentage * atop_64(max_mem) / 100;
memorystatus_available_pages_pressure = (pressure_threshold_percentage / delta_percentage) * memorystatus_delta;

相当于 atop_64(max_mem) * 15 / 100 也就是最大内存的 15%。max_memarm_vm_init() 启动时传入的,应该就是硬件内存大小了。

jetsam 如何杀进程?

memorystatus_thread() 会先取一波原因:

/* Cause */
enum {
    kMemorystatusInvalid                            = JETSAM_REASON_INVALID,
    kMemorystatusKilled                             = JETSAM_REASON_GENERIC,
    kMemorystatusKilledHiwat                        = JETSAM_REASON_MEMORY_HIGHWATER,
    kMemorystatusKilledVnodes                       = JETSAM_REASON_VNODE,
    kMemorystatusKilledVMPageShortage               = JETSAM_REASON_MEMORY_VMPAGESHORTAGE,
    kMemorystatusKilledProcThrashing                = JETSAM_REASON_MEMORY_PROCTHRASHING,
    kMemorystatusKilledFCThrashing                  = JETSAM_REASON_MEMORY_FCTHRASHING,
    kMemorystatusKilledPerProcessLimit              = JETSAM_REASON_MEMORY_PERPROCESSLIMIT,
    kMemorystatusKilledDiskSpaceShortage            = JETSAM_REASON_MEMORY_DISK_SPACE_SHORTAGE,
    kMemorystatusKilledIdleExit                     = JETSAM_REASON_MEMORY_IDLE_EXIT,
    kMemorystatusKilledZoneMapExhaustion            = JETSAM_REASON_ZONE_MAP_EXHAUSTION,
    kMemorystatusKilledVMCompressorThrashing        = JETSAM_REASON_MEMORY_VMCOMPRESSOR_THRASHING,
    kMemorystatusKilledVMCompressorSpaceShortage    = JETSAM_REASON_MEMORY_VMCOMPRESSOR_SPACE_SHORTAGE,
};

如果是上一节 memorystatus_action_needed() 里的原因则走 memorystatus_kill_hiwat_proc()hiwat 就是 high water。这时候不会立刻杀掉该进程,而是判断一下 phys_footprint 是否超过 memstat_memlimit,超过就干掉。

这一步如果成功杀掉了,那么这个循环就先结束,如果杀失败了,那就要开始愤怒模式了:

static boolean_t
memorystatus_act_aggressive(uint32_t cause, os_reason_t jetsam_reason, int *jld_idle_kills, boolean_t *corpse_list_purged, boolean_t *post_snapshot)

vm_pressure_thread 也会监控 VM Pressure,判断是否要杀进程。

memorystatus_pages_update() 会触发 vm pressure 检查,非常多地方会触发这个函数,已无力读下去。

不过最终大家都会会走 memorystatus_do_kill() 调用 jetsam_do_kill(),进入 exit_with_reason() 带一个 SIGKILL 信号。比较有意思是它的代码最末尾是:

/* Last thread to terminate will call proc_exit() */
    task_terminate_internal(task);
return(0);

我还以为是在 task_terminate_internal() 发了退出信号,但是并没有,这里面只是清理了 IPC 空间,map 之类的内核信息。注释说最后一个线程会调用 proc_exit(),原来是在这里调用的:

while (p->exit_thread != self) {
        if (sig_try_locked(p) <= 0) {
            proc_transend(p, 1);
            os_reason_free(exit_reason);
        if (get_threadtask(self) != task) {
            proc_unlock(p);
            return(0);
                    }
        proc_unlock(p);

        thread_terminate(self);
        if (!thread_can_terminate) {
            return 0;
        }

        thread_exception_return();
        /* NOTREACHED */
    }
    sig_lock_to_exit(p);
}

遍历所有线程,然后都调用 thread_terminate() 结束线程,这个函数的实现里面有判断 threadcnt == 0 时就调用 proc_exit(),这里面就会发送我们熟悉的 SIGKILL 信号然后退出进程了。

4.3 iOS 如何判断 OOM

但是这些信息内核却并没有抛给应用,所以应用也不知道自己 OOM 了。参考 Tencent/matrix 的实现,也只能用排除法。

if (info.isAppCrashed) {
    // 普通 crash 捕获框架能抓到的 crash
    s_rebootType = MatrixAppRebootTypeNormalCrash;
} else if (info.isAppQuitByUser) {
    // 用户主动关闭,来自 UIApplicationWillTerminateNotification
    s_rebootType = MatrixAppRebootTypeQuitByUser;
} else if (info.isAppQuitByExit) {
    // 利用 atexit() 注册回调
    s_rebootType = MatrixAppRebootTypeQuitByExit;
} else if (info.isAppWillSuspend || info.isAppBackgroundFetch) {
    // App 主动调用的,matrix 的注释曰: notify the app will suspend, help improve the detection of the plugins
    if (info.isAppSuspendKilled) {
        s_rebootType = MatrixAppRebootTypeAppSuspendCrash;
    } else {
        s_rebootType = MatrixAppRebootTypeAppSuspendOOM;
    }
} else if ([MatrixAppRebootAnalyzer isAppChange]) {
    // App 升级了
    s_rebootType = MatrixAppRebootTypeAPPVersionChange;
} else if ([MatrixAppRebootAnalyzer isOSChange]) {
    // 系统升级了
    s_rebootType = MatrixAppRebootTypeOSVersionChange;
} else if ([MatrixAppRebootAnalyzer isOSReboot]) {
    // 系统重启了
    s_rebootType = MatrixAppRebootTypeOSReboot;
} else if (info.isAppEnterBackground) {
    // 排除以上情况,剩下的就认为是 OOM,在后台就是后台 OOM
    s_rebootType = MatrixAppRebootTypeAppBackgroundOOM;
} else if (info.isAppEnterForeground) {
    // 在前台,判断下是否死锁
    if (info.isAppMainThreadBlocked) {
            // 死锁,来自 matrix 的卡顿监控,跟内存无关
        s_rebootType = MatrixAppRebootTypeAppForegroundDeadLoop;
        s_lastDumpFileName = info.dumpFileName;
    } else {
            // 前台 OOM
        s_rebootType = MatrixAppRebootTypeAppForegroundOOM;
        s_lastDumpFileName = @"";
    }
} else {
    s_rebootType = MatrixAppRebootTypeOtherReason;
}

五、小结

iOS/Mac 获取内存占用信息的接口比较简单,但是涉及的概念和实现却非常复杂和庞大,尤其是内核的实现,一个函数动不动就 500 行以上,如果没有配套的书籍讲解,阅读起来十分吃力。所以读这种类型的代码,还是找到关键函数往回推比较简单点。XDDD

P.S. 使用 kill -l 命令可以看到所有的 tty 信号。SIGHUP 是 1,SIGKILL 是 9。所以我们经常使用的 kill -9 <pid> 命令就是告诉该进程你被 Kill 了。

P.P.S. memorystatus_do_kill() 函数的参数叫做 victim_p XDDD

内核系列文章

参考资料