深入浅出iOS系统内核(3)— 内存管理

本文参考《Mac OS X and iOS Internals: To the Apple’s Core》 by Jonathan Levin
文章内容主要是阅读这本书的读书笔记,建议读者掌握《操作系统》,了解现代操作系统的技术特点,再阅读本文可以事半功倍。
虽然iOS系统内核使用极简的微内核架构,但内容依然十分庞大,所以会分
系统架构进程调度内存管理文件系统四个部分进行阐述。

操作系统管理所有的硬件资源,操作系统内核管理最核心的资源CPU和内存。上一篇阐述了Mach通过进程管理CPU,本文主要阐述XNU和Mach如何高效的管理内存

1 内存分配

  • 基于栈的内存分配:通常由编译器处理,因为栈中填充的通常是程序的自动变量
  • 基于堆的内存分配:用于动态内存分配,只限于用户态使用,在内层面,既没有用户对也没有栈的存在。

1.1 alloca 栈分配

按照传统,栈一般都是保存自动变量,正常情况栈由系统管理,但是在iOS中某些情况下,程序员也可以选择用栈来动态分配内存,方法是使用鲜为人知的alloca( ) 这个函数的原型和malloc( )是一样的,区别在于这个函数返回的指针是栈上的地址而不是堆中的地址。
从实现角度,alloca( )从两方面优于malloc( )

  • 在栈中非配空间只不过是简单的修改栈指针寄存器,时间消耗低,不用担心页面错误
  • 当分配空间的函数返回时,栈中分配的空间会自动释放,解决内存地址泄露问题
    但是栈空间通常比堆空间受限很多,所以alloca( )非常适合名称较短的函数中对小空间的分配

1.2 堆分配

堆是由C语言运行时维护的用户态数据结构,通过堆的使用,程序可以不用直接在页面的层次处理内存分配。Darwin的libC 采用了一个基于分配区域(allocation zone)的特殊分配算法

2 BSD内存管理

在iOS中内存的管理是由在Mach层中进行的,BSD只是对Mach接口进行了POSIX封装,方便用户态进程调用。
XNU内存管理的核心机制是虚拟内存管理,在Mach 层中进行的,Mach 控制了分页器,并且向用户态导出了各种 vm_ 和 mach_vm_ 消息接口。 为方便用户态进程使用BSD对Mach 调用进行了封装,通过current_map( ) 获得当前的Mach 内存映射,最后再调用底层的Mach 函数。

2.1 MALLOC 和 zone

BSD 的malloc 系列函数<bsd/sys/malloc.h> 头文件中。函数名为_MALLOC、_FREE、_REALLOC、_MALLOC_ZONE、_FREE_ZONE

2.2 mcache 和 slab 分配器

mcache机制是BSD 提供的基于缓存的非常高效的内存分配方法。默认实现基于mach zone,通过mach zone提供预分配好的缓存内存。
mcache具有可扩展架构,可以使用任何后端 slab 分配器。
使用mcache 机制的主要优点是速度:内存分配和维护是在每一个 CPU 自有的cache中进行的,因此可以映射到CPU的物理cache,从而极大地提升访问速度。

2.4 内存压力

Mach VM层支持VM pressure 的机制,这个机制是可用RAM量低到危险程度的处置,下面我们会详细讲,这里不展开。
当RAM量低到危险时,Mach的pageout 守护程序会查询一个进程列表,查询驻留页面数,然后向驻留页面数最高的进程发送NOTE_VM_PRESSURE ,会在进程队列中发出一个事件。被选中的进程会响应这个压力通知,iOS中的运行时会调用 didReceiveMemoryWarning 方法。

然而有些时候这些操作没有效果,当内存压力机制失败之后,** 非常时间要用非常手段 **, Jetsam机制介入。

2.3 Jestam/Memorystatus

当进程不能通过释放内存缓解内存压力时,Jestam机制开始介入。这是iOS 实现的一个低内存清理的处理机制。也称为Memorystatus,这个机制有点类似于Linux的“Out-of-Memory”杀手,最初的目的就是杀掉消耗太多内存的进程。Memorystatus维护了两个列表:

  • 快照列表:保存系统中所有进程的状态以及消耗的内存页面数
  • 优先级列表:保存要杀掉的备选进程

在iOS的用户态可以通过 sysctl(2)查询这些列表,优先级列表可以在用户态进行设置。

2.3 进程休眠

