为什么Linux需要虚拟内存?操作系统中的CPU和主存(Mainmemory)都是稀缺资源。当前操作系统中运行的所有进程都会共享系统中的CPU和内存资源。操作系统会使用CPU调度器来分配CPU时间[^1],并引入虚拟内存系统来管理物理内存,本文将分析为什么操作系统需要虚拟内存。在回答虚拟内存的必要性之前,我们需要先了解虚拟内存在操作系统中是什么,它在操作系统中扮演着怎样的角色。与软件工程中的其他抽象一样,虚拟内存是操作系统物理内存和进程之间的中间层。它为进程隐藏了物理内存的概念,为进程提供了更简洁易用的界面和更复杂的功能。.virtual-memory-layer图1-进程和操作系统之间的中间层如果我们需要从头设计一个操作系统,让系统中的进程直接访问进程中的物理地址应该是一个很自然的决定主存,早期的操作系统都是这样实现的。进程会使用目标内存的物理地址(PhysicalAddress)直接访问内存中的内容。但是,现代操作系统引入了虚拟内存,进程持有的虚拟地址(VirtualAddress)将由内存管理。单位(MemoryMangamentUnit)的转换成为物理地址[^2],然后通过物理地址访问内存:virtual-memory-system图2-虚拟内存系统的主要存储是一个比较稀缺的资源,虽然顺序读取只比磁盘快一个数量级,但它可以提供极快的随机存取速度。从内存中随机读取数据是磁盘[^3]的10万倍。充分利用内存的随机存取速度是提高程序执行效率的有效途径。操作系统以页面为单位管理内存。当进程发现需要访问的数据不在内存中时,操作系统可能会将数据以页为单位加载到内存中。这个过程是由上图中的内存管理单元(MMU)完成的。操作系统的虚拟内存作为一个抽象层,起着以下三个关键作用:虚拟内存可以利用磁盘作为缓存,提高进程访问指定内存的速度;虚拟内存可以为进程提供独立的内存空间,简化程序的链接和加载过程,并通过动态库共享内存;虚拟内存可以控制进程对物理内存的访问,隔离不同进程的访问权限,提高系统安全性;我们可以把虚拟内存看成是磁盘上的一块空间。当这部分空间被频繁访问时,这部分数据会以页为单位缓存在主存中,以加快CPU访问数据的性能。虚拟内存使用空间大的磁盘存储作为“内存”,并使用主存储缓存进行加速,让上层认为操作系统的内存大,速度快,但面积大的磁盘并不快,而且快内存也不大。virtual-memory-cache图3-虚拟内存、主存和磁盘虚拟页(VirtualPage,PP)在虚拟内存中可能处于以下三种状态——未分配(Unallocated)、未缓存(Uncached)和缓存(Cached),其中unallocatedmemorypage没有被进程申请使用,即freevirtualmemory,不占用虚拟内存盘上的任何空间。未缓存和缓存的内存页分别代表已经加载到主存中的内存页。内存页和只加载到磁盘的内存页。如上图所示,图中绿色的虚拟内存页在主存中有物理内存页(PhysicalPage,PP)支持,所以已经被缓存了,而黄色的虚拟内存页只存在于磁盘中,所以它还没有被缓存。物理内存缓存。当用户程序访问一个没有被缓存的虚拟页面时,硬件会触发页面错误中断(PageFault,PF)。在某些情况下,被访问的页面已经加载到物理内存中,但是用户程序的页表(PageTable)却没有这种对应关系。这时候我们只需要在页表中建立虚拟内存和物理内存的关系即可;在其他情况下,操作系统需要将磁盘上未缓存的虚拟页面加载到物理内存中[^4]。page-fault图4-虚拟内存的页面错误中断由于主存空间有限,当主存中没有可用空间时,操作系统会选择合适的物理内存页并将其驱逐回磁盘用于新内存放弃一个页面并选择一个页面被逐出的过程,在操作系统中称为PageReplacement。页面错误中断和页面替换技术是操作系统分页算法(Paging)的一部分,该算法的目的是充分利用内存资源作为磁盘缓存,以提高程序的运行效率。内存管理虚拟内存可以为正在运行的进程提供独立的内存空间,造成每个进程的内存都是独立的错觉。在64位操作系统上,每个进程将拥有256TiB的内存空间。内核空间和用户空间分别占用128TiB[^5],部分操作系统使用57位虚拟地址提供128PiB的寻址空间[^6]。因为每个进程的虚拟内存空间是完全独立的,它们都可以完全使用从0x0000000000000000到0x00007FFFFFFFFFFFF的整块内存。virtual-memory-space图5-操作系统的虚拟内存空间虚拟内存空间只是操作系统中的一个逻辑结构。正如我们上面所说,应用程序仍然需要访问物理内存或磁盘内容。因为操作系统增加了一个虚拟内存的中间层,我们还需要为进程实现一个地址转换器,将虚拟地址转换为物理地址。页表是虚拟内存系统中重要的数据结构。每个进程的页表存储了从虚拟内存到物理内存页的映射关系。为了在64位操作系统中存储128TiB虚拟内存的映射数据,Linux在2.6.10引入了四层页表来辅助虚拟地址转换[^7],引入了五级页表结构在4.11[^8]中,未来可能会引入更多层的页表结构来支持64位虚拟地址。four-level-page-tables图6-四级页表结构在上图所示的四级页表结构中,操作系统会使用最低12位作为页偏移量,其余32位将分成四组分别代表当前层级在上层的索引,所有的虚拟地址都可以使用上述的多级页表找到对应的物理地址。因为有多层页表结构可以用来翻译虚拟地址,所以多个进程可以通过虚拟内存来共享物理内存。我们在WhyRedissnapshotsusechildprocesses中介绍的copy-on-write就是利用了虚拟内存的这个特性。我们在linux中调用fork创建子进程时,实际上只是复制了父进程的页表。如下图所示,父子进程会通过不同的页表指向同一个物理内存:process-shared-memory图7-进程间共享内存fork过程中,还提供copy-on-write机制,也可以共享一些常用的动态库,减少物理内存占用。所有进程都可能调用同一个操作系统内核代码,C语言程序也会调用同一个标准库。独立的虚拟内存空间除了可以共享内存外,还简化了内存分配过程。当用户程序向操作系统申请堆内存时,操作系统可以分配几个连续的虚拟页面,但这些虚拟页面可以对应内存中物理上的非连续页面。内存保护操作系统中的用户程序不应该修改只读代码段,也不应该读取或修改内核中的代码和数据结构或访问私有和其他进程内存,如果它们不能访问用户进程的内存有限制,攻击者可以访问和修改其他进程的内存,从而影响系统的安全。如果每个进程都拥有独立的虚拟内存空间,那么虚拟内存中的页表可以理解为进程与物理页的“连接表”,可以存储进程与物理页的访问关系,包括readpermission,writePermissionandexecutionpermission:virtual-memory-permission图8-Readpermission,writepermissionandexecutionpermission内存管理单元可以判断当前进程是否有访问目标物理内存的权限,从而最终收敛权限管理对虚拟内存系统的作用,减少了可能带来风险的代码路径。总结虚拟内存的设计方法可以说是软件工程中常用的方法。通过结合磁盘和内存各自的优势,利用中间层更合理地调度资源,充分提高资源利用率,提供和谐统一的抽象。在实际业务场景中,类似的缓存逻辑也很常见。操作系统的虚拟内存是一个非常复杂的组成部分,没有工程师能够了解所有的细节,但是了解虚拟内存的整体设计也很有价值,我们可以从中找到很多设计软件的方法。回到今天的问题——Linux操作系统为什么需要虚拟内存:虚拟内存可以结合磁盘和物理内存的优点,为进程提供看似足够快、容量足够大的存储;虚拟内存可以为进程提供独立的内存空间,并引入多层页表结构,将虚拟内存翻译成物理内存。进程可以共享物理内存以减少开销,还可以简化程序链接、加载和内存分配过程;虚拟内存可以控制进程对物理内存的分配。访问,隔离不同进程的访问权限,提高系统的安全性;最后,我们还是看一些比较开放的相关问题。有兴趣的读者可以仔细思考以下问题:为什么每一层的页表结构只能负责8位虚拟地址的寻址?64位虚拟内存在操作系统中需要多少层页表结构来寻址?
