*OS Internals 卷一 读书笔记-第九章 Virtual Memory

132 阅读27分钟

第九章 In Memoriam: Process Virtual Memory

进程或者内核自身每个操作都会归结为内存管理。GUI, files, sockets和其它复杂对象都是内存中的结构体。所以内存管理是操作系统最重要的责任之一。

内存管理在两个层面进行 - 在内核层级, 内存在请求物理页的时候分配,通过memory management unit (MMU)进行。但Pages极易浪费所以用户态进行了进一步的管理,通过堆的抽象和其它间接APIs。这是本章的焦点

我们开头会进行命名法的介绍和定义,在呈现POSIX层和Mach提供的API之前。然后会讨论libmalloc,和与其相似的malloc、free及friends的实现。然后会介绍memory zone的概念,同时还有它在Darwin中也独树一帜的API

虽然libmalloc是内存分配的首要API,但也有其它的。libplatform和libcache会随后讨论,同时还有一些内置的内存诊断工具。然后会讨论system-wide的视角,去解释swap, memory compression,和低内存条件下的处理 - 也就是memory pressure

On the same page

所有现代CPU体系架构内存操作基础而原子的块定义为页。页大小通常是4K (Intel和ARMv7架构),但也可能设置为16K(ARMv8)或者甚至是2M(也就是所谓的Intel "huge pages")。页大小因此成为了一个内存管理的关键部分,因为它定义了内存访问的最小量。访问一个字节的数据也需要获取包含这个地址的整个页。

访问内存时,CPU使用memory management unit (MMU),它负责将虚拟内存映射到物理内存。虚拟内存到物理内存的转换是透明的 - 每个进程甚至是内核自己都直接处理虚拟地址而不直接操作物理内存。只有内核有能力修改页表,页表是与MMU共享且能够决定哪个物理页映射到哪个虚拟地址

多进程和内核不可避免需要更多物理内存。内核因此需要使用几个"tricks"以确保系统内存的最大化。

  • Copy-on-Write (CoW)是通用手段,它可以隐式地共享映射了多次的相同的虚拟页。这个技巧也叫做隐式共享implicit sharing,因为虚拟映射的用户仍认为内存映射是私有的。

    只要对这块内存仅限于读取,不需要做其它操作,那么所有读取操作都能可以从同一份拷贝中获取到。只有在其中一个用户需要修改也就是写入数据页时,这时候Copy-on-Write条件才会触发。MMU给内核发页错误的信号,这时候内核会通过traps响应。意识到这个fault是可修正的时候,内核会生成第二份物理拷贝,然后把映射缝合起来

  • 换页可以将最近未使用的物理内存页换出内存并写入备用存储中。对内存映射文件来说,这意味着将它们写入原本来自的文件(e.g.fflush(3))。对于匿名内存(malloc (2)等),这意味着写入交换区(通常称为swapping the memory)

因此物理内存(RAM)页可以被视为更大的虚拟内存空间的"windows"(或者caches)。从物理视角,一个内存页可能处于几种生命周期状态之中,如Figure 9-1所示

  • Free: 意味着物理页并未用于任何虚拟内存页,当需要出现时可以马上使用
  • Active: 意味着物理页当前用于某虚拟内存页且最后有使用过。它并不希望换出,除非没有更多inactive页存在。如果page在未来并没有被使用,其会被deactivated
  • Inactive 意味着物理页先前使用过,但经历超时后变成inactive了。这些页可以被换出,如果物理页的需求出现。或者如果inactive页任何时候被使用了,它会马上恢复成active状态

image.png

  • Speculative 意味着此页被获取或者被换入是为了满足需要,可能是也可能不是即将发生的。 这些页可能被使用后变成active或者不被使用然后在timeout之后变成inactive。一旦出现内存短缺,speculative 页会立即释放,因为它们需要马上被使用
  • Wired memory页是常驻页,不需要考虑active与非active状态,会一直存在直到显式地被"unwired"

内核也允许page throttling,这是一个人为对需要进行换页或者创建新页的task强加的延迟。这是为了阻止错误的任务霸占太多内存而影响整体系统性能表现。

vmstat(1)