在iOS 5中,Jestsam/Memorystatus 和默认的freezer 结合在一起,实现了对进程的冷冻而不是杀死。通过这种方式可以提供更好的用户体验,因为数据不会丢失,而且当内存情况好转时进程可以安全恢复。(感谢@易步指出本段错误)
用户态也可以通过pid_suspend( ) 和 pid_resume( )控制进程的休眠。
iOS 定义了 pid_hibernate,通过发送kern_hibernation_wakeup信号唤醒kernel_hibernation_thread 线程,这个线程专用于对进程冷冻操作。
实际的进程休眠操作是由jestsam_hibernate_top_proc 完成的,这个函数通过task_freeze冷冻底层的任务。
冷冻操作需要遍历任务的vm_map,然后将vm_map 传递给默认的 freezer。

3 Mach 虚拟内存 virtual memory,VM

VM是Darwin系统内存管理的核心机制。

3.1 VM架构

VM 机制主要通过内存对象(memory object)和分页器(pager)的形式管理内存。
Mach 虚拟内存的实现非常全面而且通用。这部分由两个层次构成:一层是和硬件相关的部分,另一层构建在这一层之上和硬件无关的公共层。OS X 和 iOS 使用的几乎一样的底层机制,硬件无关层以及之上的BSD 层中的机制都是一样的。

3.1.1 VM系统全貌

Mach 的 VM子系统可以说是和其要管理的内存一样复杂和充满了各种细节。然后从高层次看,可以看到两个层次:

  • 虚拟内存的层次
  • 物理内存的层次

3.1.2 虚拟内存层

虚拟内存这一层完全以一种机器无关的方式来管理虚拟内存。这一层通过几种关键的抽象表示虚拟内存:

  • vm_map
    表示任务地址空间内的一个或多个虚拟内存区域。每一个区域都是有一个独立的条目 vm_map_entry 表示。这些条目由一个双向链表vm_map_links维护。

  • vm_map_entry
    这是关键的数据结构,尽管只有在包含这个结构的映射的上下文中才会访问到这个结构。每一个vm_map_entry 都表示了虚拟内存中一块连续的区域(region)。每一个这样的区域都可以通过指定的访问保护权限进行保护(和虚拟内存页面采用同样的权限)。任务之间可以共享区域。

  • vm_map_entry
    通常指向一个vm_object,但是也可以指向一个嵌套的vm_map,即子映射(submap)。

  • vm_object
    用于将vm_map_entry 和实际支撑的内存关联起来。这个数据结构包含一个vm_page 的链表,还包含一个用于访问正确分页器的Mach 端口(称为memory_object),通过这个分页器进行页面的获取或清理操作。

  • vm_page
    vm_page 真正表示了vn_object 或部分vm_object(由vm_object中的偏移量表示)。
    vm_page 可以有多种状态:驻留内存、交换出、加密、干净和脏等。

Mach 允许使用多个分页器。事实上,默认就存在3~4个分页器。Mach 的分页器以外部实体的形式存在:是专业的任务,有点类似于其他系统上的内核交换(kernel-swapping)线程。Mach 的设计允许分页器和内核任务隔离开,设置允许用户态任务作为分页器。类似地,底层的后备存储也可以驻留在磁盘交换文件中(通过OS X 中的 default_pager 处理),可以映射到一个文件(由vnode_pager处理),可以是一个设备(由device_pager 处理)。注意:在Mach 中,每一个分页器处理的都是属于这个分页器的页面的请求,但是这些请求必须通过pageout 守护程序发出。这些守护程序(实际上就是内核线程)维护内核的页面表,并且判定哪些页面需要被清除出去。因此,这些守护程序维护的分页策略和分页器实现的分页操作是分开的。

3.1.3 物理内存层

物理内存的页面处理的是虚拟内存到物理内存的映射,因为虚拟内存中的内容最终总要存储在某个地方。这一层面只有一个抽象,那就是pmap,不过这个抽象非常重要,因为提供了机器无关的接口。这个接口隐藏了底层平台的细节,底层的细节需要在处理器层次进行分页操作,其中要处理的对象包括硬件页表项(page table entry,PTE)、翻译查找表(translation lookaside buffer,TLB)等。

每一个Mach 任务都要自己的虚拟内存空间,任务的struct task 中的 map 字段保存的就是这个虚拟内存空间。
vm_page_entry 中最关键的元素是vm_map_object,这是一个联合体,既可以包含另一个vm_map(作为子映射),也可以包含一个vm_object_t(由于这是一个联合体,所以具体的内容需要用布尔字段is_sub_map 来判断)。vm_object 是一个巨大的数据结构,其中包含了处理底层虚拟内存所需要的所有数据。vm_object的数据结构中的大部分字段都是用位表示的标志。这些字段表示了底层的内存状态(联动、物理连续和持久化等状态)和一些计数器(引用计数、驻留计数和联动计数等)。不过有3个字段需要特别注意:

