查看原文
其他

深入理解iOS Jetsam机制,助力提升Flotsam召回率

快手大前端技术 快手大前端技术 2023-12-11

导读


本文将深入介绍 iOS 系统中的 Jetsam 机制,探讨其原理和作用。Jetsam 机制负责在内存不足时终止进程以释放内存资源,了解 Jetsam 的工作原理和相关策略,可以帮助开发者优化应用程序的内存使用,提高 Flotsam 召回率。

Flotsam 是快手定义的指标,是指 iOS 上未被监控捕获到的前台异常退出,包括但不限于由于内存溢出引起的退出。与 Jetsam 不同,Flotsam 更多地关注那些零碎、尚未明确归因的异常退出。 Jetsam 的工作原理相关略,开发者来说,不仅有优化应用内存使,还能显著提高 Flotsam 召率,进一步提升应用的定性和用户体验。

全文共13430字,预计阅读时间30分钟。

#

背景介绍

OOM 是什么

Jetsam 是什么

Flotsam 是什么

异常退出类型分类

#

Jetsam 原理探究

概览

代码布局

源码解读

MemoryStatus 强杀类型

#

提升 Flotsam 召回率

分析 Jetsam系统日志

per-process-limit

vm-pageshortage

disk-space-shortage

vnode-limit

#

总结


一、背景介绍

OOM 是什么?

OOM 是 Out of Memory 的缩写,指的是内存耗尽。当设备上的可用内存不足以满足应用程序和系统进程的需求时,iOS会触发 Jetsam 机制,导致一些进程被终止,以释放内存并保持系统的稳定性。

Jetsam 机制终止进程的时候最终是通过发送 SIGKILL 异常信号来完成的,SIGKILL 信号不可以在当前进程被忽略或者被捕获,因此只能通过排除法监控 OOM 崩溃。详情可以查看 Facebook 的方案[1]

Jetsam是什么?

在iOS设备中,系统实现了一种内存管理机制,被称为 Jetsam 或者 Memorystatus,它是根据特定的内存压力情况来选择性地终止或冻结应用程序。Jetsam 是 iOS 内核中的一部分,负责监控和管理应用程序的内存使用情况。Jetsam 根据一些因素来决定终止或暂停应用程序,包括应用程序的内存占用、应用程序的状态(活动、后台、挂起等)、应用程序的优先级、系统的内存压力等。当系统内存不足时,Jetsam 将根据预定义的策略选择终止哪些应用程序以释放内存。Jetsam 机制的目的是确保系统的稳定性和性能,以在不影响用户体验的情况下管理设备内存的分配和释放。

Jetsam 原意是指:在船只处于危险状态时被抛出船外并被冲上岸的物品,或者是水手们抛上岸的物品。苹果以此来形象的表示杀掉消耗内存最多的进程并且抛弃(jettison)这些进程占用的内存页面的过程。苹果正在逐步采用 "Memorystatus" 这个专用名词,因此本文也会使用这个名词。

Flotsam是什么?

Flotsam 的含义是指 iOS上 所有其它没有监控到的前台异常退出。

由于iOS上的 OOM 是用排除法监控的,在之前的指标上,Flotsam 也是包含在 OOM 数据中的。想要快速定位解决问题,必须先准确定位问题的分类,所以我们重新定义了 OOM 指标 是只包含真正由内存溢出引起的异常退出,低内存异常退出我们重新起名归类到 Flotsam 中。

Flotsam 是参考苹果 Jetsam 的起名方式,本身含义是“废料、零碎物”,寓意是 我们暂时还没搞清楚的异常退出中的一堆零碎物,需要我们努力清理出来。

异常退出类型分类

在没有定义 Flotsam 指标之前,异常退出类型包括 Crash、Watchdog、OOM三种。定义 Flotsam 指标之后我们把 OOM 拆分成 OOM 和 Flotsam 两个指标,OOM 表示真正由于内存不足导致的系统强杀,Flotsam 表示不是由于内存导致的异常退出,但是由于 OOM 是排除法而上报为 OOM 类型的数据。

为了更好的归因和解决异常退出问题,提升应用稳定性进而提升用户体验,我们需要精准召回 Flotsam 的异常退出类型,完善监控并最终解决对应的问题。


二、Jetsam原理探究




概览

(以下内容主要翻译自苹果文档[2] ,修改了部分内容)

XNU 的 Memorystatus 子系统负责在某些资源极度短缺时恢复系统。目前,它监控以下资源:
  • memory
  • vnodes
  • compressor space
  • swap space
  • zone map VA
根据资源类型的不同,Memorystatus 子系统可能会采取多种行动。最常见的行动之一是杀死一个或多个进程以尝试恢复系统。除了监控系统级别的资源外,Memorystatus 代码还负责杀死超出其每个进程内存限制的进程。
Memorystatus 子系统包含代码以响应资源短缺并执行四种操作:
  • Kill Processes
  • Freeze Processes
  • Send warning notifications
  • Swap memory from apps
这些操作中的每一个都包含在此文件夹中各自的文档中。

代码布局

Memorystatus 代码位于 XNU 的 BSD 侧,包含以下 C 文件:

  • bsd/kern/kern_memorystatus_policy.c:包含决定何时执行哪些操作的策略代码。

  • bsd/kern/kern_memorystatus_freeze.c:冷冻器的实现。有关详细信息,请参见 doc/memorystatus/freezer.md。

  • bsd/kern/kern_memorystatus.c:包含实现 kill 和 swap 操作的机制代码。不应该包含任何策略(应该在 bsd/kern/kern_memorystatus_policy.c 中),但这是最近的重构,因此还需要进一步改进。

  • bsd/kern/kern_memorystatus_notify.c:包含发送内存压力通知的策略和机制。请参见 doc/memorystatus/notify.md。

还有以下头文件:

  • bsd/kern/kern_memorystatus_internal.h

  • bsd/sys/kern_memorystatus_notify.h

  • bsd/sys/kern_memorystatus_freeze.h

  • bsd/sys/kern_memorystatus.h


源码解读

Memorystatus 子系统围绕一个中央健康检查设计。所有健康检查中的字段都在memorystatus_system_health_t结构体中定义。有关结构定义,请参阅bsd/kern/kern_memorystatus_internal.h。
Memorystatus 子系统中的大多数监控和操作发生在 memorystatus_thread(bsd/kern/kern_memorystatus.c)中。但是,有一些同步操作会在其他线程上发生。有关特定杀死类型的更多文档,请参阅doc/memorystatus/kill.md。
每当 Memorystatus 线程被唤醒时,它会执行以下操作:
  1. 通过调用 memorystatus_health_check 来填写系统健康状态。

  2. 将此状态记录到操作系统日志中(如果我们处于启动早期,则记录到串行日志中)。

  3. 通过 memorystatus_is_system_healthy 检查系统是否健康。

  4. 如果系统不健康,则选择一项恢复操作并执行它。有关触发特定操作的条件,请参阅bsd/kern/kern_memorystatus_policy.c中的memorystatus_pick_action。请注意,即使系统在其他方面健康,如果特定资源不足,我们有时也会在健康系统上执行预防性操作。例如,即使系统在其他方面健康,如果可用页面少于15%,我们也会在超出软限制的进程上进行杀进程操作。(软限制达到阈值后不一定会强杀进程,提供了一定的灵活性,硬限制是一个严格的阈值,超过限制后 Jetsam 会立即终止进程。)

  5. 返回第1步,直到系统健康并且线程可以阻塞。

