项目中现在有关于APP物理内存、体系物理内存等内存状况的获取API,可是一直缺少获取虚拟内存相关的API。之前业务上也出现过由于虚拟内存耗尽导致的crash,后续也通过com.apple.developer.kernel.extended-virtual-addressing的设置为APP扩展虚拟内存的可用规模。本文首要依据以上背景对虚拟内存进行一些调研

task_vm_info简介

struct task_vm_info {
        mach_vm_size_t  virtual_size;       /* virtual memory size (bytes) */
        integer_t       region_count;       /* number of memory regions */
        integer_t       page_size;
        mach_vm_size_t  resident_size;      /* resident memory size (bytes) */
        mach_vm_size_t  resident_size_peak; /* peak resident size (bytes) */
        ...
        /* added for rev1 */
        mach_vm_size_t  phys_footprint;
        /* added for rev2 */
        mach_vm_address_t       min_address;
        mach_vm_address_t       max_address;
        /* added for rev3 */
        ...
        /* added for rev4 */
        uint64_t limit_bytes_remaining; //能够用来核算app的OOM阈值=(limit_bytes_remaining+phys_footprint)
        /* added for rev5 */
        integer_t decompressions;
        /* added for rev6 */
        int64_t ledger_swapins;
};

task_vm_info 结构体中能够看到以下几个和虚拟内存相关的值

  • virtual_size 当前虚拟内存的巨细
  • region_count 内存区域的个数
  • min_address 最小地址
  • max_address 最大地址

通过测验如下:

iPhone6P(12.4.8) 1G 第一次 第2次 第三次
virtual_size 4.75G 4.77G 4.77G
region_count 798 1258 1449
min_address 0x100454000 0x100630000 0x100084000
max_address 0x2a0000000 0x2a0000000 0x2a0000000
aslr 0x100454000 0x100630000 0x100084000
iPhone6s(13.3) 2G 第一次 第2次 第三次
virtual_size 4.88G 4.88G 4.88G
region_count 2848 2742 2511
min_address 0x10419c000 0x10045c000 0x100634000
max_address 0x2d8000000 0x2d8000000 0x2d8000000
aslr 0x10419c000 0x10045c000 0x100634000
iPhone13Pro(15.5) 6G 第一次 第2次 第三次
virtual_size 390.10G 390.11G 390.10G
region_count 4749 4687 4691
min_address 0x100cd4000 0x100e20000 0x1043f8000
max_address 0x3d8000000 0x3d8000000 0x3d8000000
aslr 0x100cd4000 0x100e20000 0x1043f8000

virtual_size和region_count仅代表测验时的虚拟内存用量。

能够发现:

  • 同一种机型max_address是固定的,而min_address和aslr的偏移保持一致
  • 不同机型(内存容量)的max_address发生了变化
  • 13Pro的virtual_size和另外两种机型比较不同相当大

接下来依据XNU源码探究一下原因:

Mac OS X Manual Page For posix_spawn(2)

本文中相关到的相关源码关系如下图:

iOS APP虚拟内存用量初探

设置虚拟内存规模机遇

在创立进程时,体系会加载对应的Mach-O文件,一起为该进程创立对应的_vm_map,关于该结构完整的类界说能够在vm_map.h#L460中查看。本文暂时只关注其中的min_offsetmax_offset

iOS APP虚拟内存用量初探

相关调用如下:

/*
 *        vm_map_create:
 *
 *        Creates and returns a new empty VM map with
 *        the given physical map structure, and having
 *        the given lower and upper address bounds.
 */
vm_map_t
vm_map_create(
        pmap_t          pmap,
        vm_map_offset_t min,
        vm_map_offset_t max,
        boolean_t       pageable)
map = vm_map_create(pmap,
                        0,
                        vm_compute_max_offset(result-> is64bit),
                        TRUE);

max_offset

能够看到创立之初min_offset为0, max_offset的值为vm_compute_max_offset的返回值,该办法最终会调用pmap_max_64bit_offset,其中的option参数为ARM_PMAP_MAX_OFFSET_DEVICE