memq:vm_page 对象的链表,每一项都表示一个驻留内存的虚拟内存页面。尽管一个对象可以表示一个单独的页面,但是多数情况下一个对象可以包含多个页面,所以每一个页面关联到一个对象时都会有一个偏移值
page:memory_object 对象,这是指向分页器的Mach 端口。分页器将未驻留内存的页面关联到后备存储,后备存储可以是内存映射的文件、设备和交换文件,后备存储保存了没有驻留内存的页面。换句话说,分页器(可以有多个)负责将数据从后备存储移入内存以及将数据从内存移出到后备存储。分页器对于虚拟内存子系统来说极为重要
internal:vm_page 中众多标志位之一,如果这个位为真,那么表示这个对象是由内核内部使用的。这个标志位的值决定了对象中的页面会进入哪一个pageout队列

3.2 Mach 物理内存管理

尽管内核和用户空间一样,基本上只在虚拟地址空间内操作,但是虚拟内存最终还是要翻译为物理地址的。机器的RAM 实际上是虚拟内存中开的窗口,允许程序访问虚拟内存是有限的,而且通常是不连续的区域,这些区域的上线就是机器上安装的内存。而虚拟内存中其他部分则要么延迟分配,要么共享,要么被交换到外部存储中,外部存储通常是磁盘。

然而虚拟内存和具体的底层架构相关。尽管虚拟内存和物理内存的概念在所有架构上本周都是一样的,但是具体的实现细节则各有千秋。XNU 构建与Mach 的物理内存抽象层之上,这个的抽象层成为pmap。pmap 从设计上对物理内存提供了一个统一的接口,屏蔽了架构相关的区别。这对于XNU来说非常有用,因为XNU支持的物理内存的架构包括以前的PowerPC,现在主要是Intel,然后在iOS 中还支持ARM。

3.2.1 pmap 的 API

Mach 的pmap 层逻辑上由一下两个子层构成:

  • 机器无关层
    提供了一组基本上和及其无关的API。只要求及其支持基本的虚拟内存分页的概念。VM层只考虑pamp_t 并传递这个类型的数据即可,pmap_t 是一个指向struct pmap 是指针,实际上是一个void 指针
  • 机器相关层
    将pmap绑定到一个具体的实现,处理底层敬爱个的各种细节

3.3 Mach Zone

Mach Zone的概念相当于Linux的内存缓存(memory cache)和Windows 的Pool。Zone 是一种内存区域,用于快速分配和是否频繁使用的固定大小的对象。Zone的API是内核内部使用的,在用户态不能访问。Mach中Zone的使用非常广泛。

3.3.1 Mach Zone 的结构

所有的zone 内存实际上都是在调用zinit( )时预先分配好的(zinit( )通过底层内存分配器kernel_memory_allocate( )分配内存)zalloc( )实际上是对REMOVE_FROM_ZONE 宏的封装,作用是返回zone的空闲列表中的下一个元素(如果zone已满,则调用kernel_memory_allocate( )分配这个zone在定义的alloc_size字节)。zfree( ) 使用的是相反功能的宏 ADD_TO_ZONE。这两个函数都会执行合理数量的参数检查,不过这些检查帮助不大:过去zone分配相关的bug已经导致了数据可以被黑客利用的内存损坏。zalloc( ) 最重要的客户是内核中的kalloc( ),这个函数从kalloc.*系列zone中分配内存。BSD的mcache机制也会从自己的zone中分配内存。BSD内核zone也是如此,BSD内核zone直接构建与Mach的zone之上。

3.5 Mach 分页器

进程的内存需求早晚会超过可用的RAM,系统必须有一种方法能够将不活动的页面备份起来,并且从RAM中删除,腾出更多的RAM给活动的页面使用,至少暂时能够满足活动页面的需求。在其他操作系统中,这个工作专门是由专门的内核线程完成的。在Mach 中,这些专门的任务称为分页器(pager),分页器可以是内核线程,设置建议是外部的用户态服务程序。

Mach分页器是一个内存管理器,负责将虚拟内存备份到某个特定类型的后备存储中。当内存容量不足,内存页面需要被交换出内存是,后备存储保存内存页面的内容:当换出的内存页面需要被使用时,将内存的页面恢复到RAM中。只有“脏”页面才需要进行上述的换出和换入,因为“脏”页面是在内存中修改过的页面,要从RAM中剔除时必须保存到磁盘中防止数据丢失。
要注意的是,这里提到的分页器仅仅实现了各自负责的内存对象的分页操作,这些分页器不会控制系统的分页策略。分页策略是有vm_pageout 守护线程负责的。

