0. 前言
本文以 iOS Memory 的相关内容作为主题,主要从一般操作系统的内存管理、iOS 系统内存、app 内存管理等三个层面进行了介绍,主要内容的目录如下:
iOS 是基于 BSD 发展而来,所以先理解一般的桌面操作系统的内存机制是非常有必要的。在此基础之上,本文会进一步在 iOS 系统层面进行分析,包括 iOS 整体的内存机制,以及 iOS 系统运行时的内存占用的情况。最后会将粒度缩小到 iOS 中的单个 app,讲到单个 app 的内存管理策略。
1. 操作系统的内存机制
为了从根本上更好地理解和分析 iOS 系统上的内存特性,我们首先需要正确理解一般操作系统通用的内存机制。
冯·诺依曼结构
冯·诺依曼结构(Von Neumann architecture)在 1945 年就已经被提出了, 这个概念当时十分新颖,它第一次将存储器和运算器分离,导致了以存储器为核心的现代计算机的诞生。
在冯·诺依曼结构中,存储器有着重要地位,它存放着程序的指令以及数据,在程序运行时,根据需要提供给 CPU 使用。可以想象,一个理想的存储器,应该是兼顾读写速度快、容量大、价格便宜等特点的,但是鱼和熊掌不可兼得,读写速度越快的存储器也更贵、容量更小。
但冯·诺依曼结构存在一个难以克服的问题,被称为冯·诺依曼瓶颈 —— 在目前的科技水平之下,CPU 与存储器之间的读写速率远远小于 CPU 的工作效率。简单来说就是 CPU 太快了,存储器读写速度不够快,造成了 CPU 性能的浪费。
既然现在我们没办法获得完美的存储器,那我们如何尽量突破冯·诺依曼结构的瓶颈呢?现行的解决方式就是采用多级存储,来平衡存储器的读写速率、容量、价格。
存储器的层次结构
存储器主要分为两类:易失性存储器速度更快,断电之后数据会丢失;非易失性存储器容量更大、价格更低,断电也不会丢失数据。随机访问存储器 RAM 也分为两类,其中 SRAM 速度更快,所以用作高速缓存,DRAM 用作主存。只读存储器 ROM 实际上只有最开始的时候是只读的,后来随着发展也能够进行读写了,只是沿用了之前的名字。
上图就是多层存储器的具体情况,我们平时常说的内存,实际上就是指的 L4 主存。而 L1-L3 高速缓存和主存相比,速度更快,并且它们都已经集成在 CPU 芯片内部了。其中 L0 寄存器本身就是 CPU 的组成部分之一,读写速度最快,操作耗费 0 个时钟周期。
简单来说,存储器的分级实际上就是一种缓存思想。金字塔底部的部分容量大,更便宜,主要是为了发挥其存储属性;而金字塔尖的高速缓存部分读写速度快,负责将高频使用的部分缓存起来,一定程度上优化整体的读写效率。
为什么采用缓存就能够提高效率呢?逻辑上理解起来其实很简单,具体来说就是因为存在局部性原理(Principle of locality) —— 被使用过的存储器内容在未来可能会被多次使用,以及它附近的内容也大概率被使用。当我们把这些内容放在高速缓存中,那么就可以在部分情况下节约访问存储器的时间。
CPU 寻址方式
那么,CPU 是如何访问内存的呢?内存可以被看作一个数组,数组元素是一个字节大小的空间,而数组索引则是所谓的物理地址(Physical Address)。最简单最直接的方式,就是 CPU 直接通过物理地址去访问对应的内存,这样也被叫做物理寻址。
物理寻址后来也扩展支持了分段机制,通过在 CPU 中增加段寄存器,将物理地址变成了 "段地址":"段内偏移量" 的形式,增加了物理寻址的寻址范围。
不过支持了分段机制的物理寻址,仍然有一些问题,最严重的问题之一就是地址空间缺乏保护。简单来说,因为直接暴露的是物理地址,所以进程可以访问到任何物理地址,用户进程想干嘛就干嘛,这是非常危险的。
现代处理器使用的是虚拟寻址的方式,CPU 通过访问虚拟地址(Virtual Address),经过翻译获得物理地址,才能访问内存。这个翻译过程由 CPU 中的内存管理单元(Memory Management Unit,缩写为 MMU)完成。
具体流程如上图所示:首先会在 TLB(Translation Lookaside Buffer)中进行查询,它表位于 CPU 内部,查询速度最快;如果没有命中,那么接下来会在页表(Page Table)中进行查询,页表位于物理内存中,所以查询速度较慢;最后如果发现目标页并不在物理内存中,称为缺页,此时会去磁盘中找。当然,如果页表中还找不到,那就是出错了。
翻译过程实际上和前文讲到的存储器分级类似,都体现了缓存思想:TLB 的速度最快,但是容量也最小,之后是页表,最慢的是硬盘。
虚拟内存
刚才提到,直接使用物理寻址,会有地址空间缺乏保护的严重问题。那么如何解决呢?实际上在使用了虚拟寻址之后,由于每次都会进行一个翻译过程,所以可以在翻译中增加一些额外的权限判定,对地址空间进行保护。所以,对于每个进程来说,操作系统可以为其提供一个独立的、私有的、连续的地址空间,这就是所谓的虚拟内存。
虚拟内存最大的意义就是保护了进程的地址空间,使得进程之间不能够越权进行互相地干扰。对于每个进程来说,操作系统通过虚拟内存进行"欺骗",进程只能够操作被分配的虚拟内存的部分。与此同时,进程可见的虚拟内存是一个连续的地址空间,这样也方便了程序员对内存进行管理。
对于进程来说,它的可见部分只有分配给它的虚拟内存,而虚拟内存实际上可能映射到物理内存以及硬盘的任何区域。由于硬盘读写速度并不如内存快,所以操作系统会优先使用物理内存空间,但是当物理内存空间不够时,就会将部分内存数据交换到硬盘上去存储,这就是所谓的 Swap 内存交换机制。有了内存交换机制以后,相比起物理寻址,虚拟内存实际上利用硬盘空间拓展了内存空间。
总结起来,虚拟内存有下面几个意义:保护了每个进程的地址空间、简化了内存管理、利用硬盘空间拓展了内存空间。
内存分页
基于前文的思路,虚拟内存和物理内存建立了映射的关系。为了方便映射和管理,虚拟内存和物理内存都被分割成相同大小的单位,物理内存的最小单位被称为帧(Frame),而虚拟内存的最小单位被称为页(Page)。
注意页和帧大小相同,有着类似函数的映射关系,前文提到的借助 TLB、页表进行的翻译过程,实际上和函数的映射非常类似。
内存分页最大的意义在于,支持了物理内存的离散使用。由于存在映射过程,所以虚拟内存对应的物理内存可以任意存放,这样就方便了操作