vm_map_offset_t
pmap_max_64bit_offset(
        __unused unsigned int option)
{
        vm_map_offset_t max_offset_ret = 0;
#if defined(__arm64__)
        #define SHARED_REGION_BASE_ARM64                0x180000000ULL
        #define SHARED_REGION_SIZE_ARM64                0x100000000ULL
        #define ARM64_MIN_MAX_ADDRESS (SHARED_REGION_BASE_ARM64 + SHARED_REGION_SIZE_ARM64 + 0x20000000) // end of shared region + 512MB for various purposes 
        // 0x2A0000000
        const vm_map_offset_t min_max_offset = ARM64_MIN_MAX_ADDRESS; // end of shared region + 512MB for various purposes
        if (xxx){
            ...
        } else if (option == ARM_PMAP_MAX_OFFSET_DEVICE) {
                if (arm64_pmap_max_offset_default) { //0
                        max_offset_ret = arm64_pmap_max_offset_default;
                } else if (max_mem > 0xC0000000) {   0x3D8000000
                        max_offset_ret = min_max_offset + 0x138000000; // Max offset is 13.375GB for devices with > 3GB of memory
                } else if (max_mem > 0x40000000) {   0x2D8000000
                        max_offset_ret = min_max_offset + 0x38000000;  // Max offset is 9.375GB for devices with > 1GB and <= 3GB of memory
                } else {   
                        max_offset_ret = min_max_offset;  //0x2A0000000
                }
        } else if (option == ARM_PMAP_MAX_OFFSET_JUMBO) {
                if (arm64_pmap_max_offset_default) {
                        // Allow the boot-arg to override jumbo size
                        max_offset_ret = arm64_pmap_max_offset_default;
                } else {
                        max_offset_ret = MACH_VM_MAX_ADDRESS;     // Max offset is 64GB for pmaps with special "jumbo" blessing
                }
        } else {
                panic("pmap_max_64bit_offset illegal option 0x%x\n", option);
        }
        ...
        return max_offset_ret;
}
  • 能够看到max的值确实是一个固定的值,确切的算法为(min_max_offset+物理内存对应的特定偏移),在64位情况下别离为:
  • 内存规模 min_max_offset 特定偏移 max_address
    >3G 0x2A0000000 0x138000000 0x3D8000000
    1G-3G 0x2A0000000 0x38000000 0x2D8000000
    <1G 0x2A0000000 0 0x2A0000000

min_offset

相同的,在加载Mach-O文件时,也会设置min_offset,详细的逻辑在load_segment中:

  1. load_machfile中生成aslr 链接
  2. 调用parse_machfile,读取page_zero segment,一般来说page_zero的address为0size1G
  3. (aslr+1G)再补齐为16KB的倍数赋给map->min_offset

iOS APP虚拟内存用量初探

Reserved region

从上文的数据中还能够发现iPhone13Provurtual_size非常的大,差不多为390G。通过InstrumentsVMTracker观察能够发现:

iOS APP虚拟内存用量初探

在13pro的vm区域中多了两个特殊的vm_region,别离是:

  • GPU carveout
  • commpage

这两个区域的地址规模也是固定的并且是比较特殊的地址,一起没有任何的权限,加起来大概占了385G左右。

在xnu源码中寻找相关的信息能够发现:

/**
 * Represents regions of virtual address space that should be reserved
 * (pre-mapped) in each user address space.
 */