3.4.1 分页器的类型

iOS 和 OS X 中XNU 包含的分页器种类都是一样的。下表是XNU中的内存分页器的多种类型:

内存分页器 用途
Default 负责内存交换的通用接口
VNode 内存映射文件
Device 概念类似VNode,通过IO接口将数据交换给外设
Swapfile 阻止直接映射交换文件的企图
Apple-protected 实现Apple代码加密机制
Default Freezer 在物理内存较少切没有真正交换文件的系统上,应用程序在后台时不需要真正的运行,当用户将应用切入后台,系统将应用冷冻,在应用恢复时解冻

3.6 分页策略管理

3.6.1 Pageout 守护程序

pageout 守护程序其实不是一个真的守护程序,而是一个线程。而且不是一般的线程:当kernel_bootstarp_thread( ) 完成内核初始化工作并且没有其他事情可做时,就调用vm_pageout( ) 成为了pageour 守护程序, vm_pageout( ) 永远不返回。这个线程管理页面交换的策略,判断哪些页面需要写回到其后备存储。

vm_pageout线程

vm_pageout( ) 函数讲kernel_bootstrap_thread 线程转变为pageout 守护程序,这个函数实际上重新设置了这个线程。设置完成后,调用vm_pageout_continute( ),这个函数周期性地唤醒并执行vm_page_scan( ),维护4个页面表(称为页面队列)。系统中的每一个vm_page 都通过pageq字段绑定这4个队列中的一个:

  • vm_page_queue_active
    最近活跃且驻留在内存中的页面

  • vm_page_queue_inactive
    最近不活跃的页面,因此这些页面是页面换出的备选页面。根据这些页面的使用情况,可能会被换出,也可能会被重新激活

  • vm_page_queue_free
    空闲页面表。这些页面曾经是非活跃的页面,但是被清理出去了(页面换出)

  • vm_page_queue_speculative
    这些页面是通过预读策略投机映射的页面,这些页面是不活跃的,但是很可能很快会变为活跃页面

垃圾回收线程

垃圾回收线程(vm_pageout_garbage_collect( ))偶尔会被vm_pageout_scan( ) 通过其续体唤醒。垃圾回收机制线程处理4个方面的垃圾回收工作:

  • srack_collect( )
    内核栈中的页面

  • consider_machine_collect( )
    回收机器相关的页面

  • consider_buffer_cache_collect( )
    如果确定定义了这个函数则调用这个函数。调用这通过vm_set_buffer_cleanup_callout( ) 定义这个函数。BSD 层在bufinit( ) 函数中注册了buffer_cache_gc( ) 函数

  • consider_zone_gc( )
    zone 相关的垃圾回收

3.6.2 处理页错误

vm_pageout( ) 守护程序处理的只是交换的一个方向,从物理内存换出到后备存储。而另外一个方向是页面换入,则是发生在页面错误的时候处理的。这个逻辑非常复杂,简化为一下步骤:

  • 如果陷阱的原因是页错误,那么机器级别的线程处理程序调用vm_fault( )
  • vm_fault( ) 函数调用 vm_pageout_fault( )处理实际发生错误的页面,并且从后备存储中将这个页面返回
  • PMAP_ENTER( ) 将页面插入任务的pmap

页错误有很多种,上述只是其中一种,其他类型的也错误还包括:

  • 非法访问
    访问应该没有映射到进程地址空间(即任务的vm_map)的地址。解引用应该野指针时通常会发生这种错误。发生这种错误时进程会收到SIGSEGV信号
  • 页面保护错误
    访问应该映射的地址,但是页面的保护掩码拒绝请求的访问
  • 写时复制(copy-on-write)
    页面可以被标记可读,因此如果任务试图写入页面时,会捕捉到这个错误,在重新尝试写入操作之前可以将这个页面复制出来

总结

VM系统是Mach中最重要最复杂而且最不好理解的子系统。Mach的内存管理核心是分页器,分页器允许将虚拟内存扩展到各种后背存储介质上:交换文件、内存映射文件、设备、甚至远程主机。
iOS中提高内存使用率的Freezer,以及处理内存耗尽的pageout守护程序。

参考资料

https://www.amazon.com/Mac-OS-iOS-Internals-Apples/dp/1118057651/ref=sr_1_2?ie=UTF8&qid=1331298923&sr=8-2

https://developer.apple.com/library/content/documentation/Darwin/Conceptual/KernelProgramming/About/About.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容