百度APP iOS端內存優化-原理篇
一、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/source/xnu/),代碼路徑: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/source/libmalloc/。當我們App進程需要的創建新的對象時,如調用[NSObject alloc],或釋放對象調用release方法時,請求先會走到libsystem_malloc.dylib的malloc()和free()函數,然後libsystem_malloc會向iOS的系統內核發起內存申請或釋放內存,具體來説就是調用操作系統Mach內核提供的內存分配接口去分配內存或釋放內存,蘋果操作系統Mach內核提供瞭如下內存操作的相關接口。
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/source/libmalloc/libmalloc-317.40.8/src/malloc.c.auto.html
``` 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/tarballs/objc4/
[2] libsystem_malloc.dylib源碼:https://opensource.apple.com/source/libmalloc/
[3] XNU源碼:https://github.com/apple/darwin-xnu
[4] 《深入解析Mac OS X & iOS操作系統》
[5] Mach內核介紹:
https://developer.apple.com/library/archive/documentation/Darwin/Conceptual/KernelProgramming/Mach/Mach.html
[6] Mach系統結構:
https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemTechnology/SystemTechnology.html
[7] Mach虛擬內存系統:
https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemTechnology/SystemTechnology.html
[8] Mach內存交換空間:
https://images.apple.com/media/us/osx/2013/docs/OSX_Mavericks_Core_Technology_Overview.pdf
推薦閲讀:
- 精準水位在流批一體數據倉庫的探索和實踐
- 視頻編輯場景下的文字模版技術方案
- 淺談活動場景下的圖算法在反作弊應用
- 百度工程師帶你玩轉正則
- Serverless:基於個性化服務畫像的彈性伸縮實踐
- 百度APP iOS端內存優化-原理篇
- 從稀疏表徵出發、召回方向的前沿探索
- 性能平台數據提速之路
- 採編式AIGC視頻生產流程編排實踐
- 百度工程師漫談視頻理解
- PGLBox 超大規模 GPU 端對端圖學習訓練框架正式發佈
- 百度工程師淺談分佈式日誌
- 百度工程師帶你瞭解Module Federation
- 巧用Golang泛型,簡化代碼編寫
- Go語言DDD實戰初級篇
- 百度工程師帶你玩轉正則
- Diffie-Hellman密鑰協商算法探究
- 貼吧低代碼高性能規則引擎設計
- 淺談權限系統在多利熊業務應用
- 分佈式系統關鍵路徑延遲分析實踐