#define MACH_VM_MIN_GPU_CARVEOUT_ADDRESS_RAW 0x0000001000000000ULL
#define MACH_VM_MAX_GPU_CARVEOUT_ADDRESS_RAW 0x0000007000000000ULL
#define MACH_VM_MIN_GPU_CARVEOUT_ADDRESS     ((mach_vm_offset_t) MACH_VM_MIN_GPU_CARVEOUT_ADDRESS_RAW)
#define MACH_VM_MAX_GPU_CARVEOUT_ADDRESS     ((mach_vm_offset_t) MACH_VM_MAX_GPU_CARVEOUT_ADDRESS_RAW)
#define _COMM_PAGE64_NESTING_START                (0x0000000FC0000000ULL)
#define _COMM_PAGE64_NESTING_SIZE                 (0x40000000ULL) /* 1GiB */
SECURITY_READ_ONLY_LATE(static struct vm_reserved_region) vm_reserved_regions[] = {
        {
                .vmrr_name = "GPU Carveout",
                .vmrr_addr = MACH_VM_MIN_GPU_CARVEOUT_ADDRESS,
                .vmrr_size = (vm_map_size_t)(MACH_VM_MAX_GPU_CARVEOUT_ADDRESS - MACH_VM_MIN_GPU_CARVEOUT_ADDRESS)
        },
        /*
         * Reserve the virtual memory space representing the commpage nesting region
         * to prevent user processes from allocating memory within it. The actual
         * page table entries for the commpage are inserted by vm_commpage_enter().
         * This vm_map_enter() just prevents userspace from allocating/deallocating
         * anything within the entire commpage nested region.
         */
        {
                .vmrr_name = "commpage nesting",
                .vmrr_addr = _COMM_PAGE64_NESTING_START,
                .vmrr_size = _COMM_PAGE64_NESTING_SIZE
        }
};

这两块区域别离对应了上文中的两个vm_region,其对应的地址规模别离为:

  • GPU Carveout: 0x1000000000~0x7000000000
  • commpage nesting: 0xFC0000000~0x1000000000

能够发现这两个虚拟内存区域是固定的,是用户地址空间预留出来的规模,用户态并不能请求其中的虚拟内存,因而当咱们核算APP占用的虚拟内存时,需求减去这两个预留的vm_region。

虚拟内存总巨细

上文中介绍过task_vm_info中有max_address和min_address两个字段,依据含义猜想这两个的差值可能是APP的虚拟内存总巨细。接下来进行验证代码如下:

    //fill task vm info
    task_vm_info_data_t task_vm;
    mach_msg_type_number_t task_vm_count = TASK_VM_INFO_COUNT;
    kern_return_t kr = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t) &task_vm, &task_vm_count);
    if(kr != KERN_SUCCESS) {
        __builtin_trap();
    }
    mach_vm_address_t minAddress = task_vm.min_address;
    mach_vm_address_t maxAddress = task_vm.max_address;
    //打印猜想的APP的虚拟内存总巨细
    fprintf(stdout, "vm_test: min = 0x%llx, max = 0x%llx, vm_total_size = %.2fG\n", minAddress, maxAddress, (maxAddress-minAddress)/GB);
    mach_vm_size_t virtual_used_size = task_vm.virtual_size;
    integer_t region_count = task_vm.region_count;
    static const mach_vm_address_t reserved_size = MACH_VM_MAX_GPU_CARVEOUT_ADDRESS - MACH_VM_MAX_ADDRESS;
    if (virtual_used_size > reserved_size) {
        fprintf(stdout, "vm_test: reserve vm ocuur\n");
        virtual_used_size -= reserved_size;
    }
    //打印task_vm取得的APP运用的虚拟内存巨细virtual_used_size
    fprintf(stdout, "vm_test: vm_used_size = %.2fG, region_count = %d\n", virtual_used_size/GB, region_count);
    vm_address_t address;
    int i = 0;
    //循环请求16KB内存直到请求失利
    while (true) {
        kr = vm_allocate(mach_task_self(), &address, 16*KB, VM_FLAGS_ANYWHERE);
        if (kr != KERN_SUCCESS) {
            //计算APP还能够持续请求的虚拟内存巨细
            fprintf(stdout, "vm_test: valid size = %.2fG\n", (i*16*KB)/GB);
            break;
        }
        i++;
    }

为了排除搅扰便于计算,建一个空壳工程,在viewdidload中加入上述代码测验结果如下:

  • iPhone6S
APP运用的虚拟内存巨细(virtual_size) 还能请求的虚拟内存巨细(到请求失利停止) 二者之和 猜想的虚拟内存总巨细(max_address-min_address)
4.66G 2.68G 7.34G 7.34G
4.66G 2.71G 7.37G 7.37G
4.66G 2.65G 7.31G 7.31G
  • iPhone6P