vm_stat工具(system_cmds的一部分,对*OS也是可用的(author's binpack))可以用来展示虚拟内存统计数据快照(类似于Linux's /proc/meminfo)。这个工具是围绕一个API call:

host_statistics64(mach_host_self(), // host port 
                  HOST_VM_INFO64,   //info code
            (host info64 t) stat,
                    &count)

然后将host_info64_t返回字段解析成人类可读的格式:

image.png

Memory Management APIs

由于其多personalities, XNU提供了两套不同的内存管理calls:BSD (POSIX)和Mach microkernel

POSIX Memory APIs

image.png

Mach memory APIs

XNU提供的BSD system calls实际只是对基于Mach的implementations的封装。Mach calls在用户态也是可用的,因此提供了能实现同一目的的另一套API。而Mach又有两套子系统<mach/mach_vm.h>和<vm_map.h>,它们overlap。如XNU's osfmk/vm/vm_user.c中的解释,mach_vm (48xx)最初是用于支持64-bit,而38xx 总是调用32-bit APIs (就算在64-bit platforms,除非显式使用.._64的函数)。 Table 9-5显示了这两个系统

image.png

Memory Tags

相比于使用POSIX,使用底层[mach_]vm_allocate API的另一个好处是能指定memory tag。tag是一个可以从用户态设置的8位的值,且内核不会触碰。所以Tags的作用在于标记memory allocations,方便调试工具推导指定内存区域的归属。memory tag是在mach_vm_allocate()的第4个参数, 其编码在vm_flags参数的高序字节,使用<mach/vm_statistics.h>中的VM_MAKE_TAG宏。这个头文件也包含一组由Darwin多子系统使用的VMMEMORY* tag,展示在Listing 9-6中。Apple周期性地往这个文件中添加tag,但240-255的值是为第三方所用的

image.png Memory tags会通过用户态的调用返回,[mach_]vm_region和proc_info PROC_PIDREGIONINFO flavor。 procexp(j)和vmmap(1)会显示展示region的tag

libmalloc

虽然pages从MMU视角是原子的,但大多数分配并不需要页的整数倍,通常是少于一页的。如果每次分配都rounded up到最近的页的整数倍,内存条会很快浪费完。因此用户态分配使用堆,通过malloc() API族以及free()。

堆的得名来自其数据结构,即一个二叉堆。实际上堆实现有很多种,这个术语渐渐地涵盖大多用户态内存管理(有时会错误地使用到内核内存管理中)。Darwin使用自己的库- libmalloc.dylib- 和其自己的实现- 也就是malloc zones

The scalable (default) zone

一个malloc zone是一个内存区域,在其中会发生内存分配。所有分配都使用DefaultMallocZone,除非指定其它。The default zone也是the scalable zone,也因其能够scale to different allocation sizes而得名。The zone使用几个区域,每个区域负责一种size, 如Table 9-7所示:

image.png 上图中quantum列的意思看起来是每种tag类型的一次分配对应的元数据负载

system通常启动后会每种类型的region会分配一个,预分配并将其作为后续内存分配的堆空间。如果一个将耗尽,则会分别另一个同种类的region。多region type的选择其实是基于页size的限制,同时也是基于为了平衡维护不同内存分配size的free list的开销。每个region的内存管理是独立于其它region的,使用所需的最优算法。通过这种方式,MALLOC_TINY allocations可以打包进尽可能少的几个page页中, 而MALLOC_LARGE分配allocations可以更轻易地通过调用mach_vm_mmap()满足,因为这个方法可以分配到连续的多页地址且不需要metadata开销。regions使用vmmap(1)或者procexp pid regions命令都可见,感谢memory tags,如Output 9-8所示:

image.png

Custom Zones

scalable zone是默认的zone,但进程绝不受限于只使用它。任何进程都可以轻易创建自定义的zones,通过调用malloc_create_zone(3),指定start_size(其会预pre-claim the memory region for the zone allocations)和flags。manual page中描述没有user-settable flags,但是看libmalloc源码发现几个有用的flag(大多是通过环境变量设置的,后面会讨论),以及未文档化的DISABLE_ASLR (1 << 30),虽然这个flag只有在boot-arg中enable过才有用。 返回的malloc_zone_t是一个function pointers结构,其调用传统的calls ([m/c/v/re]alloc(3)和free(3)),以及几个extensions。zone的创建者可以选择是否保留默认实现的(alloc/free等的)回调callbacks,也可以选择使用自定义的callbacks。zone可以进一步被命名,也可以提供introspection callbacks,以辅助debugging. Figure 9-9解释了malloc_zone_t, which is normally left opaque:

image.png 一旦zone callbacks设定,zone就可以使用malloc_register_zone()激活。为了显式地使用zone,调用者需要使用传统调用接口的带_zone的版本(比如malloc_zone_malloc()而非malloc()),并指定malloc_zone_t作为 首参 著名的使用自定义zone的例子是WebKit's bmalloc, boringssl,和SQLite3's Sqlite_Heap allocator。自定义allocators是一个增长在研究领域,鉴于它们的重要性。

Zone APIS

Darwin's zone architecture开启了很多有用的扩展,对于不想使用POSIX标准malloc()函数族的场景。即使自定义zone callbacks并没有实现而是使用默认的,自定义zone仍能提供Windows所称为的"private heaps" - memory regions可以以线程为粒度分配,且可以通过malloc_destroy_zone()清除。但Darwin's zone APIs 比Windows更先进。 Table 9-10 shows the useful functions, all defined in <malloc/malloc.h> save for the shaded rows (private APIs)

image.png

Table 9-10中大多数函数都会调用zone_t结构中对应的introspect pointer指向的实现。其实还有更多函数可以toggle zone locking以及列举discharged pointers (not defined for the default scalable zone)。自定义zone实现方也可能选择实现自己的introspection callbacks,如果他们坚持<malloc/malloc.h>中的定义

Zones可以被列举,从进程内或者进程外都可以。malloc_get_all_zones()API 需要的参数是 一个Mach task port(ormachtaskself(),on one's self), 一个内存读取操作(通常是,mach_vm_read()),并组织一组addresses与associated count比如malloc_get_all_zones(mach_task_self(), **nullptr**, &zones, &count),枚举甚至可以在per-pointer粒度级别,虽然这需要直接从zone的introspect structure中调用枚举器。私有的Symbolication.framework提供了一个更简单的API(当然需要注意私有framework的使用)。小小的逆向就可以暴露很多需要的prototypes:

image.png

In the Zone

如果检查Figure 9-9,会发现malloc_zone_t的flags和size在结构中都没有反映。但这个结构暴露了更大画面的一部分:malloc_zone_t只是更大的私有zone结构的头部分,私有的zone结构会padded到页大小(4k或者armv8的16K),而紧跟其后的是更多字段。padding到page size是有意为之,因为这允许malloc_zone_t字段mprotect (2)ed到只读,而其它字段fields(私有且未暴露)是可修改的。每个zone allocator处理不同,且虽然它们通常不影响average application,但理解它们的细微差别 - 它们怎样影响分配行为 - 对安全研究者是顶重要的 。迄今为止,只有one talk in SummerCon 2016[2]有深度讨论这里

如前所述 the scalable zone是进程默认的分配器。甚至在新zone创建的时候(by malloc_zone_create())这通常意味着新私有regions,但底层算法仍是相同的(除非通过设置不同的callback pointers而改变了底层算法)。

the scalable zone根据allocation regions进行工作也讨论过了,但底层算法和结构还没。 Darwin's scalable zone是受两项理论工作影响:

  1. The Magazine layer: 由Bonwick et al[3] 首次提出且使用在传奇的Solaris OS中,它提供了magazines抽象,一种per-CPU caching
  2. The Hoard allocator: 由Berger et al.14] 提出 - has inspired further multithreaded enhancements, introduced with SnowLeopard (10.6)
Magazines

memory management的挑战之一是多处理器之间并发的缩放,当进行在不同CPU上的线程同时请求内存操作时。这通常会引入锁,但它是一个非常耗时的操作,或者强制等待者让步yield (牺牲性能)或者自旋spin(因此影响系统性能和电量)。Magazines缓解了这个问题,因为它们提供per-CPU caches,可以从它们中分配chunks。代码通过其CPU index(通过_os_cpu_number(),其从comm page获取local CPU id)访问magazine。因为local to their CPUs,包括锁也是,极大减少了竞争和锁的负载。Magazines也进一步内部优化为最适合CPU cache lines的结构

magazine structure的设计中还是有相当多的其它优化。可能最重要的是保持free chunks列表分离,通过一个bitmap。有这样256个mag_free_list和一个32字节(=256)的mag_bitmap,其中每一位代表对应的free list是可用的。因为复杂度为O(1)所以很快,因为只是找bitmap中的某一位,所以很快就可以找到一个free chunk,相比于维护一个列表是O(n)的复杂度。其它的优化在 magazines to regions的连接(regions是连续的内存Table 9-7中那样).regions是通过magazine中的双链表指向的。链表是region_trailer_t的第一个元素,region_trailer_t中也包含一个指向所属magazine的index。整个region_trailer_t是region data(TINY或者SMALL)的尾部,region的首部分包括region中所有block的bitmap。嵌入这个linked list使得从region到magazine(也包括反过来)的访问很快,而且也方便magzines(和它们的区域)的回收。magazine structure如Figure 9-12所示

image.png

然而有个特殊的case需要考虑就是magazine消耗完chunks后。为处理这种情况, a special magazine - 称为depot被引入。depot是全局的(使用index -1,这样就不与valid CPU number冲突了),所以对它的锁竞争是可能的。但fallbacks到depot的场景并不常见

Hoard improvements

Hoard memory allocator是Berger et al设计的。是相对于traditional memory allocators of C/C++ 为了更好的性能的替代方案。Hoard直接解决了allocators的通用问题 - 包括heap fragmentation, 多线程的缩放性(running simultaneously on multiplecores),和"false sharing"(可能在allocator引发多线程不经意地在同一core cache line上共享了数据。

libmalloc并不直接使用Hoard,但使用了一些"inspired" changes,尤其是对MALLOC_TINY zone。tiny zone 是经常使用的,尤其是Objective-C的AutoreleasePool,其进行自动垃圾回收。相比于在zone中维护元数据metadata,它现在在magazine level做这个事情 - 允许thread affinity,更少的竞争或者说locking。Allocations仍在process level进行,但一旦分配后,频繁使用的regions会仿射到线程而不是process level。竞争并没有完全消除,但可能性被大大减少了。如果每个线程从一个不同的内存region中分配,那么两个线程之间冲突或者引发cache不一致性的可能性会显著地低。

The NanoZone

Nano zone是一个可选的zone,用于小于256 bytes的分配。它因为默认并不使用所以是可选的 - 请求这个zone的方式是在程序启动前设置MallocNanoZone=1这个环境变量; 另一个方式是通过posix_spawn的flag _POSIX_SPAWN_NANO_ALLOCATOR (0x0200)。这个flag连同_POSIX_SPAWN_DISABLE_ASLR都是kernel private的,所以没有在<sys/spawn.h>中导出(但尽管如此仍然可以使用)

因为它用于如此细微的allocations,实在没地方去"scale" to。所以nano zone并未实现a scalable zone structure(在larger allocations的情况下会falls back为default scalable zone),但仍使用the same magazines。 Figure 9-13显示了这个zone's metadata structure。

使用nano zone的程序可以很轻易地通过筛选进程列表,使用vmmap(1)或者procexp all regions。MALLOC_NANO的memory tag (11)并不容易被很轻易地看到,但也因为这个zone总是在相同的固定地址range: 0000600000000000 及往上

The Nanov2 Zone (Darwin 18+)

Darwin 18的libmalloc-166添加了一个新的私有Nano allocation zone,称为Nanov2 zone。The zone allocator是一个基于arena的allocator。Arena allocation是一种范式,在arena中一大块 operating system memory在初始中被请求分配以便所有后续小的allocations可以在内部处理,而不需要进一步的system calls或者更新外部memory management structures。The Skywalk subsystem(Darwin 16中引入会在Chapter 16中讨论)也使用arena allocations, 甚至在内核中也使用。

image.png Nanov2 zone使用一个64MB arena,其会分区成16KB的blocks,每个block会进一步分成slots。8个 arenas 可以占据512MB的region。Allocations可以按16-byte为单位增加(最大到256字节, as with the legacy Nano)。非2倍数的size(particularly 144 and 208)会导致浪费bytes(因为他们不能完全瓜分完block size),但这是不可避免的

对于traditional ('v1') Nano zone,Nanov2使用自己的metadata structure - nanozonev2_s。与v1 (Figure 9-13)相比,v2更简单,在一个NANO_SIZE_CLASSES * MAX_CURRENT_BLOCKS的nanov2_block_meta_t pointers数组中持有block metadata,和一个另外的大小差不多的数组持有对应的block locks。有一个额外的regions_lock,在添加或者修改时使用,一个madvise_lock,和一个(目前未使用的)blocks_lock。

在写书的时候(Darwin 18.2),看起来Nanov2 zone没用户,其需要通过nanov2_mode boot-arg开启。然而src/nano_malloc_common.h的一条comment 表明遗留的Nanozone不会存活太久了。这个转变可能会发生在未来的Darwin release中 - 虽然开发者仍无感知,因为Nano allocator实现是不透明的

The Purgeable Zone

Darwin's libmalloc提供的另一个扩展是purgeable memory的概念。这个特性依赖同名的purgeable Mach 虚拟机制,且提供了一个client,其具有分配内存的能力,而其分配的内存在低内存条件event事件下可以自动释放。

Purgeable zones可以被系统自动释放,而不需要用户操作。分配的时候,purgeable zone中的页会标记为VM_FLAGS_PURGABLE flag。当内存压力事件发生时(随后会讨论), pages可以被清除:libmalloc提供malloc_zone_pressure_relief,其内部会调用madvise(2)使用MADV_FREE_REUSABLE flag(一个non-POSIX扩展,如Table 9-4中所示),suggesting a mach_vm_behavior of VM_BEHAVIOR_REUSABLE。这使得kernel deactivate pages,以便其可以从physical memory中dropped掉

因此purgeable memory是不可靠的,因为压力事件是不可预测的,且可以在任何时间引发清除。其依赖应用自身确保purgeable memory的访问是安全的。相比于直接使用purgeable memory,Darwin提供了libcache(3) APIs帮助促成对这个机制的访问。libcache提供创建in-memory caches的机制,其持有keys和values,类似于一个 mutable dictionary。然而与字典不同的是,cache可以响应low memory conditions,同时移除未使用的values。

libcache APIs通常是由Foundation的NSCache[5] Objective-C class包装的。首先cache是由cache_create()调用来创建的。Cache keys可以是任何类型,而且创建者可以指定一组callback functions(文档化在cache_callbacks(3)中),使得cache可以高效hash,操作和释放values。一旦cache创建,values可以通过cache_set_and_retain(3),然后通过cache_get_and_retain(3)获取。持有一个值会增加其引用计数直到cache_release_value(3)减少其引用计数。值也可以通过cache_remove显式移除。

libcache会尽可能长地持有values,直到在内存压力时(显式销毁时)值被移除。初始化的时候,libcache创建一个memory pressure dispatch source并赋值一个event handler给它。例子可见于Listing 9-30,解释memory pressure的section。压力发生时libcache's enforce_limits会通过调用key value release callback清除any values。cache也与malloc's purgeable zones整合了:the purgeable memory callbacks(cache_value_make_[non]purgeable_cb)是直接通过malloc_make_[non]purgeable实现的

Debugging

Memory management是程序最容易出错的领域,因此现代libraries提供丰富的调试和tracing能力。GNU's libc offers <mcheck.h> functions,而Darwin's libmalloc通过环境变量提供相似的功能,可以通过命令行设置或者从parent继承

Environment variables

libmalloc使用的环境变量前缀都是Malloc。有很多环境变量且文档化在malloc(3)中,MallocHelp=1应用到任何程序时 这个库本身也文档化了,显示在Output 9-15中:

image.png Setting the variables通常就够了,虽然MallocStackLogging's case the value集("lite","malloc","vm" or otherwise)影响日志级别。像上面提到的这里面一些环境变量(above,高亮部分) 也可以使用malloc_create_zone()中第二个参数的flags来对每个zone设置。Following the embarrassing precedent of DYLD_PRINT_TO_FILE (会导致a local privilege escalation through setuid programs,III/12中讨论), libsystem_malloc.dylib refuses MallocLogFile,如果进程被视为restricted(as reported by dyld_process_is_restricted).

kdebug codes

MallocTracing环境变量设置时,malloc操作生成kdebug codes,其可以使用工具提取(在第15章讨论)。Codes使用src/trace.h中的一个TRACE_CODE宏来构造,DBGUMALLOC(51)为class,UMALLOC_EXTERNAL(0×01)或者 UMALLOC_INTERNAL(0x02)是派生类。头文件进一步定义了code自身,如Table 9-16所示显示了它们的参数

debug中的Tracing比其它mechanisms更受限,但提供了advantage of being easy to monitor at a system-wide level. kdebugview工具在本书配套网站上可用,可以用于这个和其它目的。

image.png Kdebug codes一个新颖的用法在malloc_replay工具中。这个工具读取UMALLOC* kdebug code data,并重现相同的 allocations (with random contents),以快速重现相同layout结构的操作process。这个工具是用源码形式提供的,作为libmalloc 工程的 tools/ 目录,但依赖私有的libktrace APIs (15章中讨论). Listing 15-33显示function prototypes can be recreated fairly easily,enabling manual compilation of this tool.

SamplingTools

Darwin's SamplingTools

image.png

libplatform (os_alloc_once)

很多场景下进程需要一块内存分配且在进程整个生命周期内都需要。换句话说这次分配永远都不会释放,直到进程退出。这种情况下并不需要如libmalloc般复杂的,简单的分配器即可完成。

这就是libplatform存在的地方:除了提供很多特定于平台的services,比如原子操作和CPUcache control functions(in cache(3)),它也提供os_once allocator.这是一个简单的allocator,很少直接使用但在os_alloc_once中普遍使用, as of 10.9/6.0.

The _os_alloc_once_table

Apple's system dylibs make frequent use of os_alloc_once: libsystem kernel.dylib exports the _os_alloc_once_table, which provides numbered "keys" for use by the individual dylibs.A list of those keys can be found in Libsystem's alloc_once_private.h as OS_ALLOC_ONCEKEY* #defines. Whena dylbinitializes, it specifies itsreservedkey,along withastructure size, and an initializer function (which may be NULL). os_alloc_once wil check the table entry, allocating the structure on its first call, and calling the initializer function (if any) with the structure as its argument, so ti can be initialized. Subsequent calls, however, wil note the entry has been already allocated, and just return the structure pointer. This is shown in Figure 9-18:

image.png This usage pattern thus ensures the structure is initialized exactly once when allocated. It is commonly used in both the open sourced and closed source libraries, so it's worth examining it, as is shown in the following experiment:

Swap

所有desktop和server操作系统支持swapping,但XNU的方式相当不寻常。由于其微内核, paging实际是在内核之外进行的使用一个dynamic pager daemon。Mach微内核会使用一个特殊的port (# 7)与pager通信,issue 创建和移除swap space的请求. 与这个特殊的port通信会使用两个MIG subsystems之一 - default_pager_object和default_pager_alert。The default_pager_object子系统内核用于daemon requests,而default_pager_alert是daemon生成的low space alerts. The daemon也能够通过三个特殊的Mach traps- macx_swap[on/off] 和macx_triggers 来direct the kernel

Dynamic paging (MacOS)

dynamic pager daemon仍驻留在/bin,但它的功能大缩减了. 到Darwin 16 (XNU-3789),其MIG subsystems 和macx_* Mach traps都不存在了(returning ENOTSUP with the exception of macx_triggers, when called with SWAP_COMPACT_(EN/DIS]ABLE). 唯一遗留的命令行参数是 -F,指定swap文件路径和前缀. 这个daemon在系统启动时启动,是为了确保swap directory (by default, /private/var/m/)的存在,同时在通过使用sysctl(2) call设置vm.swapfileprefix而将路径传递给内核之前,将其clears it from stale swap files.

然而这并不意味着swap不再使用 - 反而 as MacOS still hungers for swap. The /var/m 目录因此经常包含any number swap files, starting with swapfile0. The directory also contains the sleepimage,在系统休眠system hibernates时使用.

image.png 常见的系统攻击是read the swap file data,希望恢复暂存于此的敏感数据. XNU使用专用的swapfilepager防止用户态对交换文件的任何访问. The swapfile_pager是几个in-kernel pager objects之一(内核pager对象在卷II中讨论,且与user-mode dynamic_pager无关). 任何时候访问vnode associated with swap的话,swapfile_pager都会返回bzero()s的数据. 这样即使是root user也不能检查swap file的内容:

image.png 但仍阻止不了物理攻击,对手仍能访问实际的磁盘设备. 因此MacOS swap的另一个特性是默认会加密(if XNU is compiled with ENCRYPTED_SWAP, which it is). 这个设置曾经是可关闭的,通过/Library/Preferences/com.apple.virtualMemory.plist key of UseEncryptedSwap,但MacOS 10.9 the property list移除后就不可再更改了.

Compressed memory

XNU支持compressed RAM,其将未使用的pages - 不清除 - 压缩到内存中其它地方. 自很早就实现 从XNU 123 (in the pre 10.0 days) - 但直到Darwin 13才使用, 首次亮相在iOS 7.0.

乍一想,compressing RAM的想法听起来不合逻辑 - 甚至像无稽之谈. 毕竟swapping RAM into RAM看起来与这个操作的目的背道而驰(to free up RAM), 尤其是其实可以交换到磁盘. 然而这其实是有目的的.

设想如果swapping to disk simply并不是一个选项,这个选择也就合理了 - 也就是在i-Devices中, 没有swap因为使用的是NAND flash. 没有swap, i-Device用完RAM时 - 它不再能提供virtual memory. RAM 因此成为更稀有的资源. 压缩到RAM中可以使内存更耐用,将inactive RAM pages打包进less pages.Inactive RAM通常包含相对好压缩的data structures和text which compress relatively well,所以这种方式可以节省很多megabytes.

Compressor statistics可以通过多种方式展示.一种方法是Process Explorer,在interactive mode中.也可以用vm_stat(1)或者memory_pressure(1), and grep (1) for 'compress'.任一命令都可以呈现由compressor存储的pages,以及其占据的pages - 后者数字显著地比前者小,如Output 9-23所示. Compressed memory在VolumeII详细讨论.

image.png

Memory Pressure

RAM是有限的resource,系统迟早会消耗完.在*OS中尤其明显,其(unlike MacOS)没有swap. memory compression这种Tricks 可以推迟内存消耗完但低内存的情况永远会出现.

MacOS和*OS variants使用不同的方法处理low memory conditions. MacOS更优雅,使用称为memorystatus的机制 ,而*OS使用更激进的 jetsam机制. 后者基于前者且由XNU通过CONFIG_JETSAM macro开启.

MacOS's memorystatus mechanism在10.7引入. Memorystatus是一个可选mechanism允许app register for termination when idle.

Applications 使用这个mechanism通过Foundation.framework's NSProcessInfo class和其enableAutomaticTermination selector.其也可以暂时关闭, (使用 disableAutomaticTermination), or by signaling an activity - 通过 NSActivityUserInitiated或者低层级的xpc_transaction_begin. 这些Calls最终会调用libproc.dylib's proc_set_dirty().

在系统级别,可以禁用automatic application termination 通过写全局preferences:

defaults write g- NSDisableAutomaticTermination -bool TRUE

*OS: Jetsam

MacOS's memorystatus mechanism是可选的,甚至触发时也允许进程优雅地通过SIGTERM终结. 这些特性都是可能的因为desktops, laptops and servers通常有很大的RAM,和不短缺的swap. 而*OS devices没有这些奢侈器. RAM 是有限的资源 (高端设备上有2GB). Swap devices (outside compressed RAM)不存在,由于NAND P/E lifespan的限制.

因此用尽memory的情况会频繁发生所以系统必需unforgiving. The ARM variant of XNU采用了更侵略性的策略,称为Jetsam. 如名所示,低内存情况下系统会突然discards memory,彻底地杀死持有的进程(通过一个SIGKILL) 绝望地维持系统正常运转.

可以把jetsam mechanism与Linux &Android对比一下.同为移动操作系统, Android也频繁受内存短缺困扰. Linux提供Out-Of-Memory (OOM) killer,其是由一个赋予每个process的oom_score机制驱动的. 系统内存耗尽时最高得分的process被选中, and killed - 一直重复直到内存足够. Linux's oom_score是启发式的, Android通过专用daemon (lmkd)改进了它, lmkd可以基于UI(ActivityManager)来的通知调整scores.

Jetsam由priority bands操作. Priority bands是由一个链接着的processes数组构成,并作为一个虚拟的"death row" for processes. 低内存条件下, Jetsam开始从最低优先级的band (holding IDLE processes)往上按顺序executing进程, until enough memory is freed.

image.png Backboardd Process Explorer会显示Jetsam 进程priority bands在"MS" (MemoryStatus)列. As with other columns, it is sortable. A quick experiment to try is to filter a specific application (using '/') and then bring it to the foreground or send it to the background - which will modify its priority band.

The JetsamEvent reports

Programmatic API

Entitlements
sysctl

Detecting and responding to memory pressure

Memory pressure在*OS中相对频繁, 所以响应它很重要. Processes有几种方式,这样几种 APIs.

vm_pressure_monitor
memorystatus_get_level and (Darwin 19) memorystatus_available memory

另一个Darwin-specific system call, memorystatus_get_level (#453)可以用来poll the memorystatus facility. 这个function call返回一个integer,由内核提供.The memorystatus value 定义为free system memory的百分数. 但这个值相对高(in the 20s or 30s)的时候memory pressure仍可能触发,因为pressure situation 对应增长的paging需求,就算pages仍可用. memorystatus_available_memory (#534, added in Darwin 19) queries the amount of memory available for reclaiming.

APPLE: SYSTEM: MEMORYSTATUS System (event) socket

System sockets是创建在AF_SYSTEM family (32)的特殊sockets, Volume II会详述. Such sockets may be control sockets (SYSPROTO_CONTROL), or event sockets (SYSPROTO_EVENT), chosen as the protocol for the raw socket upon creation). Event sockets are further broken into vendors, classes ("top level classifications") and subclasses. Since this is private, the only vendor is KEV_VENDORAPPLE (1). The classes are defined in<sys/kern_event.h>

VM *kevents(Up to Darwin 16)

application可以通过kevent (2) facility注册low memory events. The event filter required Si EVFILT_VM, and the flags used are defined in <sys/event.h>:

image.png 使用kevent机制提供了event-driven interface- 因为vm_pressure_monitor, an interested party 可以无限地阻塞在the kqueue直到事件触发. This allows the mechanism to be used as a primitive for GCD . As of Darwin 16, support for this has been removed.

DISPATCH SOURCE TYPE MEMORYPRESSURE

Apple偏好的线程模型是GCD(上一章第8章有讨论),将blocking kevent (2) mechanism应用到a dispatch source是有意义的,这样可以在pressure发生时由一个callback block处理. 一个好例子是libcache的dispatch source. The library sets up the source in its cache_init_globals (called once from cache_create()). If the sources of libcache开源, it would look like Listing 9-30:

image.png

libMallocpressure handlers (Darwin 16)
didReceiveMemoryWarning

iOS: maintenanced

iOS引入memory maintenance daemon (/us/libexec/mmaintenanced in version 9.0). The daemon, started from the com.apple.memory-maintenance.plist LaunchDaemon plist, provides "memory housekeeping tasks" on a periodic basis, through two XPC activities: The hourly com.apple.memory-maintenance.compress forces a memory compressor sweep by calling pid_hibernate(). The daily com. apple.memory-maintenance.system-hwm监控系统的high water mark,如果limit is exceeded 可能强制重启.

image.png An additional memory-related daemon, /us/libexec/sysstatuscheck, is set in launch's embedded TEXT. bs plist to run after a user space reboot (discussed in Chapter 13). It calls mach memory info and thememory sysctls of kern.memorystatus level and hw.memsi z e to verify that enough memory has been reclaimed by the reboot operation, or hard reboot the system.