请注意,Memorystatus线程不会明确地检查唤醒它的原因。为了保持同步简单,每当检测到资源短缺时,Memorystatus线程都会被盲目唤醒,并进行全面的系统健康检查。
核心代码如下
// 类似于 RunLoop,大部分时间在等待,被唤醒后执行 memorystatus_thread_internalstatic void memorystatus_thread(void *param __unused, wait_result_t wr __unused) { jetsam_thread_state_t *jetsam_thread = jetsam_current_thread(); sched_cond_ack(&(jetsam_thread->jt_wakeup_cond)); while (1) { memorystatus_thread_internal(jetsam_thread); sched_cond_wait(&(jetsam_thread->jt_wakeup_cond), THREAD_UNINT, memorystatus_thread); }}

memorystatus_thread_internal 实现

/* * Main entrypoint for the memorystatus thread. * This thread is woken up when we're low on one of the following resources: * - available pages (free + filebacked) * - zone memory * - compressor space * * Or when thrashing is detected in the compressor or file cache. */static voidmemorystatus_thread_internal(jetsam_thread_state_t *jetsam_thread){ while (true) { bool killed; jetsam_thread->memory_reclaimed = 0; uint32_t cause = 0;
// pick an action,选择执行动作 memorystatus_action_t action = memorystatus_pick_action(jetsam_thread, &cause, highwater_remaining, suspended_swappable_apps_remaining, swappable_apps_remaining, &jetsam_thread->jld_idle_kills); if (action == MEMORYSTATUS_KILL_NONE) { break; }
if (cause == kMemorystatusKilledVMCompressorThrashing || cause == kMemorystatusKilledVMCompressorSpaceShortage) { memorystatus_log_info("memorystatus: jetsam cause=%u compression_ratio=%u\n", cause, vm_compression_ratio()); }
// 执行强杀 action killed = memorystatus_do_action(jetsam_thread, action, cause); // 计算回收了多少内存 total_memory_reclaimed += jetsam_thread->memory_reclaimed; // ... }
// 错误清理 if (jetsam_thread->errors) { memorystatus_clear_errors(); }
// 生成系统日志 if (jetsam_thread->post_snapshot) { memorystatus_post_snapshot(); }}

Picking an action

bsd/kern/kern_memorystatus_policy.c 中的 memorystatus_pick_action 负责选择 Memorystatus 线程将执行以恢复系统的操作。它根据系统健康状况执行此操作。
逻辑大致如下:
如果系统不健康,请参见 bsd/kern/kern_memorystatus_policy.c 中的 memorystatus_is_system_healthy,或者可用页面数低于 memorystatus_available_pages_pressure,执行 high watermark kills。一旦我们不再有 high watermark kills,检查我们是否应该进行 aggressive jetsam。如果不是,我们执行 MEMORYSTATUS_KILL_TOP_PROCESS 并根据系统不健康的原因选择特定的终止原因。
启用应用程序交换的系统添加了 MEMORYSTATUS_KILL_SWAPPABLE 和 MEMORYSTATUS_KILL_SWAPPABLE_SUSPENDED 操作。当系统处于压力或不健康状态并且我们发现我们的交换空间不足时,就会发生这些情况。如果交换空间不足,我们只会终止正在运行的可交换进程。
memorystatus_action 枚举值如下,可以重点关注 MEMORYSTATUS_KILL_HIWATER、MEMORYSTATUS_KILL_AGGRESSIVE 和 MEMORYSTATUS_KILL_TOP_PROCESS。
/* * The actions that the memorystatus thread can perform * when we're low on memory. * See memorystatus_pick_action to see when each action is deployed. */OS_CLOSED_ENUM(memorystatus_action, uint32_t, MEMORYSTATUS_KILL_HIWATER, // Kill 1 highwatermark process MEMORYSTATUS_KILL_AGGRESSIVE, // Do aggressive jetsam MEMORYSTATUS_KILL_TOP_PROCESS, // Kill based on jetsam priority MEMORYSTATUS_WAKE_SWAPPER, // Wake up the swap thread MEMORYSTATUS_PROCESS_SWAPIN_QUEUE, // Compact the swapin queue and move segments to the swapout queue MEMORYSTATUS_KILL_SUSPENDED_SWAPPABLE, // Kill a suspended swap-eligible processes based on jetsam priority MEMORYSTATUS_KILL_SWAPPABLE, // Kill a swap-eligible process (even if it's running) based on jetsam priority MEMORYSTATUS_KILL_NONE, // Do nothing);


Jetsam Bands

Memorystatus 子系统有 210 个优先级。系统中的每个进程(launchd 除外)都有一个 jetsam 优先级,数字越大越重要。

每个优先级都作为 TAILQ 链表进行跟踪。有一个全局数组 memstat_bucket,包含所有这些 TAILQ 列表。进程的优先级在 proc 结构中进行跟踪(参见 bsd/sys/proc_internal.h)。p_memstat_effective_priority 存储 proc 的当前 jetsam 优先级,p_memstat_list 存储 TAILQ 链接。所有列表都受 proc_list_mlock 保护。

#define JETSAM_PRIORITY_MAX 210#define MEMSTAT_BUCKET_COUNT (JETSAM_PRIORITY_MAX + 1)
typedef struct memstat_bucket { TAILQ_HEAD(, proc) list; int count; int relaunch_high_count;} memstat_bucket_t;
extern memstat_bucket_t memstat_bucket[MEMSTAT_BUCKET_COUNT];

许多 kill 类型按升序 jetsam 优先级级别杀死进程。有关更多详细信息,请参见 doc/memorystatus/kill.md。Jetsam band 要么由 RunningBoard(应用程序和 RunningBoard 管理的守护程序)断言,要么由 JetsamProperties 数据库中设置的 jetsam 优先级确定。

以下是一些 band 号码的参考值:

Band Number

Name

Description

0
JETSAM_PRIORITY_IDLE
Idle processes
30
JETSAM_PRIORITY_BACKGROUND
Docked apps on iOS. Some active daemons on other platforms.
40
JETSAM_PRIORITY_MAIL
Docked apps on watchOS. Some active daemons on other platforms.
75
JETSAM_PRIORITY_FREEZER
Suspended & frozen processes
100
JETSAM_PRIORITY_FOREGROUND
Foreground app processes

140

-

mediaserverd

160
JETSAM_PRIORITY_HOME

SpringBoard

180

JETSAM_PRIORITY_IMPORTANT

RunningBoard, watchdogd, thermalmonitord, etc..

190

JETSAM_PRIORITY_CRITICAL

CommCenter


内存监控

Memorystatus 依据 memorystatus_available_pages 指标做出大多数内存决策。该指标反映了 Memorystatus 认为可以快速释放的页面数量。该指标在 osfmk/vm/vm_page.h 中的 VM_CHECK_MEMORYSTATUS 宏中定义。

目前在非 macOS 系统中,它被定义为 pageable_external + free + secluded_over_target + purgeable。具体来说:

  • pageable_external:文件后备页数

  • free:空闲页面数量

  • secluded_over_target:(vm_page_secluded_count - vm_page_secluded_target)。该目标来自设备树 kern.secluded_mem_mb。Secluded memory 是专门为相机预留的一种特殊内存池,以使其能够在内存受限的系统上更快地启动。

  • purgeable:系统中可清除的易失性页面的数量。Purgeable memory 是一种 API,供客户端指定 VM 可以将一系列页面的内容视为易变的,并在压力下快速释放后备页面。有关 API,请参阅 osfmk/mach/vm_purgable.h。请注意,API 被意外导出时拼写错误(“purgable”而不是“purgeable”)。

由于我们在内存受到压力时清除 purgeable memory 并迅速修剪 secluded pool,因此可以将其近似为 free + file_backed。

每当分配、固定、释放页面时,都会调用 VM_CHECK_MEMORYSTATUS 宏。基本上,memorystatus_available_pages 应该始终准确到页面级别。在我们的更大内存系统上(尤其是 8GB 和 16GB 的 iPad),这可能有些过头。如果需要,它会调用 memorystatus_pages_update 实际更新 memorystatus_available_pages 并触发 Memorystatus 线程的盲目唤醒。memorystatus_pages_update 还负责唤醒 freezer 和内存压力通知线程。

本节列出了组成 Memorystatus 子系统的线程。下面将详细介绍每个线程。

Thread name
Main function
wake event
VM_memorystatus_1
memorystatus_thread
jt_wakeup_cond in jetsam_thread_state_t
VM_freezer
memorystatus_freeze_thread
memorystatus_freeze_wakeup
VM_pressure
vm_pressure_thread
vm_pressure_thread


VM_memorystatus_1

这是 jetsam 线程。它负责运行系统健康检查并执行大多数 jetsam kill 操作(有关 kill 操作的详细信息,请参见doc/memorystatus/kill.md)。

每当任何子系统确定我们的受监控资源不足时,都会通过调用 memorystatus_thread_wake 唤醒它。这个唤醒是盲目的,线程会立即进行健康检查以确定系统出了什么问题。
:实际上有三个 Memorystatus 线程:VM_memorystatus_1、VM_memorystatus_2 和 VM_memorystatus_3。但系统目前只使用 VM_memorystatus_1。苹果曾经尝试并行化 jetsam 以加快速度,但这个尝试失败了。其他线程目前只是 dead code。

VM_freezer

这是冻结线程。它负责在内存压力下冻结进程并在冻结器已满时降级进程。有关冻结器的详细信息,请参见[3]

该线程通过向memorystatus_freeze_wakeup全局变量发出thread_wakeup调用来唤醒。如果memorystatus_freeze_thread_should_run返回true,则在memorystatus_pages_update中执行此操作。也会在memorystatus_on_inactivity运行时执行。

唤醒后,冻结线程将调用memorystatus_pick_freeze_count_for_wakeup并尝试在阻塞之前冻结那么多进程。memorystatus_pick_freeze_count_for_wakeup在大多数平台上返回1。但是,如果启用了应用程序交换(M1及更高版本的iPad Pro),它将返回所有符合条件的band中的进程总数。


VM_pressure

这是memorystatus通知线程。它通过vm_pressure_response由pageout线程唤醒。vm_pressure_response也会在memorystatus_pages_update中调用。

当被唤醒时,它调用consider_vm_pressure_events,然后进入memorystatus_update_vm_pressure。该程序检查压力级别是否已更改,并发出内存压力通知。它还安排(schedules)线程调用以进行持续的压力kill。

在macOS上,该线程还执行空闲退出 kill。

Memorystatus强杀类型

下表列出了所有的 kill 原因、它们对应的 memorystatus_action_t、kill 上下文(kill 是否发生在 Memorystatus 线程上、从另一个线程同步等...),以及 kill 目标是一个进程 还是 kill 大于等于 jetsam bands 的所有进程直到系统恢复为止。
下面提供了有关每种 kill 类型的更多信息
Reason
memorystatus_action_t
context
Marches up jetsam bands?

highwater

MEMORYSTATUS_KILL_HIWATER
memorystatus_thread
Yes

vnode-limit

N/A

synchronously on thread that tries to allocate a vnode

Yes

vm-pageshortage

MEMORYSTATUS_KILL_TOP_PROCESS

memorystatus_thread

Yes

proc-thrashing

MEMORYSTATUS_KILL_AGGRESSIVE

memorystatus_thread

Yes

fc-thrashing

MEMORYSTATUS_KILL_TOP_PROCESS

memorystatus_thread

No

per-process-limit

N/A

thread that went over the process' memory limit

No

disk-space-shortage

N/A

thread that disabled the freezer

Yes

idle-exit

N/A

vm_pressure_thread

No

zone-map-exhaustion

MEMORYSTATUS_KILL_TOP_PROCESS

memorystatus_thread or thread in a zalloc

No

vm-compressor-thrashing

MEMORYSTATUS_KILL_TOP_PROCESS

memorystatus_thread

No

vm-compressor-space-shortage

MEMORYSTATUS_KILL_TOP_PROCESS

memorystatus_thread or thread in swapin

No

low-swap

MEMORYSTATUS_KILL_SUSPENDED_SWAPPABLE or MEMORYSTATUS_KILL_SWAPPABLE

memorystatus_thread

Yes

sustained-memory-pressure

N/A

vm_pressure_thread

No


highwater

这些是软限制终止。当可用页面数量低于 memorystatus_available_pages_pressure 时,memorystatus_thread 将执行这些 kill 操作。任何超出其软内存限制的进程都是可终止的。进程按照 jetsam 优先级升序的顺序被 kill 。

vnode-limit

当系统达到 vnode 限制并且 VFS 子系统无法回收任何 vnode 时,系统会按照 jetsam 优先级升序杀死进程以释放 vnode。这些 Kills 在试图获取 vnode 的线程上同步发生。

vm-pageshortage

系统遇到内存压力,需要释放后台或前台进程内存。可用页面数低于 memorystatus_available_pages_critical时, memorystatus_thread 将按优先级升序终止进程,直到可用页面高于 memorystatus_available_pages_critical。

proc-thrashing

这也被称为激进的 Jetsam。如果系统确定 idle band 仅包含虚假空闲守护程序,并且至少有 5 个守护程序在 idle band 中,系统将触发 proc thrashing jetsam。这些可以在尝试缓解  false idle 问题时杀死 foreground band 甚至高于 foreground band。

False idle daemons 是指在空闲 band 中的守护进程,它们在被终止后非常快地重新启动。这通常表明守护进程中存在编程错误(它们在执行工作时没有持有事务)。由于守护进程重新启动非常快,系统可能会陷入 jetsam 循环,其中守护进程在系统终止其他 false idle 守护进程时重新启动,这会独占两个内核。Proc thrashing jetsams 是解决此问题的最后尝试,希望通过终止与空闲守护进程交互的任何更高 band 进程来解决这个问题。

基于 launchd 提供的重启概率,可以确定 false idleness(虚假空闲)的情况。launchd 记录守护进程在 jetsam kill 和重启之间的持续时间。它通过 posix_spawnattr_set_jetsam_ttr_np 函数将最近的 10 个持续时间传递给 posix_spawn(2)。此函数将持续时间分为以下桶:

  1. 0 - 5 seconds

  2. 5 - 10 seconds

  3. > 10 seconds

如果大多数持续时间在 bucket 1 中,则将使用 POSIX_SPAWN_JETSAM_RELAUNCH_BEHAVIOR_HIGH 标记该守护进程,如果大多数持续时间在 bucket 2 中,则将使用 POSIX_SPAWN_JETSAM_RELAUNCH_BEHAVIOR_MEDIUM 标记该守护进程,如果大多数持续时间在 bucket 3 中,则将使用 POSIX_SPAWN_JETSAM_RELAUNCH_BEHAVIOR_LOW 标记该守护进程。具有 POSIX_SPAWN_JETSAM_RELAUNCH_BEHAVIOR_HIGH 的守护进程被认为是激进的 jetsam 算法中的 false idle 进程。

重启概率也会影响守护进程在 aging band 中的时间。当前,守护进程的 aging band 为 band 10。高重启概率的守护进程在 aging band 中获得 10 秒,中等重启概率授予 5 秒,低重启概率的守护进程只获得 2 秒。请参阅 bsd/kern/kern_memorystatus.c 中的 memorystatus_sysprocs_idle_time。

fc-thrashing

系统引起了过多的文件后备内存压力。具体来说,phantom cache 检测到了压力(基于系统调出和读回相同数据的速率),或者压缩器池中旧段(超过48小时)的数量超过了系统的限制,且压缩器没有用完空间或陷入了交换内存与磁盘之间的过度使用。

在这种情况下,memorystatus_thread将终止优先级最低的进程并重置 phantom cache 样本。

Phantom cache 是指在 iOS 系统中用于缓存文件的一种缓存机制。具体来说,iOS 系统会将经常访问的文件缓存到内存中,以便快速访问这些文件,提高系统性能。这些缓存被称为“phantom cache”,因为它们没有被显式地缓存到磁盘上,而是存储在系统内存中。当系统内存不足时,iOS 系统会通过释放这些 phantom cache 来腾出内存空间。
iOS 中的 phantom cache 可以缓存各种类型的文件,包括应用程序二进制文件、库文件、动态链接器、字体文件、图像文件等等。这些缓存的数据通常来自于系统框架,而不是用户应用程序。

per-process-limit

进程已经超过了其硬限制。进程将立即被终止。此操作发生在尝试分配新页面的线程上。具体来说,将页面插入进程的 pmap 中会增加该进程在其账本中的 phys_footprint。Memorystatus 基于从 launchd 传入的 JetsamProperties 的值设置 phys_footprint 账本字段的限制,并在超过限制时注册回调函数。

注意,当 phys_footprint 限制为软限制时,Memorystatus 也会注册该限制。在这种情况下,回调函数会执行模拟崩溃而不是针对每个进程的限制性 kill。这为在没有足够压力引起 highwatermark kill 的系统上超过其软限制的守护程序提供了崩溃报告。

disk-space-shortage

这只发生在带有 CONFIG_FREEZE 的平台上,目前只有 iOS 平台。当系统存储空间非常低时,CacheDelete 通过 CacheDeleteServices 设置 vm.freeze_enabled sysctl。执行此 sysctl 的线程终止每个冻结的进程,以便系统可以完全回收所有交换文件。由于冻结的进程可以在任何 <= foreground 的 band 中,系统扫描带 P_MEMSTAT_FROZEN 位设置的 proc。

有关实现,请参见 bsd/kern/kern_memorystatus_freeze.c 中的 kill_all_frozen_processes。

idle-exit

这些是 macOS 上的空闲守护进程 kill。当内存压力水平上升到高于正常水平时,内存状态通知线程调用 memorystatus_idle_exit_from_VM 来终止 1 个空闲守护进程。请注意,守护进程必须选择在 macOS 上强制退出。

zone-map-exhaustion

Zalloc 已用完 VA。如果 zone allocator 能够找到一个好的候选进程来终止,它会执行同步终止。如果不是,它会要求 memorystatus_thread 选择并终止一个进程。Memorystatus 将终止 jetsam 优先级最低的进程。

vm-compressor-thrashing

compressor 检测到系统在过去 10 毫秒内超过了特定的压缩和解压次数。memorystatus_thread 将终止具有最低 jetsam 优先级的进程并重置 compressor 抖动统计信息。

注意:这些阈值非常古老,可能无法很好地适应当前的硬件。根据 telemetry,这些 kill 非常罕见

vm-compressor-space-shortage

compressor 已经达到或接近段或压缩页的限制。请参阅 osfmk/vm/vm_compressor.c 中的 vm_compressor_low_on_spacememorystatus_thread 将按照递增的 jetsam 优先级顺序进行杀死进程,直到空间不足得到缓解。

如果 compressor 在交换段时达到其中一个限制,它将在执行 swapin 的线程上同步执行这些 kill 操作。这可能会在启用 freezer 的系统或应用程序交换时发生。

low-swap

系统在启用应用程序交换(app swap)的系统(当前为 M1 或更高版本的 iPad)上,系统无法分配更多交换文件(因为磁盘空间不足或达到静态交换文件限制)。Memorystatus 将以 jetsam 优先级升序终止符合交换条件的进程(应用程序联盟中的进程)。如果系统正在接近但尚未达到交换文件限制,系统将只 kill 处于 suspended 的应用程序。

sustained-memory-pressure

系统已经处于 kVMPressureWarning 级别,且持续时间大于等于 10 分钟,但尚未升级到 critical 级别。Memorystatus 通知线程会安排一个线程调用来执行这些 kill。系统只会杀死空闲进程,并且每次杀死之间会暂停 500 毫秒。如果系统杀死了整个空闲进程两次,但压力没有得到缓解,那么系统会放弃,因为压力来自高于空闲进程的进程。

很多系统服务(尤其是 dasd)在做工作之前会检查压力级别,因此系统无限期地处于警告级别并不好。


三、提升 Flotsam 召回率

前面的背景介绍里提到,我们把异常退出中的 OOM 指标拆分成了 OOM 和 Flotsam 两个指标,为什么要拆分呢?原因是为了精准召回避免误报。

OOM 是通过排除法来判断的,里面除了真正内存导致的 OOM 问题外,还包括 Watchdog 误判为OOM、磁盘空间不足被系统强杀、Mach Port Limit 超限被杀等多种异常退出类型,为了能够更准确的监控各种异常退出类型,我们需要调研各种异常退出类型的原理,把这些退出类型从 Flotsam 类型中拆分出去,建立更准确、更完善的异常退出监控。我们可以按照以下步骤,先通过概念转换,把 OOM 转换成 Flotsam,然后不断的提升 Flotsam 召回率,最终目标是 Flotsam 在异常退出类型中的占比降为0。

接下来重点介绍下 per-process-limit、vm-pageshortage、disk-space-shortage、vnode-limit 这几种强杀类型的原理和召回方案。在介绍之前我们先来了解下如何分析 Jetsam 系统日志。

分析 Jetsam 系统日志

Jetsam 系统日志可以从手机 设置 -> 隐私与安全性 -> 分析与改进 -> 分析数据 中导出,文件名格式为 JetsamEvent-日期.ips 。Jetsam 系统日志和崩溃系统日志不同,不包含应用程序中任何线程的堆栈回溯,采用 JSON 格式,包含设备上所有应用程序和系统进程的总体内存使用情况。

拿到一份Jetsam系统日志,分析过程如下:

1、搜索 "reason" (包含双引号,否则干扰项较多),查看被 Jetsam 强杀的进程,只有被强杀的进程才会包含 "reason“ 字段,可能存在一个或多个。

2、查看包含 "reason“ 字段的进程名,判断进程名是否为快手进程。

3、查看 "reason“ 字段的值,根据不同的强杀类型分析具体的原因。rpages * pageSize 表示进程已使用物理内存大小。

4、如果进程名为WK进程,则需要判断该进程是否由快手进程创建的,通过coalition是否一致判断。coalition表示联盟id,如果一致说明两个进程属于同一个进程联盟。

per-process-limit


 原理

per-process-limit 表示进程超出了系统对应用程序的内存限制,也就是我们经常提到的OOM(Out of Memory)异常退出类型,进程已使用物理内存大小可以通过 struct task_vm_info 中的 phys_footprint 字段获取。phys_footprint 是 macOS 和 iOS 上用于衡量进程的物理内存使用情况的指标,它代表了进程实际使用的物理内存大小。phys_footprint 的计算规则

进程初始化时时会创建 task ledgers,task ledgers 是内核维护的对该进程的“账本”,记录进程使用的各种资源,包括内存、CPU、磁盘写入等。task ledgers 初始化时注册回调 task_footprint_exceeded,当进程的已使用物理内存大小(phys_footprint)超过系统限制时执行 Jetsam 强杀的处理流程。核心代码如下

void init_task_ledgers(void) { // 注册回调 ledger_set_callback(t, task_ledgers.phys_footprint, task_footprint_exceeded, NULL, NULL);}
/* * Callback invoked when a task exceeds its physical footprint limit. */void task_footprint_exceeded(int warning, __unused const void *param0, __unused const void *param1) { memlimit_is_active = task_get_memlimit_is_active(task); memlimit_is_fatal = task_get_memlimit_is_fatal(task); memorystatus_on_ledger_footprint_exceeded(is_warning, memlimit_is_active, memlimit_is_fatal);}
/* * Callback invoked when allowable physical memory footprint exceeded * (dirty pages + IOKit mappings) * * This is invoked for both advisory, non-fatal per-task high watermarks, * as well as the fatal task memory limits. */void memorystatus_on_ledger_footprint_exceeded(boolean_t warning, boolean_t memlimit_is_active, boolean_t memlimit_is_fatal) { os_reason_t jetsam_reason = OS_REASON_NULL; /* * If this process has no high watermark or has a fatal task limit, then we have been invoked because the task * has violated either the system-wide per-task memory limit OR its own task limit. */ jetsam_reason = os_reason_create(OS_REASON_JETSAM, JETSAM_REASON_MEMORY_PERPROCESSLIMIT);
/* 强杀进程 */ if (memorystatus_kill_process_sync(proc_getpid(p), kMemorystatusKilledPerProcessLimit, jetsam_reason) != TRUE) { memorystatus_log_error("task_exceeded_footprint: failed to kill the current task (exiting?).\n"); }}

OOM阈值,即进程 footprint 内存上限

获取进程 footprint 内存上限有以下几种方式

1、线下测试,不断申请内存,记录发生OOM时申请的最大内存值

stackoverflow[4] 上有人整理过相关数据,可以作为参考,不过数据不全。

2、通过 JetsamEvent 日志计算内存限制值

分析 Jetsam 系统日志,查找崩溃原因时关注 per-process-limit 部分的 rpages。rpages 表示的是App 占用的内存页数量,pageSize 表示页大小,删减部分无关字段日志内容如下

{ "memoryStatus": { "pageSize": 16384 }, "largestProcess": "com_kwai_gif", "processes": [ { "uuid": "8f262087-6874-3bd9-8e02-50ba5d63ec18", "states": [ "frontmost" ], "rpages": 134272, "reason": "per-process-limit", "name": "com_kwai_gif", "lifetimeMax": 134272 } ]}

通过 pageSize * rpages / 1024 / 1024 =16384 * 134272 / 1024 / 1024 得到的值是 2098 MB,就可以计算出当前 App 的内存上限值 。

3、越狱机上通过 kern.max_task_pmem 获取

通过以下代码,编译后在越狱机上运行,即可打印进程的内存上限。

void get_max_task_mem(void) { int max_task_pmem = 0; /* MB */ size_t size = 0; /* * Determine a memory limit based on system having one or not. */ size = sizeof(max_task_pmem); (void)sysctlbyname("kern.max_task_pmem", &max_task_pmem, &size, NULL, 0); if (max_task_pmem <= 0) { max_task_pmem = 0; printf("kern.max_task_pmem failed, error: %s\n", strerror(errno)); } else { printf("kern.max_task_pmem success, max_task_pmem: %d MB\n", max_task_pmem); }}

执行结果

iPhone:/home root# ./JetsamDemokern.max_task_pmem success, max_task_pmem: 2098 MB

4、通过 available memory + used footprint 计算获取

iOS 13系统提供了新的 API os_proc_available_memory(),这个API的注释里有这样一句话

/** * The size returned is not representative of the total memory of the device, it * is the current dirty memory limit minus the dirty memory footprint used at the * time of the query*/

通过 os_proc_available_memory() + phys_footprint 就是当前进程的内存上限。

最佳实践

在 iOS 13.0以上的设备上,我们主要通过 os_proc_available_memory() + phys_footprint 来实时获取当前进程的 OOM 阈值。经过大量线上数据采集,我们对各机型的 OOM 阈值进行了精确的整理。该方案相比其他方案有如下优势:

  • 实时性和准确性高:由系统提供了当前进程的实时内存使用情况和系统的可用内存,因此它反映的 OOM 阈值是非常准确的。

  • 适用性好:iOS 13.0以上系统均适用。

  • 性能开销小:该API 的调用非常轻量,几乎不会对应用程序的性能产生影响。

  • 线上数据支持:不涉及私有API,可以线上开启,结合线上的大量数据,我们可以更加精确地确定每种机型的 OOM 阈值。

iOS 13.0 以下系统结合方法1线下测试整理的经验值,我们整理了所有机型的OOM阈值。

提升进程物理内存上限

苹果针对 iOS 15+提供了 Increase Memory Limit  机制,通过在 entitlement 增加 com.apple.developer.kernel.increased-memory-limit ,可以在支持的设备上使用更高的内存上限。经过线下测试和线上验证,iPhone 12 Pro/Pro Max、iPhone 13 Pro/Pro Max设备OOM阈值可以提升 1GB,iPhone XS及以上机型 提升 250 MB

召回策略

数据收集

通过定时器,实时获取应用的已使用物理内存 footprint 值,并持久化保存到本地,当发生OOM后下次启动上报。

召回标准

通过比较 footprint 值与设备的 OOM 阈值,当 footprint 值除以设备的 OOM 阈值大于特定阈值时,则认为发生了内存 OOM,表示成功召回。

上线效果

通过设置是否内存 OOM 标志,成功的区分了 Flotsam 和 OOM。

vm-pageshortage


 原理

vm-pageshortage 表示系统可用物理内存不足,开始按照 Jetsam 优先级清理后台进程甚至前台进程。每当分配、释放内存页面时,都会调用 VM_CHECK_MEMORYSTATUS 宏更新memorystatus_available_pages 。是否触发强杀取决于 available_pages 是否小于阈值。

#define VM_CHECK_MEMORYSTATUS do { \ memorystatus_pages_update( \ vm_page_pageable_external_count + \ vm_page_free_count + \ VM_PAGE_SECLUDED_COUNT_OVER_TARGET() + \ (VM_DYNAMIC_PAGING_ENABLED() ? 0 : vm_page_purgeable_count) \ ); \ } while(0)
void memorystatus_pages_update(unsigned int pages_avail) { memorystatus_available_pages = pages_avail; // 可用页面数}

可用内存页面数 memorystatus_available_pages = vm_page_pageable_external_count + vm_page_free_count + vm_page_secluded_count_over_target + vm_page_purgeable_count,当可用内存小于阈值后系统会调用 memorystatus_kill_on_VM_page_shortage,强杀进程回收内存。

boolean_t memorystatus_kill_on_VM_page_shortage() { os_reason_t jetsam_reason = os_reason_create(OS_REASON_JETSAM, JETSAM_REASON_MEMORY_VMPAGESHORTAGE); if (jetsam_reason == OS_REASON_NULL) { memorystatus_log_error("memorystatus_kill_on_VM_page_shortage -- sync: failed to allocate jetsam reason\n"); }
return memorystatus_kill_process_sync(-1, kMemorystatusKilledVMPageShortage, jetsam_reason);}


 阈值

通过分析 XNU 内核源码,可以找到系统内存不足时强杀进程的阈值。

// critical 强杀阈值(设备RAM <= 3G),可用内存 / 总内存 <= 5%unsigned long critical_threshold_percentage = 5;// critical 强杀阈值(设备RAM > 3G),可用内存 / 总内存 <= 4%unsigned long critical_threshold_percentage_larger_devices = 4;// idle 进程存在时,阈值 offset值unsigned long idle_offset_percentage = 5;// 内存压力阈值,可用内存 / 总内存 <= 15%unsigned long pressure_threshold_percentage = 15;

当 idle band 中有进程时,

memorystatus_available_pages_critical = memorystatus_available_pages_critical_base + memorystatus_available_pages_critical_idle_offset。

当 idle band 为空时,

memorystatus_available_pages_critical = memorystatus_available_pages_critical_base。实际上,这意味着在 available_pages < 10% 时杀死空闲进程,在 available_pages < 5% 时杀死所有其他进程。有一个例外,即对于内存大于等于 4GB 的设备,critical_base 是 4% 而不是 5%。

召回策略

数据收集

通过定时器,实时获取系统的可用物理内存大小,并持久化保存到本地,当发生OOM后下次启动上报,同时上报系统总内存大小。

召回标准

通过系统可用物理内存 / 系统总内存计算系统可用内存占比,考虑到定时获取系统可用物理内存大小有一定的滞后性,且针对 RAM 大小为 4G 的设备,系统可用物理内存低于 500 MB时已经处于一个很低的水位,因此当系统可用内存占比小于特定阈值时,我们认为发生了内存 OOM,表示成功召回。

上线效果

上述规则上线后 Flotsam 中 16.9%的数据成功召回为 OOM,高端机 OOM召回率提升 300%

在 OOM 召回和分析过程中,仅仅关注进程本身的内存使用情况是远远不够的。系统的整体内存状况,特别是剩余可用内存,同样至关重要。

disk-space-shortage

环境准备

由于 XNU 源码关于 CacheDelete 没有相关源码,网上关于 CacheDelete 的介绍也几乎是空白的,因此要了解 CacheDelete 的具体实现需要通过逆向手段在越狱机上进行深入的调研。

原理调研

进程冻结

正常情况下,iOS 应用程序进程主要有5种常见的状态。

(1)Not running。非运行状态,指应用程序 还没有被启动,或者已被系统终止。

(2)Inactive。前台非活动状态,指应用程序 即将进入前台状态,但当前未接收到任何事件 (可能正在执行其他代码)。应用程序通常只在 转换到其他状态时才会短暂地进入该状态。

(3)Active。前台活跃状态,指应用程序正 在前台运行,可接收事件并进行处理。这也是一 个iOS应用程序处于前台的正常模式。

(4)Background。进入后台状态,指应用程 序进入后台并可执行代码。大多数应用程序在被 挂起前都会短暂地进入该状态。

(5)Suspended。挂起状态,指应用程序进 入后台但没有执行任何代码,系统会自动地将应 用程序转移到该状态,并且在执行该操作前不会 通知应用程序。挂起时,应用程序会保留在内存 中,但不执行任何代码。当系统出现内存不足情 况时,系统可能会在未通知应用程序的情况下清 除被挂起的应用程序,为前台应用程序尽可能腾出更多的运行资源。

实际上,应用程序进程还有其他状态,比如冻结。冻结是指如果进程的内存占用非常小,当进程挂起(Suspended)时,系统会将进程脏页写入磁盘,而不是将它们留在内存中。在系统内,这个进程状态的术语就是“冻结”。这也说明了iOS 系统是存在磁盘交换(Swap)机制的,只是系统对这套机制的限制非常严格(严格限制生效的进程、严格控制进程冻结的预算等)。有关进程冻结的详细信息可以查看文档[5]

CacheDelete 调研

在越狱机上执行 ps aux | grep CacheDelete

从截图可以看出,CacheDelete 相关的进程存在多个,我们只需要重点关注其中的 CacheDelete 主进程 /System/Library/PrivateFrameworks/CacheDelete.framework/deleted 以及服务进程 /System/Library/CoreServices/CacheDeleteAppContainerCaches。

问了下 ChatGPT  CacheDelete 和 CacheDeleteAppContainerCaches 的相关概念,对这两个进程有一个初步的了解。

iOS CacheDelete 是一个操作系统中的私有框架(Private Framework),用于管理和处理缓存数据。该框架提供了一组功能和接口,用于识别、清理和管理设备上的缓存数据,以优化存储空间和提高设备性能。

CacheDelete框架通常由系统服务或应用程序使用,以执行以下操作:

1. 识别和分析设备上的缓存数据:CacheDelete框架可以扫描设备上的不同应用程序和系统组件生成的缓存数据,并提供信息和统计数据,用于识别哪些数据占用了大量的存储空间。

2. 清理和回收缓存数据:该框架提供了方法和接口,允许应用程序或系统服务根据特定的策略和规则清理缓存数据。这可以包括删除过期的、不再使用的或占用过多存储空间的缓存文件。

3. 优化存储空间和性能:通过清理不必要的缓存数据,CacheDelete框架有助于释放存储空间并提高设备的整体性能。它可以减少存储器压力,提高应用程序响应速度,并改善设备的整体用户体验。

CacheDeleteAppContainerCaches 通常关联于 iOS 的 CacheDelete 系统,它是iOS 系统用于管理和清除缓存数据的机制的一部分。

具体地,CacheDeleteAppContainerCaches 负责处理应用程序容器中的缓存数据。在 iOS 中,每个应用程序都运行在一个所谓的 "容器" 内,这是一个安全的、隔离的文件夹结构,其中包含应用程序的所有数据,包括它的缓存数据。随着时间的推移,这些缓存数据可能会增长并占用大量的存储空间,特别是对于那些频繁下载或处理大量数据的应用程序。

CacheDeleteAppContainerCaches 的主要职责是在存储空间受限或在其他特定情况下,清除这些应用程序容器中的缓存数据,从而为设备释放空间。这通常是一个安全的操作,因为缓存数据应该是可以重新生成的,也就是说,删除后不会导致数据丢失或应用程序功能受损。

总的来说,CacheDeleteAppContainerCaches 是 iOS 系统内部用于维护和释放存储空间的工具之一。

通过对 CacheDelete 主进程逆向分析发现,存在多个 LOW_DISK 相关的字符串,猜测和低磁盘阈值有关。

搜索 LOW_DISK_THRESHOL 引用点

通过对上述代码分析我们可以得出看出,首先调用了 CacheDeleteDaemonVolume 对象的 thresholds 方法,然后对这个对象调用 objectForKeyedSubscript: 方法,传入的 key 为 LOW_DISK_THRESHOL,因此我们猜测 thresholds 方法返回了一个字典 NSDictionary,  LOW_DISK_THRESHOL 的值可以通过这个字典动态获取,只要 hook 某个方法在方法内部把 thresholds 返回的字典值打印出来就可以确定低磁盘的阈值了。

可以通过以下代码,基于 Tweak 越狱开发 hook CacheDelete 进程,实现 HookCacheDelete 插件,动态打印低磁盘阈值。

/* How to Hook with LogosHooks are written with syntax similar to that of an Objective-C @implementation.You don't need to #include <substrate.h>, it will be done automatically, as willthe generation of a class list and an automatic constructor.*/
#import <SpringBoard/SpringBoard.h>#import <dlfcn.h>#import <objc/runtime.h>

%hook CacheDeleteDaemonVolume
- (CacheDeleteDaemonVolume *)initWithPath:(id)path isPrimary:(bool)isp { NSLog(@"[Flotsam Test] lowDisk low_disk hook CacheDeleteDaemonVolume init success ...");
id awesome = %orig;
if ([awesome respondsToSelector:@selector(thresholds)]) { id threshValue = [awesome performSelector:@selector(thresholds)]; // 打印 thresholds NSLog(@"[Flotsam Test] lowDisk low_disk hook CacheDeleteDaemonVolume thresholds = %@", threshValue); } return awesome;}
%end

// 初始化执行%ctor{ //NSLog(@"[Flotsam Test] lowDisk low_disk hook ctor success..."); %init(_ungrouped);}

CacheDelete 进程初始化时会初始化 CacheDeleteDaemonVolume 对象,hook CacheDeleteDaemonVolume 的初始化方法,在方法内部调用 thresholds 并打印。结果如下所示:

低磁盘阈值 LOW_DISK_THRESHOLD = 500 MB,同时可以看到一些其他级别的磁盘相关阈值, 包括 VERY_LOW_DISK_THRESHOLD,当设备的磁盘空间低于 VERY_LOW_DISK_THRESHOLD 也就是 150 MB 时,系统会弹窗提示用户。

"CACHE_DELETE_VOLUME_SIZE" = 63968497664; // 60 G,设备总磁盘空间"DESIRED_DISK_THRESHOLD" = 1283457024; // 1.2 G"LOW_DISK_THRESHOLD" = 524288000; // 500 MB"NEAR_LOW_DISK_THRESHOLD" = 1073741824; // 1 G"VERY_LOW_DISK_THRESHOLD" = 157286400; // 150 MB

CacheDeleteAppContainerCaches 调研

通过逆向分析 CacheDeleteAppContainerCaches 进程发现,存在多个 freeze 相关的字符串,包括 vm.freeze_enabled、Enabling app freezer、Disabling app freezer 等,因此可以确定执行 vm.freeze_enabled 系统调用是在 CacheDeleteAppContainerCaches 实现的。

通过 sysctlbyname 设置 vm.freeze_enabled  的代码流程如下

实现原理

至此,整个 disk-space-shortage 强杀的原理我们基本搞清楚了,CacheDelete 进程会实时监控剩余磁盘空间大小,当剩余磁盘空间大小低于阈值时,会通知 CacheDeleteAppContainerCaches 进程,CacheDeleteAppContainerCaches 通过 sysctlbyname 方法设置 vm.freeze_enabled 为 false,Jetsam 系统注册了 vm.freeze_enabled 访问和修改的回调,当 vm.freeze_enable 被置为 false 时,Jetsam 会禁用进程冻结功能,并杀掉所有已经被冻结的进程,以缓解系统磁盘空间不足的情况。

当系统剩余磁盘空间充足时,CacheDeleteAppContainerCaches 通过 sysctlbyname 方法设置 vm.freeze_enabled 为 true,Jetsam 系统会重新开启进程冻结功能。

大致流程如下图所示

线下手动触发

原理了解清楚之后,我们可以通过代码调用 sysctlbyname 设置 vm.freeze_enabled 为 false ,模拟 disk-space-shortage 强杀的流程。

void disable_freezer(int value) { int val = value; int ret = sysctlbyname("vm.freeze_enabled", NULL, NULL, &val, sizeof(val)); if (ret == 0) { printf("freeze disabled success...\n"); } else { printf("freeze disabled failed, error: %s\n", strerror(errno)); } return;}
int main(int argc, char * argv[]) { int value = 0; // disable freezer disable_freezer(value); return 0;}

编译后在越狱机上运行后,可以成功的触发 disk-space-shortage 强杀,并生成对应的 Jetsam 系统日志,也验证了我们原理分析的正确性。

召回策略

我们首先尝试通过以下召回方式

1、hook sysctlbyname 系统方法

当 CacheDelete 进程通过执行 sysctlbyname 触发 Jetsam 强杀流程时记录标志位。

结论:不可行,理论上无法 hook CacheDelete 进程对 sysctlbyname 的调用。

2、定时查询 vm.freeze_enabled 状态

定时调用 sysctlbyname("vm.freeze_enabled", &value, &size, NULL, 0); 方法查询 vm.freeze_enabled 状态,当状态变为 0时记录标志位。

结论:不可行,查询 vm.freeze_enabled需要 root 权限。

最终方案

可以结合低磁盘阈值,通过排除法辅助判断是否为 disk-space-shortage 类型的进程退出。

vnode-limit


  原理

文件系统 vnode 是操作系统内部用于表示文件系统对象的数据结构,每个文件、目录和设备都有一个对应的 vnode。vnode 提供了文件的元数据信息以及访问文件系统对象的接口和操作。

文件描述符是应用程序中用于表示打开文件或其他资源的整数值。它是操作系统为应用程序提供的一种抽象,用于访问底层文件系统资源。一个应用程序可以打开多个文件或资源,每个打开的文件或资源都会分配一个唯一的文件描述符。

多个文件描述符可以指向同一个文件系统 vnode。当应用程序打开同一个文件时,操作系统会为每个打开的文件分配一个独立的文件描述符,但它们都会指向同一个文件系统 vnode。这意味着不同的文件描述符可以对同一个文件进行读写操作,它们共享文件的当前偏移量、打开模式等状态。

Jetsam 触发流程

// 创建 vnodestatic int new_vnode(vnode_t *vpp, bool can_free) { // ... /* * Running out of vnodes tends to make a system unusable. Start killing * processes that jetsam knows are killable. */ if (memorystatus_kill_on_vnode_limit() == FALSE) { /* * If jetsam can't find any more processes to kill and there * still aren't any free vnodes, panic. Hopefully we'll get a * panic log to tell us why we ran out. */ panic("vnode table is full"); } // ...}
// 触发 vnode-limit 强杀boolean_t memorystatus_kill_on_vnode_limit(void) { os_reason_t jetsam_reason = os_reason_create(OS_REASON_JETSAM, JETSAM_REASON_VNODE); if (jetsam_reason == OS_REASON_NULL) { memorystatus_log_error("memorystatus_kill_on_vnode_limit: failed to allocate jetsam reason\n"); }
return memorystatus_kill_process_sync(-1, kMemorystatusKilledVnodes, jetsam_reason);}


  阈值

iOS 15及以上 10000,iOS 14及以下 5700

通过在越狱机上执行 sysctl -a | grep vnode,可以查看 kern.maxvnodes 阈值。

召回策略

通过监控文件打开、关闭相关接口记录已经打开的 vnode 数量进行召回。


四、总结

本文首先介绍了OOM、Jetsam、Flotsam 的相关概念,详细介绍了 Jetsam 的实现原理,针对 Jetsam 中的 per-process-limit、vm-pageshortage、disk-space-shortage、vnode-limit 这四种系统强杀类型展开介绍了对应的实现原理、触发阈值以及我们的召回策略。通过对上述强杀类型召回过程的描述,我们可以总结出一套通用的召回方法,适用于其他未知的异常退出类型。即调研实现原理 -> 确定触发阈值 -> 制定召回策略。原理调研清楚并确定阈值后,制定召回策略就变得简单了。

召回是解决的前提,在异常退出类型能够准确的召回的基础上,才能进行准确的归因、定位和解决。经过持续的努力和优化,我们的召回率已经有了显著的提升,Flotsam 召回率提高了30%+,大大提高了我们的问题定位效率。目前异常退出中仍然存在一些未知的退出类型没有召回,仍需努力!


参考资料


[1]  Reducing FOOMs in the Facebook iOS app: https://engineering.fb.com/2015/08/24/ios/reducing-fooms-in-the-facebook-ios-app/

[2] Memorystatus Subsystem: https://github.com/apple-oss-distributions/xnu/blob/main/doc/memorystatus/overview.md

[3] Freezer: https://github.com/apple-oss-distributions/xnu/blob/main/doc/memorystatus/freezer.md

[4] ios app maximum memory budget: https://stackoverflow.com/questions/5887248/ios-app-maximum-memory-budget/15200855#15200855

[5] Freezer: https://github.com/apple-oss-distributions/xnu/blob/main/doc/memorystatus/freezer.md


欢迎加入

快手主站技术部客户端团队由业界资深的移动端技术专家组成,通过领先的移动技术深耕工程架构、研发工具、动态化、数据治理等多个垂直领域,积极探索创新技术,为亿万用户打造极致体验。团队自2011年成立以来全面赋能快手生态,已经建立起业内领先的大前端技术体系,支撑快手在国内外的亿万用户。


在这里你可以获得:

  • 提升架构设计能力和代码质量

  • 通过大数据解决用户痛点的能力 

  • 持续优化业务架构、挑战高效研发效能

  • 和行业大牛并肩作战


我们期待你的加入!请发简历到:

chenguang03@kuaishou.com




继续滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存