APP运用的虚拟内存巨细(virtual_size) 还能请求的虚拟内存巨细(到请求失利停止) 二者之和 猜想的虚拟内存总巨细(max_address-min_address)
4.56G 1.93G 6.49G 6.49G
4.56G 1.93G 6.49G 6.49G
4.56G 1.92G 6.48G 6.49G
  • iPhone13Pro
APP运用的虚拟内存巨细(virtual_size) 还能请求的虚拟内存巨细(到请求失利停止) 二者之和 猜想的虚拟内存总巨细(max_address-min_address)
4.59G 6.71G 11.30G 11.30G
4.60G 6.73G 11.33G 11.33G
4.60G 6.71G 11.31G 11.31G

能够看到数据计算结果和咱们的猜想是一致的,虚拟内存总巨细会有一定的差值也是能够解释的,由于min_address是一个随机的值(aslr)。一起能够发现,虽然64位体系的寻址空间非常大,其实留给用户的虚拟内存规模并没有咱们想象的那么大,一个空壳工程就现已占用了大概4.6G的虚拟内存。一起,当虚拟内存请求失利后,往往也伴随着各种由于虚拟内存请求失利导致的错误的地址拜访从而产生crash。

iOS APP虚拟内存用量初探

虚拟内存扩容

iOS14今后,苹果供给了一个新的才能能够答应APP用户态运用更多的虚拟内存规模:Extended Virtual Addressing Entitlement | Apple Developer Documentation

com.apple.developer.kernel.extended-virtual-addressing

在源码中搜索相关关键字:

#if CONFIG_MACF
/*
 * Processes with certain entitlements are granted a jumbo-size VM map.
 */
static inline void
proc_apply_jit_and_jumbo_va_policies(proc_t p, task_t task)
{
        bool jit_entitled;
        jit_entitled = (mac_proc_check_map_anon(p, 0, 0, 0, MAP_JIT, NULL) == 0);
        if (jit_entitled || (IOTaskHasEntitlement(task,
            "com.apple.developer.kernel.extended-virtual-addressing"))) {
                vm_map_set_jumbo(get_task_map(task));
                if (jit_entitled) {
                        vm_map_set_jit_entitled(get_task_map(task));
                }
        }
}
#endif /* CONFIG_MACF */
/*
 * Expand the maximum size of an existing map to the maximum supported.
 */
void
vm_map_set_jumbo(vm_map_t map)
{
#if defined (__arm64__) && !defined(CONFIG_ARROW)
        vm_map_set_max_addr(map, ~0);
#else /* arm64 */
        (void) map;
#endif
}
/*
 * Expand the maximum size of an existing map.
 */
void
vm_map_set_max_addr(vm_map_t map, vm_map_offset_t new_max_offset)
{
#if defined(__arm64__)
        vm_map_offset_t max_supported_offset = 0;
        vm_map_offset_t old_max_offset = map->max_offset;
        max_supported_offset = pmap_max_offset(vm_map_is_64bit(map), ARM_PMAP_MAX_OFFSET_JUMBO) ;
        new_max_offset = trunc_page(new_max_offset);
        /* The address space cannot be shrunk using this routine. */
        if (old_max_offset >= new_max_offset) {
                return;
        }
        if (max_supported_offset < new_max_offset) {
                new_max_offset = max_supported_offset;
        }
        map->max_offset = new_max_offset;
        if (map->holes_list->prev->vme_end == old_max_offset) {
                /*
                 * There is already a hole at the end of the map; simply make it bigger.
                 */
                map->holes_list->prev->vme_end = map->max_offset;
        } else {
                /*
                 * There is no hole at the end, so we need to create a new hole
                 * for the new empty space we're creating.
                 */
                struct vm_map_links *new_hole = zalloc(vm_map_holes_zone);
                new_hole->start = old_max_offset;
                new_hole->end = map->max_offset;
                new_hole->prev = map->holes_list->prev;
                new_hole->next = (struct vm_map_entry *)map->holes_list;
                map->holes_list->prev->links.next = (struct vm_map_entry *)new_hole;
                map->holes_list->prev = (struct vm_map_entry *)new_hole;
        }
#else
        (void)map;
        (void)new_max_offset;
#endif
}

相同的在posix_spawn中,会判断是否添加了com.apple.developer.kernel.extended-virtual-addressing,会为当前进程对应的map设置~0的max_address。此刻pmap_max_offset函数传入的option为ARM_PMAP_MAX_OFFSET_JUMBO,依据上文代码能够发现此刻max_offset为MACH_VM_MAX_ADDRESS即0xFC0000000,而这个值相同也是上文说到的预留vm_region(commpage nesting)的开始地址。也就是说开启了虚拟内存扩容之后,用户态的地址规模为aslr...0xFC0000000

简单验证一下,虚拟内存的总量来到了59G。

iOS APP虚拟内存用量初探

物理内存扩容

关于物理内存来说,在iOS15今后,苹果相同也供给了扩容的才能com.apple.developer.kernel.increased-memory-limit | Apple Developer Documentation

com.apple.developer.kernel.increased-memory-limit

/*
 * Check for any of the various entitlements that permit a higher
 * task footprint limit or alternate accounting and apply them.
 */
static inline void
proc_footprint_entitlement_hacks(proc_t p, task_t task)
{
        proc_legacy_footprint_entitled(p, task);
        proc_ios13extended_footprint_entitled(p, task);
        proc_increased_memory_limit_entitled(p, task);
}
static inline void
proc_ios13extended_footprint_entitled(proc_t p, task_t task)
{
#pragma unused(p)
        boolean_t ios13extended_footprint_entitled;
        /* the entitlement grants a footprint limit increase */
        ios13extended_footprint_entitled = IOTaskHasEntitlement(task,
            "com.apple.developer.memory.ios13extended_footprint");
        if (ios13extended_footprint_entitled) {
                task_set_ios13extended_footprint_limit(task);
        }
}
void
memorystatus_act_on_ios13extended_footprint_entitlement(proc_t p)
{
        if (max_mem < 1500ULL * 1024 * 1024 ||
            max_mem > 2ULL * 1024 * 1024 * 1024) {
                /* ios13extended_footprint is only for 2GB devices */
                return;
        }
        /* limit to "almost 2GB" */
        proc_list_lock();
        memorystatus_raise_memlimit(p, 1800, 1800);
        proc_list_unlock();
}
static inline void
proc_increased_memory_limit_entitled(proc_t p, task_t task)
{
        static const char kIncreasedMemoryLimitEntitlement[] = "com.apple.developer.kernel.increased-memory-limit";
        bool entitled = false;
        entitled = IOTaskHasEntitlement(task, kIncreasedMemoryLimitEntitlement);
        if (entitled) {
                memorystatus_act_on_entitled_task_limit(p);
        }
}
void
memorystatus_act_on_entitled_task_limit(proc_t p)
{
        if (memorystatus_entitled_max_task_footprint_mb == 0) {
                // Entitlement is not supported on this device.
                return;
        }
        proc_list_lock();
        memorystatus_raise_memlimit(p, memorystatus_entitled_max_task_footprint_mb, memorystatus_entitled_max_task_footprint_mb);
        proc_list_unlock();
}

相同的,在posix_spawn中会判断是否添加了物理内存扩容的才能,然后调用memorystatus_raise_memlimit增加APP的OOM内存阈值。通过测验发现,不同机型的能够提升的物理内存阈值也不一样:

  • iPhone13Pro: 3.00G->4.00G
  • iPhone13: 2.05G->2.29G

比较有意思的是在源码还发现了另外一项才能**com.apple.developer.memory.ios13extended_footprint** 看源码描述是iOS13体系下2G物理内存设备的OOM阈值能够提升到1800M,可是惋惜的在xCode中并不能添加该才能,不知道发生甚么事了~~